diff --git a/basicswap/js_server.py b/basicswap/js_server.py index 8bf9853..0e3ad55 100644 --- a/basicswap/js_server.py +++ b/basicswap/js_server.py @@ -229,11 +229,12 @@ 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 } 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/css/style.css b/basicswap/static/css/style.css index d9258a1..96bfb70 100644 --- a/basicswap/static/css/style.css +++ b/basicswap/static/css/style.css @@ -278,3 +278,82 @@ select.disabled-select-enabled { .shutdown-button.shutdown-disabled svg { opacity: 0.5; } + + +/* Loading line animation */ +.loading-line { + width: 100%; + height: 2px; + background-color: #ccc; + overflow: hidden; + position: relative; +} +.loading-line::before { + content: ''; + display: block; + width: 100%; + height: 100%; + background: linear-gradient(to right, transparent, #007bff, transparent); + animation: loading 1.5s infinite; +} +@keyframes loading { + 0% { + transform: translateX(-100%); + } + 100% { + transform: translateX(100%); + } +} +/* Hide the loading line once data is loaded */ +.usd-value:not(.loading) .loading-line, +.profit-loss:not(.loading) .loading-line { + display: none; +} + + .resolution-button { + background: none; + border: none; + color: #4B5563; /* gray-600 */ + font-size: 0.875rem; /* text-sm */ + font-weight: 500; /* font-medium */ + padding: 0.25rem 0.5rem; + border-radius: 0.25rem; + transition: all 0.2s; + outline: 2px solid transparent; + outline-offset: 2px; + } + + .resolution-button:hover { + color: #1F2937; /* gray-800 */ + } + + .resolution-button:focus { + outline: 2px solid #3B82F6; /* blue-500 */ + } + + .resolution-button.active { + color: #3B82F6; /* blue-500 */ + outline: 2px solid #3B82F6; /* blue-500 */ + } + + .dark .resolution-button { + color: #9CA3AF; /* gray-400 */ + } + + .dark .resolution-button:hover { + color: #F3F4F6; /* gray-100 */ + } + + .dark .resolution-button.active { + color: #60A5FA; /* blue-400 */ + outline-color: #60A5FA; /* blue-400 */ + color: #fff; + } + + #toggle-volume.active { + @apply bg-green-500 hover:bg-green-600 focus:ring-green-300; + } + #toggle-auto-refresh[data-enabled="true"] { + @apply bg-green-500 hover:bg-green-600 focus:ring-green-300; + } + 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 0000000..ae8edc8 Binary files /dev/null and b/basicswap/static/images/coins/Bitcoin-Cash-20.png differ diff --git a/basicswap/static/images/coins/Bitcoin-Cash.png b/basicswap/static/images/coins/Bitcoin-Cash.png new file mode 100644 index 0000000..baf0eb9 Binary files /dev/null and b/basicswap/static/images/coins/Bitcoin-Cash.png differ diff --git a/basicswap/static/js/libs/chartjs-adapter-date-fns.bundle.min.js b/basicswap/static/js/libs/chartjs-adapter-date-fns.bundle.min.js new file mode 100644 index 0000000..37bffe6 --- /dev/null +++ b/basicswap/static/js/libs/chartjs-adapter-date-fns.bundle.min.js @@ -0,0 +1,7 @@ +/*! + * chartjs-adapter-date-fns v3.0.0 + * https://www.chartjs.org + * (c) 2022 chartjs-adapter-date-fns Contributors + * Released under the MIT license + */ +!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?e(require("chart.js")):"function"==typeof define&&define.amd?define(["chart.js"],e):e((t="undefined"!=typeof globalThis?globalThis:t||self).Chart)}(this,(function(t){"use strict";function e(t){if(null===t||!0===t||!1===t)return NaN;var e=Number(t);return isNaN(e)?e:e<0?Math.ceil(e):Math.floor(e)}function r(t,e){if(e.length1?"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 { + const xhr = new XMLHttpRequest(); + xhr.open('POST', '/json/readurl'); + xhr.setRequestHeader('Content-Type', 'application/json'); + xhr.timeout = 30000; + xhr.ontimeout = () => reject(new Error('Request timed out')); + xhr.onload = () => { + console.log(`Response for ${url}:`, xhr.responseText); + if (xhr.status === 200) { + try { + const response = JSON.parse(xhr.responseText); + if (response.Error) { + console.error(`API Error for ${url}:`, response.Error); + reject(new Error(response.Error)); + } else { + resolve(response); + } + } catch (error) { + console.error(`Invalid JSON response for ${url}:`, xhr.responseText); + reject(new Error(`Invalid JSON response: ${error.message}`)); + } + } else { + console.error(`HTTP Error for ${url}: ${xhr.status} ${xhr.statusText}`); + reject(new Error(`HTTP Error: ${xhr.status} ${xhr.statusText}`)); + } + }; + xhr.onerror = () => reject(new Error('Network error occurred')); + xhr.send(JSON.stringify({ + url: url, + headers: headers + })); + }); +} + +const symbolToCoinName = { + ...Object.fromEntries(Object.entries(coinNameToSymbol).map(([key, value]) => [value, key])), + 'zcoin': 'Firo' +}; + +let latestPrices = null; + +const CACHE_KEY = 'latestPricesCache'; +const CACHE_DURATION = 10 * 60 * 1000; // 10 minutes in milliseconds + +async function fetchLatestPrices() { + console.log('Checking for cached prices...'); + const cachedData = getCachedPrices(); + + if (cachedData) { + console.log('Using cached price data'); + latestPrices = cachedData; + return cachedData; + } + + console.log('Fetching latest prices...'); + const url = 'https://api.coingecko.com/api/v3/simple/price?ids=bitcoin,dash,dogecoin,decred,litecoin,particl,pivx,monero,zano,wownero,zcoin&vs_currencies=USD,BTC'; + + try { + const data = await makePostRequest(url); + console.log('Fetched price data:', data); + + latestPrices = data; + setCachedPrices(data); + return data; + } catch (error) { + console.error('Error fetching price data:', error.message); + return null; + } +} + +function getCachedPrices() { + const cachedItem = localStorage.getItem(CACHE_KEY); + if (cachedItem) { + const { data, timestamp } = JSON.parse(cachedItem); + if (Date.now() - timestamp < CACHE_DURATION) { + return data; + } + } + return null; +} + +function setCachedPrices(data) { + const cacheItem = { + data: data, + timestamp: Date.now() + }; + localStorage.setItem(CACHE_KEY, JSON.stringify(cacheItem)); +} + +const getUsdValue = async (cryptoValue, coinSymbol) => { + try { + const prices = await fetchLatestPrices(); + const apiSymbol = coinNameToSymbol[coinSymbol] || coinSymbol.toLowerCase(); + const coinData = prices[apiSymbol]; + if (coinData && coinData.usd) { + return cryptoValue * coinData.usd; + } else { + throw new Error(`Price data not available for ${coinSymbol}`); + } + } catch (error) { + console.error(`Error getting USD value for ${coinSymbol}:`, error); + throw error; + } +}; + +// Global +let jsonData = []; +let originalJsonData = []; +let isInitialLoad = true; +let tableRateModule; + +let lastRefreshTime = null; +let newEntriesCount = 0; + +let nextRefreshCountdown = 60; // Default to 60 seconds +const MIN_REFRESH_INTERVAL = 30; // Minimum refresh interval in seconds + +const isSentOffers = window.offersTableConfig.isSentOffers; + +let currentPage = 1; +const itemsPerPage = 50; + +const coinIdToName = { + 1: 'particl', 2: 'bitcoin', 3: 'litecoin', 4: 'decred', + 6: 'monero', 7: 'particl blind', 8: 'particl anon', + 9: 'wownero', 11: 'pivx', 13: 'firo' +}; + +// DOM +const toggleButton = document.getElementById('toggleView'); +const tableView = document.getElementById('tableView'); +const jsonView = document.getElementById('jsonView'); +const jsonContent = document.getElementById('jsonContent'); +const offersBody = document.getElementById('offers-body'); +const filterForm = document.getElementById('filterForm'); +const prevPageButton = document.getElementById('prevPage'); +const nextPageButton = document.getElementById('nextPage'); +const currentPageSpan = document.getElementById('currentPage'); +const totalPagesSpan = document.getElementById('totalPages'); +const lastRefreshTimeSpan = document.getElementById('lastRefreshTime'); +const newEntriesCountSpan = document.getElementById('newEntriesCount'); +const nextRefreshTimeSpan = document.getElementById('nextRefreshTime'); + +// Utility +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 getValidOffers() { + if (isSentOffers) { + return jsonData; + } else { + const currentTime = Math.floor(Date.now() / 1000); + return jsonData.filter(offer => offer.expire_at > currentTime); + } +} + +function removeExpiredOffers() { + if (isSentOffers) { + return false; + } + const currentTime = Math.floor(Date.now() / 1000); + const initialLength = jsonData.length; + jsonData = jsonData.filter(offer => offer.expire_at > currentTime); + + if (jsonData.length < initialLength) { + console.log(`Removed ${initialLength - jsonData.length} expired offers`); + return true; + } + return false; +} + +function handleNoOffersScenario() { + offersBody.innerHTML = 'No active offers available. Refreshing data...'; + fetchOffers(true); +} + +function logOfferStatus() { + const validOffers = getValidOffers(); + console.log(`Total offers: ${jsonData.length}, Valid offers: ${validOffers.length}, Current page: ${currentPage}, Total pages: ${Math.ceil(validOffers.length / itemsPerPage)}`); +} + +function isOfferExpired(offer) { + if (isSentOffers) { + return false; + } + const currentTime = Math.floor(Date.now() / 1000); + const isExpired = offer.expire_at <= currentTime; + if (isExpired) { + console.log(`Offer ${offer.offer_id} is expired. Expire time: ${offer.expire_at}, Current time: ${currentTime}`); + } + return isExpired; +} + +function setRefreshButtonLoading(isLoading) { + const refreshButton = document.getElementById('refreshOffers'); + const refreshIcon = document.getElementById('refreshIcon'); + const refreshText = document.getElementById('refreshText'); + + refreshButton.disabled = isLoading; + refreshIcon.classList.toggle('animate-spin', isLoading); + refreshText.textContent = isLoading ? 'Refreshing...' : 'Refresh'; +} + +function escapeHtml(unsafe) { + if (typeof unsafe !== 'string') { + console.warn('escapeHtml received a non-string value:', unsafe); + return ''; + } + return unsafe + .replace(/&/g, "&") + .replace(//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) { + return `${formatTimeDifference(timestamp)} ago`; +} + +function formatTimeLeft(timestamp) { + const now = Math.floor(Date.now() / 1000); + if (timestamp <= now) return "Expired"; + return formatTimeDifference(timestamp); +} + +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 ? 300 : Math.max(MIN_REFRESH_INTERVAL, Math.min(nextExpiration, 300)); +} + +function getNoOffersMessage() { + 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}`; + } + return message; +} + +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)); + } + }, + + getFallbackValue(coinSymbol) { + const value = localStorage.getItem(`fallback_${coinSymbol}_usd`); + return value ? parseFloat(value) : null; + }, + + 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; + } + coinNameValue.classList.remove('hidden'); + if (!coinNameValue.textContent.trim()) { + coinNameValue.textContent = 'N/A'; + } + }); + + document.querySelectorAll('.usd-value').forEach(usdValue => { + if (!usdValue.textContent.trim()) { + usdValue.textContent = 'N/A'; + } + }); + + document.querySelectorAll('.profit-loss').forEach(profitLoss => { + if (!profitLoss.textContent.trim() || profitLoss.textContent === 'Calculating...') { + profitLoss.textContent = 'N/A'; + } + }); + }, + + init() { + console.log('Initializing TableRateModule'); + this.initializeTable(); + } +}; + +function updateProfitLoss(row, fromCoin, toCoin, fromAmount, toAmount) { + const profitLossElement = row.querySelector('.profit-loss'); + if (!profitLossElement) { + console.warn('Profit loss element not found in row'); + return; + } + + calculateProfitLoss(fromCoin, toCoin, fromAmount, toAmount) + .then(profitLossPercentage => { + if (profitLossPercentage === null) { + profitLossElement.textContent = 'N/A'; + profitLossElement.className = 'profit-loss text-lg font-bold text-gray-500'; + console.log(`Unable to calculate profit/loss for ${fromCoin} to ${toCoin}`); + return; + } + + const colorClass = getProfitColorClass(profitLossPercentage); + profitLossElement.textContent = `${profitLossPercentage > 0 ? '+' : ''}${profitLossPercentage}%`; + profitLossElement.className = `profit-loss text-lg font-bold ${colorClass}`; + + const tooltipId = `percentage-tooltip-${row.getAttribute('data-offer-id')}`; + const tooltipElement = document.getElementById(tooltipId); + if (tooltipElement) { + const tooltipContent = createTooltipContent(isSentOffers, fromCoin, toCoin, fromAmount, toAmount); + tooltipElement.innerHTML = ` +
+ ${tooltipContent} +
+
+ `; + } + + console.log(`Updated profit/loss display: ${profitLossElement.textContent}, isSentOffers: ${isSentOffers}`); + }) + .catch(error => { + console.error('Error in updateProfitLoss:', error); + profitLossElement.textContent = 'Error'; + profitLossElement.className = 'profit-loss text-lg font-bold text-red-500'; + }); +} + +function fetchOffers(manualRefresh = false) { + return new Promise((resolve, reject) => { + const endpoint = isSentOffers ? '/json/sentoffers' : '/json/offers'; + console.log(`Fetching offers from: ${endpoint}`); + + if (newEntriesCountSpan) { + newEntriesCountSpan.textContent = 'Loading...'; + } + + if (manualRefresh) { + offersBody.innerHTML = 'Refreshing offers...'; + } + + setRefreshButtonLoading(true); + + const requestBody = { + with_extra_info: true + }; + + fetch(endpoint, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(requestBody) + }) + .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); + + if (!isSentOffers) { + removeExpiredOffers(); + } + + const validOffers = getValidOffers(); + const validItemCount = validOffers.length; + if (newEntriesCountSpan) { + newEntriesCountSpan.textContent = validItemCount; + } + console.log('Valid offers count:', validItemCount); + + lastRefreshTime = Date.now(); + nextRefreshCountdown = getTimeUntilNextExpiration(); + updateLastRefreshTime(); + updateNextRefreshTime(); + + updateOffersTable(); + updateJsonView(); + updatePaginationInfo(); + + if (validItemCount === 0) { + handleNoOffersScenario(); + } + + 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'); + jsonData = filterAndSortData(); + updateOffersTable(); + updateJsonView(); + updatePaginationInfo(); + console.log('Filters applied, table updated'); +} + +function filterAndSortData() { + console.log('Filtering and sorting data'); + + 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); + + let filteredData = originalJsonData.filter(offer => { + const coinFrom = (offer.coin_from || '').toLowerCase(); + const coinTo = (offer.coin_to || '').toLowerCase(); + const isExpired = offer.expire_at <= currentTime; + + if (!isSentOffers && isExpired) { + return false; + } + + if (isSentOffers) { + if (filters.coin_to !== 'any' && coinFrom.toLowerCase() !== filters.coin_to.toLowerCase()) { + return false; + } + if (filters.coin_from !== 'any' && coinTo.toLowerCase() !== filters.coin_from.toLowerCase()) { + return false; + } + } else { + if (filters.coin_to !== 'any' && coinTo.toLowerCase() !== filters.coin_to.toLowerCase()) { + return false; + } + if (filters.coin_from !== 'any' && coinFrom.toLowerCase() !== filters.coin_from.toLowerCase()) { + return false; + } + } + + if (isSentOffers && filters.active && filters.active !== 'any') { + const offerState = isExpired ? 'expired' : 'active'; + if (filters.active !== offerState) { + return false; + } + } + + return true; + }); + + console.log('Filtered data length:', filteredData.length); + + const sortBy = filters.sort_by || 'created_at'; + const sortDir = filters.sort_dir || 'desc'; + + filteredData.sort((a, b) => { + let aValue, bValue; + + switch (sortBy) { + case 'created_at': + aValue = a.created_at; + bValue = b.created_at; + break; + case 'rate': + aValue = parseFloat(a.rate); + bValue = parseFloat(b.rate); + break; + default: + aValue = a.created_at; + bValue = b.created_at; + } + + if (sortDir === 'asc') { + return aValue - bValue; + } else { + return bValue - aValue; + } + }); + + console.log(`Sorted offers by ${sortBy} in ${sortDir} order`); + + return filteredData; +} + +async function updateOffersTable() { + console.log('Starting updateOffersTable function'); + console.log(`Is Sent Offers page: ${isSentOffers}`); + + try { + const priceData = await fetchLatestPrices(); + if (!priceData) { + console.error('Failed to fetch latest prices. Using last known prices or proceeding without price data.'); + } else { + console.log('Latest prices fetched successfully'); + latestPrices = priceData; + } + + let validOffers = getValidOffers(); + console.log(`Valid offers: ${validOffers.length}`); + + if (validOffers.length === 0) { + console.log('No valid offers found. Handling no offers scenario.'); + handleNoOffersScenario(); + return; + } + + const totalPages = Math.max(1, Math.ceil(validOffers.length / itemsPerPage)); + currentPage = Math.min(currentPage, totalPages); + + const startIndex = (currentPage - 1) * itemsPerPage; + const endIndex = startIndex + itemsPerPage; + const itemsToDisplay = validOffers.slice(startIndex, endIndex); + + console.log(`Displaying offers ${startIndex + 1} to ${endIndex} of ${validOffers.length}`); + + offersBody.innerHTML = ''; + + for (const offer of itemsToDisplay) { + const row = createTableRow(offer, isSentOffers); + if (row) { + offersBody.appendChild(row); + try { + const fromAmount = parseFloat(offer.amount_from); + const toAmount = parseFloat(offer.amount_to); + await updateProfitLoss(row, offer.coin_from, offer.coin_to, fromAmount, toAmount); + } catch (error) { + console.error(`Error updating profit/loss for offer ${offer.offer_id}:`, error); + } + } + } + + updateRowTimes(); + initializeFlowbiteTooltips(); + updatePaginationInfo(); + + if (tableRateModule && typeof tableRateModule.initializeTable === 'function') { + tableRateModule.initializeTable(); + } + + logOfferStatus(); + + lastRefreshTime = Date.now(); + nextRefreshCountdown = getTimeUntilNextExpiration(); + updateLastRefreshTime(); + updateNextRefreshTime(); + + if (newEntriesCountSpan) { + newEntriesCountSpan.textContent = validOffers.length; + } + + } catch (error) { + console.error('Error updating offers table:', error); + offersBody.innerHTML = `An error occurred while updating the offers table. Please try again later.`; + } finally { + setRefreshButtonLoading(false); + } +} + +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 = symbolToCoinName[coinNameToSymbol[offer.coin_from]] || offer.coin_from; + const coinTo = symbolToCoinName[coinNameToSymbol[offer.coin_to]] || offer.coin_to; + + 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 { buttonClass, buttonText } = getButtonProperties(isActuallyExpired, isSentOffers, offer.is_own_offer, offer.is_revoked); + + row.innerHTML = ` + ${createTimeColumn(offer, postedTime, expiresIn)} + ${createDetailsColumn(offer)} + ${createTakerAmountColumn(offer, coinFrom, coinTo)} + ${createSwapColumn(offer, coinFrom, coinTo)} + ${createOrderbookColumn(offer, coinTo, coinFrom)} + ${createRateColumn(offer, coinFrom, coinTo)} + ${createPercentageColumn(offer)} + ${createActionColumn(offer, buttonClass, buttonText)} + ${createTooltips(offer, isSentOffers, coinFrom, coinTo, postedTime, expiresIn, isActuallyExpired)} + `; + + const fromAmount = parseFloat(offer.amount_from); + const toAmount = parseFloat(offer.amount_to); + updateProfitLoss(row, coinFrom, coinTo, fromAmount, toAmount, isSentOffers); + + return row; +} + +function prepareOfferData(offer, isSentOffers) { + const coinFrom = offer.coin_from; + const coinTo = offer.coin_to; + + 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 { buttonClass, buttonText } = getButtonProperties(isActuallyExpired, isSentOffers, offer.is_own_offer); + + const clockColor = isActuallyExpired ? "#9CA3AF" : "#3B82F6"; + + return { + coinFrom, coinTo, + postedTime, expiresIn, isActuallyExpired, + buttonClass, buttonText, clockColor + }; +} + +function getButtonProperties(isActuallyExpired, isSentOffers, isTreatedAsSentOffer, isRevoked) { + if (isRevoked) { + return { + buttonClass: 'bg-red-500 text-white hover:bg-red-600 transition duration-200', + buttonText: 'Revoked' + }; + } else 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 updateProfitLoss(row, fromCoin, toCoin, fromAmount, toAmount) { + const profitLossElement = row.querySelector('.profit-loss'); + if (!profitLossElement) { + console.warn('Profit loss element not found in row'); + return; + } + + calculateProfitLoss(fromCoin, toCoin, fromAmount, toAmount) + .then(profitLossPercentage => { + if (profitLossPercentage === null) { + profitLossElement.textContent = 'N/A'; + profitLossElement.className = 'profit-loss text-lg font-bold text-white'; + console.log(`Unable to calculate profit/loss for ${fromCoin} to ${toCoin}`); + return; + } + + const colorClass = getProfitColorClass(profitLossPercentage); + profitLossElement.textContent = `${profitLossPercentage > 0 ? '+' : ''}${profitLossPercentage}%`; + profitLossElement.className = `profit-loss text-lg font-bold ${colorClass}`; + + // Update the tooltip content + const tooltipId = `percentage-tooltip-${row.getAttribute('data-offer-id')}`; + const tooltipElement = document.getElementById(tooltipId); + if (tooltipElement) { + const tooltipContent = createTooltipContent(isSentOffers, fromCoin, toCoin, fromAmount, toAmount); + tooltipElement.innerHTML = ` +
+ ${tooltipContent} +
+
+ `; + } + + console.log(`Updated profit/loss display: ${profitLossElement.textContent}, isSentOffers: ${isSentOffers}`); + }) + .catch(error => { + console.error('Error in updateProfitLoss:', error); + profitLossElement.textContent = 'Error'; + profitLossElement.className = 'profit-loss text-lg font-bold text-red-500'; + }); +} + +async function calculateProfitLoss(fromCoin, toCoin, fromAmount, toAmount) { + console.log(`Calculating profit/loss for ${fromAmount} ${fromCoin} to ${toAmount} ${toCoin}, isSentOffers: ${isSentOffers}`); + + if (!latestPrices) { + console.error('Latest prices not available. Unable to calculate profit/loss.'); + return null; + } + + const fromSymbol = coinNameToSymbol[fromCoin] || fromCoin.toLowerCase(); + const toSymbol = coinNameToSymbol[toCoin] || toCoin.toLowerCase(); + + const fromPriceUSD = latestPrices[fromSymbol]?.usd; + const toPriceUSD = latestPrices[toSymbol]?.usd; + + if (!fromPriceUSD || !toPriceUSD) { + console.error(`Price data missing for ${fromSymbol} or ${toSymbol}`); + return null; + } + + const fromValueUSD = fromAmount * fromPriceUSD; + const toValueUSD = toAmount * toPriceUSD; + + let profitPercentage; + + if (isSentOffers) { + // Sent Offer + profitPercentage = ((toValueUSD / fromValueUSD) - 1) * 100; + } else { + // Offer Page + profitPercentage = ((fromValueUSD / toValueUSD) - 1) * 100; + } + + console.log(`From value: $${fromValueUSD.toFixed(2)}, To value: $${toValueUSD.toFixed(2)}`); + console.log(`Profit percentage: ${profitPercentage.toFixed(2)}%, isSentOffers: ${isSentOffers}`); + + return profitPercentage.toFixed(2); +} + +function getProfitColorClass(percentage) { + const numericPercentage = parseFloat(percentage); + if (numericPercentage > 0) return 'text-green-500'; + if (numericPercentage < 0) return 'text-red-500'; + if (numericPercentage === 0) return 'text-yellowr-400'; + return 'text-white'; +} + +function getMarketRate(fromCoin, toCoin) { + return new Promise((resolve) => { + console.log(`Attempting to get market rate for ${fromCoin} to ${toCoin}`); + if (!latestPrices) { + console.warn('Latest prices object is not available'); + resolve(null); + return; + } + const fromPrice = latestPrices[fromCoin.toLowerCase()]?.usd; + const toPrice = latestPrices[toCoin.toLowerCase()]?.usd; + if (!fromPrice || !toPrice) { + console.warn(`Missing price data for ${!fromPrice ? fromCoin : toCoin}`); + resolve(null); + return; + } + const rate = toPrice / fromPrice; + console.log(`Market rate calculated: ${rate} ${toCoin}/${fromCoin}`); + resolve(rate); + }); +} + +function getTimerColor(offer) { + const now = Math.floor(Date.now() / 1000); + const offerAge = now - offer.created_at; + const offerLifespan = offer.expire_at - offer.created_at; + const timeLeft = offer.expire_at - now; + + // New listing: + if (offerAge < offerLifespan * 0.1) { + return "#10B981"; // Green + } + + // Almost expired: + if (timeLeft < offerLifespan * 0.1) { + return "#9CA3AF"; // Gray + } + + // Fade from green to blue to gray + const bluePhase = (offerAge - offerLifespan * 0.1) / (offerLifespan * 0.8); + const r = Math.round(16 + (59 - 16) * bluePhase); + const g = Math.round(185 + (130 - 185) * bluePhase); + const b = Math.round(129 + (246 - 129) * bluePhase); + + return `rgb(${r}, ${g}, ${b})`; +} + +function createTimeColumn(offer, postedTime, expiresIn) { + const timerColor = getTimerColor(offer); + + return ` + +
+
+ + + + + + +
+ +
+ + `; +} + +function createDetailsColumn(offer) { + const addrFrom = offer.addr_from || ''; + return ` + + + Recipient: ${escapeHtml(addrFrom.substring(0, 10))}... + + + + `; +} + +function createTakerAmountColumn(offer, coinFrom, coinTo) { + const fromAmount = parseFloat(offer.amount_from); + const fromSymbol = getCoinSymbol(coinFrom); + const fromPriceUSD = latestPrices[coinNameToSymbol[coinFrom]]?.usd || 0; + const fromValueUSD = fromAmount * fromPriceUSD; + + return ` + + +
+ ${fromAmount.toFixed(4)} +
${coinFrom}
+
USD: (${fromValueUSD.toFixed(2)})
+
+
+ + `; +} + +function createSwapColumn(offer, coinFrom, coinTo) { + return ` + + +
+ + ${coinFrom} + + + + ${coinTo} + +
+
+ + `; +} + +function createOrderbookColumn(offer, coinTo, coinFrom) { + const toAmount = parseFloat(offer.amount_to); + const toSymbol = getCoinSymbol(coinTo); + const toPriceUSD = latestPrices[coinNameToSymbol[coinTo]]?.usd || 0; + const toValueUSD = toAmount * toPriceUSD; + return ` + + + + `; +} + +function calculateInverseRate(rate) { + return (1 / parseFloat(rate)).toFixed(8); +} + +function createRateColumn(offer, coinFrom, coinTo) { + const rate = parseFloat(offer.rate); + const inverseRate = 1 / rate; + const fromSymbol = getCoinSymbol(coinFrom); + const toSymbol = getCoinSymbol(coinTo); + + // Get USD prices + const fromPriceUSD = latestPrices[coinNameToSymbol[coinFrom]]?.usd || 0; + const toPriceUSD = latestPrices[coinNameToSymbol[coinTo]]?.usd || 0; + + // Calculate USD equivalent of the rate + const rateInUSD = rate * toPriceUSD; + + console.log(`Rate calculation for ${fromSymbol} to ${toSymbol}:`); + console.log(`Rate: ${rate} ${toSymbol}/${fromSymbol}`); + console.log(`Inverse Rate: ${inverseRate} ${fromSymbol}/${toSymbol}`); + console.log(`${fromSymbol} price: $${fromPriceUSD}`); + console.log(`${toSymbol} price: $${toPriceUSD}`); + console.log(`Rate in USD: $${rateInUSD.toFixed(2)}`); + + return ` + +
+
+ + ${rate.toFixed(6)} ${toSymbol}/${fromSymbol} + + + ${inverseRate.toFixed(6)} ${fromSymbol}/${toSymbol} + + + ($${rateInUSD.toFixed(2)}) + +
+
+ + `; +} + +function createPercentageColumn(offer) { + return ` + +
+
+ + Calculating... + +
+
+ + `; +} + +function createActionColumn(offer, buttonClass, buttonText) { + return ` + + + + `; +} + +function createTooltips(offer, isSentOffers, coinFrom, coinTo, postedTime, expiresIn, isActuallyExpired) { + const rate = parseFloat(offer.rate); + const fromSymbol = coinNameToSymbol[coinFrom] || coinFrom.toLowerCase(); + const toSymbol = coinNameToSymbol[coinTo] || coinTo.toLowerCase(); + + const fromPriceUSD = latestPrices[fromSymbol]?.usd || 0; + const toPriceUSD = latestPrices[toSymbol]?.usd || 0; + const rateInUSD = rate * toPriceUSD; + + + const combinedRateTooltip = createCombinedRateTooltip(offer, coinFrom, coinTo); + + + const fromAmount = parseFloat(offer.amount_from); + const toAmount = parseFloat(offer.amount_to); + const percentageTooltipContent = createTooltipContent(isSentOffers, coinFrom, coinTo, fromAmount, toAmount); + + return ` + + + + + + + + + + + + + + `; +} + +function createTooltipContent(isSentOffers, coinFrom, coinTo, fromAmount, toAmount) { + const fromSymbol = coinNameToSymbol[coinFrom] || coinFrom.toLowerCase(); + const toSymbol = coinNameToSymbol[coinTo] || coinTo.toLowerCase(); + const fromPriceUSD = latestPrices[fromSymbol]?.usd; + const toPriceUSD = latestPrices[toSymbol]?.usd; + + if (!fromPriceUSD || !toPriceUSD) { + return `

Unable to calculate profit/loss

+

Price data is missing for one or both coins.

`; + } + + const fromValueUSD = fromAmount * fromPriceUSD; + const toValueUSD = toAmount * toPriceUSD; + const profitUSD = toValueUSD - fromValueUSD; + let profitPercentage; + + if (isSentOffers) { + // Sent Offer Page + profitPercentage = ((toValueUSD / fromValueUSD) - 1) * 100; + } else { + // Offer Page + profitPercentage = ((fromValueUSD / toValueUSD) - 1) * 100; + } + + const profitLabel = isSentOffers ? "Profit" : "Savings"; + const actionLabel = isSentOffers ? "selling" : "buying"; + const directionLabel = isSentOffers ? "receiving" : "paying"; + + return ` +

Profit/Loss Calculation:

+

You are ${actionLabel} ${fromAmount} ${coinFrom} (${fromValueUSD.toFixed(2)} USD)
and ${directionLabel} ${toAmount} ${coinTo} (${toValueUSD.toFixed(2)} USD).

+

Percentage: ${profitPercentage.toFixed(2)}%

+

USD ${profitLabel}: ${profitUSD > 0 ? '+' : ''}${profitUSD.toFixed(2)} USD

+

Calculation:

+

Percentage = ${isSentOffers ? + "((To Amount in USD / From Amount in USD) - 1) * 100" : + "((From Amount in USD / To Amount in USD) - 1) * 100"}

+

USD ${profitLabel} = ${isSentOffers ? + "To Amount in USD - From Amount in USD" : + "From Amount in USD - To Amount in USD"}

+

Interpretation:

+ ${isSentOffers ? ` +

Positive percentage: You're making a profit

+

Negative percentage: You're taking a loss

+ ` : ` +

Positive percentage: You're saving money compared to market rates

+

Negative percentage: You're paying more than current market rates

+ `} +

Note: ${isSentOffers ? + "As a seller, a positive percentage means you're selling
for more than the current market value." : + "As a buyer, a positive percentage indicates potential
savings compared to current market rates."}

+ `; +} +function createCombinedRateTooltip(offer, coinFrom, coinTo) { + const rate = parseFloat(offer.rate); + const inverseRate = 1 / rate; + const fromSymbol = coinNameToSymbol[coinFrom] || coinFrom.toLowerCase(); + const toSymbol = coinNameToSymbol[coinTo] || coinTo.toLowerCase(); + + const fromPriceUSD = latestPrices[fromSymbol]?.usd || 0; + const toPriceUSD = latestPrices[toSymbol]?.usd || 0; + const rateInUSD = rate * toPriceUSD; + + const marketRate = fromPriceUSD / toPriceUSD; + + const percentDiff = ((rate - marketRate) / marketRate) * 100; + const aboveOrBelow = percentDiff > 0 ? "above" : "below"; + + const action = isSentOffers ? "selling" : "buying"; + + return ` +

Exchange Rate Explanation:

+

This offer is ${action} ${coinFrom} for ${coinTo}
at a rate that is ${Math.abs(percentDiff).toFixed(2)}% ${aboveOrBelow} market price.

+

Exchange Rates:

+

1 ${coinFrom} = ${rate.toFixed(6)} ${toSymbol}

+

1 ${coinTo} = ${inverseRate.toFixed(6)} ${fromSymbol}

+

USD Equivalent:

+

1 ${coinFrom} = $${rateInUSD.toFixed(2)} USD

+

Current market prices:

+

${coinFrom}: $${fromPriceUSD.toFixed(2)} USD

+

${coinTo}: $${toPriceUSD.toFixed(2)} USD

+

Market rate: 1 ${coinFrom} = ${marketRate.toFixed(6)} ${toSymbol}

+ `; +} + +function updatePaginationInfo() { + const validOffers = getValidOffers(); + 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); + + prevPageButton.style.display = currentPage === 1 ? 'none' : 'inline-flex'; + nextPageButton.style.display = (currentPage === totalPages || validItemCount === 0) ? 'none' : 'inline-flex'; + + if (lastRefreshTime) { + lastRefreshTimeSpan.textContent = new Date(lastRefreshTime).toLocaleTimeString(); + } + + const newEntriesCountSpan = document.getElementById('newEntriesCount'); + if (newEntriesCountSpan) { + newEntriesCountSpan.textContent = validItemCount; + } + + logOfferStatus(); +} + +function updateJsonView() { + jsonContent.textContent = JSON.stringify(jsonData, null, 2); +} + +function updateLastRefreshTime() { + lastRefreshTimeSpan.textContent = new Date(lastRefreshTime).toLocaleTimeString(); +} + +function updateNextRefreshTime() { + if (isSentOffers) return; + + const nextRefreshTimeSpan = document.getElementById('nextRefreshTime'); + 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`; + // console.log(`Next refresh in: ${minutes}m ${seconds}s`); +} + +function updateRowTimes() { + const currentTime = Math.floor(Date.now() / 1000); + jsonData.forEach(offer => { + const row = document.querySelector(`[data-offer-id="${offer.offer_id}"]`); + if (!row) return; + + const timeColumn = row.querySelector('td:first-child'); + if (!timeColumn) return; + + const postedTime = formatTimeAgo(offer.created_at); + const expiresIn = formatTimeLeft(offer.expire_at); + const timerColor = getTimerColor(offer); + + const svg = timeColumn.querySelector('svg'); + if (svg) { + svg.querySelector('g').setAttribute('stroke', timerColor); + svg.querySelector('polyline').setAttribute('stroke', timerColor); + } + + const textContainer = timeColumn.querySelector('.xl\\:block'); + if (textContainer) { + textContainer.innerHTML = ` +
Posted: ${escapeHtml(postedTime)}
+
Expires in: ${escapeHtml(expiresIn)}
+ `; + } + + const tooltipElement = document.getElementById(`tooltip-active${offer.offer_id}`); + if (tooltipElement) { + const tooltipContent = tooltipElement.querySelector('.active-revoked-expired'); + if (tooltipContent) { + tooltipContent.innerHTML = ` + +
Posted: ${postedTime}
+
Expires in: ${expiresIn}
+
+ `; + } + } + }); +} + +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 updateCoinFilterOptions() { + const coinToSelect = document.getElementById('coin_to'); + const coinFromSelect = document.getElementById('coin_from'); + + const updateOptions = (select) => { + const options = select.options; + for (let i = 0; i < options.length; i++) { + const option = options[i]; + if (option.value !== 'any') { + const displayName = symbolToCoinName[option.value] || option.value; + option.textContent = displayName; + } + } + }; + + updateOptions(coinToSelect); + updateOptions(coinFromSelect); +} + +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 startRefreshCountdown() { + if (isSentOffers) return; + + console.log('Starting refresh countdown'); + + function refreshCycle() { + console.log(`Refresh cycle started. Current countdown: ${nextRefreshCountdown}`); + checkExpiredAndFetchNew().then(() => { + console.log(`Refresh cycle completed. Next refresh in ${nextRefreshCountdown} seconds`); + refreshInterval = setTimeout(refreshCycle, nextRefreshCountdown * 1000); + }); + } + + refreshCycle(); + + setInterval(() => { + if (nextRefreshCountdown > 0) { + nextRefreshCountdown--; + updateNextRefreshTime(); + } + }, 1000); +} + +function checkExpiredAndFetchNew() { + if (isSentOffers) return Promise.resolve(); + console.log('Starting checkExpiredAndFetchNew'); + const currentTime = Math.floor(Date.now() / 1000); + const expiredOffers = jsonData.filter(offer => offer.expire_at <= currentTime); + + console.log(`Checking for expired offers. Current time: ${currentTime}`); + console.log(`Found ${expiredOffers.length} expired offers.`); + + jsonData = jsonData.filter(offer => offer.expire_at > currentTime); + + console.log('Fetching new offers...'); + + return 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) + ); + + console.log(`Found ${brandNewListings.length} new listings to add.`); + jsonData = [...jsonData, ...brandNewListings]; + newEntriesCount += brandNewListings.length; + + updateOffersTable(); + updateJsonView(); + updatePaginationInfo(); + + if (jsonData.length === 0) { + console.log('No active offers. Displaying message to user.'); + handleNoOffersScenario(); + } + + const timeUntilNextExpiration = getTimeUntilNextExpiration(); + nextRefreshCountdown = Math.max(MIN_REFRESH_INTERVAL, Math.min(timeUntilNextExpiration, 300)); // Between 30 seconds and 5 minutes + console.log(`Next check scheduled in ${nextRefreshCountdown} seconds`); + }) + .catch(error => { + console.error('Error fetching new listings:', error); + nextRefreshCountdown = 60; + console.log(`Error occurred. Next check scheduled in ${nextRefreshCountdown} seconds`); + }); +} + +// Event +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 = getValidOffers(); + const totalPages = Math.ceil(validOffers.length / itemsPerPage); + if (currentPage < totalPages && validOffers.length > 0) { + currentPage++; + updateOffersTable(); + updatePaginationInfo(); + console.log(`Moved to page ${currentPage} of ${totalPages}`); + } else { + console.log('No more pages or all offers have expired'); + nextPageButton.classList.add('invisible'); + nextPageButton.style.display = 'none'; + if (validOffers.length === 0) { + handleNoOffersScenario(); + } + } +}); + +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); +}); + +// Init +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; + } +} + +function continueInitialization() { + if (typeof volumeToggle !== 'undefined' && volumeToggle.init) { + volumeToggle.init(); + } else { + console.warn('volumeToggle is not defined or does not have an init method'); + } + + updateCoinFilterImages(); + fetchOffers().then(() => { + applyFilters(); + startRefreshCountdown(); + }); + + function updateTimesLoop() { + updateRowTimes(); + requestAnimationFrame(updateTimesLoop); + } + requestAnimationFrame(updateTimesLoop); + + setInterval(updateRowTimes, 900000); + + console.log('Initialization completed'); +} + +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); + } + + filterForm.addEventListener('submit', (e) => { + e.preventDefault(); + applyFilters(); + }); + + filterForm.addEventListener('change', applyFilters); + + document.getElementById('coin_to').addEventListener('change', applyFilters); + document.getElementById('coin_from').addEventListener('change', applyFilters); + document.getElementById('sort_by').addEventListener('change', applyFilters); + document.getElementById('sort_dir').addEventListener('change', applyFilters); + + document.getElementById('clearFilters').addEventListener('click', () => { + filterForm.reset(); + jsonData = [...originalJsonData]; + currentPage = 1; + applyFilters(); + updateCoinFilterImages(); + }); +}); + +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 %} - + +
-
-
-
-
-
-
-
-
-
- - +
+
+
+
+ +
+
+
+
- - -
- {{ input_arrow_down_svg | safe }} - -
- - -
-
-

{{ arrow_right_svg | safe }}

-
-
- - +
{{ input_arrow_down_svg | safe }}
+
+
+

{{ arrow_right_svg | safe }}

+
+
+ + +
+ {{ input_arrow_down_svg | safe }} + +
- -

Sort By:

@@ -1323,7 +245,7 @@ app.init();
{{ input_arrow_down_svg | safe }} - @@ -1332,14 +254,12 @@ app.init();
{{ input_arrow_down_svg | safe }} - +
- - - - {% if sent_offers %}
@@ -1374,304 +292,141 @@ 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: + +

+
+
+ +

Page:1 of 1

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