diff --git a/basicswap/static/js/dropdown.js b/basicswap/static/js/dropdown.js new file mode 100644 index 0000000..6fdf12a --- /dev/null +++ b/basicswap/static/js/dropdown.js @@ -0,0 +1,190 @@ +(function(window) { + 'use strict'; + + function positionElement(targetEl, triggerEl, placement = 'bottom', offsetDistance = 8) { + targetEl.style.visibility = 'hidden'; + targetEl.style.display = 'block'; + + const triggerRect = triggerEl.getBoundingClientRect(); + const targetRect = targetEl.getBoundingClientRect(); + const scrollLeft = window.pageXOffset || document.documentElement.scrollLeft; + const scrollTop = window.pageYOffset || document.documentElement.scrollTop; + + let top, left; + + top = triggerRect.bottom + offsetDistance; + left = triggerRect.left + (triggerRect.width - targetRect.width) / 2; + + switch (placement) { + case 'bottom-start': + left = triggerRect.left; + break; + case 'bottom-end': + left = triggerRect.right - targetRect.width; + break; + } + + const viewport = { + width: window.innerWidth, + height: window.innerHeight + }; + + if (left < 10) left = 10; + if (left + targetRect.width > viewport.width - 10) { + left = viewport.width - targetRect.width - 10; + } + + targetEl.style.position = 'fixed'; + targetEl.style.top = `${Math.round(top)}px`; + targetEl.style.left = `${Math.round(left)}px`; + targetEl.style.margin = '0'; + targetEl.style.maxHeight = `${viewport.height - top - 10}px`; + targetEl.style.overflow = 'auto'; + targetEl.style.visibility = 'visible'; + } + + class Dropdown { + constructor(targetEl, triggerEl, options = {}) { + this._targetEl = targetEl; + this._triggerEl = triggerEl; + this._options = { + placement: options.placement || 'bottom', + offset: options.offset || 5, + onShow: options.onShow || function() {}, + onHide: options.onHide || function() {} + }; + this._visible = false; + this._initialized = false; + this._handleScroll = this._handleScroll.bind(this); + this._handleResize = this._handleResize.bind(this); + this._handleOutsideClick = this._handleOutsideClick.bind(this); + this.init(); + } + + init() { + if (!this._initialized) { + this._targetEl.style.margin = '0'; + this._targetEl.style.display = 'none'; + this._targetEl.style.position = 'fixed'; + this._targetEl.style.zIndex = '50'; + + this._setupEventListeners(); + this._initialized = true; + } + } + + _setupEventListeners() { + this._triggerEl.addEventListener('click', (e) => { + e.stopPropagation(); + this.toggle(); + }); + + document.addEventListener('click', this._handleOutsideClick); + document.addEventListener('keydown', (e) => { + if (e.key === 'Escape') this.hide(); + }); + window.addEventListener('scroll', this._handleScroll, true); + window.addEventListener('resize', this._handleResize); + } + + _handleScroll() { + if (this._visible) { + requestAnimationFrame(() => { + positionElement( + this._targetEl, + this._triggerEl, + this._options.placement, + this._options.offset + ); + }); + } + } + + _handleResize() { + if (this._visible) { + requestAnimationFrame(() => { + positionElement( + this._targetEl, + this._triggerEl, + this._options.placement, + this._options.offset + ); + }); + } + } + + _handleOutsideClick(e) { + if (this._visible && + !this._targetEl.contains(e.target) && + !this._triggerEl.contains(e.target)) { + this.hide(); + } + } + + show() { + if (!this._visible) { + this._targetEl.style.display = 'block'; + this._targetEl.style.visibility = 'hidden'; + + requestAnimationFrame(() => { + positionElement( + this._targetEl, + this._triggerEl, + this._options.placement, + this._options.offset + ); + + this._visible = true; + this._options.onShow(); + }); + } + } + + hide() { + if (this._visible) { + this._targetEl.style.display = 'none'; + this._visible = false; + this._options.onHide(); + } + } + + toggle() { + if (this._visible) { + this.hide(); + } else { + this.show(); + } + } + + destroy() { + document.removeEventListener('click', this._handleOutsideClick); + window.removeEventListener('scroll', this._handleScroll, true); + window.removeEventListener('resize', this._handleResize); + this._initialized = false; + } + } + + function initDropdowns() { + document.querySelectorAll('[data-dropdown-toggle]').forEach(triggerEl => { + const targetId = triggerEl.getAttribute('data-dropdown-toggle'); + const targetEl = document.getElementById(targetId); + + if (targetEl) { + const placement = triggerEl.getAttribute('data-dropdown-placement'); + new Dropdown(targetEl, triggerEl, { + placement: placement || 'bottom-start' + }); + } + }); + } + + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', initDropdowns); + } else { + initDropdowns(); + } + + window.Dropdown = Dropdown; + window.initDropdowns = initDropdowns; + +})(window); diff --git a/basicswap/static/js/libs/flowbite.js b/basicswap/static/js/libs/flowbite.js deleted file mode 100644 index e2c52c2..0000000 --- a/basicswap/static/js/libs/flowbite.js +++ /dev/null @@ -1,2 +0,0 @@ -!function(t,e){"object"==typeof exports&&"object"==typeof module?module.exports=e():"function"==typeof define&&define.amd?define("Flowbite",[],e):"object"==typeof exports?exports.Flowbite=e():t.Flowbite=e()}(self,(function(){return function(){"use strict";var t={647:function(t,e,i){i.r(e)},853:function(t,e,i){i.r(e),i.d(e,{afterMain:function(){return w},afterRead:function(){return y},afterWrite:function(){return O},applyStyles:function(){return P},arrow:function(){return Q},auto:function(){return a},basePlacements:function(){return c},beforeMain:function(){return b},beforeRead:function(){return _},beforeWrite:function(){return L},bottom:function(){return o},clippingParents:function(){return u},computeStyles:function(){return it},createPopper:function(){return Pt},createPopperBase:function(){return Ht},createPopperLite:function(){return St},detectOverflow:function(){return mt},end:function(){return l},eventListeners:function(){return ot},flip:function(){return yt},hide:function(){return wt},left:function(){return s},main:function(){return E},modifierPhases:function(){return k},offset:function(){return Lt},placements:function(){return g},popper:function(){return h},popperGenerator:function(){return Tt},popperOffsets:function(){return It},preventOverflow:function(){return Ot},read:function(){return m},reference:function(){return f},right:function(){return r},start:function(){return d},top:function(){return n},variationPlacements:function(){return v},viewport:function(){return p},write:function(){return I}});var n="top",o="bottom",r="right",s="left",a="auto",c=[n,o,r,s],d="start",l="end",u="clippingParents",p="viewport",h="popper",f="reference",v=c.reduce((function(t,e){return t.concat([e+"-"+d,e+"-"+l])}),[]),g=[].concat(c,[a]).reduce((function(t,e){return t.concat([e,e+"-"+d,e+"-"+l])}),[]),_="beforeRead",m="read",y="afterRead",b="beforeMain",E="main",w="afterMain",L="beforeWrite",I="write",O="afterWrite",k=[_,m,y,b,E,w,L,I,O];function x(t){return t?(t.nodeName||"").toLowerCase():null}function A(t){if(null==t)return window;if("[object Window]"!==t.toString()){var e=t.ownerDocument;return e&&e.defaultView||window}return t}function C(t){return t instanceof A(t).Element||t instanceof Element}function T(t){return t instanceof A(t).HTMLElement||t instanceof HTMLElement}function H(t){return"undefined"!=typeof ShadowRoot&&(t instanceof A(t).ShadowRoot||t instanceof ShadowRoot)}var P={name:"applyStyles",enabled:!0,phase:"write",fn:function(t){var e=t.state;Object.keys(e.elements).forEach((function(t){var i=e.styles[t]||{},n=e.attributes[t]||{},o=e.elements[t];T(o)&&x(o)&&(Object.assign(o.style,i),Object.keys(n).forEach((function(t){var e=n[t];!1===e?o.removeAttribute(t):o.setAttribute(t,!0===e?"":e)})))}))},effect:function(t){var e=t.state,i={popper:{position:e.options.strategy,left:"0",top:"0",margin:"0"},arrow:{position:"absolute"},reference:{}};return Object.assign(e.elements.popper.style,i.popper),e.styles=i,e.elements.arrow&&Object.assign(e.elements.arrow.style,i.arrow),function(){Object.keys(e.elements).forEach((function(t){var n=e.elements[t],o=e.attributes[t]||{},r=Object.keys(e.styles.hasOwnProperty(t)?e.styles[t]:i[t]).reduce((function(t,e){return t[e]="",t}),{});T(n)&&x(n)&&(Object.assign(n.style,r),Object.keys(o).forEach((function(t){n.removeAttribute(t)})))}))}},requires:["computeStyles"]};function S(t){return t.split("-")[0]}var j=Math.max,D=Math.min,z=Math.round;function M(){var t=navigator.userAgentData;return null!=t&&t.brands?t.brands.map((function(t){return t.brand+"/"+t.version})).join(" "):navigator.userAgent}function q(){return!/^((?!chrome|android).)*safari/i.test(M())}function V(t,e,i){void 0===e&&(e=!1),void 0===i&&(i=!1);var n=t.getBoundingClientRect(),o=1,r=1;e&&T(t)&&(o=t.offsetWidth>0&&z(n.width)/t.offsetWidth||1,r=t.offsetHeight>0&&z(n.height)/t.offsetHeight||1);var s=(C(t)?A(t):window).visualViewport,a=!q()&&i,c=(n.left+(a&&s?s.offsetLeft:0))/o,d=(n.top+(a&&s?s.offsetTop:0))/r,l=n.width/o,u=n.height/r;return{width:l,height:u,top:d,right:c+l,bottom:d+u,left:c,x:c,y:d}}function B(t){var e=V(t),i=t.offsetWidth,n=t.offsetHeight;return Math.abs(e.width-i)<=1&&(i=e.width),Math.abs(e.height-n)<=1&&(n=e.height),{x:t.offsetLeft,y:t.offsetTop,width:i,height:n}}function R(t,e){var i=e.getRootNode&&e.getRootNode();if(t.contains(e))return!0;if(i&&H(i)){var n=e;do{if(n&&t.isSameNode(n))return!0;n=n.parentNode||n.host}while(n)}return!1}function W(t){return A(t).getComputedStyle(t)}function F(t){return["table","td","th"].indexOf(x(t))>=0}function K(t){return((C(t)?t.ownerDocument:t.document)||window.document).documentElement}function N(t){return"html"===x(t)?t:t.assignedSlot||t.parentNode||(H(t)?t.host:null)||K(t)}function U(t){return T(t)&&"fixed"!==W(t).position?t.offsetParent:null}function X(t){for(var e=A(t),i=U(t);i&&F(i)&&"static"===W(i).position;)i=U(i);return i&&("html"===x(i)||"body"===x(i)&&"static"===W(i).position)?e:i||function(t){var e=/firefox/i.test(M());if(/Trident/i.test(M())&&T(t)&&"fixed"===W(t).position)return null;var i=N(t);for(H(i)&&(i=i.host);T(i)&&["html","body"].indexOf(x(i))<0;){var n=W(i);if("none"!==n.transform||"none"!==n.perspective||"paint"===n.contain||-1!==["transform","perspective"].indexOf(n.willChange)||e&&"filter"===n.willChange||e&&n.filter&&"none"!==n.filter)return i;i=i.parentNode}return null}(t)||e}function Y(t){return["top","bottom"].indexOf(t)>=0?"x":"y"}function G(t,e,i){return j(t,D(e,i))}function $(t){return Object.assign({},{top:0,right:0,bottom:0,left:0},t)}function J(t,e){return e.reduce((function(e,i){return e[i]=t,e}),{})}var Q={name:"arrow",enabled:!0,phase:"main",fn:function(t){var e,i=t.state,a=t.name,d=t.options,l=i.elements.arrow,u=i.modifiersData.popperOffsets,p=S(i.placement),h=Y(p),f=[s,r].indexOf(p)>=0?"height":"width";if(l&&u){var v=function(t,e){return $("number"!=typeof(t="function"==typeof t?t(Object.assign({},e.rects,{placement:e.placement})):t)?t:J(t,c))}(d.padding,i),g=B(l),_="y"===h?n:s,m="y"===h?o:r,y=i.rects.reference[f]+i.rects.reference[h]-u[h]-i.rects.popper[f],b=u[h]-i.rects.reference[h],E=X(l),w=E?"y"===h?E.clientHeight||0:E.clientWidth||0:0,L=y/2-b/2,I=v[_],O=w-g[f]-v[m],k=w/2-g[f]/2+L,x=G(I,k,O),A=h;i.modifiersData[a]=((e={})[A]=x,e.centerOffset=x-k,e)}},effect:function(t){var e=t.state,i=t.options.element,n=void 0===i?"[data-popper-arrow]":i;null!=n&&("string"!=typeof n||(n=e.elements.popper.querySelector(n)))&&R(e.elements.popper,n)&&(e.elements.arrow=n)},requires:["popperOffsets"],requiresIfExists:["preventOverflow"]};function Z(t){return t.split("-")[1]}var tt={top:"auto",right:"auto",bottom:"auto",left:"auto"};function et(t){var e,i=t.popper,a=t.popperRect,c=t.placement,d=t.variation,u=t.offsets,p=t.position,h=t.gpuAcceleration,f=t.adaptive,v=t.roundOffsets,g=t.isFixed,_=u.x,m=void 0===_?0:_,y=u.y,b=void 0===y?0:y,E="function"==typeof v?v({x:m,y:b}):{x:m,y:b};m=E.x,b=E.y;var w=u.hasOwnProperty("x"),L=u.hasOwnProperty("y"),I=s,O=n,k=window;if(f){var x=X(i),C="clientHeight",T="clientWidth";if(x===A(i)&&"static"!==W(x=K(i)).position&&"absolute"===p&&(C="scrollHeight",T="scrollWidth"),c===n||(c===s||c===r)&&d===l)O=o,b-=(g&&x===k&&k.visualViewport?k.visualViewport.height:x[C])-a.height,b*=h?1:-1;if(c===s||(c===n||c===o)&&d===l)I=r,m-=(g&&x===k&&k.visualViewport?k.visualViewport.width:x[T])-a.width,m*=h?1:-1}var H,P=Object.assign({position:p},f&&tt),S=!0===v?function(t){var e=t.x,i=t.y,n=window.devicePixelRatio||1;return{x:z(e*n)/n||0,y:z(i*n)/n||0}}({x:m,y:b}):{x:m,y:b};return m=S.x,b=S.y,h?Object.assign({},P,((H={})[O]=L?"0":"",H[I]=w?"0":"",H.transform=(k.devicePixelRatio||1)<=1?"translate("+m+"px, "+b+"px)":"translate3d("+m+"px, "+b+"px, 0)",H)):Object.assign({},P,((e={})[O]=L?b+"px":"",e[I]=w?m+"px":"",e.transform="",e))}var it={name:"computeStyles",enabled:!0,phase:"beforeWrite",fn:function(t){var e=t.state,i=t.options,n=i.gpuAcceleration,o=void 0===n||n,r=i.adaptive,s=void 0===r||r,a=i.roundOffsets,c=void 0===a||a,d={placement:S(e.placement),variation:Z(e.placement),popper:e.elements.popper,popperRect:e.rects.popper,gpuAcceleration:o,isFixed:"fixed"===e.options.strategy};null!=e.modifiersData.popperOffsets&&(e.styles.popper=Object.assign({},e.styles.popper,et(Object.assign({},d,{offsets:e.modifiersData.popperOffsets,position:e.options.strategy,adaptive:s,roundOffsets:c})))),null!=e.modifiersData.arrow&&(e.styles.arrow=Object.assign({},e.styles.arrow,et(Object.assign({},d,{offsets:e.modifiersData.arrow,position:"absolute",adaptive:!1,roundOffsets:c})))),e.attributes.popper=Object.assign({},e.attributes.popper,{"data-popper-placement":e.placement})},data:{}},nt={passive:!0};var ot={name:"eventListeners",enabled:!0,phase:"write",fn:function(){},effect:function(t){var e=t.state,i=t.instance,n=t.options,o=n.scroll,r=void 0===o||o,s=n.resize,a=void 0===s||s,c=A(e.elements.popper),d=[].concat(e.scrollParents.reference,e.scrollParents.popper);return r&&d.forEach((function(t){t.addEventListener("scroll",i.update,nt)})),a&&c.addEventListener("resize",i.update,nt),function(){r&&d.forEach((function(t){t.removeEventListener("scroll",i.update,nt)})),a&&c.removeEventListener("resize",i.update,nt)}},data:{}},rt={left:"right",right:"left",bottom:"top",top:"bottom"};function st(t){return t.replace(/left|right|bottom|top/g,(function(t){return rt[t]}))}var at={start:"end",end:"start"};function ct(t){return t.replace(/start|end/g,(function(t){return at[t]}))}function dt(t){var e=A(t);return{scrollLeft:e.pageXOffset,scrollTop:e.pageYOffset}}function lt(t){return V(K(t)).left+dt(t).scrollLeft}function ut(t){var e=W(t),i=e.overflow,n=e.overflowX,o=e.overflowY;return/auto|scroll|overlay|hidden/.test(i+o+n)}function pt(t){return["html","body","#document"].indexOf(x(t))>=0?t.ownerDocument.body:T(t)&&ut(t)?t:pt(N(t))}function ht(t,e){var i;void 0===e&&(e=[]);var n=pt(t),o=n===(null==(i=t.ownerDocument)?void 0:i.body),r=A(n),s=o?[r].concat(r.visualViewport||[],ut(n)?n:[]):n,a=e.concat(s);return o?a:a.concat(ht(N(s)))}function ft(t){return Object.assign({},t,{left:t.x,top:t.y,right:t.x+t.width,bottom:t.y+t.height})}function vt(t,e,i){return e===p?ft(function(t,e){var i=A(t),n=K(t),o=i.visualViewport,r=n.clientWidth,s=n.clientHeight,a=0,c=0;if(o){r=o.width,s=o.height;var d=q();(d||!d&&"fixed"===e)&&(a=o.offsetLeft,c=o.offsetTop)}return{width:r,height:s,x:a+lt(t),y:c}}(t,i)):C(e)?function(t,e){var i=V(t,!1,"fixed"===e);return i.top=i.top+t.clientTop,i.left=i.left+t.clientLeft,i.bottom=i.top+t.clientHeight,i.right=i.left+t.clientWidth,i.width=t.clientWidth,i.height=t.clientHeight,i.x=i.left,i.y=i.top,i}(e,i):ft(function(t){var e,i=K(t),n=dt(t),o=null==(e=t.ownerDocument)?void 0:e.body,r=j(i.scrollWidth,i.clientWidth,o?o.scrollWidth:0,o?o.clientWidth:0),s=j(i.scrollHeight,i.clientHeight,o?o.scrollHeight:0,o?o.clientHeight:0),a=-n.scrollLeft+lt(t),c=-n.scrollTop;return"rtl"===W(o||i).direction&&(a+=j(i.clientWidth,o?o.clientWidth:0)-r),{width:r,height:s,x:a,y:c}}(K(t)))}function gt(t,e,i,n){var o="clippingParents"===e?function(t){var e=ht(N(t)),i=["absolute","fixed"].indexOf(W(t).position)>=0&&T(t)?X(t):t;return C(i)?e.filter((function(t){return C(t)&&R(t,i)&&"body"!==x(t)})):[]}(t):[].concat(e),r=[].concat(o,[i]),s=r[0],a=r.reduce((function(e,i){var o=vt(t,i,n);return e.top=j(o.top,e.top),e.right=D(o.right,e.right),e.bottom=D(o.bottom,e.bottom),e.left=j(o.left,e.left),e}),vt(t,s,n));return a.width=a.right-a.left,a.height=a.bottom-a.top,a.x=a.left,a.y=a.top,a}function _t(t){var e,i=t.reference,a=t.element,c=t.placement,u=c?S(c):null,p=c?Z(c):null,h=i.x+i.width/2-a.width/2,f=i.y+i.height/2-a.height/2;switch(u){case n:e={x:h,y:i.y-a.height};break;case o:e={x:h,y:i.y+i.height};break;case r:e={x:i.x+i.width,y:f};break;case s:e={x:i.x-a.width,y:f};break;default:e={x:i.x,y:i.y}}var v=u?Y(u):null;if(null!=v){var g="y"===v?"height":"width";switch(p){case d:e[v]=e[v]-(i[g]/2-a[g]/2);break;case l:e[v]=e[v]+(i[g]/2-a[g]/2)}}return e}function mt(t,e){void 0===e&&(e={});var i=e,s=i.placement,a=void 0===s?t.placement:s,d=i.strategy,l=void 0===d?t.strategy:d,v=i.boundary,g=void 0===v?u:v,_=i.rootBoundary,m=void 0===_?p:_,y=i.elementContext,b=void 0===y?h:y,E=i.altBoundary,w=void 0!==E&&E,L=i.padding,I=void 0===L?0:L,O=$("number"!=typeof I?I:J(I,c)),k=b===h?f:h,x=t.rects.popper,A=t.elements[w?k:b],T=gt(C(A)?A:A.contextElement||K(t.elements.popper),g,m,l),H=V(t.elements.reference),P=_t({reference:H,element:x,strategy:"absolute",placement:a}),S=ft(Object.assign({},x,P)),j=b===h?S:H,D={top:T.top-j.top+O.top,bottom:j.bottom-T.bottom+O.bottom,left:T.left-j.left+O.left,right:j.right-T.right+O.right},z=t.modifiersData.offset;if(b===h&&z){var M=z[a];Object.keys(D).forEach((function(t){var e=[r,o].indexOf(t)>=0?1:-1,i=[n,o].indexOf(t)>=0?"y":"x";D[t]+=M[i]*e}))}return D}var yt={name:"flip",enabled:!0,phase:"main",fn:function(t){var e=t.state,i=t.options,l=t.name;if(!e.modifiersData[l]._skip){for(var u=i.mainAxis,p=void 0===u||u,h=i.altAxis,f=void 0===h||h,_=i.fallbackPlacements,m=i.padding,y=i.boundary,b=i.rootBoundary,E=i.altBoundary,w=i.flipVariations,L=void 0===w||w,I=i.allowedAutoPlacements,O=e.options.placement,k=S(O),x=_||(k===O||!L?[st(O)]:function(t){if(S(t)===a)return[];var e=st(t);return[ct(t),e,ct(e)]}(O)),A=[O].concat(x).reduce((function(t,i){return t.concat(S(i)===a?function(t,e){void 0===e&&(e={});var i=e,n=i.placement,o=i.boundary,r=i.rootBoundary,s=i.padding,a=i.flipVariations,d=i.allowedAutoPlacements,l=void 0===d?g:d,u=Z(n),p=u?a?v:v.filter((function(t){return Z(t)===u})):c,h=p.filter((function(t){return l.indexOf(t)>=0}));0===h.length&&(h=p);var f=h.reduce((function(e,i){return e[i]=mt(t,{placement:i,boundary:o,rootBoundary:r,padding:s})[S(i)],e}),{});return Object.keys(f).sort((function(t,e){return f[t]-f[e]}))}(e,{placement:i,boundary:y,rootBoundary:b,padding:m,flipVariations:L,allowedAutoPlacements:I}):i)}),[]),C=e.rects.reference,T=e.rects.popper,H=new Map,P=!0,j=A[0],D=0;D=0,B=V?"width":"height",R=mt(e,{placement:z,boundary:y,rootBoundary:b,altBoundary:E,padding:m}),W=V?q?r:s:q?o:n;C[B]>T[B]&&(W=st(W));var F=st(W),K=[];if(p&&K.push(R[M]<=0),f&&K.push(R[W]<=0,R[F]<=0),K.every((function(t){return t}))){j=z,P=!1;break}H.set(z,K)}if(P)for(var N=function(t){var e=A.find((function(e){var i=H.get(e);if(i)return i.slice(0,t).every((function(t){return t}))}));if(e)return j=e,"break"},U=L?3:1;U>0;U--){if("break"===N(U))break}e.placement!==j&&(e.modifiersData[l]._skip=!0,e.placement=j,e.reset=!0)}},requiresIfExists:["offset"],data:{_skip:!1}};function bt(t,e,i){return void 0===i&&(i={x:0,y:0}),{top:t.top-e.height-i.y,right:t.right-e.width+i.x,bottom:t.bottom-e.height+i.y,left:t.left-e.width-i.x}}function Et(t){return[n,r,o,s].some((function(e){return t[e]>=0}))}var wt={name:"hide",enabled:!0,phase:"main",requiresIfExists:["preventOverflow"],fn:function(t){var e=t.state,i=t.name,n=e.rects.reference,o=e.rects.popper,r=e.modifiersData.preventOverflow,s=mt(e,{elementContext:"reference"}),a=mt(e,{altBoundary:!0}),c=bt(s,n),d=bt(a,o,r),l=Et(c),u=Et(d);e.modifiersData[i]={referenceClippingOffsets:c,popperEscapeOffsets:d,isReferenceHidden:l,hasPopperEscaped:u},e.attributes.popper=Object.assign({},e.attributes.popper,{"data-popper-reference-hidden":l,"data-popper-escaped":u})}};var Lt={name:"offset",enabled:!0,phase:"main",requires:["popperOffsets"],fn:function(t){var e=t.state,i=t.options,o=t.name,a=i.offset,c=void 0===a?[0,0]:a,d=g.reduce((function(t,i){return t[i]=function(t,e,i){var o=S(t),a=[s,n].indexOf(o)>=0?-1:1,c="function"==typeof i?i(Object.assign({},e,{placement:t})):i,d=c[0],l=c[1];return d=d||0,l=(l||0)*a,[s,r].indexOf(o)>=0?{x:l,y:d}:{x:d,y:l}}(i,e.rects,c),t}),{}),l=d[e.placement],u=l.x,p=l.y;null!=e.modifiersData.popperOffsets&&(e.modifiersData.popperOffsets.x+=u,e.modifiersData.popperOffsets.y+=p),e.modifiersData[o]=d}};var It={name:"popperOffsets",enabled:!0,phase:"read",fn:function(t){var e=t.state,i=t.name;e.modifiersData[i]=_t({reference:e.rects.reference,element:e.rects.popper,strategy:"absolute",placement:e.placement})},data:{}};var Ot={name:"preventOverflow",enabled:!0,phase:"main",fn:function(t){var e=t.state,i=t.options,a=t.name,c=i.mainAxis,l=void 0===c||c,u=i.altAxis,p=void 0!==u&&u,h=i.boundary,f=i.rootBoundary,v=i.altBoundary,g=i.padding,_=i.tether,m=void 0===_||_,y=i.tetherOffset,b=void 0===y?0:y,E=mt(e,{boundary:h,rootBoundary:f,padding:g,altBoundary:v}),w=S(e.placement),L=Z(e.placement),I=!L,O=Y(w),k="x"===O?"y":"x",x=e.modifiersData.popperOffsets,A=e.rects.reference,C=e.rects.popper,T="function"==typeof b?b(Object.assign({},e.rects,{placement:e.placement})):b,H="number"==typeof T?{mainAxis:T,altAxis:T}:Object.assign({mainAxis:0,altAxis:0},T),P=e.modifiersData.offset?e.modifiersData.offset[e.placement]:null,z={x:0,y:0};if(x){if(l){var M,q="y"===O?n:s,V="y"===O?o:r,R="y"===O?"height":"width",W=x[O],F=W+E[q],K=W-E[V],N=m?-C[R]/2:0,U=L===d?A[R]:C[R],$=L===d?-C[R]:-A[R],J=e.elements.arrow,Q=m&&J?B(J):{width:0,height:0},tt=e.modifiersData["arrow#persistent"]?e.modifiersData["arrow#persistent"].padding:{top:0,right:0,bottom:0,left:0},et=tt[q],it=tt[V],nt=G(0,A[R],Q[R]),ot=I?A[R]/2-N-nt-et-H.mainAxis:U-nt-et-H.mainAxis,rt=I?-A[R]/2+N+nt+it+H.mainAxis:$+nt+it+H.mainAxis,st=e.elements.arrow&&X(e.elements.arrow),at=st?"y"===O?st.clientTop||0:st.clientLeft||0:0,ct=null!=(M=null==P?void 0:P[O])?M:0,dt=W+rt-ct,lt=G(m?D(F,W+ot-ct-at):F,W,m?j(K,dt):K);x[O]=lt,z[O]=lt-W}if(p){var ut,pt="x"===O?n:s,ht="x"===O?o:r,ft=x[k],vt="y"===k?"height":"width",gt=ft+E[pt],_t=ft-E[ht],yt=-1!==[n,s].indexOf(w),bt=null!=(ut=null==P?void 0:P[k])?ut:0,Et=yt?gt:ft-A[vt]-C[vt]-bt+H.altAxis,wt=yt?ft+A[vt]+C[vt]-bt-H.altAxis:_t,Lt=m&&yt?function(t,e,i){var n=G(t,e,i);return n>i?i:n}(Et,ft,wt):G(m?Et:gt,ft,m?wt:_t);x[k]=Lt,z[k]=Lt-ft}e.modifiersData[a]=z}},requiresIfExists:["offset"]};function kt(t,e,i){void 0===i&&(i=!1);var n,o,r=T(e),s=T(e)&&function(t){var e=t.getBoundingClientRect(),i=z(e.width)/t.offsetWidth||1,n=z(e.height)/t.offsetHeight||1;return 1!==i||1!==n}(e),a=K(e),c=V(t,s,i),d={scrollLeft:0,scrollTop:0},l={x:0,y:0};return(r||!r&&!i)&&(("body"!==x(e)||ut(a))&&(d=(n=e)!==A(n)&&T(n)?{scrollLeft:(o=n).scrollLeft,scrollTop:o.scrollTop}:dt(n)),T(e)?((l=V(e,!0)).x+=e.clientLeft,l.y+=e.clientTop):a&&(l.x=lt(a))),{x:c.left+d.scrollLeft-l.x,y:c.top+d.scrollTop-l.y,width:c.width,height:c.height}}function xt(t){var e=new Map,i=new Set,n=[];function o(t){i.add(t.name),[].concat(t.requires||[],t.requiresIfExists||[]).forEach((function(t){if(!i.has(t)){var n=e.get(t);n&&o(n)}})),n.push(t)}return t.forEach((function(t){e.set(t.name,t)})),t.forEach((function(t){i.has(t.name)||o(t)})),n}var At={placement:"bottom",modifiers:[],strategy:"absolute"};function Ct(){for(var t=arguments.length,e=new Array(t),i=0;it._options.maxValue&&(i.value=t._options.maxValue.toString()),null!==t._options.minValue&&parseInt(i.value)=this._options.maxValue||(this._targetEl.value=(this.getCurrentValue()+1).toString(),this._options.onIncrement(this))},t.prototype.decrement=function(){null!==this._options.minValue&&this.getCurrentValue()<=this._options.minValue||(this._targetEl.value=(this.getCurrentValue()-1).toString(),this._options.onDecrement(this))},t.prototype.updateOnIncrement=function(t){this._options.onIncrement=t},t.prototype.updateOnDecrement=function(t){this._options.onDecrement=t},t}();function c(){document.querySelectorAll("[data-input-counter]").forEach((function(t){var e=t.id,i=document.querySelector('[data-input-counter-increment="'+e+'"]'),n=document.querySelector('[data-input-counter-decrement="'+e+'"]'),r=t.getAttribute("data-input-counter-min"),s=t.getAttribute("data-input-counter-max");t?o.default.instanceExists("InputCounter",t.getAttribute("id"))||new a(t,i||null,n||null,{minValue:r?parseInt(r):null,maxValue:s?parseInt(s):null}):console.error('The target element with id "'.concat(e,'" does not exist. Please check the data-input-counter attribute.'))}))}e.initInputCounters=c,"undefined"!=typeof window&&(window.InputCounter=a,window.initInputCounters=c),e.default=a},16:function(t,e,i){var n=this&&this.__assign||function(){return n=Object.assign||function(t){for(var e,i=1,n=arguments.length;i { + this.priceUpdatePaused = false; + if (!this.isConnected()) { + this.connect(); + } + this.startHealthCheck(); + }, 1000); }, startHealthCheck() { @@ -217,7 +224,6 @@ const WebSocketManager = { performHealthCheck() { if (!this.isConnected()) { - console.warn('Health check: Connection lost, attempting reconnect'); this.handleReconnect(); return; } @@ -225,13 +231,11 @@ const WebSocketManager = { const now = Date.now(); const lastCheck = this.connectionState.lastHealthCheck; if (lastCheck && (now - lastCheck) > 60000) { - console.warn('Health check: Connection stale, refreshing'); this.handleReconnect(); return; } this.connectionState.lastHealthCheck = now; - console.log('Health check passed'); }, connect() { @@ -248,7 +252,6 @@ const WebSocketManager = { const wsPort = config.port || window.ws_port || '11700'; if (!wsPort) { - console.error('WebSocket port not configured'); this.connectionState.isConnecting = false; return false; } @@ -258,7 +261,6 @@ const WebSocketManager = { this.connectionState.connectTimeout = setTimeout(() => { if (this.connectionState.isConnecting) { - console.log('⏳ Connection attempt timed out'); this.cleanup(); this.handleReconnect(); } @@ -266,7 +268,6 @@ const WebSocketManager = { return true; } catch (error) { - console.error('Error creating WebSocket:', error); this.connectionState.isConnecting = false; this.handleReconnect(); return false; @@ -277,7 +278,6 @@ const WebSocketManager = { if (!this.ws) return; this.handlers.open = () => { - console.log('🟢 WebSocket connected successfully'); this.connectionState.isConnecting = false; this.reconnectAttempts = 0; clearTimeout(this.connectionState.connectTimeout); @@ -291,18 +291,15 @@ const WebSocketManager = { const message = JSON.parse(event.data); this.handleMessage(message); } catch (error) { - console.error('Error processing WebSocket message:', error); updateConnectionStatus('error'); } }; this.handlers.error = (error) => { - console.error('WebSocket error:', error); updateConnectionStatus('error'); }; this.handlers.close = (event) => { - console.log('🔴 WebSocket closed:', event.code, event.reason); this.connectionState.isConnecting = false; window.ws = null; updateConnectionStatus('disconnected'); @@ -320,7 +317,6 @@ const WebSocketManager = { handleMessage(message) { if (this.messageQueue.length >= this.maxQueueSize) { - console.warn('Message queue full, dropping oldest message'); this.messageQueue.shift(); } @@ -359,7 +355,6 @@ const WebSocketManager = { this.messageQueue = []; } catch (error) { - console.error('Error processing message queue:', error); } finally { this.processingQueue = false; } @@ -372,8 +367,6 @@ const WebSocketManager = { this.reconnectAttempts++; if (this.reconnectAttempts <= this.maxReconnectAttempts) { - console.log(`Attempting to reconnect (${this.reconnectAttempts}/${this.maxReconnectAttempts})`); - const delay = Math.min( this.reconnectDelay * Math.pow(1.5, this.reconnectAttempts - 1), 30000 @@ -385,9 +378,7 @@ const WebSocketManager = { } }, delay); } else { - console.error('Max reconnection attempts reached'); updateConnectionStatus('error'); - setTimeout(() => { this.reconnectAttempts = 0; this.connect(); @@ -396,8 +387,6 @@ const WebSocketManager = { }, cleanup() { - console.log('Cleaning up WebSocket resources'); - clearTimeout(this.debounceTimeout); clearTimeout(this.reconnectTimeout); clearTimeout(this.connectionState.connectTimeout); @@ -862,7 +851,7 @@ function continueInitialization() { //console.log('Initialization completed'); } -function initializeFlowbiteTooltips() { +function initializeTooltips() { if (typeof Tooltip === 'undefined') { console.warn('Tooltip is not defined. Make sure the required library is loaded.'); return; @@ -1074,6 +1063,11 @@ function getEmptyPriceData() { } async function fetchLatestPrices() { + if (WebSocketManager.isPageHidden || WebSocketManager.priceUpdatePaused) { + const cachedData = CacheManager.get('prices_coingecko'); + return cachedData?.value || getEmptyPriceData(); + } + const PRICES_CACHE_KEY = 'prices_coingecko'; const minRequestInterval = 60000; const currentTime = Date.now(); @@ -1081,7 +1075,6 @@ async function fetchLatestPrices() { if (!window.isManualRefresh) { const lastRequestTime = window.lastPriceRequest || 0; if (currentTime - lastRequestTime < minRequestInterval) { - console.log('Request too soon, using cache'); const cachedData = CacheManager.get(PRICES_CACHE_KEY); if (cachedData) { return cachedData.value; @@ -1089,10 +1082,10 @@ async function fetchLatestPrices() { } } window.lastPriceRequest = currentTime; + if (!window.isManualRefresh) { const cachedData = CacheManager.get(PRICES_CACHE_KEY); if (cachedData && cachedData.remainingTime > 60000) { - console.log('Using cached price data'); latestPrices = cachedData.value; Object.entries(cachedData.value).forEach(([coin, prices]) => { if (prices.usd) { @@ -1102,11 +1095,12 @@ async function fetchLatestPrices() { return cachedData.value; } } + try { - console.log('Initiating fresh price data fetch...'); const existingCache = CacheManager.get(PRICES_CACHE_KEY, true); const fallbackData = existingCache ? existingCache.value : null; const url = `${offersConfig.apiEndpoints.coinGecko}/simple/price?ids=bitcoin,bitcoin-cash,dash,dogecoin,decred,litecoin,particl,pivx,monero,zano,wownero,zcoin&vs_currencies=USD,BTC&api_key=${offersConfig.apiKeys.coinGecko}`; + const response = await fetch('/json/readurl', { method: 'POST', headers: { @@ -1121,19 +1115,21 @@ async function fetchLatestPrices() { } }) }); + if (!response.ok) { throw new Error(`HTTP Error: ${response.status} ${response.statusText}`); } + const data = await response.json(); + if (data.Error) { if (fallbackData) { - console.log('Using fallback data due to API error'); return fallbackData; } throw new Error(data.Error); } + if (data && Object.keys(data).length > 0) { - console.log('Processing fresh price data'); latestPrices = data; CacheManager.set(PRICES_CACHE_KEY, data, CACHE_DURATION); @@ -1144,10 +1140,11 @@ async function fetchLatestPrices() { }); return data; } + if (fallbackData) { - console.log('Using fallback data due to empty response'); return fallbackData; } + const fallbackPrices = {}; Object.keys(getEmptyPriceData()).forEach(coin => { const fallbackValue = tableRateModule.getFallbackValue(coin); @@ -1155,14 +1152,13 @@ async function fetchLatestPrices() { fallbackPrices[coin] = { usd: fallbackValue, btc: null }; } }); + if (Object.keys(fallbackPrices).length > 0) { return fallbackPrices; } - console.warn('No price data received'); + return null; } catch (error) { - console.error('Price Fetch Error:', error); - const fallbackPrices = {}; Object.keys(getEmptyPriceData()).forEach(coin => { const fallbackValue = tableRateModule.getFallbackValue(coin); @@ -1556,7 +1552,7 @@ async function updateOffersTable() { offersBody.appendChild(fragment); requestAnimationFrame(() => { - initializeFlowbiteTooltips(); + initializeTooltips(); updateRowTimes(); updatePaginationControls(totalPages); @@ -2014,14 +2010,12 @@ function createTooltips(offer, treatAsSentOffer, coinFrom, coinTo, fromAmount, t Grey: Less than 5 minutes left or expired

-
-
${createRecipientTooltip(uniqueId, identityInfo, identity, successRate, totalBids)} diff --git a/basicswap/static/js/pricechart.js b/basicswap/static/js/pricechart.js index 7ff1628..96534e7 100644 --- a/basicswap/static/js/pricechart.js +++ b/basicswap/static/js/pricechart.js @@ -35,7 +35,19 @@ const config = { sixMonths: { days: 180, interval: 'daily' }, day: { days: 1, interval: 'hourly' } }, - currentResolution: 'year' + currentResolution: 'year', + requestTimeout: 60000, // 60 sec + retryDelays: [5000, 15000, 30000], + rateLimits: { + coingecko: { + requestsPerMinute: 50, + minInterval: 1200 // 1.2 sec + }, + cryptocompare: { + requestsPerMinute: 30, + minInterval: 2000 // 2 sec + } + } }; // UTILS @@ -82,8 +94,13 @@ const api = { 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.timeout = config.requestTimeout; + + xhr.ontimeout = () => { + logger.warn(`Request timed out for ${url}`); + reject(new AppError('Request timed out')); + }; + xhr.onload = () => { logger.log(`Response for ${url}:`, xhr.responseText); if (xhr.status === 200) { @@ -104,7 +121,12 @@ const api = { reject(new AppError(`HTTP Error: ${xhr.status} ${xhr.statusText}`, 'HTTPError')); } }; - xhr.onerror = () => reject(new AppError('Network error occurred', 'NetworkError')); + + xhr.onerror = () => { + logger.error(`Network error occurred for ${url}`); + reject(new AppError('Network error occurred', 'NetworkError')); + }; + xhr.send(JSON.stringify({ url: url, headers: headers @@ -123,6 +145,12 @@ const api = { try { return await api.makePostRequest(url, headers); } catch (error) { + logger.error(`CryptoCompare request failed for ${coin}:`, error); + const cachedData = cache.get(`coinData_${coin}`); + if (cachedData) { + logger.info(`Using cached data for ${coin}`); + return cachedData.value; + } return { error: error.message }; } }); @@ -282,11 +310,11 @@ const api = { const rateLimiter = { lastRequestTime: {}, minRequestInterval: { - coingecko: 30000, - cryptocompare: 2000 + coingecko: config.rateLimits.coingecko.minInterval, + cryptocompare: config.rateLimits.cryptocompare.minInterval }, requestQueue: {}, - retryDelays: [2000, 5000, 10000], + retryDelays: config.retryDelays, canMakeRequest: function(apiName) { const now = Date.now(); @@ -328,6 +356,19 @@ const rateLimiter = { await new Promise(resolve => setTimeout(resolve, delay)); return this.queueRequest(apiName, requestFn, retryCount + 1); } + + if ((error.message.includes('timeout') || error.name === 'NetworkError') && + retryCount < this.retryDelays.length) { + const delay = this.retryDelays[retryCount]; + logger.warn(`Request failed, retrying in ${delay/1000} seconds...`, { + apiName, + retryCount, + error: error.message + }); + await new Promise(resolve => setTimeout(resolve, delay)); + return this.queueRequest(apiName, requestFn, retryCount + 1); + } + throw error; } }; @@ -336,10 +377,12 @@ const rateLimiter = { return await this.requestQueue[apiName]; } catch (error) { - if (error.message.includes('429')) { + if (error.message.includes('429') || + error.message.includes('timeout') || + error.name === 'NetworkError') { const cachedData = cache.get(`coinData_${apiName}`); if (cachedData) { - console.log('Rate limit reached, using cached data'); + console.log('Using cached data due to request failure'); return cachedData.value; } } @@ -439,8 +482,8 @@ displayCoinData: (coin, data) => { } updateUI(false); } catch (error) { - logger.error('Failed to parse cache item:', error.message); - localStorage.removeItem(key); + logger.error(`Failed to display data for ${coin}:`, error.message); + updateUI(true); // Show error state in UI } }, @@ -664,6 +707,8 @@ const chartModule = { family: "'Inter', sans-serif" }, color: 'rgba(156, 163, 175, 1)', + maxRotation: 0, + minRotation: 0, callback: function(value) { const date = new Date(value); if (config.currentResolution === 'day') { @@ -855,74 +900,73 @@ const chartModule = { return hourlyData; }, - updateChart: async (coinSymbol, forceRefresh = false) => { - try { - chartModule.showChartLoader(); - chartModule.loadStartTime = Date.now(); - const cacheKey = `chartData_${coinSymbol}_${config.currentResolution}`; - let cachedData = !forceRefresh ? cache.get(cacheKey) : null; - let data; - if (cachedData && Object.keys(cachedData.value).length > 0) { - data = cachedData.value; - //console.log(`Using cached data for ${coinSymbol} (${config.currentResolution})`); - } else { - //console.log(`Fetching fresh data for ${coinSymbol} (${config.currentResolution})`); - const allData = await api.fetchHistoricalDataXHR([coinSymbol]); - data = allData[coinSymbol]; - if (!data || Object.keys(data).length === 0) { - throw new Error(`No data returned for ${coinSymbol}`); + updateChart: async (coinSymbol, forceRefresh = false) => { + try { + const currentChartData = chartModule.chart?.data.datasets[0].data || []; + if (currentChartData.length === 0) { + chartModule.showChartLoader(); + } + chartModule.loadStartTime = Date.now(); + const cacheKey = `chartData_${coinSymbol}_${config.currentResolution}`; + let cachedData = !forceRefresh ? cache.get(cacheKey) : null; + let data; + if (cachedData && Object.keys(cachedData.value).length > 0) { + data = cachedData.value; + console.log(`Using cached data for ${coinSymbol}`); + } else { + try { + const allData = await api.fetchHistoricalDataXHR([coinSymbol]); + data = allData[coinSymbol]; + if (!data || Object.keys(data).length === 0) { + throw new Error(`No data returned for ${coinSymbol}`); + } + cache.set(cacheKey, data, config.cacheTTL); + } catch (error) { + if (error.message.includes('429') && currentChartData.length > 0) { + console.warn(`Rate limit hit for ${coinSymbol}, maintaining current chart`); + return; + } + const expiredCache = localStorage.getItem(cacheKey); + if (expiredCache) { + try { + const parsedCache = JSON.parse(expiredCache); + data = parsedCache.value; + console.log(`Using expired cache data for ${coinSymbol}`); + } catch (cacheError) { + throw error; + } + } else { + throw error; + } + } + } + const chartData = chartModule.prepareChartData(coinSymbol, data); + if (chartData.length > 0 && chartModule.chart) { + chartModule.chart.data.datasets[0].data = chartData; + chartModule.chart.data.datasets[0].label = `${coinSymbol} Price (USD)`; + if (coinSymbol === 'WOW') { + chartModule.chart.options.scales.x.time.unit = 'hour'; + } else { + const resolution = config.resolutions[config.currentResolution]; + chartModule.chart.options.scales.x.time.unit = + resolution.interval === 'hourly' ? 'hour' : + config.currentResolution === 'year' ? 'month' : 'day'; + } + chartModule.chart.update('active'); + chartModule.currentCoin = coinSymbol; + const loadTime = Date.now() - chartModule.loadStartTime; + ui.updateLoadTimeAndCache(loadTime, cachedData); + } + } catch (error) { + console.error(`Error updating chart for ${coinSymbol}:`, error); + if (!(chartModule.chart?.data.datasets[0].data.length > 0)) { + chartModule.chart.data.datasets[0].data = []; + chartModule.chart.update('active'); + } + } finally { + chartModule.hideChartLoader(); } - //console.log(`Caching new data for ${cacheKey}`); - cache.set(cacheKey, data, config.cacheTTL); - cachedData = null; - } - - const chartData = chartModule.prepareChartData(coinSymbol, data); - //console.log(`Prepared chart data for ${coinSymbol}:`, chartData.slice(0, 5)); - - if (chartData.length === 0) { - throw new Error(`No valid chart data for ${coinSymbol}`); - } - - if (chartModule.chart) { - chartModule.chart.data.datasets[0].data = chartData; - chartModule.chart.data.datasets[0].label = `${coinSymbol} Price (USD)`; - - if (coinSymbol === 'WOW') { - chartModule.chart.options.scales.x.time.unit = 'hour'; - chartModule.chart.options.scales.x.ticks.maxTicksLimit = 24; - } else { - const resolution = config.resolutions[config.currentResolution]; - chartModule.chart.options.scales.x.time.unit = resolution.interval === 'hourly' ? 'hour' : 'day'; - if (config.currentResolution === 'year' || config.currentResolution === 'sixMonths') { - chartModule.chart.options.scales.x.time.unit = 'month'; - } - if (config.currentResolution === 'year') { - chartModule.chart.options.scales.x.ticks.maxTicksLimit = 12; - } else if (config.currentResolution === 'sixMonths') { - chartModule.chart.options.scales.x.ticks.maxTicksLimit = 6; - } else if (config.currentResolution === 'day') { - chartModule.chart.options.scales.x.ticks.maxTicksLimit = 24; - } - } - - chartModule.chart.update('active'); - } else { - //console.error('Chart object not initialized'); - throw new 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); - ui.displayErrorMessage(`Failed to update chart for ${coinSymbol}: ${error.message}`); - } finally { - chartModule.hideChartLoader(); - } - }, + }, showChartLoader: () => { const loader = document.getElementById('chart-loader'); @@ -994,8 +1038,8 @@ const app = { disabled: 'Auto-refresh: disabled', justRefreshed: 'Just refreshed', }, - cacheTTL: 5 * 60 * 1000, // 5 minutes - minimumRefreshInterval: 60 * 1000, // 1 minute + cacheTTL: 5 * 60 * 1000, // 5 min + minimumRefreshInterval: 60 * 1000, // 1 min init: () => { console.log('Initializing app...'); @@ -1203,142 +1247,138 @@ setupEventListeners: () => { app.updateNextRefreshTime(); }, refreshAllData: async () => { - if (app.isRefreshing) { - console.log('Refresh already in progress, skipping...'); - return; + if (app.isRefreshing) { + console.log('Refresh already in progress, skipping...'); + return; + } + + const lastGeckoRequest = rateLimiter.lastRequestTime['coingecko'] || 0; + const timeSinceLastRequest = Date.now() - lastGeckoRequest; + const waitTime = Math.max(0, rateLimiter.minRequestInterval.coingecko - timeSinceLastRequest); + + if (waitTime > 0) { + const seconds = Math.ceil(waitTime / 1000); + ui.displayErrorMessage(`Rate limit: Please wait ${seconds} seconds before refreshing`); + + let remainingTime = seconds; + const countdownInterval = setInterval(() => { + remainingTime--; + if (remainingTime > 0) { + ui.displayErrorMessage(`Rate limit: Please wait ${remainingTime} seconds before refreshing`); + } else { + clearInterval(countdownInterval); + ui.hideErrorMessage(); + } + }, 1000); + + return; + } + + console.log('Starting refresh of all data...'); + app.isRefreshing = true; + ui.showLoader(); + chartModule.showChartLoader(); + + try { + ui.hideErrorMessage(); + cache.clear(); + + const btcUpdateSuccess = await app.updateBTCPrice(); + if (!btcUpdateSuccess) { + console.warn('BTC price update failed, continuing with cached or default value'); } + await new Promise(resolve => setTimeout(resolve, 1000)); - const lastGeckoRequest = rateLimiter.lastRequestTime['coingecko'] || 0; - const timeSinceLastRequest = Date.now() - lastGeckoRequest; - const waitTime = Math.max(0, rateLimiter.minRequestInterval.coingecko - timeSinceLastRequest); - - if (waitTime > 0) { - const seconds = Math.ceil(waitTime / 1000); - ui.displayErrorMessage(`Rate limit: Please wait ${seconds} seconds before refreshing`); - - - let remainingTime = seconds; - const countdownInterval = setInterval(() => { - remainingTime--; - if (remainingTime > 0) { - ui.displayErrorMessage(`Rate limit: Please wait ${remainingTime} seconds before refreshing`); - } else { - clearInterval(countdownInterval); - ui.hideErrorMessage(); - } - }, 1000); - - return; + const allCoinData = await api.fetchCoinGeckoDataXHR(); + if (allCoinData.error) { + throw new Error(`CoinGecko API Error: ${allCoinData.error}`); } - console.log('Starting refresh of all data...'); - app.isRefreshing = true; - ui.showLoader(); - chartModule.showChartLoader(); + const failedCoins = []; - try { - ui.hideErrorMessage(); - cache.clear(); + for (const coin of config.coins) { + const symbol = coin.symbol.toLowerCase(); + const coinData = allCoinData[symbol]; - const btcUpdateSuccess = await app.updateBTCPrice(); - if (!btcUpdateSuccess) { - console.warn('BTC price update failed, continuing with cached or default value'); - } - - await new Promise(resolve => setTimeout(resolve, 1000)); - - const allCoinData = await api.fetchCoinGeckoDataXHR(); - if (allCoinData.error) { - throw new Error(`CoinGecko API Error: ${allCoinData.error}`); - } - - const failedCoins = []; - - for (const coin of config.coins) { - const symbol = coin.symbol.toLowerCase(); - const coinData = allCoinData[symbol]; - - try { - if (!coinData) { - throw new Error(`No data received`); - } - - coinData.displayName = coin.displayName || coin.symbol; - ui.displayCoinData(coin.symbol, coinData); - - const cacheKey = `coinData_${coin.symbol}`; - cache.set(cacheKey, coinData); - - } catch (coinError) { - console.warn(`Failed to update ${coin.symbol}: ${coinError.message}`); - failedCoins.push(coin.symbol); + try { + if (!coinData) { + throw new Error(`No data received`); } + + coinData.displayName = coin.displayName || coin.symbol; + ui.displayCoinData(coin.symbol, coinData); + + const cacheKey = `coinData_${coin.symbol}`; + cache.set(cacheKey, coinData); + + } catch (coinError) { + console.warn(`Failed to update ${coin.symbol}: ${coinError.message}`); + failedCoins.push(coin.symbol); } + } - await new Promise(resolve => setTimeout(resolve, 1000)); + await new Promise(resolve => setTimeout(resolve, 1000)); - if (chartModule.currentCoin) { - try { - await chartModule.updateChart(chartModule.currentCoin, true); - } catch (chartError) { - console.error('Chart update failed:', chartError); - - } + if (chartModule.currentCoin) { + try { + await chartModule.updateChart(chartModule.currentCoin, true); + } catch (chartError) { + console.error('Chart update failed:', chartError); } + } - app.lastRefreshedTime = new Date(); - localStorage.setItem('lastRefreshedTime', app.lastRefreshedTime.getTime().toString()); - ui.updateLastRefreshedTime(); + app.lastRefreshedTime = new Date(); + localStorage.setItem('lastRefreshedTime', app.lastRefreshedTime.getTime().toString()); + ui.updateLastRefreshedTime(); - if (failedCoins.length > 0) { - const failureMessage = failedCoins.length === config.coins.length - ? 'Failed to update any coin data' - : `Failed to update some coins: ${failedCoins.join(', ')}`; + if (failedCoins.length > 0) { + const failureMessage = failedCoins.length === config.coins.length + ? 'Failed to update any coin data' + : `Failed to update some coins: ${failedCoins.join(', ')}`; - let countdown = 5; - ui.displayErrorMessage(`${failureMessage} (${countdown}s)`); + let countdown = 5; + ui.displayErrorMessage(`${failureMessage} (${countdown}s)`); - const countdownInterval = setInterval(() => { - countdown--; - if (countdown > 0) { - ui.displayErrorMessage(`${failureMessage} (${countdown}s)`); - } else { - clearInterval(countdownInterval); - ui.hideErrorMessage(); - } - }, 1000); - } - - console.log(`Refresh completed. Failed coins: ${failedCoins.length}`); - - } catch (error) { - console.error('Critical error during refresh:', error); - - - let countdown = 10; - ui.displayErrorMessage(`Refresh failed: ${error.message}. Please try again later. (${countdown}s)`); - const countdownInterval = setInterval(() => { countdown--; if (countdown > 0) { - ui.displayErrorMessage(`Refresh failed: ${error.message}. Please try again later. (${countdown}s)`); + ui.displayErrorMessage(`${failureMessage} (${countdown}s)`); } else { clearInterval(countdownInterval); ui.hideErrorMessage(); } }, 1000); - - } finally { - ui.hideLoader(); - chartModule.hideChartLoader(); - app.isRefreshing = false; - - if (app.isAutoRefreshEnabled) { - app.scheduleNextRefresh(); - } } - }, + + console.log(`Refresh completed. Failed coins: ${failedCoins.length}`); + + } catch (error) { + console.error('Critical error during refresh:', error); + + let countdown = 10; + ui.displayErrorMessage(`Refresh failed: ${error.message}. Please try again later. (${countdown}s)`); + + const countdownInterval = setInterval(() => { + countdown--; + if (countdown > 0) { + ui.displayErrorMessage(`Refresh failed: ${error.message}. Please try again later. (${countdown}s)`); + } else { + clearInterval(countdownInterval); + ui.hideErrorMessage(); + } + }, 1000); + + } finally { + ui.hideLoader(); + chartModule.hideChartLoader(); + app.isRefreshing = false; + + if (app.isAutoRefreshEnabled) { + app.scheduleNextRefresh(); + } + } +}, updateNextRefreshTime: () => { console.log('Updating next refresh time display'); diff --git a/basicswap/static/js/tabs.js b/basicswap/static/js/tabs.js new file mode 100644 index 0000000..a8e9c76 --- /dev/null +++ b/basicswap/static/js/tabs.js @@ -0,0 +1,115 @@ +(function(window) { + 'use strict'; + + class Tabs { + constructor(tabsEl, items = [], options = {}) { + this._tabsEl = tabsEl; + this._items = items; + this._activeTab = options.defaultTabId ? this.getTab(options.defaultTabId) : null; + this._options = { + defaultTabId: options.defaultTabId || null, + activeClasses: options.activeClasses || 'text-blue-600 hover:text-blue-600 dark:text-blue-500 dark:hover:text-blue-500 border-blue-600 dark:border-blue-500', + inactiveClasses: options.inactiveClasses || 'dark:border-transparent text-gray-500 hover:text-gray-600 dark:text-gray-400 border-gray-100 hover:border-gray-300 dark:border-gray-700 dark:hover:text-gray-300', + onShow: options.onShow || function() {} + }; + this._initialized = false; + this.init(); + } + + init() { + if (this._items.length && !this._initialized) { + if (!this._activeTab) { + this.setActiveTab(this._items[0]); + } + + this.show(this._activeTab.id, true); + + this._items.forEach(tab => { + tab.triggerEl.addEventListener('click', () => { + this.show(tab.id); + }); + }); + + this._initialized = true; + } + } + + show(tabId, force = false) { + const tab = this.getTab(tabId); + + if ((tab !== this._activeTab) || force) { + this._items.forEach(t => { + if (t !== tab) { + t.triggerEl.classList.remove(...this._options.activeClasses.split(' ')); + t.triggerEl.classList.add(...this._options.inactiveClasses.split(' ')); + t.targetEl.classList.add('hidden'); + t.triggerEl.setAttribute('aria-selected', false); + } + }); + + tab.triggerEl.classList.add(...this._options.activeClasses.split(' ')); + tab.triggerEl.classList.remove(...this._options.inactiveClasses.split(' ')); + tab.triggerEl.setAttribute('aria-selected', true); + tab.targetEl.classList.remove('hidden'); + + this.setActiveTab(tab); + this._options.onShow(this, tab); + } + } + + getTab(id) { + return this._items.find(t => t.id === id); + } + + getActiveTab() { + return this._activeTab; + } + + setActiveTab(tab) { + this._activeTab = tab; + } + } + + function initTabs() { + document.querySelectorAll('[data-tabs-toggle]').forEach(tabsEl => { + const items = []; + let defaultTabId = null; + + tabsEl.querySelectorAll('[role="tab"]').forEach(triggerEl => { + const isActive = triggerEl.getAttribute('aria-selected') === 'true'; + const tab = { + id: triggerEl.getAttribute('data-tabs-target'), + triggerEl: triggerEl, + targetEl: document.querySelector(triggerEl.getAttribute('data-tabs-target')) + }; + items.push(tab); + + if (isActive) { + defaultTabId = tab.id; + } + }); + + new Tabs(tabsEl, items, { + defaultTabId: defaultTabId + }); + }); + } + + const style = document.createElement('style'); + style.textContent = ` + [data-tabs-toggle] [role="tab"] { + cursor: pointer; + } + `; + document.head.appendChild(style); + + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', initTabs); + } else { + initTabs(); + } + + window.Tabs = Tabs; + window.initTabs = initTabs; + +})(window); diff --git a/basicswap/static/js/tooltips.js b/basicswap/static/js/tooltips.js new file mode 100644 index 0000000..d528994 --- /dev/null +++ b/basicswap/static/js/tooltips.js @@ -0,0 +1,309 @@ +(function(window) { + 'use strict'; + + const tooltipContainer = document.createElement('div'); + tooltipContainer.className = 'tooltip-container'; + + const style = document.createElement('style'); + style.textContent = ` + [role="tooltip"] { + position: absolute; + z-index: 9999; + transition: opacity 0.2s ease-in-out; + pointer-events: auto; + opacity: 0; + visibility: hidden; + } + + .tooltip-container { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 0; + overflow: visible; + pointer-events: none; + z-index: 9999; + } + `; + + function ensureContainerExists() { + if (!document.body.contains(tooltipContainer)) { + document.body.appendChild(tooltipContainer); + } + } + + + function rafThrottle(callback) { + let requestId = null; + let lastArgs = null; + + const later = (context) => { + requestId = null; + callback.apply(context, lastArgs); + }; + + return function(...args) { + lastArgs = args; + if (requestId === null) { + requestId = requestAnimationFrame(() => later(this)); + } + }; + } + + function positionElement(targetEl, triggerEl, placement = 'top', offsetDistance = 8) { + const triggerRect = triggerEl.getBoundingClientRect(); + const targetRect = targetEl.getBoundingClientRect(); + let top, left; + + switch (placement) { + case 'top': + top = triggerRect.top - targetRect.height - offsetDistance; + left = triggerRect.left + (triggerRect.width - targetRect.width) / 2; + break; + case 'bottom': + top = triggerRect.bottom + offsetDistance; + left = triggerRect.left + (triggerRect.width - targetRect.width) / 2; + break; + case 'left': + top = triggerRect.top + (triggerRect.height - targetRect.height) / 2; + left = triggerRect.left - targetRect.width - offsetDistance; + break; + case 'right': + top = triggerRect.top + (triggerRect.height - targetRect.height) / 2; + left = triggerRect.right + offsetDistance; + break; + } + + const viewport = { + width: window.innerWidth, + height: window.innerHeight + }; + + if (left < 0) left = 0; + if (top < 0) top = 0; + if (left + targetRect.width > viewport.width) + left = viewport.width - targetRect.width; + if (top + targetRect.height > viewport.height) + top = viewport.height - targetRect.height; + + targetEl.style.transform = `translate(${Math.round(left)}px, ${Math.round(top)}px)`; + } + + const tooltips = new WeakMap(); + + class Tooltip { + constructor(targetEl, triggerEl, options = {}) { + ensureContainerExists(); + + this._targetEl = targetEl; + this._triggerEl = triggerEl; + this._options = { + placement: options.placement || 'top', + triggerType: options.triggerType || 'hover', + offset: options.offset || 8, + onShow: options.onShow || function() {}, + onHide: options.onHide || function() {} + }; + this._visible = false; + this._initialized = false; + this._hideTimeout = null; + this._showTimeout = null; + + if (this._targetEl.parentNode !== tooltipContainer) { + tooltipContainer.appendChild(this._targetEl); + } + + this._targetEl.style.visibility = 'hidden'; + this._targetEl.style.opacity = '0'; + + this._showHandler = this.show.bind(this); + this._hideHandler = this._handleHide.bind(this); + this._updatePosition = rafThrottle(() => { + if (this._visible) { + positionElement( + this._targetEl, + this._triggerEl, + this._options.placement, + this._options.offset + ); + } + }); + + this.init(); + } + + init() { + if (!this._initialized) { + this._setupEventListeners(); + this._initialized = true; + positionElement( + this._targetEl, + this._triggerEl, + this._options.placement, + this._options.offset + ); + } + } + + _setupEventListeners() { + this._triggerEl.addEventListener('mouseenter', this._showHandler); + this._triggerEl.addEventListener('mouseleave', this._hideHandler); + this._triggerEl.addEventListener('focus', this._showHandler); + this._triggerEl.addEventListener('blur', this._hideHandler); + + this._targetEl.addEventListener('mouseenter', () => { + clearTimeout(this._hideTimeout); + clearTimeout(this._showTimeout); + this._visible = true; + this._targetEl.style.visibility = 'visible'; + this._targetEl.style.opacity = '1'; + }); + + this._targetEl.addEventListener('mouseleave', this._hideHandler); + + if (this._options.triggerType === 'click') { + this._triggerEl.addEventListener('click', this._showHandler); + } + + window.addEventListener('scroll', this._updatePosition, { passive: true }); + document.addEventListener('scroll', this._updatePosition, { passive: true, capture: true }); + window.addEventListener('resize', this._updatePosition, { passive: true }); + + let rafId; + const smoothUpdate = () => { + if (this._visible) { + this._updatePosition(); + rafId = requestAnimationFrame(smoothUpdate); + } + }; + + this._startSmoothUpdate = () => { + if (!rafId) rafId = requestAnimationFrame(smoothUpdate); + }; + + this._stopSmoothUpdate = () => { + if (rafId) { + cancelAnimationFrame(rafId); + rafId = null; + } + }; + } + + _handleHide() { + clearTimeout(this._hideTimeout); + clearTimeout(this._showTimeout); + + this._hideTimeout = setTimeout(() => { + if (this._visible) { + this.hide(); + } + }, 100); + } + + show() { + clearTimeout(this._hideTimeout); + clearTimeout(this._showTimeout); + + this._showTimeout = setTimeout(() => { + if (!this._visible) { + positionElement( + this._targetEl, + this._triggerEl, + this._options.placement, + this._options.offset + ); + + this._targetEl.style.visibility = 'visible'; + this._targetEl.style.opacity = '1'; + this._visible = true; + this._startSmoothUpdate(); + this._options.onShow(); + } + }, 20); + } + + hide() { + this._targetEl.style.opacity = '0'; + this._targetEl.style.visibility = 'hidden'; + this._visible = false; + this._stopSmoothUpdate(); + this._options.onHide(); + } + + destroy() { + clearTimeout(this._hideTimeout); + clearTimeout(this._showTimeout); + this._stopSmoothUpdate(); + + this._triggerEl.removeEventListener('mouseenter', this._showHandler); + this._triggerEl.removeEventListener('mouseleave', this._hideHandler); + this._triggerEl.removeEventListener('focus', this._showHandler); + this._triggerEl.removeEventListener('blur', this._hideHandler); + this._targetEl.removeEventListener('mouseenter', this._showHandler); + this._targetEl.removeEventListener('mouseleave', this._hideHandler); + + if (this._options.triggerType === 'click') { + this._triggerEl.removeEventListener('click', this._showHandler); + } + + window.removeEventListener('scroll', this._updatePosition); + document.removeEventListener('scroll', this._updatePosition, true); + window.removeEventListener('resize', this._updatePosition); + + this._targetEl.style.visibility = ''; + this._targetEl.style.opacity = ''; + this._targetEl.style.transform = ''; + + if (this._targetEl.parentNode === tooltipContainer) { + document.body.appendChild(this._targetEl); + } + + this._initialized = false; + } + + toggle() { + if (this._visible) { + this.hide(); + } else { + this.show(); + } + } + } + + document.head.appendChild(style); + + function initTooltips() { + ensureContainerExists(); + + document.querySelectorAll('[data-tooltip-target]').forEach(triggerEl => { + if (tooltips.has(triggerEl)) return; + + const targetId = triggerEl.getAttribute('data-tooltip-target'); + const targetEl = document.getElementById(targetId); + + if (targetEl) { + const placement = triggerEl.getAttribute('data-tooltip-placement'); + const triggerType = triggerEl.getAttribute('data-tooltip-trigger'); + + const tooltip = new Tooltip(targetEl, triggerEl, { + placement: placement || 'top', + triggerType: triggerType || 'hover', + offset: 8 + }); + + tooltips.set(triggerEl, tooltip); + } + }); + } + + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', initTooltips); + } else { + initTooltips(); + } + + window.Tooltip = Tooltip; + window.initTooltips = initTooltips; + +})(window); diff --git a/basicswap/templates/header.html b/basicswap/templates/header.html index 33ab064..b1132df 100644 --- a/basicswap/templates/header.html +++ b/basicswap/templates/header.html @@ -8,13 +8,18 @@ {% endif %} + - - - + + + + + +