From bcd251c4df40d9180e3bc18f131a17c6c11e3be7 Mon Sep 17 00:00:00 2001 From: gerlofvanek Date: Tue, 8 Oct 2024 23:15:40 +0200 Subject: [PATCH] ui: Dynamic offers tables + various updates. --- basicswap/js_server.py | 5 +- .../static/images/coins/Bitcoin-Cash-20.png | Bin 0 -> 2050 bytes .../static/images/coins/Bitcoin-Cash.png | Bin 0 -> 7257 bytes .../chartjs-adapter-date-fns.bundle.min.js | 7 + basicswap/static/js/offerstable.js | 1609 +++++++++++++++ basicswap/static/js/pricechart.js | 1253 ++++++++++++ basicswap/templates/header.html | 41 +- basicswap/templates/offers.html | 1720 +++-------------- 8 files changed, 3148 insertions(+), 1487 deletions(-) create mode 100644 basicswap/static/images/coins/Bitcoin-Cash-20.png create mode 100644 basicswap/static/images/coins/Bitcoin-Cash.png create mode 100644 basicswap/static/js/libs/chartjs-adapter-date-fns.bundle.min.js create mode 100644 basicswap/static/js/offerstable.js create mode 100644 basicswap/static/js/pricechart.js diff --git a/basicswap/js_server.py b/basicswap/js_server.py index 8bf9853..2bce88e 100644 --- a/basicswap/js_server.py +++ b/basicswap/js_server.py @@ -229,11 +229,14 @@ def js_offers(self, url_split, post_string, is_json, sent=False) -> bytes: 'amount_to': ci_to.format_amount((o.amount_from * o.rate) // ci_from.COIN()), 'rate': ci_to.format_amount(o.rate), 'min_bid_amount': ci_from.format_amount(o.min_bid_amount), + 'is_expired': o.expire_at <= swap_client.getTime(), + 'is_own_offer': o.was_sent, + 'amount_variable': o.amount_negotiable, + 'rate_variable': o.rate_negotiable } if with_extra_info: offer_data['amount_negotiable'] = o.amount_negotiable offer_data['rate_negotiable'] = o.rate_negotiable - if o.swap_type == SwapTypes.XMR_SWAP: _, xmr_offer = swap_client.getXmrOffer(o.offer_id) offer_data['lock_time_1'] = xmr_offer.lock_time_1 diff --git a/basicswap/static/images/coins/Bitcoin-Cash-20.png b/basicswap/static/images/coins/Bitcoin-Cash-20.png new file mode 100644 index 0000000000000000000000000000000000000000..ae8edc8193a1253aa42b6396b78e6f04e0a6b1b9 GIT binary patch literal 2050 zcmaJ?c~nz(7LMRyt2h=Asj|EVWi8oR5(t=(L?Q$w>~dH_-U|dsUQC`ONC8DLAkYza zEQbonB6}(5RIQ>^sDOZq2*awBRz*P>MUe{guwds8r@wRFZ@K6D?)~m^&db{AyG>tj zqaFr>(dT-x_~>c0^yz4!dv2DQK6-qCutN}kScE7AatOl!;b;ipN(3uTsI3NTG;rY;nummSq!r)XA8Op|B+&ok=fiNCIfM_U2ETvwG7gr<;E7Z!6;B}HNhAjp;h;#8A_A3zRAIT| zfdwgqaIy6!0Fo5N(Oba)peR@9zUG0Z}zH7A)K&fusnRMaQBR94H296i*h} z(UD4aab!}7L^g-XrZ{srWM>zuCzC-Y5m&fesR9v5h0qFD{1=z>w_KX191^WE>f=2^LF1SgEjGs_n8=NG?u4gV{UE>eLVgh+Q5R@$!fU*0ZEF0_gZtY^>O; zCAHffH5{3QRI8f-TEbc{WApRi>Uq7&o@awu6&ts<_e2#nlU8{hO@BDn*`mGe>Vi9C zRUofJ<79uqud!qK-xn(KK^Br%a^`T+>Gs8nI1jn)obyHZt%VvS@Sj`yuKN)_GrQnA z^K$$Ti`?ly6mv<|O@~%})7IDXa{4=7$>(Xgn}7FoM)o!QW^>H*czAZ*My0D8!91?z zaY}a)sa)p{Yjbjc!ikpb*^KKC)WzOzKZNUaCAdynq-Nb{sEGUOp#R{T@uyS57l%ph z&y!-!CK=^vCayYfLAK@D(x|&pSJzhMK#q^+J3VGVW0|B@Y-c`V&(C`HCH7K~)yt;| z9$^vLYT48yZ5Qgjwm!o!H@$yrWv&teWnf zbEg*HnFhCf=A7~>?B@c&z*%>eU^r`8*l_hk020gB@73Lu0v544TjBcd@%71m-lZ1! z^D&c}E#W$q41;7`Sp4P%tsQfYT3+;bxwV68YbzX*P7y~i$i*4IJgJdD+KE4YU7=hb zowxSnn?20{y}j&sMWxH!F81Zrb7w2FC;UX+*YC046wJ0Kd5cOMSl?`|V+Ply2z0h= zaMiIneBsG=1oLpwDpfMhOT zE2JOJd(6wb)=;B+sv=GEA=`^(9Jso$xmRW4KJa|8XzC?N&CY>~!dltfbHA0b3@3gb zDiy*$U(XY5T(h=|Hsri2YfWD#W{IzC&BYxEJ9Xt3jmr0UUR>U3?ZMkUV3*E(vz zBvR|%I`ta%Prmm`1Nd4IxEf_-R<-Gu16S&6w@jZCV|7hqT-eF|lJ5EU5jP)P2yM@| q+7xuxPTydIW#;P_iHjNwhEW(|%hT_5LleNIzcQEY%c^BYrT-To+ep0t literal 0 HcmV?d00001 diff --git a/basicswap/static/images/coins/Bitcoin-Cash.png b/basicswap/static/images/coins/Bitcoin-Cash.png new file mode 100644 index 0000000000000000000000000000000000000000..baf0eb97e525f1530881227a845454edc4ced5d7 GIT binary patch literal 7257 zcmaJ`bySqy)*nJZq`QP+2t@{l7-EJ_=|)nN7tqeH0m(=MB{A?D1xN(i5{N-MyLiAcQlP)|!f)Dth9Mx}Um|EHDbT-28EfhQ z6 z8(X-ZqVnI<-SnhD_Gq*l90Kw7_U88%=66NeK?Gqi7(_q_A|%9jBf;n4z|x&Z%Zw6t>dL`#8g$o^{;NH^4Fdz!&v6w1pIs6nZgq!#`T-A_j`o5ha z{Q3Q$Z%zWc9oLxOBA6UE=%cdy@5r_Oa3aEI=W)?^*<8!Lc|>rjyie+OsxD3?`x#c}Zh9 zRi9j3eJ!6@r%x$XlL`j9XU3Xan;lQ*tSl|8Z9+1H_KdPKkUoJ2jVZjvFrqIO5SbTD?d?mSJ>xjAsk}Z8 zJwXg#zHsudFr;=w;H9jJ#O|#|kt$Wr+dR0otkN@Dk5iOB4~kcgCve_(y&Rx(IwFW# zN`D9}rOf^Dz-Ii0elDY#N7t8{EpCtQIJ)gSujM_qv z@bXGJXI?FD{vq}4M#N^^*ONU=^YtWs|NKi>m7b^`mP?h33e!1tI75wOL+;%19o};9 zqNeAtaTIhjknw3+^A6mSJ|WSPOgByhb8#pO;wdi5%a1Y*CnA-3#^3euti?CmWyYR$u9Qgo zr;fwa*6^e8WH-2>_D{;?T&JeB+L~9>n)l&`Oe(IDL{fG&4S|cTKh2h0PG;3>TO<*= zD*QAgBF#L_?z>gPl`ai+<{~)@Cc3g6>*bza!!4~Y6WeSsuo4tjkdCVud_wbBes!9; z;qqBT`KqaB$WD>55Gx#KvXs%YB4_Jo%-3uBh2LO9Vm+A<9QM>Wy=`)l{b%3#oF*LW z4AQv)`QUa%2b8vW{7I5BNautc4Zia2tY61XAPTHnOtq;+Kg?H%5IeBB2=*K`bDS<4 zFJvqvHIV6-KQKxgSf5=X6>tNh?BVD zeL7oic~Ts;Tix))XZ^G0{u#*9^G$#|6FLxzPZ1f!QppuXAGy&4(Qdy@w{e&!TLC+| z$ z72CxWe|CZt>fzDOC`;hqdcJ%#iDOT9_2Fmd>#64QWE(MkF6hSVk+Kkx#)+pnqT~+@ zluuwkx_e`_=+^NQ#1^aTK3WAV!({vz9fyuq`aTykyqnk`@+tLMpJ>ml-x6gz%o>wc z?6*J9T#jHxayEHIUC#&XNQZOQ?IDTktTW)udwuD?1SGEzJyh|BKXUKvFUP<2?i#f~ zrs2cWQRWAICf>$mW)~SQ3**zr4pNn}q;bh_B3MHJj1?BgD<|>V=Wc_0c9iZsufKO? z;M;v+4jATmfIj-nLd%2R@pvON1#kW#Ju)1mt!tMph+Mgs5vOz{@3rXp}9kn<^c`r1 zCK$lJRC*2#iZGXQt|w3CvVT?2?mhPI4u`=0Ny8^klOm2b1*S)N1GvIeG6)itnhOPK zdj*QG{$v~Ezh+vvO4tMZLE(0Mgzb^P&n_MqKJutG1>YSGU_QR|lqAoDGs{eG)zKHd z>U&7Qq&pLV|IB+a+C#dyBkHyWch4rvj z%-#}u@>5Rmgt*^?{%iKVI>OcrBj2t4z!v|_21(5Id^i9Fa)EDzVW_0?TeNy2zmm#J zR{Q1NE!bCicX^NBCiaa8Y{Q_>lE?8=zNN(@jVLaas{(R?+5P^F`(r=S=gSVVR)N3m zCbW8Bs00aff;wzbMN?qwxX!UboB0H5w3F(hz^N2F5GEvYk==AGdXi(lfZcr!w*4OO z$?|=_Zba*hA@YJapaUjZjoix!3pS{h|9ENk+5f(RMbq|-Z4boCiCELo=_9gh-Si^) z)7ER40?u-w7Izfc6LI`X6XyL@MF3DdEm6KQ67-|Y{(8SJTl$T98nM9hf#KOiONDbP zA|P;+g0=2g#Hq8jJ~uVNg8`r5`2vnuO${mQSKQj|L0TT5JZ`XW79coxCSRo@R1Iyy z6KfnRDM+uG_(&s`iF;c|U-U#2SW+96slC&yibuV6a>TWHKu6eq(>as4iSqur+X0I=Rfqvj)V| zZ;X#~&^@SqKWC0}iX9HK`Cb&xfYD14#ijhLK0-6Y>UGM$Px7HwoPgib#WhKiZ+pkL zq1-elWG3``Dp~s-Cier|H^cPkSg&^K9Q&_?Y#@plq8Hgh<1`1$8f?z}WpX7?J@!>G z9Vmt?!EI3j9T1P<#^UHY$zJv}h0&s7X=j+b!BI zd7JqPbEiKbL1ZJDNXW!7(!k-)`2&5Q0->y!3pcM7Qn`W>w(I#wRxQHXbAntoq!sN) z#;u*O^$$J9B(A2+i6Gs>ZE$c{amEkrlBHp$-{ZJMi|JAuyb4Uf=7mHk@Gg##>P?oH zHt>#rc1#PKP{ei}xtdlAQaO2jRy2!5RP~vC@#f|<8n{BK#OkWp-X-rnG>yb+P8!S5 zc8?o8FTNc+)jNZ&)7HlCBl>xhm5iVxLQ#T}Hy^W6( z(p9iNKHo1Vw$&5l(9n8w&=`dII*nvMeZ`}1d+l&o4zcJPT3Fw1s5Xt%0J|(38^x(n zM2L4j6L}q3WQQ5PWOyCT)rhLQZ{zQ$Mf&L+34$FBfzS;RxT;9z$H^v9+f)m2b#eTBcTpI(8}aHGPuW`6=PqhY&WJqf zW3vG6X#ru8!n8Lxip;&+es$?HHg8<%;B+w&T>dJuE@52Pyheo+uUr^>JCc7h_G-c! z_Y+kvgN9zM<}hWsG-2a5MNx)`SZxuEIMY|_<4uy+@wid=TVok-?>t|KXom?h2N&YW zN>E0mB#d1z1tu1)uP&+-pICNv_>V5a*QRs!O2fa@1%{V=MiiZOvmSN}wSn<~b|yhH zl!Jf?P;)9@-TURqoI;n-c_#`>l2YT6wIUq3pr?h~LrJtH((hMTnVt5uIBybSpqxix zKF8uzXAEV0f1%nL-^efi=*iw!&TnjAT2|zPx3_8WUzmb+qijo!9&slK94TZ+-?n>S zA$uTgn1uASmYq$<2U@ukIuP~1wB&+pRFikpg-mF!F$oVB3rxLn1#@i;$}}iwW+nz} zq-uIjtR)k2?SnQj>5nOZ=P0CtveCiv&b*~7^O2JQ8x9VW{2mk$rJmFz*zPyCPlMjW z>U-%%7eLEH0H0M654jlUi9;PZCoxh};MNWU;+LF8ZLe;Vt(>!F)lxuJ!6h@XOaqZZ z1T4sV#`lQgVHsI)&HJPZ^*1BCo23V~T$yq@1& zIGM<$$rH@fSYttIl>=oT7#2TD)R?6mVTc2qDFh4pgqglv+eWcV4mi%w} zVy&uxe_KO|X-YSGjW+xfj_Gr$=XgNNZE1c3t%9Qix1);#Km4#)nO4Ca<}O8T>!^T^ zlPcRYBH2=+!qwHh#*Fz>GKpq8z35D`ywXfQO4%chqBUoz#9)n@V&kJI*Pyzrwi!=+ zcz{+)2;~Ho4#2Fuf7T>_?9?yJt6l#sN8L79h~{cHIpG`7~#vi|p)`C>A?|JgoOs>$`?u&fQz#VduFLB0AjDk+`zFbKc|ph5<)~eNKEX z<-*r`Gv`)QORL}A6eX+5yn{R+)68Z-PU#0_DD~MYwQEQ76amX!Mq%wuKu8$ocH`dU z*3QdetPROygUqC_%vQWvpRk*XE+Z8fK|UY8EI{Omv1*TUBI8^b-qk{JwsY(EM)Llu zFwWc_T~q1o@O!3Hmt{LIQ%UuT2IE$ksG34vI^N+N(VAQ zo}FVwoH+ci&ScWdZna!ry=SWs(txi^h-C~c+m*^9Ti_&hL^n)d{O}ut`C7KN)#?7qaaSG;_JP}ScUC``06gl zxI9xO=Y_7>bM-}))~^H-gjzMF*;}i%Yc0lQc6Xx{LZwYXVuna7^(Kb|-Ov&?>on=# zlBYx7_m#`KKCC8r1F$Wed~e%e8NG8cFT>-zP=Qc%8g%E`3a_;`I(`S|>ov<4M*G0(u_vx$zTn+cR;Ez9z%- z)WiaS2wZtoavsCtrju5t*ZdRcwb-hf!d>-sa!R=i#OEOizrLC1R$kX%9{M7)n$ig= zRMavsw{J#l=MwKaU-%^cn` zz=9o5Aw6?(-h{Npv(&7C_Le#Q_DAmmv|p)it5TLK?6KN5p`LTHI#Ald4)3H3zZAA7 zRf(y|)-+=!R7&G8aLd(31|2E_0px#l~pB zHD`}+rT4j$W{B~IxKl++^t#bed{1?X=5p(r4Nc-eWN2*5+Q;z`HXiD3Ri_HHEh|C| zO@xZJfAp)js|B{l2ec?_BzG8=E0BMumS-|-TeqVf-0PIPji?Ty{e-dB_SaqKAt73N zFVfkN=_TOm?6!V;Ty#qyPcHJOUBA*o9Xr!#uas#Uwqp=|>mCE!N^7(ir+?dW^DFqs z(OipML{k05h1O98sf^Um4vzF0Sw>(zgtHEZ*2>%+Uy9eXLFI8&QfB^dBkp2i+MYHY zkhNTnT`YH+Z z_g!7`RPY1>mRJzubhVUk*s~B z#h2?o{5AVlxogdvrbj>Q-ahglxv%fbFG}quw?`mCfgSwfg-smQm`Ecp_Q+nuB+NYP zluIidtGo0cIMn87_xhkOKo+7oHD;oQL;lLKKck@1wYv{fyhY_xpd}<9LsO)t(90|A z+USrYy8m81s7R%(p9!v@BK#n!N0hUgwww4;&NV~y)?jUXxPVGB->5;kaidNugos2} zW2#Z?VP;1kn4AvMVi}odFA+@Dsu=u2-udE9bWLwWkcI<+0QK( zf~b=VeoAV(7wypV#os36OGd^45$2!B@fGJZC5lwuc}) z6t8_8cvjp8hs(hQNM(6@(t&9rf%m6Ud&{E1FA%asvBLtc>0%0MeEPeayQX$6#v7}# zP}@J}c9dD4^0T}x_^tVJZW2`pRjj%M6gq}>O}bTKV7GgdyPRyB4JTK6oj=9_?aFdq z=Hx>c)&TC2-@x05Y4^Zk!Uud0 zRm*t3?wL=Q^+0EX%uYR7gn{{P$NgsIVfBl$-|E9oRgX6~S9(4((9eD(ihA;xUR_U?ihN|E&qa9@bZo+PM{Lo7qD1?"s":"")+" required, but only "+e.length+" present")}function n(t){r(1,arguments);var e=Object.prototype.toString.call(t);return t instanceof Date||"object"==typeof t&&"[object Date]"===e?new Date(t.getTime()):"number"==typeof t||"[object Number]"===e?new Date(t):("string"!=typeof t&&"[object String]"!==e||"undefined"==typeof console||(console.warn("Starting with v2.0.0-beta.1 date-fns doesn't accept strings as date arguments. Please use `parseISO` to parse strings. See: https://git.io/fjule"),console.warn((new Error).stack)),new Date(NaN))}function a(t,a){r(2,arguments);var i=n(t),o=e(a);return isNaN(o)?new Date(NaN):o?(i.setDate(i.getDate()+o),i):i}function i(t,a){r(2,arguments);var i=n(t),o=e(a);if(isNaN(o))return new Date(NaN);if(!o)return i;var u=i.getDate(),s=new Date(i.getTime());s.setMonth(i.getMonth()+o+1,0);var c=s.getDate();return u>=c?s:(i.setFullYear(s.getFullYear(),s.getMonth(),u),i)}function o(t,a){r(2,arguments);var i=n(t).getTime(),o=e(a);return new Date(i+o)}var u=36e5;function s(t,a){r(1,arguments);var i=a||{},o=i.locale,u=o&&o.options&&o.options.weekStartsOn,s=null==u?0:e(u),c=null==i.weekStartsOn?s:e(i.weekStartsOn);if(!(c>=0&&c<=6))throw new RangeError("weekStartsOn must be between 0 and 6 inclusively");var d=n(t),l=d.getDay(),f=(l0?1:o}function m(t){r(1,arguments);var e=n(t);return!isNaN(e)}function w(t,e){r(2,arguments);var a=n(t),i=n(e),o=a.getFullYear()-i.getFullYear(),u=a.getMonth()-i.getMonth();return 12*o+u}function g(t,e){r(2,arguments);var a=n(t),i=n(e);return a.getFullYear()-i.getFullYear()}function v(t,e){var r=t.getFullYear()-e.getFullYear()||t.getMonth()-e.getMonth()||t.getDate()-e.getDate()||t.getHours()-e.getHours()||t.getMinutes()-e.getMinutes()||t.getSeconds()-e.getSeconds()||t.getMilliseconds()-e.getMilliseconds();return r<0?-1:r>0?1:r}function y(t,e){r(2,arguments);var a=n(t),i=n(e),o=v(a,i),u=Math.abs(f(a,i));a.setDate(a.getDate()-o*u);var s=v(a,i)===-o,c=o*(u-s);return 0===c?0:c}function b(t,e){r(2,arguments);var a=n(t),i=n(e);return a.getTime()-i.getTime()}var T=36e5;function p(t){r(1,arguments);var e=n(t);return e.setHours(23,59,59,999),e}function C(t){r(1,arguments);var e=n(t),a=e.getMonth();return e.setFullYear(e.getFullYear(),a+1,0),e.setHours(23,59,59,999),e}function M(t){r(1,arguments);var e=n(t);return p(e).getTime()===C(e).getTime()}function D(t,e){r(2,arguments);var a,i=n(t),o=n(e),u=h(i,o),s=Math.abs(w(i,o));if(s<1)a=0;else{1===i.getMonth()&&i.getDate()>27&&i.setDate(30),i.setMonth(i.getMonth()-u*s);var c=h(i,o)===-u;M(n(t))&&1===s&&1===h(t,o)&&(c=!1),a=u*(s-c)}return 0===a?0:a}var x={lessThanXSeconds:{one:"less than a second",other:"less than {{count}} seconds"},xSeconds:{one:"1 second",other:"{{count}} seconds"},halfAMinute:"half a minute",lessThanXMinutes:{one:"less than a minute",other:"less than {{count}} minutes"},xMinutes:{one:"1 minute",other:"{{count}} minutes"},aboutXHours:{one:"about 1 hour",other:"about {{count}} hours"},xHours:{one:"1 hour",other:"{{count}} hours"},xDays:{one:"1 day",other:"{{count}} days"},aboutXWeeks:{one:"about 1 week",other:"about {{count}} weeks"},xWeeks:{one:"1 week",other:"{{count}} weeks"},aboutXMonths:{one:"about 1 month",other:"about {{count}} months"},xMonths:{one:"1 month",other:"{{count}} months"},aboutXYears:{one:"about 1 year",other:"about {{count}} years"},xYears:{one:"1 year",other:"{{count}} years"},overXYears:{one:"over 1 year",other:"over {{count}} years"},almostXYears:{one:"almost 1 year",other:"almost {{count}} years"}};function k(t){return function(e){var r=e||{},n=r.width?String(r.width):t.defaultWidth;return t.formats[n]||t.formats[t.defaultWidth]}}var U={date:k({formats:{full:"EEEE, MMMM do, y",long:"MMMM do, y",medium:"MMM d, y",short:"MM/dd/yyyy"},defaultWidth:"full"}),time:k({formats:{full:"h:mm:ss a zzzz",long:"h:mm:ss a z",medium:"h:mm:ss a",short:"h:mm a"},defaultWidth:"full"}),dateTime:k({formats:{full:"{{date}} 'at' {{time}}",long:"{{date}} 'at' {{time}}",medium:"{{date}}, {{time}}",short:"{{date}}, {{time}}"},defaultWidth:"full"})},Y={lastWeek:"'last' eeee 'at' p",yesterday:"'yesterday at' p",today:"'today at' p",tomorrow:"'tomorrow at' p",nextWeek:"eeee 'at' p",other:"P"};function N(t){return function(e,r){var n,a=r||{};if("formatting"===(a.context?String(a.context):"standalone")&&t.formattingValues){var i=t.defaultFormattingWidth||t.defaultWidth,o=a.width?String(a.width):i;n=t.formattingValues[o]||t.formattingValues[i]}else{var u=t.defaultWidth,s=a.width?String(a.width):t.defaultWidth;n=t.values[s]||t.values[u]}return n[t.argumentCallback?t.argumentCallback(e):e]}}function S(t){return function(e,r){var n=String(e),a=r||{},i=a.width,o=i&&t.matchPatterns[i]||t.matchPatterns[t.defaultMatchWidth],u=n.match(o);if(!u)return null;var s,c=u[0],d=i&&t.parsePatterns[i]||t.parsePatterns[t.defaultParseWidth];return s="[object Array]"===Object.prototype.toString.call(d)?function(t,e){for(var r=0;r0?"in "+n:n+" ago":n},formatLong:U,formatRelative:function(t,e,r,n){return Y[t]},localize:{ordinalNumber:function(t,e){var r=Number(t),n=r%100;if(n>20||n<10)switch(n%10){case 1:return r+"st";case 2:return r+"nd";case 3:return r+"rd"}return r+"th"},era:N({values:{narrow:["B","A"],abbreviated:["BC","AD"],wide:["Before Christ","Anno Domini"]},defaultWidth:"wide"}),quarter:N({values:{narrow:["1","2","3","4"],abbreviated:["Q1","Q2","Q3","Q4"],wide:["1st quarter","2nd quarter","3rd quarter","4th quarter"]},defaultWidth:"wide",argumentCallback:function(t){return Number(t)-1}}),month:N({values:{narrow:["J","F","M","A","M","J","J","A","S","O","N","D"],abbreviated:["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"],wide:["January","February","March","April","May","June","July","August","September","October","November","December"]},defaultWidth:"wide"}),day:N({values:{narrow:["S","M","T","W","T","F","S"],short:["Su","Mo","Tu","We","Th","Fr","Sa"],abbreviated:["Sun","Mon","Tue","Wed","Thu","Fri","Sat"],wide:["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"]},defaultWidth:"wide"}),dayPeriod:N({values:{narrow:{am:"a",pm:"p",midnight:"mi",noon:"n",morning:"morning",afternoon:"afternoon",evening:"evening",night:"night"},abbreviated:{am:"AM",pm:"PM",midnight:"midnight",noon:"noon",morning:"morning",afternoon:"afternoon",evening:"evening",night:"night"},wide:{am:"a.m.",pm:"p.m.",midnight:"midnight",noon:"noon",morning:"morning",afternoon:"afternoon",evening:"evening",night:"night"}},defaultWidth:"wide",formattingValues:{narrow:{am:"a",pm:"p",midnight:"mi",noon:"n",morning:"in the morning",afternoon:"in the afternoon",evening:"in the evening",night:"at night"},abbreviated:{am:"AM",pm:"PM",midnight:"midnight",noon:"noon",morning:"in the morning",afternoon:"in the afternoon",evening:"in the evening",night:"at night"},wide:{am:"a.m.",pm:"p.m.",midnight:"midnight",noon:"noon",morning:"in the morning",afternoon:"in the afternoon",evening:"in the evening",night:"at night"}},defaultFormattingWidth:"wide"})},match:{ordinalNumber:(P={matchPattern:/^(\d+)(th|st|nd|rd)?/i,parsePattern:/\d+/i,valueCallback:function(t){return parseInt(t,10)}},function(t,e){var r=String(t),n=e||{},a=r.match(P.matchPattern);if(!a)return null;var i=a[0],o=r.match(P.parsePattern);if(!o)return null;var u=P.valueCallback?P.valueCallback(o[0]):o[0];return{value:u=n.valueCallback?n.valueCallback(u):u,rest:r.slice(i.length)}}),era:S({matchPatterns:{narrow:/^(b|a)/i,abbreviated:/^(b\.?\s?c\.?|b\.?\s?c\.?\s?e\.?|a\.?\s?d\.?|c\.?\s?e\.?)/i,wide:/^(before christ|before common era|anno domini|common era)/i},defaultMatchWidth:"wide",parsePatterns:{any:[/^b/i,/^(a|c)/i]},defaultParseWidth:"any"}),quarter:S({matchPatterns:{narrow:/^[1234]/i,abbreviated:/^q[1234]/i,wide:/^[1234](th|st|nd|rd)? quarter/i},defaultMatchWidth:"wide",parsePatterns:{any:[/1/i,/2/i,/3/i,/4/i]},defaultParseWidth:"any",valueCallback:function(t){return t+1}}),month:S({matchPatterns:{narrow:/^[jfmasond]/i,abbreviated:/^(jan|feb|mar|apr|may|jun|jul|aug|sep|oct|nov|dec)/i,wide:/^(january|february|march|april|may|june|july|august|september|october|november|december)/i},defaultMatchWidth:"wide",parsePatterns:{narrow:[/^j/i,/^f/i,/^m/i,/^a/i,/^m/i,/^j/i,/^j/i,/^a/i,/^s/i,/^o/i,/^n/i,/^d/i],any:[/^ja/i,/^f/i,/^mar/i,/^ap/i,/^may/i,/^jun/i,/^jul/i,/^au/i,/^s/i,/^o/i,/^n/i,/^d/i]},defaultParseWidth:"any"}),day:S({matchPatterns:{narrow:/^[smtwf]/i,short:/^(su|mo|tu|we|th|fr|sa)/i,abbreviated:/^(sun|mon|tue|wed|thu|fri|sat)/i,wide:/^(sunday|monday|tuesday|wednesday|thursday|friday|saturday)/i},defaultMatchWidth:"wide",parsePatterns:{narrow:[/^s/i,/^m/i,/^t/i,/^w/i,/^t/i,/^f/i,/^s/i],any:[/^su/i,/^m/i,/^tu/i,/^w/i,/^th/i,/^f/i,/^sa/i]},defaultParseWidth:"any"}),dayPeriod:S({matchPatterns:{narrow:/^(a|p|mi|n|(in the|at) (morning|afternoon|evening|night))/i,any:/^([ap]\.?\s?m\.?|midnight|noon|(in the|at) (morning|afternoon|evening|night))/i},defaultMatchWidth:"any",parsePatterns:{any:{am:/^a/i,pm:/^p/i,midnight:/^mi/i,noon:/^no/i,morning:/morning/i,afternoon:/afternoon/i,evening:/evening/i,night:/night/i}},defaultParseWidth:"any"})},options:{weekStartsOn:0,firstWeekContainsDate:1}};function H(t,n){r(2,arguments);var a=e(n);return o(t,-a)}function E(t,e){for(var r=t<0?"-":"",n=Math.abs(t).toString();n.length0?r:1-r;return E("yy"===e?n%100:n,e.length)},M:function(t,e){var r=t.getUTCMonth();return"M"===e?String(r+1):E(r+1,2)},d:function(t,e){return E(t.getUTCDate(),e.length)},a:function(t,e){var r=t.getUTCHours()/12>=1?"pm":"am";switch(e){case"a":case"aa":return r.toUpperCase();case"aaa":return r;case"aaaaa":return r[0];default:return"am"===r?"a.m.":"p.m."}},h:function(t,e){return E(t.getUTCHours()%12||12,e.length)},H:function(t,e){return E(t.getUTCHours(),e.length)},m:function(t,e){return E(t.getUTCMinutes(),e.length)},s:function(t,e){return E(t.getUTCSeconds(),e.length)},S:function(t,e){var r=e.length,n=t.getUTCMilliseconds();return E(Math.floor(n*Math.pow(10,r-3)),e.length)}},F=864e5;function W(t){r(1,arguments);var e=1,a=n(t),i=a.getUTCDay(),o=(i=o.getTime()?a+1:e.getTime()>=s.getTime()?a:a-1}function Q(t){r(1,arguments);var e=L(t),n=new Date(0);n.setUTCFullYear(e,0,4),n.setUTCHours(0,0,0,0);var a=W(n);return a}var R=6048e5;function I(t){r(1,arguments);var e=n(t),a=W(e).getTime()-Q(e).getTime();return Math.round(a/R)+1}function G(t,a){r(1,arguments);var i=a||{},o=i.locale,u=o&&o.options&&o.options.weekStartsOn,s=null==u?0:e(u),c=null==i.weekStartsOn?s:e(i.weekStartsOn);if(!(c>=0&&c<=6))throw new RangeError("weekStartsOn must be between 0 and 6 inclusively");var d=n(t),l=d.getUTCDay(),f=(l=1&&l<=7))throw new RangeError("firstWeekContainsDate must be between 1 and 7 inclusively");var f=new Date(0);f.setUTCFullYear(o+1,0,l),f.setUTCHours(0,0,0,0);var h=G(f,a),m=new Date(0);m.setUTCFullYear(o,0,l),m.setUTCHours(0,0,0,0);var w=G(m,a);return i.getTime()>=h.getTime()?o+1:i.getTime()>=w.getTime()?o:o-1}function j(t,n){r(1,arguments);var a=n||{},i=a.locale,o=i&&i.options&&i.options.firstWeekContainsDate,u=null==o?1:e(o),s=null==a.firstWeekContainsDate?u:e(a.firstWeekContainsDate),c=X(t,n),d=new Date(0);d.setUTCFullYear(c,0,s),d.setUTCHours(0,0,0,0);var l=G(d,n);return l}var B=6048e5;function z(t,e){r(1,arguments);var a=n(t),i=G(a,e).getTime()-j(a,e).getTime();return Math.round(i/B)+1}var A="midnight",Z="noon",K="morning",$="afternoon",_="evening",J="night",V={G:function(t,e,r){var n=t.getUTCFullYear()>0?1:0;switch(e){case"G":case"GG":case"GGG":return r.era(n,{width:"abbreviated"});case"GGGGG":return r.era(n,{width:"narrow"});default:return r.era(n,{width:"wide"})}},y:function(t,e,r){if("yo"===e){var n=t.getUTCFullYear(),a=n>0?n:1-n;return r.ordinalNumber(a,{unit:"year"})}return O.y(t,e)},Y:function(t,e,r,n){var a=X(t,n),i=a>0?a:1-a;return"YY"===e?E(i%100,2):"Yo"===e?r.ordinalNumber(i,{unit:"year"}):E(i,e.length)},R:function(t,e){return E(L(t),e.length)},u:function(t,e){return E(t.getUTCFullYear(),e.length)},Q:function(t,e,r){var n=Math.ceil((t.getUTCMonth()+1)/3);switch(e){case"Q":return String(n);case"QQ":return E(n,2);case"Qo":return r.ordinalNumber(n,{unit:"quarter"});case"QQQ":return r.quarter(n,{width:"abbreviated",context:"formatting"});case"QQQQQ":return r.quarter(n,{width:"narrow",context:"formatting"});default:return r.quarter(n,{width:"wide",context:"formatting"})}},q:function(t,e,r){var n=Math.ceil((t.getUTCMonth()+1)/3);switch(e){case"q":return String(n);case"qq":return E(n,2);case"qo":return r.ordinalNumber(n,{unit:"quarter"});case"qqq":return r.quarter(n,{width:"abbreviated",context:"standalone"});case"qqqqq":return r.quarter(n,{width:"narrow",context:"standalone"});default:return r.quarter(n,{width:"wide",context:"standalone"})}},M:function(t,e,r){var n=t.getUTCMonth();switch(e){case"M":case"MM":return O.M(t,e);case"Mo":return r.ordinalNumber(n+1,{unit:"month"});case"MMM":return r.month(n,{width:"abbreviated",context:"formatting"});case"MMMMM":return r.month(n,{width:"narrow",context:"formatting"});default:return r.month(n,{width:"wide",context:"formatting"})}},L:function(t,e,r){var n=t.getUTCMonth();switch(e){case"L":return String(n+1);case"LL":return E(n+1,2);case"Lo":return r.ordinalNumber(n+1,{unit:"month"});case"LLL":return r.month(n,{width:"abbreviated",context:"standalone"});case"LLLLL":return r.month(n,{width:"narrow",context:"standalone"});default:return r.month(n,{width:"wide",context:"standalone"})}},w:function(t,e,r,n){var a=z(t,n);return"wo"===e?r.ordinalNumber(a,{unit:"week"}):E(a,e.length)},I:function(t,e,r){var n=I(t);return"Io"===e?r.ordinalNumber(n,{unit:"week"}):E(n,e.length)},d:function(t,e,r){return"do"===e?r.ordinalNumber(t.getUTCDate(),{unit:"date"}):O.d(t,e)},D:function(t,e,a){var i=function(t){r(1,arguments);var e=n(t),a=e.getTime();e.setUTCMonth(0,1),e.setUTCHours(0,0,0,0);var i=e.getTime(),o=a-i;return Math.floor(o/F)+1}(t);return"Do"===e?a.ordinalNumber(i,{unit:"dayOfYear"}):E(i,e.length)},E:function(t,e,r){var n=t.getUTCDay();switch(e){case"E":case"EE":case"EEE":return r.day(n,{width:"abbreviated",context:"formatting"});case"EEEEE":return r.day(n,{width:"narrow",context:"formatting"});case"EEEEEE":return r.day(n,{width:"short",context:"formatting"});default:return r.day(n,{width:"wide",context:"formatting"})}},e:function(t,e,r,n){var a=t.getUTCDay(),i=(a-n.weekStartsOn+8)%7||7;switch(e){case"e":return String(i);case"ee":return E(i,2);case"eo":return r.ordinalNumber(i,{unit:"day"});case"eee":return r.day(a,{width:"abbreviated",context:"formatting"});case"eeeee":return r.day(a,{width:"narrow",context:"formatting"});case"eeeeee":return r.day(a,{width:"short",context:"formatting"});default:return r.day(a,{width:"wide",context:"formatting"})}},c:function(t,e,r,n){var a=t.getUTCDay(),i=(a-n.weekStartsOn+8)%7||7;switch(e){case"c":return String(i);case"cc":return E(i,e.length);case"co":return r.ordinalNumber(i,{unit:"day"});case"ccc":return r.day(a,{width:"abbreviated",context:"standalone"});case"ccccc":return r.day(a,{width:"narrow",context:"standalone"});case"cccccc":return r.day(a,{width:"short",context:"standalone"});default:return r.day(a,{width:"wide",context:"standalone"})}},i:function(t,e,r){var n=t.getUTCDay(),a=0===n?7:n;switch(e){case"i":return String(a);case"ii":return E(a,e.length);case"io":return r.ordinalNumber(a,{unit:"day"});case"iii":return r.day(n,{width:"abbreviated",context:"formatting"});case"iiiii":return r.day(n,{width:"narrow",context:"formatting"});case"iiiiii":return r.day(n,{width:"short",context:"formatting"});default:return r.day(n,{width:"wide",context:"formatting"})}},a:function(t,e,r){var n=t.getUTCHours()/12>=1?"pm":"am";switch(e){case"a":case"aa":return r.dayPeriod(n,{width:"abbreviated",context:"formatting"});case"aaa":return r.dayPeriod(n,{width:"abbreviated",context:"formatting"}).toLowerCase();case"aaaaa":return r.dayPeriod(n,{width:"narrow",context:"formatting"});default:return r.dayPeriod(n,{width:"wide",context:"formatting"})}},b:function(t,e,r){var n,a=t.getUTCHours();switch(n=12===a?Z:0===a?A:a/12>=1?"pm":"am",e){case"b":case"bb":return r.dayPeriod(n,{width:"abbreviated",context:"formatting"});case"bbb":return r.dayPeriod(n,{width:"abbreviated",context:"formatting"}).toLowerCase();case"bbbbb":return r.dayPeriod(n,{width:"narrow",context:"formatting"});default:return r.dayPeriod(n,{width:"wide",context:"formatting"})}},B:function(t,e,r){var n,a=t.getUTCHours();switch(n=a>=17?_:a>=12?$:a>=4?K:J,e){case"B":case"BB":case"BBB":return r.dayPeriod(n,{width:"abbreviated",context:"formatting"});case"BBBBB":return r.dayPeriod(n,{width:"narrow",context:"formatting"});default:return r.dayPeriod(n,{width:"wide",context:"formatting"})}},h:function(t,e,r){if("ho"===e){var n=t.getUTCHours()%12;return 0===n&&(n=12),r.ordinalNumber(n,{unit:"hour"})}return O.h(t,e)},H:function(t,e,r){return"Ho"===e?r.ordinalNumber(t.getUTCHours(),{unit:"hour"}):O.H(t,e)},K:function(t,e,r){var n=t.getUTCHours()%12;return"Ko"===e?r.ordinalNumber(n,{unit:"hour"}):E(n,e.length)},k:function(t,e,r){var n=t.getUTCHours();return 0===n&&(n=24),"ko"===e?r.ordinalNumber(n,{unit:"hour"}):E(n,e.length)},m:function(t,e,r){return"mo"===e?r.ordinalNumber(t.getUTCMinutes(),{unit:"minute"}):O.m(t,e)},s:function(t,e,r){return"so"===e?r.ordinalNumber(t.getUTCSeconds(),{unit:"second"}):O.s(t,e)},S:function(t,e){return O.S(t,e)},X:function(t,e,r,n){var a=(n._originalDate||t).getTimezoneOffset();if(0===a)return"Z";switch(e){case"X":return et(a);case"XXXX":case"XX":return rt(a);default:return rt(a,":")}},x:function(t,e,r,n){var a=(n._originalDate||t).getTimezoneOffset();switch(e){case"x":return et(a);case"xxxx":case"xx":return rt(a);default:return rt(a,":")}},O:function(t,e,r,n){var a=(n._originalDate||t).getTimezoneOffset();switch(e){case"O":case"OO":case"OOO":return"GMT"+tt(a,":");default:return"GMT"+rt(a,":")}},z:function(t,e,r,n){var a=(n._originalDate||t).getTimezoneOffset();switch(e){case"z":case"zz":case"zzz":return"GMT"+tt(a,":");default:return"GMT"+rt(a,":")}},t:function(t,e,r,n){var a=n._originalDate||t;return E(Math.floor(a.getTime()/1e3),e.length)},T:function(t,e,r,n){return E((n._originalDate||t).getTime(),e.length)}};function tt(t,e){var r=t>0?"-":"+",n=Math.abs(t),a=Math.floor(n/60),i=n%60;if(0===i)return r+String(a);var o=e||"";return r+String(a)+o+E(i,2)}function et(t,e){return t%60==0?(t>0?"-":"+")+E(Math.abs(t)/60,2):rt(t,e)}function rt(t,e){var r=e||"",n=t>0?"-":"+",a=Math.abs(t);return n+E(Math.floor(a/60),2)+r+E(a%60,2)}var nt=V;function at(t,e){switch(t){case"P":return e.date({width:"short"});case"PP":return e.date({width:"medium"});case"PPP":return e.date({width:"long"});default:return e.date({width:"full"})}}function it(t,e){switch(t){case"p":return e.time({width:"short"});case"pp":return e.time({width:"medium"});case"ppp":return e.time({width:"long"});default:return e.time({width:"full"})}}var ot={p:it,P:function(t,e){var r,n=t.match(/(P+)(p+)?/),a=n[1],i=n[2];if(!i)return at(t,e);switch(a){case"P":r=e.dateTime({width:"short"});break;case"PP":r=e.dateTime({width:"medium"});break;case"PPP":r=e.dateTime({width:"long"});break;default:r=e.dateTime({width:"full"})}return r.replace("{{date}}",at(a,e)).replace("{{time}}",it(i,e))}},ut=ot,st=["D","DD"],ct=["YY","YYYY"];function dt(t){return-1!==st.indexOf(t)}function lt(t){return-1!==ct.indexOf(t)}function ft(t,e,r){if("YYYY"===t)throw new RangeError("Use `yyyy` instead of `YYYY` (in `".concat(e,"`) for formatting years to the input `").concat(r,"`; see: https://git.io/fxCyr"));if("YY"===t)throw new RangeError("Use `yy` instead of `YY` (in `".concat(e,"`) for formatting years to the input `").concat(r,"`; see: https://git.io/fxCyr"));if("D"===t)throw new RangeError("Use `d` instead of `D` (in `".concat(e,"`) for formatting days of the month to the input `").concat(r,"`; see: https://git.io/fxCyr"));if("DD"===t)throw new RangeError("Use `dd` instead of `DD` (in `".concat(e,"`) for formatting days of the month to the input `").concat(r,"`; see: https://git.io/fxCyr"))}var ht=/[yYQqMLwIdDecihHKkms]o|(\w)\1*|''|'(''|[^'])+('|$)|./g,mt=/P+p+|P+|p+|''|'(''|[^'])+('|$)|./g,wt=/^'([^]*?)'?$/,gt=/''/g,vt=/[a-zA-Z]/;function yt(t){return t.match(wt)[1].replace(gt,"'")}function bt(t,e){if(null==t)throw new TypeError("assign requires that input parameter not be null or undefined");for(var r in e=e||{})e.hasOwnProperty(r)&&(t[r]=e[r]);return t}function Tt(t,a,i){r(2,arguments);var o=i||{},u=o.locale,s=u&&u.options&&u.options.weekStartsOn,c=null==s?0:e(s),d=null==o.weekStartsOn?c:e(o.weekStartsOn);if(!(d>=0&&d<=6))throw new RangeError("weekStartsOn must be between 0 and 6 inclusively");var l=n(t),f=e(a),h=l.getUTCDay(),m=f%7,w=(m+7)%7,g=(w0,a=n?e:1-e;if(a<=50)r=t||100;else{var i=a+50;r=t+100*Math.floor(i/100)-(t>=i%100?100:0)}return n?r:1-r}var Jt=[31,28,31,30,31,30,31,31,30,31,30,31],Vt=[31,29,31,30,31,30,31,31,30,31,30,31];function te(t){return t%400==0||t%4==0&&t%100!=0}var ee={G:{priority:140,parse:function(t,e,r,n){switch(e){case"G":case"GG":case"GGG":return r.era(t,{width:"abbreviated"})||r.era(t,{width:"narrow"});case"GGGGG":return r.era(t,{width:"narrow"});default:return r.era(t,{width:"wide"})||r.era(t,{width:"abbreviated"})||r.era(t,{width:"narrow"})}},set:function(t,e,r,n){return e.era=r,t.setUTCFullYear(r,0,1),t.setUTCHours(0,0,0,0),t},incompatibleTokens:["R","u","t","T"]},y:{priority:130,parse:function(t,e,r,n){var a=function(t){return{year:t,isTwoDigitYear:"yy"===e}};switch(e){case"y":return Zt(4,t,a);case"yo":return r.ordinalNumber(t,{unit:"year",valueCallback:a});default:return Zt(e.length,t,a)}},validate:function(t,e,r){return e.isTwoDigitYear||e.year>0},set:function(t,e,r,n){var a=t.getUTCFullYear();if(r.isTwoDigitYear){var i=_t(r.year,a);return t.setUTCFullYear(i,0,1),t.setUTCHours(0,0,0,0),t}var o="era"in e&&1!==e.era?1-r.year:r.year;return t.setUTCFullYear(o,0,1),t.setUTCHours(0,0,0,0),t},incompatibleTokens:["Y","R","u","w","I","i","e","c","t","T"]},Y:{priority:130,parse:function(t,e,r,n){var a=function(t){return{year:t,isTwoDigitYear:"YY"===e}};switch(e){case"Y":return Zt(4,t,a);case"Yo":return r.ordinalNumber(t,{unit:"year",valueCallback:a});default:return Zt(e.length,t,a)}},validate:function(t,e,r){return e.isTwoDigitYear||e.year>0},set:function(t,e,r,n){var a=X(t,n);if(r.isTwoDigitYear){var i=_t(r.year,a);return t.setUTCFullYear(i,0,n.firstWeekContainsDate),t.setUTCHours(0,0,0,0),G(t,n)}var o="era"in e&&1!==e.era?1-r.year:r.year;return t.setUTCFullYear(o,0,n.firstWeekContainsDate),t.setUTCHours(0,0,0,0),G(t,n)},incompatibleTokens:["y","R","u","Q","q","M","L","I","d","D","i","t","T"]},R:{priority:130,parse:function(t,e,r,n){return Kt("R"===e?4:e.length,t)},set:function(t,e,r,n){var a=new Date(0);return a.setUTCFullYear(r,0,4),a.setUTCHours(0,0,0,0),W(a)},incompatibleTokens:["G","y","Y","u","Q","q","M","L","w","d","D","e","c","t","T"]},u:{priority:130,parse:function(t,e,r,n){return Kt("u"===e?4:e.length,t)},set:function(t,e,r,n){return t.setUTCFullYear(r,0,1),t.setUTCHours(0,0,0,0),t},incompatibleTokens:["G","y","Y","R","w","I","i","e","c","t","T"]},Q:{priority:120,parse:function(t,e,r,n){switch(e){case"Q":case"QQ":return Zt(e.length,t);case"Qo":return r.ordinalNumber(t,{unit:"quarter"});case"QQQ":return r.quarter(t,{width:"abbreviated",context:"formatting"})||r.quarter(t,{width:"narrow",context:"formatting"});case"QQQQQ":return r.quarter(t,{width:"narrow",context:"formatting"});default:return r.quarter(t,{width:"wide",context:"formatting"})||r.quarter(t,{width:"abbreviated",context:"formatting"})||r.quarter(t,{width:"narrow",context:"formatting"})}},validate:function(t,e,r){return e>=1&&e<=4},set:function(t,e,r,n){return t.setUTCMonth(3*(r-1),1),t.setUTCHours(0,0,0,0),t},incompatibleTokens:["Y","R","q","M","L","w","I","d","D","i","e","c","t","T"]},q:{priority:120,parse:function(t,e,r,n){switch(e){case"q":case"qq":return Zt(e.length,t);case"qo":return r.ordinalNumber(t,{unit:"quarter"});case"qqq":return r.quarter(t,{width:"abbreviated",context:"standalone"})||r.quarter(t,{width:"narrow",context:"standalone"});case"qqqqq":return r.quarter(t,{width:"narrow",context:"standalone"});default:return r.quarter(t,{width:"wide",context:"standalone"})||r.quarter(t,{width:"abbreviated",context:"standalone"})||r.quarter(t,{width:"narrow",context:"standalone"})}},validate:function(t,e,r){return e>=1&&e<=4},set:function(t,e,r,n){return t.setUTCMonth(3*(r-1),1),t.setUTCHours(0,0,0,0),t},incompatibleTokens:["Y","R","Q","M","L","w","I","d","D","i","e","c","t","T"]},M:{priority:110,parse:function(t,e,r,n){var a=function(t){return t-1};switch(e){case"M":return Bt(pt,t,a);case"MM":return Zt(2,t,a);case"Mo":return r.ordinalNumber(t,{unit:"month",valueCallback:a});case"MMM":return r.month(t,{width:"abbreviated",context:"formatting"})||r.month(t,{width:"narrow",context:"formatting"});case"MMMMM":return r.month(t,{width:"narrow",context:"formatting"});default:return r.month(t,{width:"wide",context:"formatting"})||r.month(t,{width:"abbreviated",context:"formatting"})||r.month(t,{width:"narrow",context:"formatting"})}},validate:function(t,e,r){return e>=0&&e<=11},set:function(t,e,r,n){return t.setUTCMonth(r,1),t.setUTCHours(0,0,0,0),t},incompatibleTokens:["Y","R","q","Q","L","w","I","D","i","e","c","t","T"]},L:{priority:110,parse:function(t,e,r,n){var a=function(t){return t-1};switch(e){case"L":return Bt(pt,t,a);case"LL":return Zt(2,t,a);case"Lo":return r.ordinalNumber(t,{unit:"month",valueCallback:a});case"LLL":return r.month(t,{width:"abbreviated",context:"standalone"})||r.month(t,{width:"narrow",context:"standalone"});case"LLLLL":return r.month(t,{width:"narrow",context:"standalone"});default:return r.month(t,{width:"wide",context:"standalone"})||r.month(t,{width:"abbreviated",context:"standalone"})||r.month(t,{width:"narrow",context:"standalone"})}},validate:function(t,e,r){return e>=0&&e<=11},set:function(t,e,r,n){return t.setUTCMonth(r,1),t.setUTCHours(0,0,0,0),t},incompatibleTokens:["Y","R","q","Q","M","w","I","D","i","e","c","t","T"]},w:{priority:100,parse:function(t,e,r,n){switch(e){case"w":return Bt(Dt,t);case"wo":return r.ordinalNumber(t,{unit:"week"});default:return Zt(e.length,t)}},validate:function(t,e,r){return e>=1&&e<=53},set:function(t,a,i,o){return G(function(t,a,i){r(2,arguments);var o=n(t),u=e(a),s=z(o,i)-u;return o.setUTCDate(o.getUTCDate()-7*s),o}(t,i,o),o)},incompatibleTokens:["y","R","u","q","Q","M","L","I","d","D","i","t","T"]},I:{priority:100,parse:function(t,e,r,n){switch(e){case"I":return Bt(Dt,t);case"Io":return r.ordinalNumber(t,{unit:"week"});default:return Zt(e.length,t)}},validate:function(t,e,r){return e>=1&&e<=53},set:function(t,a,i,o){return W(function(t,a){r(2,arguments);var i=n(t),o=e(a),u=I(i)-o;return i.setUTCDate(i.getUTCDate()-7*u),i}(t,i,o),o)},incompatibleTokens:["y","Y","u","q","Q","M","L","w","d","D","e","c","t","T"]},d:{priority:90,subPriority:1,parse:function(t,e,r,n){switch(e){case"d":return Bt(Ct,t);case"do":return r.ordinalNumber(t,{unit:"date"});default:return Zt(e.length,t)}},validate:function(t,e,r){var n=te(t.getUTCFullYear()),a=t.getUTCMonth();return n?e>=1&&e<=Vt[a]:e>=1&&e<=Jt[a]},set:function(t,e,r,n){return t.setUTCDate(r),t.setUTCHours(0,0,0,0),t},incompatibleTokens:["Y","R","q","Q","w","I","D","i","e","c","t","T"]},D:{priority:90,subPriority:1,parse:function(t,e,r,n){switch(e){case"D":case"DD":return Bt(Mt,t);case"Do":return r.ordinalNumber(t,{unit:"date"});default:return Zt(e.length,t)}},validate:function(t,e,r){return te(t.getUTCFullYear())?e>=1&&e<=366:e>=1&&e<=365},set:function(t,e,r,n){return t.setUTCMonth(0,r),t.setUTCHours(0,0,0,0),t},incompatibleTokens:["Y","R","q","Q","M","L","w","I","d","E","i","e","c","t","T"]},E:{priority:90,parse:function(t,e,r,n){switch(e){case"E":case"EE":case"EEE":return r.day(t,{width:"abbreviated",context:"formatting"})||r.day(t,{width:"short",context:"formatting"})||r.day(t,{width:"narrow",context:"formatting"});case"EEEEE":return r.day(t,{width:"narrow",context:"formatting"});case"EEEEEE":return r.day(t,{width:"short",context:"formatting"})||r.day(t,{width:"narrow",context:"formatting"});default:return r.day(t,{width:"wide",context:"formatting"})||r.day(t,{width:"abbreviated",context:"formatting"})||r.day(t,{width:"short",context:"formatting"})||r.day(t,{width:"narrow",context:"formatting"})}},validate:function(t,e,r){return e>=0&&e<=6},set:function(t,e,r,n){return(t=Tt(t,r,n)).setUTCHours(0,0,0,0),t},incompatibleTokens:["D","i","e","c","t","T"]},e:{priority:90,parse:function(t,e,r,n){var a=function(t){var e=7*Math.floor((t-1)/7);return(t+n.weekStartsOn+6)%7+e};switch(e){case"e":case"ee":return Zt(e.length,t,a);case"eo":return r.ordinalNumber(t,{unit:"day",valueCallback:a});case"eee":return r.day(t,{width:"abbreviated",context:"formatting"})||r.day(t,{width:"short",context:"formatting"})||r.day(t,{width:"narrow",context:"formatting"});case"eeeee":return r.day(t,{width:"narrow",context:"formatting"});case"eeeeee":return r.day(t,{width:"short",context:"formatting"})||r.day(t,{width:"narrow",context:"formatting"});default:return r.day(t,{width:"wide",context:"formatting"})||r.day(t,{width:"abbreviated",context:"formatting"})||r.day(t,{width:"short",context:"formatting"})||r.day(t,{width:"narrow",context:"formatting"})}},validate:function(t,e,r){return e>=0&&e<=6},set:function(t,e,r,n){return(t=Tt(t,r,n)).setUTCHours(0,0,0,0),t},incompatibleTokens:["y","R","u","q","Q","M","L","I","d","D","E","i","c","t","T"]},c:{priority:90,parse:function(t,e,r,n){var a=function(t){var e=7*Math.floor((t-1)/7);return(t+n.weekStartsOn+6)%7+e};switch(e){case"c":case"cc":return Zt(e.length,t,a);case"co":return r.ordinalNumber(t,{unit:"day",valueCallback:a});case"ccc":return r.day(t,{width:"abbreviated",context:"standalone"})||r.day(t,{width:"short",context:"standalone"})||r.day(t,{width:"narrow",context:"standalone"});case"ccccc":return r.day(t,{width:"narrow",context:"standalone"});case"cccccc":return r.day(t,{width:"short",context:"standalone"})||r.day(t,{width:"narrow",context:"standalone"});default:return r.day(t,{width:"wide",context:"standalone"})||r.day(t,{width:"abbreviated",context:"standalone"})||r.day(t,{width:"short",context:"standalone"})||r.day(t,{width:"narrow",context:"standalone"})}},validate:function(t,e,r){return e>=0&&e<=6},set:function(t,e,r,n){return(t=Tt(t,r,n)).setUTCHours(0,0,0,0),t},incompatibleTokens:["y","R","u","q","Q","M","L","I","d","D","E","i","e","t","T"]},i:{priority:90,parse:function(t,e,r,n){var a=function(t){return 0===t?7:t};switch(e){case"i":case"ii":return Zt(e.length,t);case"io":return r.ordinalNumber(t,{unit:"day"});case"iii":return r.day(t,{width:"abbreviated",context:"formatting",valueCallback:a})||r.day(t,{width:"short",context:"formatting",valueCallback:a})||r.day(t,{width:"narrow",context:"formatting",valueCallback:a});case"iiiii":return r.day(t,{width:"narrow",context:"formatting",valueCallback:a});case"iiiiii":return r.day(t,{width:"short",context:"formatting",valueCallback:a})||r.day(t,{width:"narrow",context:"formatting",valueCallback:a});default:return r.day(t,{width:"wide",context:"formatting",valueCallback:a})||r.day(t,{width:"abbreviated",context:"formatting",valueCallback:a})||r.day(t,{width:"short",context:"formatting",valueCallback:a})||r.day(t,{width:"narrow",context:"formatting",valueCallback:a})}},validate:function(t,e,r){return e>=1&&e<=7},set:function(t,a,i,o){return t=function(t,a){r(2,arguments);var i=e(a);i%7==0&&(i-=7);var o=1,u=n(t),s=u.getUTCDay(),c=((i%7+7)%7=1&&e<=12},set:function(t,e,r,n){var a=t.getUTCHours()>=12;return a&&r<12?t.setUTCHours(r+12,0,0,0):a||12!==r?t.setUTCHours(r,0,0,0):t.setUTCHours(0,0,0,0),t},incompatibleTokens:["H","K","k","t","T"]},H:{priority:70,parse:function(t,e,r,n){switch(e){case"H":return Bt(xt,t);case"Ho":return r.ordinalNumber(t,{unit:"hour"});default:return Zt(e.length,t)}},validate:function(t,e,r){return e>=0&&e<=23},set:function(t,e,r,n){return t.setUTCHours(r,0,0,0),t},incompatibleTokens:["a","b","h","K","k","t","T"]},K:{priority:70,parse:function(t,e,r,n){switch(e){case"K":return Bt(Ut,t);case"Ko":return r.ordinalNumber(t,{unit:"hour"});default:return Zt(e.length,t)}},validate:function(t,e,r){return e>=0&&e<=11},set:function(t,e,r,n){return t.getUTCHours()>=12&&r<12?t.setUTCHours(r+12,0,0,0):t.setUTCHours(r,0,0,0),t},incompatibleTokens:["a","b","h","H","k","t","T"]},k:{priority:70,parse:function(t,e,r,n){switch(e){case"k":return Bt(kt,t);case"ko":return r.ordinalNumber(t,{unit:"hour"});default:return Zt(e.length,t)}},validate:function(t,e,r){return e>=1&&e<=24},set:function(t,e,r,n){var a=r<=24?r%24:r;return t.setUTCHours(a,0,0,0),t},incompatibleTokens:["a","b","h","H","K","t","T"]},m:{priority:60,parse:function(t,e,r,n){switch(e){case"m":return Bt(Nt,t);case"mo":return r.ordinalNumber(t,{unit:"minute"});default:return Zt(e.length,t)}},validate:function(t,e,r){return e>=0&&e<=59},set:function(t,e,r,n){return t.setUTCMinutes(r,0,0),t},incompatibleTokens:["t","T"]},s:{priority:50,parse:function(t,e,r,n){switch(e){case"s":return Bt(St,t);case"so":return r.ordinalNumber(t,{unit:"second"});default:return Zt(e.length,t)}},validate:function(t,e,r){return e>=0&&e<=59},set:function(t,e,r,n){return t.setUTCSeconds(r,0),t},incompatibleTokens:["t","T"]},S:{priority:30,parse:function(t,e,r,n){return Zt(e.length,t,(function(t){return Math.floor(t*Math.pow(10,3-e.length))}))},set:function(t,e,r,n){return t.setUTCMilliseconds(r),t},incompatibleTokens:["t","T"]},X:{priority:10,parse:function(t,e,r,n){switch(e){case"X":return zt(Rt,t);case"XX":return zt(It,t);case"XXXX":return zt(Gt,t);case"XXXXX":return zt(jt,t);default:return zt(Xt,t)}},set:function(t,e,r,n){return e.timestampIsSet?t:new Date(t.getTime()-r)},incompatibleTokens:["t","T","x"]},x:{priority:10,parse:function(t,e,r,n){switch(e){case"x":return zt(Rt,t);case"xx":return zt(It,t);case"xxxx":return zt(Gt,t);case"xxxxx":return zt(jt,t);default:return zt(Xt,t)}},set:function(t,e,r,n){return e.timestampIsSet?t:new Date(t.getTime()-r)},incompatibleTokens:["t","T","X"]},t:{priority:40,parse:function(t,e,r,n){return At(t)},set:function(t,e,r,n){return[new Date(1e3*r),{timestampIsSet:!0}]},incompatibleTokens:"*"},T:{priority:20,parse:function(t,e,r,n){return At(t)},set:function(t,e,r,n){return[new Date(r),{timestampIsSet:!0}]},incompatibleTokens:"*"}},re=ee,ne=/[yYQqMLwIdDecihHKkms]o|(\w)\1*|''|'(''|[^'])+('|$)|./g,ae=/P+p+|P+|p+|''|'(''|[^'])+('|$)|./g,ie=/^'([^]*?)'?$/,oe=/''/g,ue=/\S/,se=/[a-zA-Z]/;function ce(t,e){if(e.timestampIsSet)return t;var r=new Date(0);return r.setFullYear(t.getUTCFullYear(),t.getUTCMonth(),t.getUTCDate()),r.setHours(t.getUTCHours(),t.getUTCMinutes(),t.getUTCSeconds(),t.getUTCMilliseconds()),r}function de(t){return t.match(ie)[1].replace(oe,"'")}var le=36e5,fe={dateTimeDelimiter:/[T ]/,timeZoneDelimiter:/[Z ]/i,timezone:/([Z+-].*)$/},he=/^-?(?:(\d{3})|(\d{2})(?:-?(\d{2}))?|W(\d{2})(?:-?(\d{1}))?|)$/,me=/^(\d{2}(?:[.,]\d*)?)(?::?(\d{2}(?:[.,]\d*)?))?(?::?(\d{2}(?:[.,]\d*)?))?$/,we=/^([+-])(\d{2})(?::?(\d{2}))?$/;function ge(t){var e,r={},n=t.split(fe.dateTimeDelimiter);if(n.length>2)return r;if(/:/.test(n[0])?(r.date=null,e=n[0]):(r.date=n[0],e=n[1],fe.timeZoneDelimiter.test(r.date)&&(r.date=t.split(fe.timeZoneDelimiter)[0],e=t.substr(r.date.length,t.length))),e){var a=fe.timezone.exec(e);a?(r.time=e.replace(a[1],""),r.timezone=a[1]):r.time=e}return r}function ve(t,e){var r=new RegExp("^(?:(\\d{4}|[+-]\\d{"+(4+e)+"})|(\\d{2}|[+-]\\d{"+(2+e)+"})$)"),n=t.match(r);if(!n)return{year:null};var a=n[1]&&parseInt(n[1]),i=n[2]&&parseInt(n[2]);return{year:null==i?a:100*i,restDateString:t.slice((n[1]||n[2]).length)}}function ye(t,e){if(null===e)return null;var r=t.match(he);if(!r)return null;var n=!!r[4],a=be(r[1]),i=be(r[2])-1,o=be(r[3]),u=be(r[4]),s=be(r[5])-1;if(n)return function(t,e,r){return e>=1&&e<=53&&r>=0&&r<=6}(0,u,s)?function(t,e,r){var n=new Date(0);n.setUTCFullYear(t,0,4);var a=n.getUTCDay()||7,i=7*(e-1)+r+1-a;return n.setUTCDate(n.getUTCDate()+i),n}(e,u,s):new Date(NaN);var c=new Date(0);return function(t,e,r){return e>=0&&e<=11&&r>=1&&r<=(Me[e]||(De(t)?29:28))}(e,i,o)&&function(t,e){return e>=1&&e<=(De(t)?366:365)}(e,a)?(c.setUTCFullYear(e,i,Math.max(a,o)),c):new Date(NaN)}function be(t){return t?parseInt(t):1}function Te(t){var e=t.match(me);if(!e)return null;var r=pe(e[1]),n=pe(e[2]),a=pe(e[3]);return function(t,e,r){if(24===t)return 0===e&&0===r;return r>=0&&r<60&&e>=0&&e<60&&t>=0&&t<25}(r,n,a)?r*le+6e4*n+1e3*a:NaN}function pe(t){return t&&parseFloat(t.replace(",","."))||0}function Ce(t){if("Z"===t)return 0;var e=t.match(we);if(!e)return 0;var r="+"===e[1]?-1:1,n=parseInt(e[2]),a=e[3]&&parseInt(e[3])||0;return function(t,e){return e>=0&&e<=59}(0,a)?r*(n*le+6e4*a):NaN}var Me=[31,null,31,30,31,30,31,31,30,31,30,31];function De(t){return t%400==0||t%4==0&&t%100}const xe={datetime:"MMM d, yyyy, h:mm:ss aaaa",millisecond:"h:mm:ss.SSS aaaa",second:"h:mm:ss aaaa",minute:"h:mm aaaa",hour:"ha",day:"MMM d",week:"PP",month:"MMM yyyy",quarter:"qqq - yyyy",year:"yyyy"};t._adapters._date.override({_id:"date-fns",formats:function(){return xe},parse:function(t,a){if(null==t)return null;const i=typeof t;return"number"===i||t instanceof Date?t=n(t):"string"===i&&(t="string"==typeof a?function(t,a,i,o){r(3,arguments);var u=String(t),s=String(a),d=o||{},l=d.locale||q;if(!l.match)throw new RangeError("locale must contain match property");var f=l.options&&l.options.firstWeekContainsDate,h=null==f?1:e(f),m=null==d.firstWeekContainsDate?h:e(d.firstWeekContainsDate);if(!(m>=1&&m<=7))throw new RangeError("firstWeekContainsDate must be between 1 and 7 inclusively");var w=l.options&&l.options.weekStartsOn,g=null==w?0:e(w),v=null==d.weekStartsOn?g:e(d.weekStartsOn);if(!(v>=0&&v<=6))throw new RangeError("weekStartsOn must be between 0 and 6 inclusively");if(""===s)return""===u?n(i):new Date(NaN);var y,b={firstWeekContainsDate:m,weekStartsOn:v,locale:l},T=[{priority:10,subPriority:-1,set:ce,index:0}],p=s.match(ae).map((function(t){var e=t[0];return"p"===e||"P"===e?(0,ut[e])(t,l.formatLong,b):t})).join("").match(ne),C=[];for(y=0;y0&&ue.test(u))return new Date(NaN);var P=T.map((function(t){return t.priority})).sort((function(t,e){return e-t})).filter((function(t,e,r){return r.indexOf(t)===e})).map((function(t){return T.filter((function(e){return e.priority===t})).sort((function(t,e){return e.subPriority-t.subPriority}))})).map((function(t){return t[0]})),E=n(i);if(isNaN(E))return new Date(NaN);var O=H(E,c(E)),F={};for(y=0;y=1&&f<=7))throw new RangeError("firstWeekContainsDate must be between 1 and 7 inclusively");var h=s.options&&s.options.weekStartsOn,w=null==h?0:e(h),g=null==u.weekStartsOn?w:e(u.weekStartsOn);if(!(g>=0&&g<=6))throw new RangeError("weekStartsOn must be between 0 and 6 inclusively");if(!s.localize)throw new RangeError("locale must contain localize property");if(!s.formatLong)throw new RangeError("locale must contain formatLong property");var v=n(t);if(!m(v))throw new RangeError("Invalid time value");var y=c(v),b=H(v,y),T={firstWeekContainsDate:f,weekStartsOn:g,locale:s,_originalDate:v},p=o.match(mt).map((function(t){var e=t[0];return"p"===e||"P"===e?(0,ut[e])(t,s.formatLong,T):t})).join("").match(ht).map((function(e){if("''"===e)return"'";var r=e[0];if("'"===r)return yt(e);var n=nt[r];if(n)return!u.useAdditionalWeekYearTokens&<(e)&&ft(e,a,t),!u.useAdditionalDayOfYearTokens&&dt(e)&&ft(e,a,t),n(b,e,s.localize,T);if(r.match(vt))throw new RangeError("Format string contains an unescaped latin alphabet character `"+r+"`");return e})).join("");return p}(t,a,this.options)},add:function(t,n,s){switch(s){case"millisecond":return o(t,n);case"second":return function(t,n){r(2,arguments);var a=e(n);return o(t,1e3*a)}(t,n);case"minute":return function(t,n){r(2,arguments);var a=e(n);return o(t,6e4*a)}(t,n);case"hour":return function(t,n){r(2,arguments);var a=e(n);return o(t,a*u)}(t,n);case"day":return a(t,n);case"week":return function(t,n){r(2,arguments);var i=e(n),o=7*i;return a(t,o)}(t,n);case"month":return i(t,n);case"quarter":return function(t,n){r(2,arguments);var a=e(n),o=3*a;return i(t,o)}(t,n);case"year":return function(t,n){r(2,arguments);var a=e(n);return i(t,12*a)}(t,n);default:return t}},diff:function(t,e,a){switch(a){case"millisecond":return b(t,e);case"second":return function(t,e){r(2,arguments);var n=b(t,e)/1e3;return n>0?Math.floor(n):Math.ceil(n)}(t,e);case"minute":return function(t,e){r(2,arguments);var n=b(t,e)/6e4;return n>0?Math.floor(n):Math.ceil(n)}(t,e);case"hour":return function(t,e){r(2,arguments);var n=b(t,e)/T;return n>0?Math.floor(n):Math.ceil(n)}(t,e);case"day":return y(t,e);case"week":return function(t,e){r(2,arguments);var n=y(t,e)/7;return n>0?Math.floor(n):Math.ceil(n)}(t,e);case"month":return D(t,e);case"quarter":return function(t,e){r(2,arguments);var n=D(t,e)/3;return n>0?Math.floor(n):Math.ceil(n)}(t,e);case"year":return function(t,e){r(2,arguments);var a=n(t),i=n(e),o=h(a,i),u=Math.abs(g(a,i));a.setFullYear("1584"),i.setFullYear("1584");var s=h(a,i)===-o,c=o*(u-s);return 0===c?0:c}(t,e);default:return 0}},startOf:function(t,e,a){switch(e){case"second":return function(t){r(1,arguments);var e=n(t);return e.setMilliseconds(0),e}(t);case"minute":return function(t){r(1,arguments);var e=n(t);return e.setSeconds(0,0),e}(t);case"hour":return function(t){r(1,arguments);var e=n(t);return e.setMinutes(0,0,0),e}(t);case"day":return d(t);case"week":return s(t);case"isoWeek":return s(t,{weekStartsOn:+a});case"month":return function(t){r(1,arguments);var e=n(t);return e.setDate(1),e.setHours(0,0,0,0),e}(t);case"quarter":return function(t){r(1,arguments);var e=n(t),a=e.getMonth(),i=a-a%3;return e.setMonth(i,1),e.setHours(0,0,0,0),e}(t);case"year":return function(t){r(1,arguments);var e=n(t),a=new Date(0);return a.setFullYear(e.getFullYear(),0,1),a.setHours(0,0,0,0),a}(t);default:return t}},endOf:function(t,a){switch(a){case"second":return function(t){r(1,arguments);var e=n(t);return e.setMilliseconds(999),e}(t);case"minute":return function(t){r(1,arguments);var e=n(t);return e.setSeconds(59,999),e}(t);case"hour":return function(t){r(1,arguments);var e=n(t);return e.setMinutes(59,59,999),e}(t);case"day":return p(t);case"week":return function(t,a){r(1,arguments);var i=a||{},o=i.locale,u=o&&o.options&&o.options.weekStartsOn,s=null==u?0:e(u),c=null==i.weekStartsOn?s:e(i.weekStartsOn);if(!(c>=0&&c<=6))throw new RangeError("weekStartsOn must be between 0 and 6 inclusively");var d=n(t),l=d.getDay(),f=6+(l/g, ">") + .replace(/"/g, """) + .replace(/'/g, "'"); +} + +function formatTimeDifference(timestamp) { + const now = Math.floor(Date.now() / 1000); + const diff = Math.abs(now - timestamp); + + if (diff < 60) return `${diff} seconds`; + if (diff < 3600) return `${Math.floor(diff / 60)} minutes`; + if (diff < 86400) return `${Math.floor(diff / 3600)} hours`; + if (diff < 2592000) return `${Math.floor(diff / 86400)} days`; + if (diff < 31536000) return `${Math.floor(diff / 2592000)} months`; + return `${Math.floor(diff / 31536000)} years`; +} + +function formatTimeAgo(timestamp) { + const timeDiff = formatTimeDifference(timestamp); + return `${timeDiff} ago`; +} + +function formatTimeLeft(timestamp) { + const now = Math.floor(Date.now() / 1000); + if (timestamp <= now) return "Expired"; + return formatTimeDifference(timestamp); +} + +function formatTimestamp(timestamp, withAgo = true, isExpired = false) { + console.log("Incoming timestamp:", timestamp, typeof timestamp); + + if (typeof timestamp === 'string' && isNaN(Date.parse(timestamp))) { + return timestamp; + } + + if (!timestamp || isNaN(timestamp)) { + console.log("Returning N/A due to invalid input"); + return "N/A"; + } + + try { + const date = new Date(typeof timestamp === 'number' ? timestamp * 1000 : timestamp); + console.log("Parsed date:", date); + + if (isNaN(date.getTime())) { + console.log("Invalid date after parsing"); + return "N/A"; + } + + const now = new Date(); + const diffTime = Math.abs(now - date); + const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24)); + + if (isExpired) { + if (date > now) { + const hours = Math.floor(diffTime / (1000 * 60 * 60)); + const minutes = Math.floor((diffTime % (1000 * 60 * 60)) / (1000 * 60)); + return `${hours}h ${minutes}min`; + } else { + return "Expired"; + } + } + + if (diffDays <= 1) { + const hours = date.getHours().toString().padStart(2, '0'); + const minutes = date.getMinutes().toString().padStart(2, '0'); + return `${hours}:${minutes}${withAgo ? ' ago' : ''}`; + } else if (diffDays <= 7) { + const options = { weekday: 'short' }; + return date.toLocaleDateString(undefined, options); + } else { + const options = { month: 'short', day: 'numeric' }; + return date.toLocaleDateString(undefined, options); + } + } catch (error) { + console.error("Error formatting timestamp:", error); + return "N/A"; + } +} + +function normalizeCoinName(name) { + return name.toLowerCase().replace(/\s+/g, ' ').trim(); +} + +function getCoinSymbol(fullName) { + const symbolMap = { + 'Bitcoin': 'BTC', 'Litecoin': 'LTC', 'Monero': 'XMR', + 'Particl': 'PART', 'Particl Blind': 'PART', 'Particl Anon': 'PART', + 'PIVX': 'PIVX', 'Firo': 'FIRO', 'Dash': 'DASH', + 'Decred': 'DCR', 'Wownero': 'WOW', 'Bitcoin Cash': 'BCH' + }; + return symbolMap[fullName] || fullName; +} + +function formatSmallNumber(num) { + if (Math.abs(num) < 0.000001) { + return num.toExponential(8); + } else if (Math.abs(num) < 0.01) { + return num.toFixed(8); + } else { + return num.toFixed(4); + } +} + +// Table Rate +window.tableRateModule = { + coinNameToSymbol: { + 'Bitcoin': 'BTC', + 'Particl': 'PART', + 'Particl Blind': 'PART', + 'Particl Anon': 'PART', + 'Monero': 'XMR', + 'Wownero': 'WOW', + 'Litecoin': 'LTC', + 'Firo': 'FIRO', + 'Dash': 'DASH', + 'PIVX': 'PIVX', + 'Decred': 'DCR', + 'Zano': 'ZANO', + 'Bitcoin Cash': 'BCH' + }, + + cache: {}, + processedOffers: new Set(), + + getCachedValue(key) { + const cachedItem = localStorage.getItem(key); + if (cachedItem) { + const parsedItem = JSON.parse(cachedItem); + if (Date.now() < parsedItem.expiry) { + return parsedItem.value; + } else { + localStorage.removeItem(key); + } + } + return null; + }, + + setCachedValue(key, value, ttl = 900000) { + const item = { + value: value, + expiry: Date.now() + ttl, + }; + localStorage.setItem(key, JSON.stringify(item)); + }, + + setFallbackValue(coinSymbol, value) { + this.setCachedValue(`fallback_${coinSymbol}_usd`, value, 24 * 60 * 60 * 1000); + }, + + isNewOffer(offerId) { + if (this.processedOffers.has(offerId)) { + return false; + } + this.processedOffers.add(offerId); + return true; + }, + + formatUSD(value) { + if (Math.abs(value) < 0.000001) { + return value.toExponential(8) + ' USD'; + } else if (Math.abs(value) < 0.01) { + return value.toFixed(8) + ' USD'; + } else { + return value.toFixed(2) + ' USD'; + } + }, + + formatNumber(value, decimals) { + if (Math.abs(value) < 0.000001) { + return value.toExponential(decimals); + } else if (Math.abs(value) < 0.01) { + return value.toFixed(decimals); + } else { + return value.toFixed(Math.min(2, decimals)); + } + }, + + async updateUsdValue(cryptoCell, coinFullNameOrSymbol, isRate = false) { + console.log('updateUsdValue called with:', { coinFullNameOrSymbol, isRate }); + + if (!coinFullNameOrSymbol) { + console.error('No coin name or symbol provided'); + return; + } + + let coinSymbol = this.coinNameToSymbol[coinFullNameOrSymbol] || coinFullNameOrSymbol; + console.log('Resolved coin symbol:', coinSymbol); + + const cryptoValue = parseFloat(cryptoCell.textContent); + console.log('Crypto value:', cryptoValue); + + if (isNaN(cryptoValue) || cryptoValue <= 0) { + console.error('Invalid or non-positive crypto value'); + return; + } + + const usdCell = cryptoCell.closest('td').querySelector('.usd-value'); + if (!usdCell) { + console.error("USD cell not found."); + return; + } + + const o16Value = usdCell.getAttribute('data-o16') || 'N/A'; + console.log('o16 value:', o16Value); + + const isWownero = coinSymbol.toUpperCase() === 'WOW'; + + try { + const [fromRate, toRate] = await Promise.all([ + this.getExchangeRate(coinSymbol), + this.getExchangeRate(o16Value) + ]); + console.log(`Exchange rates - ${coinSymbol}: ${fromRate}, ${o16Value}: ${toRate}`); + + let usdValue = null; + let exchangeRate = null; + + if (fromRate !== null && fromRate > 0) { + usdValue = cryptoValue * fromRate; + console.log(`Calculated USD value for ${coinSymbol}:`, usdValue); + + this.setFallbackValue(coinSymbol, fromRate); + } + + if (usdValue === null) { + const fallbackValue = this.getFallbackValue(coinSymbol); + if (fallbackValue !== null) { + usdValue = cryptoValue * fallbackValue; + console.log(`Using fallback value for ${coinSymbol} USD:`, fallbackValue); + } + } + + if (fromRate !== null && toRate !== null && fromRate > 0 && toRate > 0) { + exchangeRate = fromRate / toRate; + console.log(`Calculated exchange rate ${coinSymbol}/${o16Value}:`, exchangeRate); + } + + if (usdValue !== null) { + usdCell.textContent = `${this.formatUSD(usdValue)}/${o16Value}`; + usdCell.removeAttribute('data-is-fallback'); + } else { + usdCell.textContent = `N/A/${o16Value}`; + usdCell.setAttribute('data-is-fallback', 'true'); + console.warn(`No valid price available for ${coinSymbol} USD`); + } + + const rateKey = `rate_${coinSymbol}_${o16Value}`; + let cachedRate = this.getCachedValue(rateKey); + if (cachedRate === null && exchangeRate !== null) { + cachedRate = exchangeRate; + this.setCachedValue(rateKey, cachedRate); + } else if (cachedRate === null && usdValue !== null && toRate !== null && toRate > 0) { + cachedRate = usdValue / (cryptoValue * toRate); + this.setCachedValue(rateKey, cachedRate); + } + + const marketPercentageKey = `market_percentage_${coinSymbol}_${o16Value}`; + let cachedMarketPercentage = this.getCachedValue(marketPercentageKey); + if (cachedMarketPercentage === null && exchangeRate !== null) { + const marketRate = await this.getExchangeRate(o16Value); + if (marketRate !== null && marketRate > 0) { + cachedMarketPercentage = ((exchangeRate - marketRate) / marketRate) * 100; + this.setCachedValue(marketPercentageKey, cachedMarketPercentage); + } else { + console.warn(`Invalid market rate for ${o16Value}, unable to calculate market percentage`); + } + } + + const rateCell = cryptoCell.closest('tr').querySelector('.coinname-value[data-coinname]'); + if (rateCell && cachedRate !== null) { + rateCell.textContent = this.formatNumber(cachedRate, 8); + const cachedRateElement = rateCell.closest('td').querySelector('.cached-rate'); + if (cachedRateElement) { + cachedRateElement.textContent = cachedRate; + } + } + + if (usdValue !== null || isWownero) { + const row = cryptoCell.closest('tr'); + if (row) { + this.updateProfitLoss(row, cachedMarketPercentage); + this.updateProfitValue(row); + } else { + console.error("Row not found for updating profit/loss and value."); + } + } + } catch (error) { + console.error(`Error in updateUsdValue for ${coinSymbol}:`, error); + + const fallbackValue = this.getFallbackValue(coinSymbol); + if (fallbackValue !== null) { + const usdValue = cryptoValue * fallbackValue; + usdCell.textContent = `${this.formatUSD(usdValue)}/${o16Value}`; + usdCell.setAttribute('data-is-fallback', 'true'); + console.warn(`Using fallback value for ${coinSymbol} due to error:`, fallbackValue); + + const row = cryptoCell.closest('tr'); + if (row) { + this.updateProfitLoss(row, null); + this.updateProfitValue(row); + } + } else { + usdCell.textContent = `N/A/${o16Value}`; + usdCell.setAttribute('data-is-fallback', 'true'); + console.warn(`No valid fallback price for ${coinSymbol}. Using N/A.`); + } + } + }, + + setFallbackValue(coinSymbol, value) { + localStorage.setItem(`fallback_${coinSymbol}_usd`, value.toString()); + }, + + getFallbackValue(coinSymbol) { + const value = localStorage.getItem(`fallback_${coinSymbol}_usd`); + return value ? parseFloat(value) : null; + }, + + async getExchangeRate(coinSymbol) { + console.log(`Fetching exchange rate for ${coinSymbol}`); + const cacheKey = `coinData_${coinSymbol}`; + let cachedData = cache.get(cacheKey); + let data; + + if (cachedData) { + console.log(`Using cached data for ${coinSymbol}`); + data = cachedData.value; + } else { + console.log(`Fetching fresh data for ${coinSymbol}`); + + const coin = config.coins.find(c => c.symbol.toLowerCase() === coinSymbol.toLowerCase()); + + if (!coin) { + return null; + } + + if (coin.usesCoinGecko) { + data = await api.fetchCoinGeckoDataXHR(coinSymbol); + } else if (coin.usesCryptoCompare) { + data = await api.fetchCryptoCompareDataXHR(coinSymbol); + } else { + console.error(`No API source configured for ${coinSymbol}`); + return null; + } + + cache.set(cacheKey, data); + } + + console.log(`Data received for ${coinSymbol}:`, data); + return this.extractExchangeRate(data, coinSymbol); + }, + + extractExchangeRate(data, coinSymbol) { + console.log(`Extracting exchange rate for ${coinSymbol}`); + const coin = config.coins.find(c => c.symbol === coinSymbol); + if (!coin) { + console.error(`Configuration not found for coin: ${coinSymbol}`); + return null; + } + + if (data.error) { + console.error(`Error in data for ${coinSymbol}:`, data.error); + return null; + } + + let rate; + if (coin.usesCoinGecko) { + if (!data.market_data || !data.market_data.current_price || !data.market_data.current_price.usd) { + console.error(`Invalid CoinGecko data structure for ${coinSymbol}:`, data); + return null; + } + rate = data.market_data.current_price.usd; + } else { + if (!data.RAW || !data.RAW[coinSymbol] || !data.RAW[coinSymbol].USD || typeof data.RAW[coinSymbol].USD.PRICE !== 'number') { + console.error(`Invalid CryptoCompare data structure for ${coinSymbol}:`, data); + return null; + } + rate = data.RAW[coinSymbol].USD.PRICE; + } + + if (rate <= 0) { + console.error(`Invalid rate for ${coinSymbol}: ${rate}`); + return null; + } + + return rate; + }, + + updateProfitLoss(row, cachedMarketPercentage = null) { + const usdCells = row.querySelectorAll('.usd-value'); + if (usdCells.length < 2) { + console.error("Not enough USD value cells found."); + return; + } + const [buyingUSDString, sellingUSDString] = Array.from(usdCells).map(cell => cell.textContent.split('/')[0].trim()); + const buyingUSD = buyingUSDString === 'N/A' ? NaN : parseFloat(buyingUSDString); + const sellingUSD = sellingUSDString === 'N/A' ? NaN : parseFloat(sellingUSDString); + + console.log('ProfitLoss calculation inputs:', { buyingUSD, sellingUSD }); + + const profitLossCell = row.querySelector('.profit-loss'); + if (!profitLossCell) { + console.error("Profit/loss cell not found."); + return; + } + + if ((!isNaN(sellingUSD) && !isNaN(buyingUSD) && buyingUSD > 0) || cachedMarketPercentage !== null) { + let profitLossPercentage; + if (cachedMarketPercentage !== null) { + profitLossPercentage = cachedMarketPercentage; + } else { + profitLossPercentage = ((sellingUSD - buyingUSD) / buyingUSD) * 100; + } + console.log('Calculated profit/loss percentage:', profitLossPercentage); + + let formattedPercentage; + if (Math.abs(profitLossPercentage) < 0.000001) { + formattedPercentage = profitLossPercentage.toExponential(6); + } else if (Math.abs(profitLossPercentage) < 0.01) { + formattedPercentage = profitLossPercentage.toFixed(6); + } else { + formattedPercentage = profitLossPercentage.toFixed(2); + } + + profitLossCell.textContent = `${profitLossPercentage >= 0 ? '+' : ''}${formattedPercentage}%`; + profitLossCell.className = 'profit-loss ' + (profitLossPercentage > 0 ? 'text-green-500' : + profitLossPercentage < 0 ? 'text-red-500' : 'text-yellow-500'); + + const cachedMarketPercentageElement = profitLossCell.closest('td').querySelector('.cached-market-percentage'); + if (cachedMarketPercentageElement) { + cachedMarketPercentageElement.textContent = profitLossPercentage; + } + } else { + profitLossCell.textContent = 'N/A'; + profitLossCell.className = 'profit-loss text-yellow-500'; + } + }, + + updateProfitValue(row) { + const usdCells = row.querySelectorAll('.usd-value'); + if (usdCells.length < 2) { + console.error("Not enough USD value cells found."); + return; + } + const [buyingUSDString, sellingUSDString] = Array.from(usdCells).map(cell => cell.textContent.split('/')[0].trim()); + const buyingUSD = parseFloat(buyingUSDString); + const sellingUSD = parseFloat(sellingUSDString); + + const profitValueCell = row.querySelector('.profit-value'); + if (!profitValueCell) { + console.error("Profit value cell not found."); + return; + } + + if (!isNaN(sellingUSD) && !isNaN(buyingUSD)) { + const profitValue = sellingUSD - buyingUSD; + profitValueCell.textContent = this.formatUSD(profitValue); + profitValueCell.classList.remove('hidden'); + } else { + profitValueCell.textContent = 'N/A'; + profitValueCell.classList.remove('hidden'); + } + }, + + initializeTable() { + console.log('Initializing table'); + document.querySelectorAll('.coinname-value').forEach(coinNameValue => { + const coinFullNameOrSymbol = coinNameValue.getAttribute('data-coinname'); + console.log('Processing coin:', coinFullNameOrSymbol); + if (!coinFullNameOrSymbol || coinFullNameOrSymbol === 'Unknown') { + console.warn('Missing or unknown coin name/symbol in data-coinname attribute'); + return; + } + const isRate = coinNameValue.closest('td').querySelector('.ratetype') !== null; + coinNameValue.classList.remove('hidden'); + this.updateUsdValue(coinNameValue, coinFullNameOrSymbol, isRate); + }); + }, + + init() { + console.log('Initializing TableRateModule'); + this.initializeTable(); + } +}; + +// Main +function fetchOffers(manualRefresh = false) { + return new Promise((resolve, reject) => { + const endpoint = isSentOffers ? '/json/sentoffers' : '/json/offers'; + console.log(`Fetching offers from: ${endpoint}`); + + const newEntriesCountSpan = document.getElementById('newEntriesCount'); + if (newEntriesCountSpan) { + newEntriesCountSpan.textContent = 'Loading...'; + } + + if (manualRefresh) { + offersBody.innerHTML = 'Refreshing offers...'; + } + + setRefreshButtonLoading(true); + + fetch(endpoint) + .then(response => { + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + return response.json(); + }) + .then(data => { + console.log('Raw data received:', data.length, 'offers'); + + let newData = Array.isArray(data) ? data : Object.values(data); + console.log('Processed data length before filtering:', newData.length); + + newData = newData.map(offer => ({ + ...offer, + offer_id: String(offer.offer_id || ''), + swap_type: String(offer.swap_type || 'N/A'), + addr_from: String(offer.addr_from || ''), + coin_from: String(offer.coin_from || ''), + coin_to: String(offer.coin_to || ''), + amount_from: String(offer.amount_from || '0'), + amount_to: String(offer.amount_to || '0'), + rate: String(offer.rate || '0'), + created_at: Number(offer.created_at || 0), + expire_at: Number(offer.expire_at || 0), + is_own_offer: Boolean(offer.is_own_offer), + amount_negotiable: Boolean(offer.amount_negotiable) + })); + + if (!isSentOffers) { + const currentTime = Math.floor(Date.now() / 1000); + const beforeFilterCount = newData.length; + newData = newData.filter(offer => { + const keepOffer = !isOfferExpired(offer); + if (!keepOffer) { + console.log('Filtered out expired offer:', offer.offer_id); + } + return keepOffer; + }); + console.log(`Filtered out ${beforeFilterCount - newData.length} expired offers`); + } + + console.log('Processed data length after filtering:', newData.length); + + if (isInitialLoad || manualRefresh) { + console.log('Initial load or manual refresh - replacing all data'); + jsonData = newData; + originalJsonData = [...newData]; + isInitialLoad = false; + } else { + console.log('Updating existing data'); + console.log('Current jsonData length:', jsonData.length); + + const mergedData = [...jsonData]; + newData.forEach(newOffer => { + const existingIndex = mergedData.findIndex(existing => existing.offer_id === newOffer.offer_id); + if (existingIndex !== -1) { + mergedData[existingIndex] = newOffer; + } else { + mergedData.push(newOffer); + } + }); + + jsonData = isSentOffers ? mergedData : mergedData.filter(offer => !isOfferExpired(offer)); + } + + console.log('Final jsonData length:', jsonData.length); + + offerCache.clear(); + jsonData.forEach(offer => offerCache.set(offer.offer_id, offer)); + + const validItemCount = isSentOffers ? jsonData.length : jsonData.filter(offer => !isOfferExpired(offer)).length; + if (newEntriesCountSpan) { + newEntriesCountSpan.textContent = validItemCount; + } + console.log('Valid offers count:', validItemCount); + + lastRefreshTime = Date.now(); + nextRefreshCountdown = getTimeUntilNextExpiration(); + updateLastRefreshTime(); + updateNextRefreshTime(); + applyFilters(); + updateOffersTable(); + updateJsonView(); + updatePaginationInfo(); + + if (manualRefresh) { + console.log('Offers refreshed successfully'); + } + + resolve(); + }) + .catch(error => { + console.error(`Error fetching ${isSentOffers ? 'sent offers' : 'offers'}:`, error); + + let errorMessage = 'An error occurred while fetching offers. '; + if (error.message.includes('HTTP error')) { + errorMessage += 'The server returned an error. '; + } else if (error.message.includes('empty data')) { + errorMessage += 'No offer data was received. '; + } else if (error.name === 'TypeError') { + errorMessage += 'There was a problem parsing the response. '; + } else { + errorMessage += 'Please check your network connection. '; + } + errorMessage += 'Please try again later.'; + + if (typeof ui !== 'undefined' && ui.displayErrorMessage) { + ui.displayErrorMessage(errorMessage); + } else { + console.error(errorMessage); + offersBody.innerHTML = `${escapeHtml(errorMessage)}`; + } + + isInitialLoad = false; + updateOffersTable(); + updateJsonView(); + updatePaginationInfo(); + + if (newEntriesCountSpan) { + newEntriesCountSpan.textContent = '0'; + } + + reject(error); + }) + .finally(() => { + setRefreshButtonLoading(false); + }); + }); +} + +function applyFilters() { + console.log('Applying filters'); + console.log('Is Sent Offers:', isSentOffers); + + const formData = new FormData(filterForm); + const filters = Object.fromEntries(formData); + console.log('Raw filters:', filters); + + if (filters.coin_to !== 'any') { + filters.coin_to = coinIdToName[filters.coin_to] || filters.coin_to; + } + if (filters.coin_from !== 'any') { + filters.coin_from = coinIdToName[filters.coin_from] || filters.coin_from; + } + + console.log('Processed filters:', filters); + + const currentTime = Math.floor(Date.now() / 1000); + + jsonData = originalJsonData.filter(offer => { + const coinFrom = (offer.coin_from || '').toLowerCase(); + const coinTo = (offer.coin_to || '').toLowerCase(); + const isExpired = offer.expire_at <= currentTime; + + console.log(`Offer - id: ${offer.offer_id}, coinFrom: ${coinFrom}, coinTo: ${coinTo}, isExpired: ${isExpired}`); + + if (!isSentOffers && isExpired) { + console.log(`Filtered out: offer expired`); + return false; + } + + if (isSentOffers) { + if (filters.coin_to !== 'any' && coinFrom.toLowerCase() !== filters.coin_to.toLowerCase()) { + console.log(`Filtered out sent offer: coin to send mismatch - ${coinFrom} !== ${filters.coin_to}`); + return false; + } + if (filters.coin_from !== 'any' && coinTo.toLowerCase() !== filters.coin_from.toLowerCase()) { + console.log(`Filtered out sent offer: coin to receive mismatch - ${coinTo} !== ${filters.coin_from}`); + return false; + } + } else { + if (filters.coin_to !== 'any' && coinTo.toLowerCase() !== filters.coin_to.toLowerCase()) { + console.log(`Filtered out offer: bid mismatch - ${coinTo} !== ${filters.coin_to}`); + return false; + } + if (filters.coin_from !== 'any' && coinFrom.toLowerCase() !== filters.coin_from.toLowerCase()) { + console.log(`Filtered out offer: offer mismatch - ${coinFrom} !== ${filters.coin_from}`); + return false; + } + } + + if (isSentOffers && filters.active && filters.active !== 'any') { + const offerState = isExpired ? 'expired' : 'active'; + if (filters.active !== offerState) { + console.log(`Filtered out: state mismatch - ${offerState} !== ${filters.active}`); + return false; + } + } + + console.log('Offer passed all filters'); + return true; + }); + + console.log('Filtered data length:', jsonData.length); + + if (filters.sort_by) { + jsonData.sort((a, b) => { + const aValue = a[filters.sort_by]; + const bValue = b[filters.sort_by]; + + if (filters.sort_by === 'created_at') { + return (filters.sort_dir === 'asc' ? 1 : -1) * (Number(aValue) - Number(bValue)); + } else { + return (filters.sort_dir === 'asc' ? 1 : -1) * String(aValue).localeCompare(String(bValue)); + } + }); + } + + currentPage = 1; + updateOffersTable(); + updateJsonView(); + updatePaginationInfo(); +} + +function initializeFlowbiteTooltips() { + if (typeof Tooltip === 'undefined') { + console.warn('Tooltip is not defined. Make sure the required library is loaded.'); + return; + } + + const tooltipElements = document.querySelectorAll('[data-tooltip-target]'); + tooltipElements.forEach((el) => { + const tooltipId = el.getAttribute('data-tooltip-target'); + const tooltipElement = document.getElementById(tooltipId); + if (tooltipElement) { + new Tooltip(tooltipElement, el); + } + }); +} + +function updateOffersTable() { + console.log('Updating offers table'); + console.log('Current jsonData length:', jsonData.length); + console.log('Is Sent Offers:', isSentOffers); + console.log('Current Page:', currentPage); + + if (isInitialLoad) { + offersBody.innerHTML = 'Loading offers...'; + return; + } + + if (typeof initializeFlowbiteTooltips === 'function') { + initializeFlowbiteTooltips(); + } else { + console.warn('initializeFlowbiteTooltips is not defined. Skipping tooltip initialization.'); + } + + const currentTime = Math.floor(Date.now() / 1000); + const validOffers = jsonData.filter(offer => { + if (isSentOffers) { + offer.isExpired = offer.expire_at <= currentTime; + return true; + } else { + return offer.expire_at > currentTime; + } + }); + console.log('Valid offers after filtering:', validOffers.length); + + const totalPages = Math.max(1, Math.ceil(validOffers.length / itemsPerPage)); + currentPage = Math.max(1, Math.min(currentPage, totalPages)); + + const startIndex = (currentPage - 1) * itemsPerPage; + const endIndex = startIndex + itemsPerPage; + const itemsToDisplay = validOffers.slice(startIndex, endIndex); + console.log('Items to display:', itemsToDisplay.length); + + offersBody.innerHTML = ''; + + if (itemsToDisplay.length === 0) { + const formData = new FormData(filterForm); + const filters = Object.fromEntries(formData); + let message = 'No offers available'; + if (filters.coin_to !== 'any') { + const coinToName = coinIdToName[filters.coin_to] || filters.coin_to; + message += ` for bids to ${coinToName}`; + } + if (filters.coin_from !== 'any') { + const coinFromName = coinIdToName[filters.coin_from] || filters.coin_from; + message += ` for offers from ${coinFromName}`; + } + if (isSentOffers && filters.active && filters.active !== 'any') { + message += ` with status: ${filters.active}`; + } + offersBody.innerHTML = `${message}`; + console.log(message); + return; + } + + itemsToDisplay.forEach(offer => { + const row = createTableRow(offer, isSentOffers); + if (row) { + offersBody.appendChild(row); + } + }); + console.log('Rows added to table:', itemsToDisplay.length); + + updateRowTimes(); + initializeFlowbiteTooltips(); + updatePaginationInfo(validOffers.length); + + if (tableRateModule && typeof tableRateModule.initializeTable === 'function') { + setTimeout(() => { + tableRateModule.initializeTable(); + }, 0); + } else { + console.warn('tableRateModule not found or initializeTable method not available'); + } +} +function updateOffersTable() { + console.log('Updating offers table'); + console.log('Current jsonData length:', jsonData.length); + console.log('Is Sent Offers:', isSentOffers); + console.log('Current Page:', currentPage); + + if (isInitialLoad) { + offersBody.innerHTML = 'Loading offers...'; + return; + } + + const currentTime = Math.floor(Date.now() / 1000); + const validOffers = isSentOffers ? jsonData : jsonData.filter(offer => offer.expire_at > currentTime); + console.log('Valid offers after filtering:', validOffers.length); + + const totalPages = Math.max(1, Math.ceil(validOffers.length / itemsPerPage)); + currentPage = Math.max(1, Math.min(currentPage, totalPages)); + + const startIndex = (currentPage - 1) * itemsPerPage; + const endIndex = startIndex + itemsPerPage; + const itemsToDisplay = validOffers.slice(startIndex, endIndex); + console.log('Items to display:', itemsToDisplay.length); + + offersBody.innerHTML = ''; + + if (itemsToDisplay.length === 0) { + const message = getNoOffersMessage(); + offersBody.innerHTML = `${message}`; + console.log(message); + } else { + itemsToDisplay.forEach(offer => { + const row = createTableRow(offer, isSentOffers); + if (row) { + offersBody.appendChild(row); + } + }); + console.log('Rows added to table:', itemsToDisplay.length); + } + + updateRowTimes(); + initializeFlowbiteTooltips(); + updatePaginationInfo(validOffers.length); + + if (tableRateModule && typeof tableRateModule.initializeTable === 'function') { + setTimeout(() => { + tableRateModule.initializeTable(); + }, 0); + } else { + console.warn('tableRateModule not found or initializeTable method not available'); + } +} + +function performFullRefresh() { + console.log('Performing full refresh'); + const currentTime = Math.floor(Date.now() / 1000); + + fetchOffers(true) + .then(() => { + jsonData = jsonData.filter(offer => { + if (isSentOffers) { + offer.isExpired = offer.expire_at <= currentTime; + return true; + } else { + return offer.expire_at > currentTime; + } + }); + + applyFilters(); + updateOffersTable(); + updateJsonView(); + updatePaginationInfo(); + + countdownToFullRefresh = 900; + updateNextFullRefreshTime(); + + console.log('Full refresh completed'); + }) + .catch(error => { + console.error('Error during full refresh:', error); + }); +} + +function updateNextRefreshTime() { + if (!nextRefreshTimeSpan) { + console.warn('nextRefreshTime element not found'); + return; + } + + const minutes = Math.floor(nextRefreshCountdown / 60); + const seconds = nextRefreshCountdown % 60; + + nextRefreshTimeSpan.textContent = `${minutes}m ${seconds}s`; + + updateNextFullRefreshTime(); +} + + +function refreshTableData() { + console.log('Refreshing table data'); + setRefreshButtonLoading(true); + + const offersBody = document.getElementById('offers-body'); + if (offersBody) { + offersBody.innerHTML = 'Refreshing offers...'; + } + + const endpoint = isSentOffers ? '/json/sentoffers' : '/json/offers'; + fetch(endpoint) + .then(response => response.json()) + .then(newData => { + console.log('Received raw data:', newData); + console.log('Number of offers received:', Array.isArray(newData) ? newData.length : Object.keys(newData).length); + + let processedData = Array.isArray(newData) ? newData : Object.values(newData); + + if (!isSentOffers) { + const currentTime = Math.floor(Date.now() / 1000); + const beforeFilterCount = processedData.length; + processedData = processedData.filter(offer => !isOfferExpired(offer)); + console.log(`Filtered out ${beforeFilterCount - processedData.length} expired offers`); + } + + const existingOfferIds = new Set(jsonData.map(offer => offer.offer_id)); + const newOffers = processedData.filter(offer => !existingOfferIds.has(offer.offer_id)); + console.log(`Found ${newOffers.length} new offers`); + + jsonData = processedData; + originalJsonData = [...processedData]; + + console.log('Final number of offers in jsonData:', jsonData.length); + + lastRefreshTime = Date.now(); + localStorage.setItem('lastRefreshedTime', lastRefreshTime.toString()); + updateLastRefreshTime(); + + newEntriesCount = newOffers.length; + + applyFilters(); + updateOffersTable(); + updateJsonView(); + updatePaginationInfo(); + tableRateModule.initializeTable(); + + console.log('Table data refreshed successfully'); + setRefreshButtonLoading(false); + }) + .catch(error => { + console.error('Error refreshing table data:', error); + setRefreshButtonLoading(false); + if (offersBody) { + offersBody.innerHTML = 'Failed to refresh offers. Please try again.'; + } + }); +} + +function updateRowTimes() { + const currentTime = Math.floor(Date.now() / 1000); + + document.querySelectorAll('[data-offer-id]').forEach(row => { + const offerId = row.getAttribute('data-offer-id'); + let offer = offerCache.get(offerId); + + if (!offer) { + offer = jsonData.find(o => o.offer_id === offerId); + if (offer) { + offerCache.set(offerId, offer); + } else { + console.warn(`Offer not found for ID: ${offerId}`); + return; + } + } + + const timeColumn = row.querySelector('td:first-child'); + if (!timeColumn) return; + + const timeDiv = timeColumn.querySelector('div.flex.flex-col'); + if (!timeDiv) return; + + const postedTime = formatTimeAgo(offer.created_at); + const expiresIn = formatTimeLeft(offer.expire_at); + + timeDiv.innerHTML = ` +
Posted: ${postedTime}
+
Expires in: ${expiresIn}
+ `; + + const tooltipElement = document.getElementById(`tooltip-active${offerId}`); + if (tooltipElement) { + const tooltipContent = tooltipElement.querySelector('.active-revoked-expired'); + if (tooltipContent) { + tooltipContent.innerHTML = ` + +
Posted: ${postedTime}
+
Expires in: ${expiresIn}
+
+ `; + } + } + }); +} + +function checkExpiredAndFetchNew() { + const currentTime = Math.floor(Date.now() / 1000); + const expiredOffers = jsonData.filter(offer => offer.expire_at <= currentTime); + + if (expiredOffers.length > 0) { + console.log(`Found ${expiredOffers.length} expired offers. Removing and checking for new listings.`); + + jsonData = jsonData.filter(offer => offer.expire_at > currentTime); + + fetch('/json/offers') + .then(response => response.json()) + .then(data => { + let newListings = Array.isArray(data) ? data : Object.values(data); + newListings = newListings.filter(offer => !isOfferExpired(offer)); + + const brandNewListings = newListings.filter(newOffer => + !jsonData.some(existingOffer => existingOffer.offer_id === newOffer.offer_id) + ); + + if (brandNewListings.length > 0) { + console.log(`Found ${brandNewListings.length} new listings to add.`); + jsonData = [...jsonData, ...brandNewListings]; + newEntriesCount += brandNewListings.length; + } else { + console.log('No new listings found during expiry check.'); + } + + updateOffersTable(); + updateJsonView(); + updatePaginationInfo(); + + nextRefreshCountdown = getTimeUntilNextExpiration(); + console.log(`Next expiration check in ${nextRefreshCountdown} seconds`); + }) + .catch(error => { + console.error('Error fetching new listings during expiry check:', error); + }); + } else { + console.log('No expired offers found during this check.'); + + nextRefreshCountdown = getTimeUntilNextExpiration(); + console.log(`Next expiration check in ${nextRefreshCountdown} seconds`); + } +} + +function createTableRow(offer, isSentOffers) { + const row = document.createElement('tr'); + row.className = `opacity-100 text-gray-500 dark:text-gray-100 hover:bg-coolGray-200 dark:hover:bg-gray-600`; + row.setAttribute('data-offer-id', offer.offer_id); + + const { + coinFrom, coinTo, coinFromSymbol, coinToSymbol, + postedTime, expiresIn, isActuallyExpired, isTreatedAsSentOffer, + formattedRate, buttonClass, buttonText, clockColor + } = prepareOfferData(offer, isSentOffers); + + row.innerHTML = ` + ${createTimeColumn(offer, postedTime, expiresIn, clockColor)} + ${createDetailsColumn(offer)} + ${createTakerAmountColumn(offer, coinFrom, coinFromSymbol, coinTo)} + ${createSwapColumn(offer, coinFrom, coinTo)} + ${createOrderbookColumn(offer, coinTo, coinToSymbol, coinFrom)} + ${createRateColumn(offer, coinFrom, coinTo, formattedRate)} + ${createPercentageColumn(offer)} + ${createActionColumn(offer, buttonClass, buttonText)} + ${createTooltips(offer, isSentOffers, coinFrom, coinTo, postedTime, expiresIn, isActuallyExpired)} + `; + + return row; +} + +function prepareOfferData(offer, isSentOffers) { + const coinFrom = offer.coin_from; + const coinTo = offer.coin_to; + const coinFromSymbol = getCoinSymbol(coinFrom); + const coinToSymbol = getCoinSymbol(coinTo); + + const postedTime = formatTimeAgo(offer.created_at); + const expiresIn = formatTimeLeft(offer.expire_at); + + const currentTime = Math.floor(Date.now() / 1000); + const isActuallyExpired = currentTime > offer.expire_at; + + const rateValue = parseFloat(offer.rate); + const formattedRate = formatSmallNumber(rateValue); + + const { buttonClass, buttonText } = getButtonProperties(isActuallyExpired, isSentOffers, offer.is_own_offer); + + const clockColor = isActuallyExpired ? "#9CA3AF" : "#3B82F6"; + + return { + coinFrom, coinTo, coinFromSymbol, coinToSymbol, + postedTime, expiresIn, isActuallyExpired, + formattedRate, buttonClass, buttonText, clockColor + }; +} + +function getButtonProperties(isActuallyExpired, isSentOffers, isTreatedAsSentOffer) { + if (isActuallyExpired && isSentOffers) { + return { + buttonClass: 'bg-gray-400 text-white dark:border-gray-300 text-white hover:bg-red-700 transition duration-200', + buttonText: 'Expired' + }; + } else if (isTreatedAsSentOffer) { + return { + buttonClass: 'bg-gray-300 bold text-white bold hover:bg-green-600 transition duration-200', + buttonText: 'Edit' + }; + } else { + return { + buttonClass: 'bg-blue-500 text-white hover:bg-green-600 transition duration-200', + buttonText: 'Swap' + }; + } +} + +function createTimeColumn(offer, postedTime, expiresIn, clockColor) { + return ` + +
+ + + + + + + +
+ + `; +} + +function createDetailsColumn(offer) { + const addrFrom = offer.addr_from || ''; + const amountVariable = offer.amount_variable !== undefined ? offer.amount_variable : false; + return ` + + + Recipient: ${escapeHtml(addrFrom.substring(0, 10))}... + + + + `; +} + +function createTakerAmountColumn(offer, coinFrom, coinFromSymbol, coinTo) { + return ` + + + + ${offer.amount_from.substring(0, 7)} +
${coinFrom}
+
+
+ + + `; +} + +function createSwapColumn(offer, coinFrom, coinTo) { + return ` + + +
+ + ${coinFrom} + + + + ${coinTo} + +
+
+ + `; +} + +function createOrderbookColumn(offer, coinTo, coinToSymbol, coinFrom) { + return ` + + + + ${escapeHtml(offer.amount_to.substring(0, 7))} +
${escapeHtml(coinTo)}
+
+
+ + + `; +} + +function createRateColumn(offer, coinFrom, coinTo, formattedRate) { + return ` + +
+ +
+ RATE: + ${formattedRate} +
+
+ + +
+
+ +
+ +
+ + `; +} + +function createPercentageColumn(offer) { + return ` + +
+
+ +
+ +
+ + `; +} + +function createActionColumn(offer, buttonClass, buttonText) { + return ` + + + + `; +} + +function createTooltips(offer, isSentOffers, coinFrom, coinTo, postedTime, expiresIn, isActuallyExpired) { + return ` + + + + + + + + + + + + + + `; +} + +function updatePaginationInfo() { + const currentTime = Math.floor(Date.now() / 1000); + const validOffers = isSentOffers ? jsonData : jsonData.filter(offer => offer.expire_at > currentTime); + const validItemCount = validOffers.length; + const totalPages = Math.max(1, Math.ceil(validItemCount / itemsPerPage)); + + currentPage = Math.max(1, Math.min(currentPage, totalPages)); + + currentPageSpan.textContent = currentPage; + totalPagesSpan.textContent = totalPages; + + prevPageButton.classList.toggle('invisible', currentPage === 1 || validItemCount === 0); + nextPageButton.classList.toggle('invisible', currentPage === totalPages || validItemCount === 0 || validItemCount <= itemsPerPage); + + prevPageButton.style.display = currentPage === 1 ? 'none' : 'inline-flex'; + nextPageButton.style.display = currentPage === totalPages ? 'none' : 'inline-flex'; + + if (lastRefreshTime) { + lastRefreshTimeSpan.textContent = new Date(lastRefreshTime).toLocaleTimeString(); + } + + const newEntriesCountSpan = document.getElementById('newEntriesCount'); + if (newEntriesCountSpan) { + newEntriesCountSpan.textContent = validItemCount; + } + + console.log(`Pagination: Page ${currentPage} of ${totalPages}, Valid items: ${validItemCount}`); +} + +function updateJsonView() { + jsonContent.textContent = JSON.stringify(jsonData, null, 2); +} + +function updateLastRefreshTime() { + lastRefreshTimeSpan.textContent = new Date(lastRefreshTime).toLocaleTimeString(); +} + +function updateNextFullRefreshTime() { + const nextFullRefreshTimeSpan = document.getElementById('nextFullRefreshTime'); + if (nextFullRefreshTimeSpan) { + const minutes = Math.floor(Math.max(0, countdownToFullRefresh) / 60); + const seconds = Math.max(0, countdownToFullRefresh) % 60; + nextFullRefreshTimeSpan.textContent = `${minutes}m ${seconds}s`; + } +} + +function getTimeUntilNextExpiration() { + const currentTime = Math.floor(Date.now() / 1000); + const nextExpiration = jsonData.reduce((earliest, offer) => { + const timeUntilExpiration = offer.expire_at - currentTime; + return timeUntilExpiration > 0 && timeUntilExpiration < earliest ? timeUntilExpiration : earliest; + }, Infinity); + + return nextExpiration === Infinity ? 600 : Math.min(nextExpiration, 600); +} + +// Event listeners +toggleButton.addEventListener('click', () => { + tableView.classList.toggle('hidden'); + jsonView.classList.toggle('hidden'); + toggleButton.textContent = tableView.classList.contains('hidden') ? 'Show Table View' : 'Show JSON View'; +}); + +filterForm.addEventListener('submit', (e) => { + e.preventDefault(); + applyFilters(); +}); + +filterForm.addEventListener('change', applyFilters); + +document.getElementById('coin_to').addEventListener('change', (event) => { + console.log('Coin To filter changed:', event.target.value); + applyFilters(); +}); + +document.getElementById('coin_from').addEventListener('change', (event) => { + console.log('Coin From filter changed:', event.target.value); + applyFilters(); +}); + +prevPageButton.addEventListener('click', () => { + if (currentPage > 1) { + currentPage--; + updateOffersTable(); + updatePaginationInfo(); + } +}); + +nextPageButton.addEventListener('click', () => { + const validOffers = isSentOffers ? jsonData : jsonData.filter(offer => !isOfferExpired(offer)); + const totalPages = Math.ceil(validOffers.length / itemsPerPage); + if (currentPage < totalPages) { + currentPage++; + updateOffersTable(); + updatePaginationInfo(); + } + console.log(`Moved to page ${currentPage} of ${totalPages}`); +}); + +document.getElementById('clearFilters').addEventListener('click', () => { + filterForm.reset(); + jsonData = [...originalJsonData]; + currentPage = 1; + updateOffersTable(); + updateJsonView(); + updateCoinFilterImages(); +}); + +document.getElementById('refreshOffers').addEventListener('click', () => { + console.log('Refresh button clicked'); + fetchOffers(true); +}); + +function updateCoinFilterImages() { + const coinToSelect = document.getElementById('coin_to'); + const coinFromSelect = document.getElementById('coin_from'); + const coinToButton = document.getElementById('coin_to_button'); + const coinFromButton = document.getElementById('coin_from_button'); + + function updateButtonImage(select, button) { + const selectedOption = select.options[select.selectedIndex]; + const imagePath = selectedOption.getAttribute('data-image'); + if (imagePath && select.value !== 'any') { + button.style.backgroundImage = `url(${imagePath})`; + button.style.backgroundSize = 'contain'; + button.style.backgroundRepeat = 'no-repeat'; + button.style.backgroundPosition = 'center'; + } else { + button.style.backgroundImage = 'none'; + } + } + + updateButtonImage(coinToSelect, coinToButton); + updateButtonImage(coinFromSelect, coinFromButton); +} + +function startRefreshCountdown() { + console.log('Starting refresh countdown'); + + setInterval(() => { + nextRefreshCountdown--; + countdownToFullRefresh--; + + if (nextRefreshCountdown <= 0) { + checkExpiredAndFetchNew(); + nextRefreshCountdown = getTimeUntilNextExpiration(); + } + + if (countdownToFullRefresh <= 0) { + performFullRefresh(); + countdownToFullRefresh = 900; + } + + updateNextRefreshTime(); + updateNextFullRefreshTime(); + }, 1000); +} + +function initializeTableRateModule() { + if (typeof window.tableRateModule !== 'undefined') { + tableRateModule = window.tableRateModule; + console.log('tableRateModule loaded successfully'); + return true; + } else { + console.warn('tableRateModule not found. Waiting for it to load...'); + return false; + } +} + +// Init +document.addEventListener('DOMContentLoaded', () => { + console.log('DOM content loaded, initializing...'); + + if (initializeTableRateModule()) { + continueInitialization(); + } else { + let retryCount = 0; + const maxRetries = 5; + const retryInterval = setInterval(() => { + retryCount++; + if (initializeTableRateModule()) { + clearInterval(retryInterval); + continueInitialization(); + } else if (retryCount >= maxRetries) { + console.error('Failed to load tableRateModule after multiple attempts. Some functionality may be limited.'); + clearInterval(retryInterval); + continueInitialization(); + } + }, 1000); + } +}); + +function continueInitialization() { + if (typeof volumeToggle !== 'undefined' && volumeToggle.init) { + volumeToggle.init(); + } else { + console.warn('volumeToggle is not defined or does not have an init method'); + } + updateOffersTable(); + updateJsonView(); + updateCoinFilterImages(); + fetchOffers(); + startRefreshCountdown(); + initializeTableWithCache(); + updateNextFullRefreshTime(); + + jsonData.forEach(offer => offerCache.set(offer.offer_id, offer)); + + function updateTimesLoop() { + updateRowTimes(); + requestAnimationFrame(updateTimesLoop); + } + requestAnimationFrame(updateTimesLoop); + + setInterval(updateRowTimes, 900000); + + setInterval(performFullRefresh, 30 * 60 * 1000); +} + +console.log('Offers Table Module fully initialized'); diff --git a/basicswap/static/js/pricechart.js b/basicswap/static/js/pricechart.js new file mode 100644 index 0000000..780347d --- /dev/null +++ b/basicswap/static/js/pricechart.js @@ -0,0 +1,1253 @@ +// Config +const config = { + apiKeys: getAPIKeys(), + coins: [ + { symbol: 'BTC', usesCryptoCompare: true, usesCoinGecko: false, historicalDays: 30 }, + { symbol: 'XMR', usesCryptoCompare: true, usesCoinGecko: false, historicalDays: 30 }, + { symbol: 'PART', usesCryptoCompare: true, usesCoinGecko: false, historicalDays: 30 }, + { symbol: 'PIVX', usesCryptoCompare: true, usesCoinGecko: false, historicalDays: 30 }, + { symbol: 'FIRO', usesCryptoCompare: true, usesCoinGecko: false, historicalDays: 30 }, + { symbol: 'DASH', usesCryptoCompare: true, usesCoinGecko: false, historicalDays: 30 }, + { symbol: 'LTC', usesCryptoCompare: true, usesCoinGecko: false, historicalDays: 30 }, + { symbol: 'DOGE', usesCryptoCompare: true, usesCoinGecko: false, historicalDays: 30 }, + { symbol: 'ETH', usesCryptoCompare: true, usesCoinGecko: false, historicalDays: 30 }, + { symbol: 'DCR', usesCryptoCompare: true, usesCoinGecko: false, historicalDays: 30 }, + { symbol: 'ZANO', usesCryptoCompare: true, usesCoinGecko: false, historicalDays: 30 }, + { symbol: 'WOW', usesCryptoCompare: false, usesCoinGecko: true, historicalDays: 30 }, + { symbol: 'BCH', usesCryptoCompare: true, usesCoinGecko: false, historicalDays: 30 } + ], + apiEndpoints: { + cryptoCompare: 'https://min-api.cryptocompare.com/data/pricemultifull', + coinGecko: 'https://api.coingecko.com/api/v3/coins', + cryptoCompareHistorical: 'https://min-api.cryptocompare.com/data/v2/histoday' + }, + chartColors: { + default: { + lineColor: 'rgba(77, 132, 240, 1)', + backgroundColor: 'rgba(77, 132, 240, 0.1)' + } + }, + showVolume: false, + specialCoins: [''], + resolutions: { + month: { days: 30, interval: 'daily' }, + week: { days: 7, interval: 'daily' }, + day: { days: 1, interval: 'hourly' } + }, + currentResolution: 'month' +}; + +// Utils +const utils = { + formatNumber: (number, decimals = 2) => + number.toFixed(decimals).replace(/\B(?=(\d{3})+(?!\d))/g, ','), + + formatDate: (timestamp, resolution) => { + const date = new Date(timestamp); + const options = { + day: { hour: '2-digit', minute: '2-digit', hour12: true }, + week: { month: 'short', day: 'numeric' }, + month: { year: 'numeric', month: 'short', day: 'numeric' } + }; + return date.toLocaleString('en-US', { ...options[resolution], timeZone: 'UTC' }); + }, + + debounce: (func, delay) => { + let timeoutId; + return (...args) => { + clearTimeout(timeoutId); + timeoutId = setTimeout(() => func(...args), delay); + }; + } +}; + +// Error +class AppError extends Error { + constructor(message, type = 'AppError') { + super(message); + this.name = type; + } +} + +// Log +const logger = { + log: (message) => console.log(`[AppLog] ${new Date().toISOString()}: ${message}`), + warn: (message) => console.warn(`[AppWarn] ${new Date().toISOString()}: ${message}`), + error: (message) => console.error(`[AppError] ${new Date().toISOString()}: ${message}`) +}; + +// API +const api = { + makePostRequest: (url, headers = {}) => { + return new Promise((resolve, reject) => { + const xhr = new XMLHttpRequest(); + xhr.open('POST', '/json/readurl'); + xhr.setRequestHeader('Content-Type', 'application/json'); + xhr.timeout = 30000; + xhr.ontimeout = () => reject(new AppError('Request timed out')); + xhr.onload = () => { + logger.log(`Response for ${url}:`, xhr.responseText); + if (xhr.status === 200) { + try { + const response = JSON.parse(xhr.responseText); + if (response.Error) { + logger.error(`API Error for ${url}:`, response.Error); + reject(new AppError(response.Error, 'APIError')); + } else { + resolve(response); + } + } catch (error) { + logger.error(`Invalid JSON response for ${url}:`, xhr.responseText); + reject(new AppError(`Invalid JSON response: ${error.message}`, 'ParseError')); + } + } else { + logger.error(`HTTP Error for ${url}: ${xhr.status} ${xhr.statusText}`); + reject(new AppError(`HTTP Error: ${xhr.status} ${xhr.statusText}`, 'HTTPError')); + } + }; + xhr.onerror = () => reject(new AppError('Network error occurred', 'NetworkError')); + xhr.send(JSON.stringify({ + url: url, + headers: headers + })); + }); + }, + + fetchCryptoCompareDataXHR: (coin) => { + const url = `${config.apiEndpoints.cryptoCompare}?fsyms=${coin}&tsyms=USD,BTC&api_key=${config.apiKeys.cryptoCompare}`; + const headers = { + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36', + 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8', + 'Accept-Language': 'en-US,en;q=0.5', + }; + return api.makePostRequest(url, headers).catch(error => ({ + error: error.message + })); + }, + + fetchCoinGeckoDataXHR: (coin) => { + const coinConfig = config.coins.find(c => c.symbol === coin); + if (!coinConfig) { + logger.error(`No configuration found for coin: ${coin}`); + return Promise.reject(new AppError(`No configuration found for coin: ${coin}`)); + } + let coinId; + switch (coin) { + case 'WOW': + coinId = 'wownero'; + break; + default: + coinId = coin.toLowerCase(); + } + const url = `${config.apiEndpoints.coinGecko}/${coinId}?localization=false&tickers=false&market_data=true&community_data=false&developer_data=false&sparkline=false`; + logger.log(`Fetching data for ${coin} from CoinGecko: ${url}`); + return api.makePostRequest(url) + .then(data => { + logger.log(`Raw CoinGecko data for ${coin}:`, data); + if (!data.market_data || !data.market_data.current_price) { + throw new AppError(`Invalid data structure received for ${coin}`); + } + return data; + }) + .catch(error => { + logger.error(`Error fetching CoinGecko data for ${coin}:`, error); + return { + error: error.message + }; + }); + }, + +fetchHistoricalDataXHR: (coinSymbol) => { + const coin = config.coins.find(c => c.symbol === coinSymbol); + if (!coin) { + logger.error(`No configuration found for coin: ${coinSymbol}`); + return Promise.reject(new AppError(`No configuration found for coin: ${coinSymbol}`)); + } + + let url; + const resolutionConfig = config.resolutions[config.currentResolution]; + + if (coin.usesCoinGecko) { + let coinId; + switch (coinSymbol) { + case 'ZANO': + coinId = 'zano'; + break; + case 'WOW': + coinId = 'wownero'; + break; + default: + coinId = coinSymbol.toLowerCase(); + } + + url = `${config.apiEndpoints.coinGecko}/${coinId}/market_chart?vs_currency=usd&days=2`; + } else { + + if (config.currentResolution === 'day') { + url = `https://min-api.cryptocompare.com/data/v2/histohour?fsym=${coinSymbol}&tsym=USD&limit=24&api_key=${config.apiKeys.cryptoCompare}`; + } else if (config.currentResolution === 'week') { + url = `${config.apiEndpoints.cryptoCompareHistorical}?fsym=${coinSymbol}&tsym=USD&limit=7&aggregate=24&api_key=${config.apiKeys.cryptoCompare}`; + } else { + url = `${config.apiEndpoints.cryptoCompareHistorical}?fsym=${coinSymbol}&tsym=USD&limit=30&aggregate=24&api_key=${config.apiKeys.cryptoCompare}`; + } + } + + logger.log(`Fetching historical data for ${coinSymbol}:`, url); + + return api.makePostRequest(url) + .then(response => { + logger.log(`Received historical data for ${coinSymbol}:`, JSON.stringify(response, null, 2)); + return response; + }) + .catch(error => { + logger.error(`Error fetching historical data for ${coinSymbol}:`, error); + throw error; + }); +}, + +}; + +// Cache +const cache = { + ttl: 15 * 60 * 1000, + set: (key, value, customTtl = null) => { + const item = { + value: value, + timestamp: Date.now(), + expiresAt: Date.now() + (customTtl || cache.ttl) + }; + localStorage.setItem(key, JSON.stringify(item)); + }, + get: (key) => { + const itemStr = localStorage.getItem(key); + if (!itemStr) { + return null; + } + try { + const item = JSON.parse(itemStr); + const now = Date.now(); + if (now < item.expiresAt) { + return { + value: item.value, + remainingTime: item.expiresAt - now + }; + } else { + localStorage.removeItem(key); + } + } catch (e) { + logger.error('Error parsing cache item:', e); + localStorage.removeItem(key); + } + return null; + }, + isValid: (key) => { + return cache.get(key) !== null; + }, + clear: () => { + Object.keys(localStorage).forEach(key => { + if (key.startsWith('coinData_') || key.startsWith('chartData_')) { + localStorage.removeItem(key); + } + }); + } +}; + +// UI +const ui = { + displayCoinData: (coin, data) => { + const coinConfig = config.coins.find(c => c.symbol === coin); + let priceUSD, priceBTC, priceChange1d, volume24h; + const updateUI = (isError = false) => { + const priceUsdElement = document.querySelector(`#${coin.toLowerCase()}-price-usd`); + const volumeDiv = document.querySelector(`#${coin.toLowerCase()}-volume-div`); + const volumeElement = document.querySelector(`#${coin.toLowerCase()}-volume-24h`); + const btcPriceDiv = document.querySelector(`#${coin.toLowerCase()}-btc-price-div`); + const priceBtcElement = document.querySelector(`#${coin.toLowerCase()}-price-btc`); + if (priceUsdElement) { + priceUsdElement.textContent = isError ? 'N/A' : `$ ${ui.formatPrice(coin, priceUSD)}`; + } + if (volumeDiv && volumeElement) { + volumeElement.textContent = isError ? 'N/A' : `${utils.formatNumber(volume24h, 0)} USD`; + volumeDiv.style.display = volumeToggle.isVisible ? 'flex' : 'none'; + } + if (btcPriceDiv && priceBtcElement && coin !== 'BTC') { + priceBtcElement.textContent = isError ? 'N/A' : `${priceBTC.toFixed(8)} BTC`; + btcPriceDiv.style.display = 'flex'; + } + ui.updatePriceChangeContainer(coin, isError ? null : priceChange1d); + }; + try { + if (data.error) { + throw new Error(data.error); + } + if (coinConfig.usesCoinGecko) { + if (!data.market_data) { + throw new Error(`Invalid CoinGecko data structure for ${coin}`); + } + priceUSD = data.market_data.current_price.usd; + priceBTC = data.market_data.current_price.btc; + priceChange1d = data.market_data.price_change_percentage_24h; + volume24h = data.market_data.total_volume.usd; + } else if (coinConfig.usesCryptoCompare) { + if (!data.RAW || !data.RAW[coin] || !data.RAW[coin].USD) { + throw new Error(`Invalid CryptoCompare data structure for ${coin}`); + } + priceUSD = data.RAW[coin].USD.PRICE; + priceBTC = data.RAW[coin].BTC.PRICE; + priceChange1d = data.RAW[coin].USD.CHANGEPCT24HOUR; + volume24h = data.RAW[coin].USD.TOTALVOLUME24HTO; + } + if (isNaN(priceUSD) || isNaN(priceBTC) || isNaN(volume24h)) { + throw new Error(`Invalid numeric values in data for ${coin}`); + } + updateUI(false); + } catch (error) { + logger.error(`Error displaying data for ${coin}:`, error.message); + updateUI(true); + } + }, + + showLoader: () => { + const loader = document.getElementById('loader'); + if (loader) { + loader.classList.remove('hidden'); + } + }, + +hideLoader: () => { + const loader = document.getElementById('loader'); + if (loader) { + loader.classList.add('hidden'); + } + }, + + showCoinLoader: (coinSymbol) => { + const loader = document.getElementById(`${coinSymbol.toLowerCase()}-loader`); + if (loader) { + loader.classList.remove('hidden'); + } + }, + + hideCoinLoader: (coinSymbol) => { + const loader = document.getElementById(`${coinSymbol.toLowerCase()}-loader`); + if (loader) { + loader.classList.add('hidden'); + } + }, + + updateCacheStatus: (isCached) => { + const cacheStatusElement = document.getElementById('cache-status'); + if (cacheStatusElement) { + cacheStatusElement.textContent = isCached ? 'Cached' : 'Live'; + cacheStatusElement.classList.toggle('text-green-500', isCached); + cacheStatusElement.classList.toggle('text-blue-500', !isCached); + } + }, + + updateLoadTimeAndCache: (loadTime, cachedData) => { + const loadTimeElement = document.getElementById('load-time'); + const cacheStatusElement = document.getElementById('cache-status'); + + if (loadTimeElement) { + loadTimeElement.textContent = `Load time: ${loadTime}ms`; + } + + if (cacheStatusElement) { + if (cachedData && cachedData.remainingTime) { + const remainingMinutes = Math.ceil(cachedData.remainingTime / 60000); + cacheStatusElement.textContent = `Cached: ${remainingMinutes} min left`; + cacheStatusElement.classList.add('text-green-500'); + cacheStatusElement.classList.remove('text-blue-500'); + } else { + cacheStatusElement.textContent = 'Live'; + cacheStatusElement.classList.add('text-blue-500'); + cacheStatusElement.classList.remove('text-green-500'); + } + } + + ui.updateLastRefreshedTime(); + }, + + updatePriceChangeContainer: (coin, priceChange) => { + const container = document.querySelector(`#${coin.toLowerCase()}-price-change-container`); + if (container) { + container.innerHTML = priceChange !== null ? + (priceChange >= 0 ? ui.positivePriceChangeHTML(priceChange) : ui.negativePriceChangeHTML(priceChange)) : + 'N/A'; + } + }, + + updateLastRefreshedTime: () => { + const lastRefreshedElement = document.getElementById('last-refreshed-time'); + if (lastRefreshedElement && app.lastRefreshedTime) { + const formattedTime = app.lastRefreshedTime.toLocaleTimeString(); + lastRefreshedElement.textContent = `Last Refreshed: ${formattedTime}`; + } + }, + + positivePriceChangeHTML: (value) => ` +
+ + + + ${value.toFixed(2)}% +
+ `, + + negativePriceChangeHTML: (value) => ` +
+ + + + ${Math.abs(value).toFixed(2)}% +
+ `, + + formatPrice: (coin, price) => { + if (typeof price !== 'number' || isNaN(price)) { + logger.error(`Invalid price for ${coin}:`, price); + return 'N/A'; + } + if (price < 0.000001) return price.toExponential(2); + if (price < 0.001) return price.toFixed(8); + if (price < 1) return price.toFixed(4); + if (price < 1000) return price.toFixed(2); + return price.toFixed(1); + }, + + setActiveContainer: (containerId) => { + const containerIds = ['btc', 'xmr', 'part', 'pivx', 'firo', 'dash', 'ltc', 'doge', 'eth', 'dcr', 'zano', 'wow', 'bch'].map(id => `${id}-container`); + containerIds.forEach(id => { + const container = document.getElementById(id); + if (container) { + const innerDiv = container.querySelector('div'); + innerDiv.classList.toggle('active-container', id === containerId); + } + }); + }, + + displayErrorMessage: (message) => { + const errorOverlay = document.getElementById('error-overlay'); + const errorMessage = document.getElementById('error-message'); + const chartContainer = document.querySelector('.container-to-blur'); + if (errorOverlay && errorMessage && chartContainer) { + errorOverlay.classList.remove('hidden'); + errorMessage.textContent = message; + chartContainer.classList.add('blurred'); + } + }, + + hideErrorMessage: () => { + const errorOverlay = document.getElementById('error-overlay'); + const containersToBlur = document.querySelectorAll('.container-to-blur'); + if (errorOverlay) { + errorOverlay.classList.add('hidden'); + containersToBlur.forEach(container => container.classList.remove('blurred')); + } + } +}; + +// Chart +const chartModule = { + chart: null, + currentCoin: 'BTC', + loadStartTime: 0, + verticalLinePlugin: { + id: 'verticalLine', + beforeDraw: (chart, args, options) => { + if (chart.tooltip._active && chart.tooltip._active.length) { + const activePoint = chart.tooltip._active[0]; + const ctx = chart.ctx; + const x = activePoint.element.x; + const topY = chart.scales.y.top; + const bottomY = chart.scales.y.bottom; + ctx.save(); + ctx.beginPath(); + ctx.moveTo(x, topY); + ctx.lineTo(x, bottomY); + ctx.lineWidth = options.lineWidth || 1; + ctx.strokeStyle = options.lineColor || 'rgba(77, 132, 240, 0.5)'; + ctx.stroke(); + ctx.restore(); + } + } + }, + +initChart: () => { + const ctx = document.getElementById('coin-chart').getContext('2d'); + if (!ctx) { + logger.error('Failed to get chart context. Make sure the canvas element exists.'); + return; + } + + const gradient = ctx.createLinearGradient(0, 0, 0, 400); + gradient.addColorStop(0, 'rgba(77, 132, 240, 0.2)'); + gradient.addColorStop(1, 'rgba(77, 132, 240, 0)'); + + chartModule.chart = new Chart(ctx, { + type: 'line', + data: { + datasets: [{ + label: 'Price', + data: [], + borderColor: 'rgba(77, 132, 240, 1)', + backgroundColor: gradient, + tension: 0.4, + fill: true, + borderWidth: 3, + pointRadius: 0, + }] + }, + options: { + responsive: true, + maintainAspectRatio: false, + interaction: { + intersect: false, + mode: 'index' + }, + scales: { + x: { + type: 'time', + time: { + unit: 'day', + displayFormats: { + hour: 'ha', + day: 'MMM d' + } + }, + ticks: { + source: 'data', + maxTicksLimit: 10, + font: { + size: 12, + family: "'Inter', sans-serif" + }, + color: 'rgba(156, 163, 175, 1)' + }, + grid: { + display: false + } + }, + y: { + beginAtZero: false, + ticks: { + font: { + size: 12, + family: "'Inter', sans-serif" + }, + color: 'rgba(156, 163, 175, 1)', + callback: (value) => '$' + value.toLocaleString() + }, + grid: { + display: false + } + } + }, + plugins: { + legend: { + display: false + }, + tooltip: { + mode: 'index', + intersect: false, + backgroundColor: 'rgba(255, 255, 255, 0.9)', + titleColor: 'rgba(17, 24, 39, 1)', + bodyColor: 'rgba(55, 65, 81, 1)', + borderColor: 'rgba(226, 232, 240, 1)', + borderWidth: 1, + cornerRadius: 4, + padding: 8, + displayColors: false, + callbacks: { + title: (tooltipItems) => { + const date = new Date(tooltipItems[0].parsed.x); + return date.toLocaleString('en-US', { + month: 'short', + day: 'numeric', + year: 'numeric', + hour: 'numeric', + minute: 'numeric', + hour12: true, + timeZone: 'UTC' + }); + }, + label: (item) => { + const value = item.parsed.y; + return `${chartModule.currentCoin}: $${value.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 8 })}`; + } + } + }, + verticalLine: { + lineWidth: 1, + lineColor: 'rgba(77, 132, 240, 0.5)' + } + }, + elements: { + point: { + backgroundColor: 'transparent', + borderColor: 'rgba(77, 132, 240, 1)', + borderWidth: 2, + radius: 0, + hoverRadius: 4, + hitRadius: 6, + hoverBorderWidth: 2 + }, + line: { + backgroundColor: gradient, + borderColor: 'rgba(77, 132, 240, 1)', + fill: true + } + } + }, + plugins: [chartModule.verticalLinePlugin] + }); + + console.log('Chart initialized:', chartModule.chart); + }, + +prepareChartData: (coinSymbol, data) => { + console.log(`Preparing chart data for ${coinSymbol}:`, JSON.stringify(data, null, 2)); + const coin = config.coins.find(c => c.symbol === coinSymbol); + if (!data || typeof data !== 'object' || data.error) { + console.error(`Invalid data received for ${coinSymbol}:`, data); + return []; + } + try { + let preparedData; + if (coin.usesCoinGecko) { + if (!data.prices || !Array.isArray(data.prices)) { + throw new Error(`Invalid CoinGecko data structure for ${coinSymbol}`); + } + preparedData = data.prices.map(entry => ({ + x: new Date(entry[0]), + y: entry[1] + })); + + if (config.currentResolution === 'day') { + + preparedData = chartModule.ensureHourlyData(preparedData); + } else { + + preparedData = preparedData.filter((_, index) => index % 24 === 0); + } + } else { + + if (!data.Data || !data.Data.Data || !Array.isArray(data.Data.Data)) { + throw new Error(`Invalid CryptoCompare data structure for ${coinSymbol}`); + } + preparedData = data.Data.Data.map(d => ({ + x: new Date(d.time * 1000), + y: d.close + })); + } + + const expectedDataPoints = config.currentResolution === 'day' ? 24 : config.resolutions[config.currentResolution].days; + if (preparedData.length < expectedDataPoints) { + console.warn(`Insufficient data points for ${coinSymbol}. Expected ${expectedDataPoints}, got ${preparedData.length}`); + } + + console.log(`Prepared data for ${coinSymbol}:`, preparedData.slice(0, 5)); + return preparedData; + } catch (error) { + console.error(`Error preparing chart data for ${coinSymbol}:`, error); + return []; + } +}, + +ensureHourlyData: (data) => { + const now = new Date(); + const twentyFourHoursAgo = new Date(now.getTime() - 24 * 60 * 60 * 1000); + const hourlyData = []; + + for (let i = 0; i < 24; i++) { + const targetTime = new Date(twentyFourHoursAgo.getTime() + i * 60 * 60 * 1000); + const closestDataPoint = data.reduce((prev, curr) => + Math.abs(curr.x - targetTime) < Math.abs(prev.x - targetTime) ? curr : prev + ); + + hourlyData.push({ + x: targetTime, + y: closestDataPoint.y + }); + } + + return hourlyData; +}, + + updateChart: async (coinSymbol, forceRefresh = false) => { + try { + chartModule.showChartLoader(); + chartModule.loadStartTime = Date.now(); + + const cacheKey = `chartData_${coinSymbol}_${config.currentResolution}`; + const cacheDuration = 15 * 60 * 1000; // 15 minutes in milliseconds + let cachedData = !forceRefresh ? cache.get(cacheKey) : null; + let data; + + if (cachedData) { + data = cachedData.value; + console.log(`Using cached data for ${coinSymbol} (${config.currentResolution})`); + } else { + console.log(`Fetching fresh data for ${coinSymbol} (${config.currentResolution})`); + data = await api.fetchHistoricalDataXHR(coinSymbol); + if (data.error) { + throw new Error(data.error); + } + cache.set(cacheKey, data, cacheDuration); + cachedData = null; + } + + const chartData = chartModule.prepareChartData(coinSymbol, data); + + if (chartModule.chart) { + chartModule.chart.data.datasets[0].data = chartData; + chartModule.chart.data.datasets[0].label = `${coinSymbol} Price (USD)`; + + const coin = config.coins.find(c => c.symbol === coinSymbol); + let apiSource = coin.usesCoinGecko ? 'CoinGecko' : 'CryptoCompare'; + let currency = 'USD'; + + const chartTitle = document.getElementById('chart-title'); + if (chartTitle) { + chartTitle.textContent = `${coinSymbol} Price Chart`; + } + + chartModule.chart.options.scales.y.title = { + display: true, + text: `Price (${currency}) - ${coinSymbol} - ${apiSource}` + }; + + if (coinSymbol === 'WOW') { + chartModule.chart.options.scales.y.ticks.callback = (value) => { + return '$' + value.toFixed(4); + }; + + chartModule.chart.options.plugins.tooltip.callbacks.label = (context) => { + let label = context.dataset.label || ''; + if (label) { + label += ': '; + } + if (context.parsed.y !== null) { + label += '$' + context.parsed.y.toFixed(4); + } + return label; + }; + } else { + chartModule.chart.options.scales.y.ticks.callback = (value) => { + return '$' + ui.formatPrice(coinSymbol, value); + }; + + chartModule.chart.options.plugins.tooltip.callbacks.label = (context) => { + let label = context.dataset.label || ''; + if (label) { + label += ': '; + } + if (context.parsed.y !== null) { + label += '$' + ui.formatPrice(coinSymbol, context.parsed.y); + } + return label; + }; + } + + if (config.currentResolution === 'day') { + chartModule.chart.options.scales.x = { + type: 'time', + time: { + unit: 'hour', + displayFormats: { + hour: 'HH:mm' + }, + tooltipFormat: 'MMM d, yyyy HH:mm' + }, + ticks: { + source: 'data', + maxTicksLimit: 24, + callback: function(value, index, values) { + const date = new Date(value); + return date.getUTCHours().toString().padStart(2, '0') + ':00'; + } + } + }; + } else { + chartModule.chart.options.scales.x = { + type: 'time', + time: { + unit: 'day', + displayFormats: { + day: 'MMM d' + }, + tooltipFormat: 'MMM d, yyyy' + }, + ticks: { + source: 'data', + maxTicksLimit: 10 + } + }; + } + + console.log('Updating chart with data:', chartData.slice(0, 5)); + chartModule.chart.update('active'); + } else { + console.error('Chart object not initialized'); + } + + chartModule.currentCoin = coinSymbol; + const loadTime = Date.now() - chartModule.loadStartTime; + ui.updateLoadTimeAndCache(loadTime, cachedData); + + } catch (error) { + console.error(`Error updating chart for ${coinSymbol}:`, error); + let errorMessage = `Failed to update chart for ${coinSymbol}`; + if (error.message) { + errorMessage += `: ${error.message}`; + } + ui.displayErrorMessage(errorMessage); + } finally { + chartModule.hideChartLoader(); + } + }, + + showChartLoader: () => { + document.getElementById('chart-loader').classList.remove('hidden'); + document.getElementById('coin-chart').classList.add('hidden'); + }, + + hideChartLoader: () => { + document.getElementById('chart-loader').classList.add('hidden'); + document.getElementById('coin-chart').classList.remove('hidden'); + } +}; + +Chart.register(chartModule.verticalLinePlugin); + + const volumeToggle = { + isVisible: localStorage.getItem('volumeToggleState') === 'true', + init: () => { + const toggleButton = document.getElementById('toggle-volume'); + if (toggleButton) { + toggleButton.addEventListener('click', volumeToggle.toggle); + volumeToggle.updateVolumeDisplay(); + } + }, + toggle: () => { + volumeToggle.isVisible = !volumeToggle.isVisible; + localStorage.setItem('volumeToggleState', volumeToggle.isVisible.toString()); + volumeToggle.updateVolumeDisplay(); + }, + updateVolumeDisplay: () => { + const volumeDivs = document.querySelectorAll('[id$="-volume-div"]'); + volumeDivs.forEach(div => { + div.style.display = volumeToggle.isVisible ? 'flex' : 'none'; + }); + const toggleButton = document.getElementById('toggle-volume'); + if (toggleButton) { + updateButtonStyles(toggleButton, volumeToggle.isVisible, 'green'); + } + } + }; + + function updateButtonStyles(button, isActive, color) { + button.classList.toggle('text-' + color + '-500', isActive); + button.classList.toggle('text-gray-600', !isActive); + button.classList.toggle('dark:text-' + color + '-400', isActive); + button.classList.toggle('dark:text-gray-400', !isActive); + } + +const app = { + btcPriceUSD: 0, + autoRefreshInterval: null, + nextRefreshTime: null, + lastRefreshedTime: null, + isAutoRefreshEnabled: localStorage.getItem('autoRefreshEnabled') === 'true', + refreshTexts: { + label: 'Auto-refresh in', + disabled: 'Auto-refresh: disabled', + justRefreshed: 'Just refreshed', + }, + + init: () => { + window.addEventListener('load', app.onLoad); + app.loadLastRefreshedTime(); + }, + + onLoad: async () => { + ui.showLoader(); + try { + volumeToggle.init(); + await app.updateBTCPrice(); + const chartContainer = document.getElementById('coin-chart'); + if (chartContainer) { + chartModule.initChart(); + chartModule.showChartLoader(); + } else { + console.warn('Chart container not found, skipping chart initialization'); + } + for (const coin of config.coins) { + await app.loadCoinData(coin); + } + if (chartModule.chart) { + config.currentResolution = 'month'; + await chartModule.updateChart('BTC'); + app.updateResolutionButtons('BTC'); + } + ui.setActiveContainer('btc-container'); + config.coins.forEach(coin => { + const container = document.getElementById(`${coin.symbol.toLowerCase()}-container`); + if (container) { + container.addEventListener('click', () => { + ui.setActiveContainer(`${coin.symbol.toLowerCase()}-container`); + if (chartModule.chart) { + if (coin.symbol === 'WOW') { + config.currentResolution = 'day'; + } + chartModule.updateChart(coin.symbol); + app.updateResolutionButtons(coin.symbol); + } + }); + } + }); + const refreshAllButton = document.getElementById('refresh-all'); + if (refreshAllButton) { + refreshAllButton.addEventListener('click', app.refreshAllData); + } + app.initializeSelectImages(); + const headers = document.querySelectorAll('th'); + headers.forEach((header, index) => { + header.addEventListener('click', () => app.sortTable(index, header.classList.contains('disabled'))); + }); + const closeErrorButton = document.getElementById('close-error'); + if (closeErrorButton) { + closeErrorButton.addEventListener('click', ui.hideErrorMessage); + } + app.initAutoRefresh(); + } catch (error) { + console.error('Error during initialization:', error); + ui.displayErrorMessage('Failed to initialize the dashboard. Please try refreshing the page.'); + } finally { + ui.hideLoader(); + if (chartModule.chart) { + chartModule.hideChartLoader(); + } + } + }, + + loadCoinData: async (coin) => { + const cacheKey = `coinData_${coin.symbol}`; + let cachedData = cache.get(cacheKey); + let data; + if (cachedData) { + data = cachedData.value; + } else { + try { + ui.showCoinLoader(coin.symbol); + if (coin.usesCoinGecko) { + data = await api.fetchCoinGeckoDataXHR(coin.symbol); + } else { + data = await api.fetchCryptoCompareDataXHR(coin.symbol); + } + if (data.error) { + throw new Error(data.error); + } + cache.set(cacheKey, data); + cachedData = null; + } catch (error) { + console.error(`Error fetching ${coin.symbol} data:`, error.message); + data = { + error: error.message + }; + } finally { + ui.hideCoinLoader(coin.symbol); + } + } + ui.displayCoinData(coin.symbol, data); + ui.updateLoadTimeAndCache(0, cachedData); + }, + + initAutoRefresh: () => { + const toggleAutoRefreshButton = document.getElementById('toggle-auto-refresh'); + if (toggleAutoRefreshButton) { + toggleAutoRefreshButton.addEventListener('click', app.toggleAutoRefresh); + app.updateAutoRefreshButton(); + } + + if (app.isAutoRefreshEnabled) { + const storedNextRefreshTime = localStorage.getItem('nextRefreshTime'); + if (storedNextRefreshTime) { + const nextRefreshTime = parseInt(storedNextRefreshTime); + if (nextRefreshTime > Date.now()) { + app.nextRefreshTime = nextRefreshTime; + app.startAutoRefresh(); + } else { + app.startAutoRefresh(true); + } + } else { + app.startAutoRefresh(true); + } + } + }, + + startAutoRefresh: (resetTimer = false) => { + app.stopAutoRefresh(); + + if (resetTimer || !app.nextRefreshTime) { + app.nextRefreshTime = Date.now() + 15 * 60 * 1000; + } + + const timeUntilNextRefresh = Math.max(0, app.nextRefreshTime - Date.now()); + + if (timeUntilNextRefresh === 0) { + app.nextRefreshTime = Date.now() + 15 * 60 * 1000; + } + + app.autoRefreshInterval = setTimeout(() => { + app.refreshAllData(); + app.startAutoRefresh(true); + }, timeUntilNextRefresh); + + localStorage.setItem('nextRefreshTime', app.nextRefreshTime.toString()); + app.updateNextRefreshTime(); + app.isAutoRefreshEnabled = true; + localStorage.setItem('autoRefreshEnabled', 'true'); + }, + + stopAutoRefresh: () => { + if (app.autoRefreshInterval) { + clearTimeout(app.autoRefreshInterval); + app.autoRefreshInterval = null; + } + app.nextRefreshTime = null; + localStorage.removeItem('nextRefreshTime'); + app.updateNextRefreshTime(); + app.isAutoRefreshEnabled = false; + localStorage.setItem('autoRefreshEnabled', 'false'); + }, + + toggleAutoRefresh: () => { + if (app.isAutoRefreshEnabled) { + app.stopAutoRefresh(); + } else { + app.startAutoRefresh(); + } + app.updateAutoRefreshButton(); + }, + + updateNextRefreshTime: () => { + const nextRefreshSpan = document.getElementById('next-refresh-time'); + const labelElement = document.getElementById('next-refresh-label'); + const valueElement = document.getElementById('next-refresh-value'); + + if (nextRefreshSpan && labelElement && valueElement) { + if (app.nextRefreshTime) { + const timeUntilRefresh = Math.max(0, Math.ceil((app.nextRefreshTime - Date.now()) / 1000)); + + if (timeUntilRefresh === 0) { + labelElement.textContent = ''; + valueElement.textContent = app.refreshTexts.justRefreshed; + } else { + const minutes = Math.floor(timeUntilRefresh / 60); + const seconds = timeUntilRefresh % 60; + labelElement.textContent = `${app.refreshTexts.label}: `; + valueElement.textContent = `${minutes}:${seconds.toString().padStart(2, '0')}`; + } + + if (timeUntilRefresh > 0) { + setTimeout(app.updateNextRefreshTime, 1000); + } + } else { + labelElement.textContent = ''; + valueElement.textContent = app.refreshTexts.disabled; + } + } + }, + + updateAutoRefreshButton: () => { + const button = document.getElementById('toggle-auto-refresh'); + if (button) { + if (app.isAutoRefreshEnabled) { + button.classList.remove('text-gray-600', 'dark:text-gray-400'); + button.classList.add('text-green-500', 'dark:text-green-400'); + } else { + button.classList.remove('text-green-500', 'dark:text-green-400'); + button.classList.add('text-gray-600', 'dark:text-gray-400'); + } + button.title = app.isAutoRefreshEnabled ? 'Disable Auto-Refresh' : 'Enable Auto-Refresh'; + + const svg = button.querySelector('svg'); + if (svg) { + if (app.isAutoRefreshEnabled) { + svg.classList.add('animate-spin'); + } else { + svg.classList.remove('animate-spin'); + } + } + } + }, + + refreshAllData: async () => { + ui.showLoader(); + chartModule.showChartLoader(); + try { + cache.clear(); + await app.updateBTCPrice(); + for (const coin of config.coins) { + await app.loadCoinData(coin); + } + if (chartModule.currentCoin) { + await chartModule.updateChart(chartModule.currentCoin, true); + } + + app.lastRefreshedTime = new Date(); + localStorage.setItem('lastRefreshedTime', app.lastRefreshedTime.getTime().toString()); + ui.updateLastRefreshedTime(); + + } catch (error) { + console.error('Error refreshing all data:', error); + ui.displayErrorMessage('Failed to refresh all data. Please try again.'); + } finally { + ui.hideLoader(); + chartModule.hideChartLoader(); + } + }, + + updateLastRefreshedTime: () => { + const lastRefreshedElement = document.getElementById('last-refreshed-time'); + if (lastRefreshedElement && app.lastRefreshedTime) { + const formattedTime = app.lastRefreshedTime.toLocaleTimeString(); + lastRefreshedElement.textContent = `Last Refreshed: ${formattedTime}`; + } + }, + + loadLastRefreshedTime: () => { + const storedTime = localStorage.getItem('lastRefreshedTime'); + if (storedTime) { + app.lastRefreshedTime = new Date(parseInt(storedTime)); + ui.updateLastRefreshedTime(); + } + }, + + updateBTCPrice: async () => { + try { + const btcData = await api.fetchCryptoCompareDataXHR('BTC'); + if (btcData.error) { + console.error('Error fetching BTC price:', btcData.error); + app.btcPriceUSD = 0; + } else if (btcData.RAW && btcData.RAW.BTC && btcData.RAW.BTC.USD) { + app.btcPriceUSD = btcData.RAW.BTC.USD.PRICE; + } else { + console.error('Unexpected BTC data structure:', btcData); + app.btcPriceUSD = 0; + } + } catch (error) { + console.error('Error fetching BTC price:', error); + app.btcPriceUSD = 0; + } + console.log('Current BTC price:', app.btcPriceUSD); + }, + + sortTable: (columnIndex) => { + const sortableColumns = [5, 6]; + if (!sortableColumns.includes(columnIndex)) return; + const table = document.querySelector('table'); + if (!table) { + console.error("Table not found for sorting."); + return; + } + const rows = Array.from(table.querySelectorAll('tbody tr')); + const sortIcon = document.getElementById(`sort-icon-${columnIndex}`); + if (!sortIcon) { + console.error("Sort icon not found."); + return; + } + const sortOrder = sortIcon.textContent === '↓' ? 1 : -1; + sortIcon.textContent = sortOrder === 1 ? '↑' : '↓'; + rows.sort((a, b) => { + const aValue = a.cells[columnIndex]?.textContent.trim() || ''; + const bValue = b.cells[columnIndex]?.textContent.trim() || ''; + return aValue.localeCompare(bValue, undefined, { + numeric: true, + sensitivity: 'base' + }) * sortOrder; + }); + const tbody = table.querySelector('tbody'); + if (tbody) { + rows.forEach(row => tbody.appendChild(row)); + } else { + console.error("Table body not found."); + } + }, + + initializeSelectImages: () => { + const updateSelectedImage = (selectId) => { + const select = document.getElementById(selectId); + const button = document.getElementById(`${selectId}_button`); + if (!select || !button) { + console.error(`Elements not found for ${selectId}`); + return; + } + const selectedOption = select.options[select.selectedIndex]; + const imageURL = selectedOption?.getAttribute('data-image'); + requestAnimationFrame(() => { + if (imageURL) { + button.style.backgroundImage = `url('${imageURL}')`; + button.style.backgroundSize = '25px 25px'; + button.style.backgroundPosition = 'center'; + button.style.backgroundRepeat = 'no-repeat'; + } else { + button.style.backgroundImage = 'none'; + } + button.style.minWidth = '25px'; + button.style.minHeight = '25px'; + }); + }; + const handleSelectChange = (event) => { + updateSelectedImage(event.target.id); + }; + ['coin_to', 'coin_from'].forEach(selectId => { + const select = document.getElementById(selectId); + if (select) { + select.addEventListener('change', handleSelectChange); + updateSelectedImage(selectId); + } else { + console.error(`Select element not found for ${selectId}`); + } + }); + }, + + updateResolutionButtons: (coinSymbol) => { + const resolutionButtons = document.querySelectorAll('.resolution-button'); + resolutionButtons.forEach(button => { + const resolution = button.id.split('-')[1]; + if (coinSymbol === 'WOW') { + if (resolution === 'day') { + button.classList.remove('text-gray-400', 'cursor-not-allowed', 'opacity-50', 'outline-none'); + button.classList.add('active'); + button.disabled = false; + } else { + button.classList.add('text-gray-400', 'cursor-not-allowed', 'opacity-50', 'outline-none'); + button.classList.remove('active'); + button.disabled = true; + } + } else { + button.classList.remove('text-gray-400', 'cursor-not-allowed', 'opacity-50', 'outline-none'); + button.classList.toggle('active', resolution === config.currentResolution); + button.disabled = false; + } + }); + }, +}; + +const resolutionButtons = document.querySelectorAll('.resolution-button'); +resolutionButtons.forEach(button => { + button.addEventListener('click', () => { + const resolution = button.id.split('-')[1]; + const currentCoin = chartModule.currentCoin; + + if (currentCoin !== 'WOW' || resolution === 'day') { + config.currentResolution = resolution; + chartModule.updateChart(currentCoin, true); + app.updateResolutionButtons(currentCoin); + } + }); +}); + +app.init(); diff --git a/basicswap/templates/header.html b/basicswap/templates/header.html index 290e4fa..377a7cb 100644 --- a/basicswap/templates/header.html +++ b/basicswap/templates/header.html @@ -7,6 +7,7 @@ {% endif %} + @@ -91,6 +92,8 @@ document.addEventListener('DOMContentLoaded', function() { const shutdownButtons = document.querySelectorAll('.shutdown-button'); const shutdownModal = document.getElementById('shutdownModal'); const closeModalButton = document.getElementById('closeShutdownModal'); + const confirmShutdownButton = document.getElementById('confirmShutdown'); + const shutdownWarning = document.getElementById('shutdownWarning'); function updateShutdownButtons() { const activeSwaps = parseInt(shutdownButtons[0].getAttribute('data-active-swaps') || '0'); @@ -99,7 +102,7 @@ document.addEventListener('DOMContentLoaded', function() { if (activeSwaps > 0) { button.classList.add('shutdown-disabled'); button.setAttribute('data-disabled', 'true'); - button.setAttribute('title', 'Cannot shutdown while swaps are in progress'); + button.setAttribute('title', 'Caution: Swaps in progress'); } else { button.classList.remove('shutdown-disabled'); button.removeAttribute('data-disabled'); @@ -109,6 +112,14 @@ document.addEventListener('DOMContentLoaded', function() { } function showShutdownModal() { + const activeSwaps = parseInt(shutdownButtons[0].getAttribute('data-active-swaps') || '0'); + if (activeSwaps > 0) { + shutdownWarning.classList.remove('hidden'); + confirmShutdownButton.textContent = 'Yes, Shut Down Anyway'; + } else { + shutdownWarning.classList.add('hidden'); + confirmShutdownButton.textContent = 'Yes, Shut Down'; + } shutdownModal.classList.remove('hidden'); document.body.style.overflow = 'hidden'; } @@ -120,15 +131,18 @@ document.addEventListener('DOMContentLoaded', function() { shutdownButtons.forEach(button => { button.addEventListener('click', function(e) { - if (this.hasAttribute('data-disabled')) { - e.preventDefault(); - showShutdownModal(); - } + e.preventDefault(); + showShutdownModal(); }); }); closeModalButton.addEventListener('click', hideShutdownModal); + confirmShutdownButton.addEventListener('click', function() { + const shutdownToken = document.querySelector('.shutdown-button').getAttribute('href').split('/').pop(); + window.location.href = '/shutdown/' + shutdownToken; + }); + shutdownModal.addEventListener('click', function(e) { if (e.target === this) { hideShutdownModal(); @@ -236,18 +250,23 @@ document.addEventListener('DOMContentLoaded', function() { - + + {% include 'inc_messages.html' %} + {% if show_chart %} +
-
-
- {% set coin_data = { - 'BTC': {'name': 'Bitcoin', 'symbol': 'BTC', 'image': 'Bitcoin.png'}, - 'XMR': {'name': 'Monero', 'symbol': 'XMR', 'image': 'Monero.png'}, - 'PART': {'name': 'Particl', 'symbol': 'PART', 'image': 'Particl.png'}, - 'LTC': {'name': 'Litecoin', 'symbol': 'LTC', 'image': 'Litecoin.png'}, - 'FIRO': {'name': 'Firo', 'symbol': 'FIRO', 'image': 'Firo.png'}, - 'PIVX': {'name': 'PIVX', 'symbol': 'PIVX', 'image': 'PIVX.png'}, - 'DASH': {'name': 'Dash', 'symbol': 'DASH', 'image': 'Dash.png'}, - 'ETH': {'name': 'Ethereum', 'symbol': 'ETH', 'image': 'Ethereum.png'}, - 'DOGE': {'name': 'Dogecoin', 'symbol': 'DOGE', 'image': 'Doge.png'}, - 'DCR': {'name': 'Decred', 'symbol': 'DCR', 'image': 'Decred.png'}, - 'ZANO': {'name': 'Zano', 'symbol': 'ZANO', 'image': 'Zano.png'}, - 'WOW': {'name': 'Wownero', 'symbol': 'WOW', 'image': 'Wownero.png'} - } %} - {% for coin_symbol, coin in coin_data.items() if coin_symbol in enabled_chart_coins %} -
-
-
- {{ coin.name }} -

- {{ coin.name }} {% if coin.symbol != coin.name %}({{ coin.symbol }}){% endif %} -

-
- -
- - -

- -

-
-
-
-
-
- {% if coin_symbol != 'BTC' %} -
- BTC: - -
- {% endif %} -
- VOL: -
-
-
-
-
- {% endfor %} +
+
+ {% set coin_data = { + 'BTC': {'name': 'Bitcoin', 'symbol': 'BTC', 'image': 'Bitcoin.png', 'show': true}, + 'XMR': {'name': 'Monero', 'symbol': 'XMR', 'image': 'Monero.png', 'show': true}, + 'PART': {'name': 'Particl', 'symbol': 'PART', 'image': 'Particl.png', 'show': true}, + 'LTC': {'name': 'Litecoin', 'symbol': 'LTC', 'image': 'Litecoin.png', 'show': true}, + 'FIRO': {'name': 'Firo', 'symbol': 'FIRO', 'image': 'Firo.png', 'show': true}, + 'PIVX': {'name': 'PIVX', 'symbol': 'PIVX', 'image': 'PIVX.png', 'show': true}, + 'DASH': {'name': 'Dash', 'symbol': 'DASH', 'image': 'Dash.png', 'show': true}, + 'ETH': {'name': 'Ethereum', 'symbol': 'ETH', 'image': 'Ethereum.png', 'show': false}, + 'DOGE': {'name': 'Dogecoin', 'symbol': 'DOGE', 'image': 'Doge.png', 'show': false}, + 'DCR': {'name': 'Decred', 'symbol': 'DCR', 'image': 'Decred.png', 'show': true}, + 'ZANO': {'name': 'Zano', 'symbol': 'ZANO', 'image': 'Zano.png', 'show': false}, + 'BCH': {'name': 'BCH', 'symbol': 'BCH', 'image': 'Bitcoin-cash.png', 'show': true}, + 'WOW': {'name': 'Wownero', 'symbol': 'WOW', 'image': 'Wownero.png', 'show': true} +} %} +{% for coin_symbol, coin in coin_data.items() if coin_symbol in enabled_chart_coins and coin.show %} +
+
+
+ {{ coin.name }} +

+ {{ coin.name }} {% if coin.symbol != coin.name %}({{ coin.symbol }}) + {% endif %} +

+
+
+ +

+

+
+
+
+
+
+ {% if coin_symbol != 'BTC' %} +
+ BTC:
{% endif %}
+ VOL: +
+
+
-
+
+ {% endfor %} +
+
{% endif %} - + +
-
-
-
-
-
-
-
-
-
- - +
+
+
+
+ +
+
+
+
+
+
@@ -1293,7 +222,6 @@ app.init();
-

{{ arrow_right_svg | safe }}

@@ -1314,7 +242,6 @@ app.init();
-

Sort By:

@@ -1339,7 +266,6 @@ app.init();
- - {% if sent_offers %}
@@ -1374,304 +299,149 @@ app.init();
{% endif %} - - +
-
-
+ -
-
-
-
-
- - - - - - - - - - - - - - - {% for o in offers %} - - - - - - {% if o[9] == true %} - - - - - - - {% else %} - - - - - - - {% endif %} - - - - - - - - - - - - - - - - - -{% endfor %} - -
-
- Time -
-
-
- Max send -
-
-
- Swap -
-
-
- Max Recv -
-
-
- Rate - -
-
-
- Market +/- - -
-
-
- Trade -
-
-
- - - - - - - -
-
- - - {{ o[5]|truncate(7, true, '', 0) }} -
{{ o[3] }}
-
-
- -
- -
- - {{ o[3] }} - - {{ arrow_right_svg | safe }} - - {{ o[2] }} - -
-
-
- - - {{ o[4]|truncate(7, true, '', 0) }} -
{{ o[2] }}
-
-
- -
- - - {{ o[5]|truncate(7, true, '', 0) }} -
{{ o[3] }}
-
- -
-
- -
- - {{ o[3] }} - - {{ arrow_right_svg | safe }} - - {{ o[2] }} - -
-
-
- - - {{ o[4]|truncate(7, true, '', 0) }} -
{{ o[2] }}
-
-
- -
- - - /{{ o[16] }} -
{{ o[6]|truncate(10,true,'',0) }} {{ o[17] }}/{{ o[16] }}
-
-
-
- -
- - +
+
+ +
+
+
+
+ +
-
-
-
- {% if filters.page_no > 1 %} -
- -
- {% endif %} -
-
-

Page: {{ filters.page_no }}

-
-
- {% if offers_count > 20 %} -
- -
- {% endif %} -
-
-
-
- +
+
+ + + + + + + + +
+ +
+
+
+
+
+ + + + + + + + + + + + + + + + +
+
+ Time +
+
+
+ Max send +
+
+
+ Swap +
+
+
+ Max Recv +
+
+
+ Rate + +
+
+
+ Market +/- + +
+
+
+ Trade +
+
+
+
+
+ +
+
+
+
+

Last refreshed: + Never +

+

Listings: + +

+

Next refresh: + +

+

Full refresh: + +

+
+
+ +

Page:1 of 1

+ +
-
- -{% include 'footer.html' %} - - + + + + + + + {% include 'footer.html' %}