@stellar-expert/ui-framework 1.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (124) hide show
  1. package/.npmrc +1 -0
  2. package/LICENSE +21 -0
  3. package/README.md +3 -0
  4. package/account/account-address.js +127 -0
  5. package/account/account-address.scss +20 -0
  6. package/account/available-balance.js +21 -0
  7. package/account/identicon.js +91 -0
  8. package/account/identicon.scss +6 -0
  9. package/account/signer-key.js +65 -0
  10. package/api/api-cache.js +5 -0
  11. package/api/explorer-api-call.js +37 -0
  12. package/api/explorer-api-hooks.js +202 -0
  13. package/api/explorer-api-paginated-list-hooks.js +439 -0
  14. package/api/explorer-batch-info-loader.js +85 -0
  15. package/api/explorer-tx-api.js +29 -0
  16. package/asset/amount.js +53 -0
  17. package/asset/asset-icon.js +34 -0
  18. package/asset/asset-issuer.js +22 -0
  19. package/asset/asset-link.js +93 -0
  20. package/asset/asset-link.scss +26 -0
  21. package/asset/asset-list-hooks.js +60 -0
  22. package/asset/asset-meta-hooks.js +78 -0
  23. package/asset/asset-selector.js +72 -0
  24. package/asset/asset-selector.scss +41 -0
  25. package/basic-styles/base.scss +53 -0
  26. package/basic-styles/form.scss +90 -0
  27. package/basic-styles/grid.scss +388 -0
  28. package/basic-styles/link.scss +17 -0
  29. package/basic-styles/lists.scss +68 -0
  30. package/basic-styles/loader.scss +22 -0
  31. package/basic-styles/responsive.scss +29 -0
  32. package/basic-styles/scrollbars.scss +19 -0
  33. package/basic-styles/spacing.scss +50 -0
  34. package/basic-styles/table.scss +104 -0
  35. package/basic-styles/themes.scss +59 -0
  36. package/basic-styles/typography.scss +191 -0
  37. package/basic-styles/variables.scss +26 -0
  38. package/claimable-balance/claimable-balance-claimants.js +12 -0
  39. package/contract/sc-val.js +70 -0
  40. package/contract/wasm-section-parser.js +159 -0
  41. package/contract/wasm-section-reader.js +79 -0
  42. package/contract/xdr-reader.js +148 -0
  43. package/controls/active-icon.scss +23 -0
  44. package/controls/button-group.js +20 -0
  45. package/controls/button-group.scss +48 -0
  46. package/controls/button.js +74 -0
  47. package/controls/button.scss +148 -0
  48. package/controls/code-block.js +35 -0
  49. package/controls/code-block.scss +74 -0
  50. package/controls/dropdown.js +281 -0
  51. package/controls/dropdown.scss +161 -0
  52. package/controls/external-link.js +5 -0
  53. package/controls/info-tooltip.js +17 -0
  54. package/controls/info-tooltip.scss +4 -0
  55. package/controls/slider.js +20 -0
  56. package/controls/slider.scss +22 -0
  57. package/controls/tabs.js +94 -0
  58. package/controls/tabs.scss +73 -0
  59. package/controls/tooltip.js +214 -0
  60. package/controls/tooltip.scss +97 -0
  61. package/controls/update-highlighter.js +28 -0
  62. package/controls/update-highlighter.scss +10 -0
  63. package/date/date-selector.js +44 -0
  64. package/date/elapsed-time.js +22 -0
  65. package/date/utc-timestamp.js +23 -0
  66. package/dex/price-dynamic.js +45 -0
  67. package/dex/price-dynamic.scss +36 -0
  68. package/directory/directory-hooks.js +89 -0
  69. package/effect/effect-description.js +289 -0
  70. package/effect/op-effects-view.js +11 -0
  71. package/effect/op-effects.scss +8 -0
  72. package/errors/error-boundary.js +98 -0
  73. package/fonts/font.scss +23 -0
  74. package/fonts/icons/config.json +1858 -0
  75. package/fonts/icons/icons-embedded.scss +644 -0
  76. package/horizon/horizon-account-helpers.js +104 -0
  77. package/horizon/horizon-client-helpers.js +52 -0
  78. package/horizon/horizon-ledger-helpers.js +36 -0
  79. package/horizon/horizon-orderbook-helpers.js +14 -0
  80. package/horizon/horizon-trades-helper.js +89 -0
  81. package/horizon/horizon-transaction-helpers.js +36 -0
  82. package/index.js +85 -0
  83. package/index.scss +16 -0
  84. package/interaction/autofocus.js +9 -0
  85. package/interaction/block-select.js +54 -0
  86. package/interaction/block-select.scss +24 -0
  87. package/interaction/copy-to-clipboard.js +19 -0
  88. package/interaction/inline-progress.js +16 -0
  89. package/interaction/qr-code.js +45 -0
  90. package/interaction/responsive.js +21 -0
  91. package/interaction/spoiler.js +40 -0
  92. package/interaction/spoiler.scss +9 -0
  93. package/interaction/theme-selector.js +11 -0
  94. package/ledger/ledger-entry-href-formatter.js +22 -0
  95. package/ledger/ledger-entry-link.js +59 -0
  96. package/meta/page-meta-tags.js +229 -0
  97. package/module/dynamic-module.js +48 -0
  98. package/package.json +41 -0
  99. package/state/on-screen-hooks.js +23 -0
  100. package/state/page-visibility-helpers.js +17 -0
  101. package/state/page-visibility-hooks.js +12 -0
  102. package/state/screen-orientation-hooks.js +16 -0
  103. package/state/state-hooks.js +77 -0
  104. package/state/stellar-network-hooks.js +45 -0
  105. package/state/theme.js +29 -0
  106. package/stellar/key-type.js +92 -0
  107. package/stellar/ledger-generic-id.js +40 -0
  108. package/stellar/signature-hint-utils.js +65 -0
  109. package/toast/toast-notification-instance.js +43 -0
  110. package/toast/toast-notification.js +46 -0
  111. package/toast/toast-notifications-block.js +55 -0
  112. package/toast/toast-notifications.scss +166 -0
  113. package/tx/op-accounting-changes.js +27 -0
  114. package/tx/op-description-view.js +925 -0
  115. package/tx/op-description.scss +98 -0
  116. package/tx/op-icon.js +93 -0
  117. package/tx/parser/op-balance-changes.js +62 -0
  118. package/tx/parser/op-descriptor.js +49 -0
  119. package/tx/parser/tx-details-parser.js +75 -0
  120. package/tx/parser/tx-matcher.js +372 -0
  121. package/tx/parser/type-filter-matcher.js +127 -0
  122. package/tx/tx-fee-effect.js +14 -0
  123. package/tx/tx-list-hooks.js +19 -0
  124. package/tx/tx-operations-list.js +105 -0
@@ -0,0 +1,214 @@
1
+ import React, {useState, useRef} from 'react'
2
+ import cn from 'classnames'
3
+ import './tooltip.scss'
4
+
5
+ /**
6
+ * Compute tooltip position
7
+ * @param {EventTarget} target - Mouse hover target
8
+ * @param {Element} node - Tooltip object
9
+ * @param {'top'|'bottom'|'left'|'right'} desiredPlace - Desired tooltip place
10
+ * @param {Object} offset
11
+ * @return {{place: String, position: {top: Number, left: Number}}}
12
+ */
13
+ function calculateTooltipPosition(target, node, desiredPlace, offset) {
14
+ //dimensions of node and target
15
+ const {width: tipWidth, height: tipHeight} = getBoundingRect(node),
16
+ {width: targetWidth, height: targetHeight} = getBoundingRect(target)
17
+ //mouse offset
18
+ const {mouseX, mouseY} = getMouseOffset(target),
19
+ {offsetX, offsetY} = parseOffset(offset),
20
+ defaultOffset = getDefaultPosition(targetWidth, targetHeight, tipWidth, tipHeight)
21
+
22
+ // Get the edge offset of the tooltip
23
+ function getTipOffsetLeft(place) {
24
+ return mouseX + defaultOffset[place].l + offsetX
25
+ }
26
+
27
+ function getTipOffsetRight(place) {
28
+ return mouseX + defaultOffset[place].r + offsetX
29
+ }
30
+
31
+ function getTipOffsetTop(place) {
32
+ return mouseY + defaultOffset[place].t + offsetY
33
+ }
34
+
35
+ function getTipOffsetBottom(place) {
36
+ return mouseY + defaultOffset[place].b + offsetY
37
+ }
38
+
39
+ function isOutside(p) {
40
+ return getTipOffsetLeft(p) < 0 || //outside left
41
+ getTipOffsetRight(p) > window.innerWidth || //outside right
42
+ getTipOffsetTop(p) < 0 || //outside top
43
+ getTipOffsetBottom(p) > window.innerHeight //outside bottom
44
+ }
45
+
46
+ let place
47
+ if (!isOutside(desiredPlace)) {
48
+ place = desiredPlace
49
+ } else {
50
+ const possiblePlacements = (['top', 'bottom', 'left', 'right']).filter(p => !isOutside(p))
51
+ if (possiblePlacements.length > 0 && isOutside(desiredPlace)) {
52
+ place = possiblePlacements[0]
53
+ }
54
+ }
55
+
56
+ const {top: parentTop, left: parentLeft} = target.getBoundingClientRect()
57
+
58
+ return {
59
+ place,
60
+ position: {
61
+ top: (getTipOffsetTop(place) - parentTop) | 0,
62
+ left: (getTipOffsetLeft(place) - parentLeft) | 0
63
+ }
64
+ }
65
+ }
66
+
67
+ /**
68
+ * @param {Element} node
69
+ * @return {PositionRect}
70
+ */
71
+ function getBoundingRect(node) {
72
+ const {width, height, top, left} = node.getBoundingClientRect()
73
+ return {
74
+ width: width | 0,
75
+ height: height | 0,
76
+ top: top | 0,
77
+ left: left | 0
78
+ }
79
+ }
80
+
81
+ /**
82
+ * @param {Element} element
83
+ * @return {{mouseX: Number, mouseY: Number}}
84
+ */
85
+ function getMouseOffset(element) {
86
+ const {width, height, top, left} = getBoundingRect(element)
87
+ return {
88
+ mouseX: (left + (width / 2)) | 0,
89
+ mouseY: (top + (height / 2)) | 0
90
+ }
91
+ }
92
+
93
+ /**
94
+ * @param {Number} targetWidth
95
+ * @param {Number} targetHeight
96
+ * @param {Number} tipWidth
97
+ * @param {Number} tipHeight
98
+ * */
99
+ function getDefaultPosition(targetWidth, targetHeight, tipWidth, tipHeight) {
100
+ const notchSize = 6
101
+ return {
102
+ top: {
103
+ l: -tipWidth / 2,
104
+ r: tipWidth / 2,
105
+ t: -targetHeight / 2 - tipHeight - notchSize,
106
+ b: -targetHeight / 2
107
+ },
108
+ bottom: {
109
+ l: -tipWidth / 2,
110
+ r: tipWidth / 2,
111
+ t: targetHeight / 2 + notchSize,
112
+ b: targetHeight / 2 + tipHeight
113
+ },
114
+ left: {
115
+ l: -tipWidth - targetWidth / 2,
116
+ r: -targetWidth / 2 + notchSize,
117
+ t: -tipHeight / 2,
118
+ b: tipHeight / 2
119
+ },
120
+ right: {
121
+ l: targetWidth / 2 + notchSize,
122
+ r: tipWidth + targetWidth / 2,
123
+ t: -tipHeight / 2,
124
+ b: tipHeight / 2
125
+ }
126
+ }
127
+ }
128
+
129
+ /**
130
+ * @param {PositionOffset} offset
131
+ * @return {{offsetX: Number, offsetY: Number}}
132
+ */
133
+ function parseOffset(offset) {
134
+ let offsetX = 0,
135
+ offsetY = 0
136
+
137
+ for (let key in Object.keys(offset)) {
138
+ const value = parseInt(offset[key])
139
+ if (key === 'top') {
140
+ offsetY -= value
141
+ } else if (key === 'bottom') {
142
+ offsetY += value
143
+ } else if (key === 'left') {
144
+ offsetX -= value
145
+ } else if (key === 'right') {
146
+ offsetX += value
147
+ }
148
+ }
149
+
150
+ return {offsetX, offsetY}
151
+ }
152
+
153
+ /**
154
+ * Tooltip component
155
+ * @param {any} trigger
156
+ * @param {PositionDescriptor} desiredPlace
157
+ * @param {PositionOffset} offset?
158
+ * @param {String} maxWidth?
159
+ * @param {any} children?
160
+ * @constructor
161
+ */
162
+ export const Tooltip = React.memo(function Tooltip({trigger, desiredPlace = 'top', offset = {}, children, maxWidth = '20em', ...op}) {
163
+ const [visible, setVisible] = useState(false),
164
+ [place, setPlace] = useState('top'),
165
+ [position, setPosition] = useState({top: 0, left: 0}),
166
+ content = useRef(null)
167
+
168
+ function mouseEnter(e) {
169
+ if (visible) return
170
+ const {place, position} = calculateTooltipPosition(e.currentTarget, content.current, desiredPlace, offset)
171
+ setVisible(true)
172
+ setPosition(position)
173
+ setPlace(place)
174
+ }
175
+
176
+ function mouseLeave(e) {
177
+ if (!visible) return
178
+ setVisible(false)
179
+ }
180
+
181
+ const triggerProps = {
182
+ onMouseEnter: e => mouseEnter(e),
183
+ onMouseLeave: e => mouseLeave(e),
184
+ ...op
185
+ }
186
+ const contentStyle = {
187
+ maxWidth,
188
+ left: position.left + 'px',
189
+ top: position.top + 'px'
190
+ }
191
+ return React.cloneElement(trigger, triggerProps, <div className="tooltip-wrapper" style={contentStyle}>
192
+ <div ref={content} className={cn('tooltip', place)}>
193
+ <div className="tooltip-content">{children}</div>
194
+ </div>
195
+ </div>)
196
+ })
197
+
198
+ /**
199
+ * @typedef {'top'|'bottom'|'left'|'right'} PositionDescriptor
200
+ */
201
+ /**
202
+ * @typedef {Object} PositionRect
203
+ * @property {Number} top - Top offset
204
+ * @property {Number} left - Left offset
205
+ * @property {Number} width - Element width
206
+ * @property {Number} height - Element height
207
+ */
208
+ /**
209
+ * @typedef {Object} PositionOffset
210
+ * @property {Number} top? - Top offset
211
+ * @property {Number} bottom? - Bottom offset
212
+ * @property {Number} left? - Left offset
213
+ * @property {Number} right? - Right offset
214
+ */
@@ -0,0 +1,97 @@
1
+ @use "../basic-styles/variables";
2
+
3
+ $tooltip-notch-shadow-color: var(--color-border-shadow);
4
+
5
+ .tooltip-wrapper {
6
+ width: 30rem;
7
+ max-width: 30rem;
8
+ position: absolute;
9
+ visibility: hidden;
10
+ opacity: 0;
11
+ transition: opacity 0.15s ease 0.4s, visibility 0.15s ease 0.4s;
12
+ z-index: 10;
13
+ }
14
+
15
+ .tooltip {
16
+ position: relative;
17
+ font-size: 0.9*$font-size-base;
18
+ font-style: normal;
19
+ line-height: 1.3;
20
+ max-width: 30rem;
21
+ background: var(--color-bg);
22
+ border-color: var(--color-contrast-border);
23
+ border-radius: $border-radius-input;
24
+ box-shadow: 0 2px 4px -1px var(--color-backdrop);
25
+ padding: 1rem 1.4rem;
26
+ letter-spacing: normal;
27
+ word-break: normal;
28
+
29
+ &:before {
30
+ content: "";
31
+ background: var(--color-bg);
32
+ height: .8rem;
33
+ width: .8rem;
34
+ position: absolute;
35
+ z-index: 9;
36
+ transform: rotate(45deg);
37
+ }
38
+
39
+ &.top:before {
40
+ left: 50%;
41
+ bottom: -.25em;
42
+ box-shadow: 1px 1px 0 $tooltip-notch-shadow-color;
43
+ margin-left: -0.4rem;
44
+ }
45
+
46
+ &.bottom:before {
47
+ left: 50%;
48
+ top: -.25em;
49
+ box-shadow: -1px -1px 0 $tooltip-notch-shadow-color;
50
+ margin-left: -0.4rem;
51
+ }
52
+
53
+ &.left:before {
54
+ right: -.25em;
55
+ top: 50%;
56
+ margin-top: -0.4em;
57
+ box-shadow: 1px -1px 0 $tooltip-notch-shadow-color;
58
+ }
59
+
60
+ &.right:before {
61
+ left: -.25em;
62
+ top: 50%;
63
+ margin-top: -0.4em;
64
+ box-shadow: -1px 1px 0 $tooltip-notch-shadow-color;
65
+ }
66
+
67
+ .tooltip-content {
68
+ color: var(--color-text);
69
+ z-index: 5;
70
+ position: relative;
71
+ }
72
+ }
73
+
74
+ .trigger {
75
+ vertical-align: text-top;
76
+ font-size: 1.2rem;
77
+ position: relative;
78
+ top: -.3rem;
79
+ left: .2rem;
80
+ color: var(--color-primary);
81
+ transition: color 0.15s ease 0.3s;
82
+
83
+ &:before {
84
+ opacity: 0.4;
85
+ }
86
+
87
+ &:hover {
88
+ &:before {
89
+ opacity: 1;
90
+ }
91
+
92
+ & > .tooltip-wrapper {
93
+ visibility: visible;
94
+ opacity: 1;
95
+ }
96
+ }
97
+ }
@@ -0,0 +1,28 @@
1
+ import React, {useRef} from 'react'
2
+ import PropTypes from 'prop-types'
3
+ import cn from 'classnames'
4
+ import {useDependantState} from '../state/state-hooks'
5
+ import './update-highlighter.scss'
6
+
7
+ export const UpdateHighlighter = React.memo(function UpdateHighlighter({children}) {
8
+ const timerRef = useRef(0)
9
+ const [active, setActiveState] = useDependantState(() => {
10
+ //reset the timer in case if previous animation wasn't finished yet
11
+ if (timerRef.current) {
12
+ clearTimeout(timerRef.current)
13
+ useRef.current = 0
14
+ }
15
+ //schedule class reset
16
+ timerRef.current = setTimeout(function () {
17
+ timerRef.current = 0
18
+ setActiveState(false)
19
+ }, 600)
20
+ return true
21
+ }, [children])
22
+
23
+ return <span className={cn({highlighter: active})}>{children}</span>
24
+ })
25
+
26
+ UpdateHighlighter.propTypes = {
27
+ children: PropTypes.any.isRequired
28
+ }
@@ -0,0 +1,10 @@
1
+ @use "../basic-styles/variables";
2
+
3
+ .highlighter {
4
+ animation: highlighter 0.6s ease-in;
5
+ }
6
+
7
+ @keyframes highlighter {
8
+ from {background-color: transparentize($color-primary, 0.5);}
9
+ to {background-color: transparent;}
10
+ }
@@ -0,0 +1,44 @@
1
+ import React, {useCallback, useEffect, useState} from 'react'
2
+ import {debounce} from 'throttle-debounce'
3
+ import {normalizeDate, toUnixTimestamp} from '@stellar-expert/formatter'
4
+
5
+ export function trimIsoDateSeconds(date) {
6
+ if (typeof date === 'number') {
7
+ date = new Date(date)
8
+ }
9
+ return date.toISOString().replace(/:\d{2}\.\d*Z/, '')
10
+ }
11
+
12
+ const minSelectableValue = trimIsoDateSeconds(new Date('2015-09-30T16:46:00Z'))
13
+
14
+ /**
15
+ * @param {String} value
16
+ * @param {Function} onChange
17
+ * @constructor
18
+ */
19
+ export function DateSelector({value, onChange, min, max, ref, ...otherProps}) {
20
+ const [date, setDate] = useState(value ? trimIsoDateSeconds(normalizeDate(value)) : '')
21
+ useEffect(() => {
22
+ if (value) {
23
+ setDate(trimIsoDateSeconds(normalizeDate(value)))
24
+ } else {
25
+ setDate('')
26
+ }
27
+ }, [value])
28
+
29
+ const selectDate = useCallback(debounce(400, function (newDate) {
30
+ if (value !== newDate) {
31
+ setDate(newDate)
32
+ if (onChange) {
33
+ onChange(toUnixTimestamp(normalizeDate(newDate)))
34
+ }
35
+ }
36
+ }), [onChange, value])
37
+
38
+ if (max === undefined) {
39
+ max = trimIsoDateSeconds(new Date(new Date().getTime() + 24 * 60 * 60 * 1000))
40
+ }
41
+ return <input type="datetime-local" value={date} className="date-selector condensed" step={60} ref={ref}
42
+ min={min || minSelectableValue} max={max} onChange={e => selectDate(e.target.value)}
43
+ style={{width: '11em', overflow: 'hidden'}} {...otherProps}/>
44
+ }
@@ -0,0 +1,22 @@
1
+ import React from 'react'
2
+ import TimeAgo from 'react-timeago'
3
+
4
+ const timeUnits = {
5
+ second: 's',
6
+ minute: 'm',
7
+ hour: 'h',
8
+ day: 'd',
9
+ week: 'w',
10
+ month: 'mo',
11
+ year: 'y'
12
+ }
13
+
14
+ export const ElapsedTime = React.memo(function ElapsedTime({ts, className, suffix}) {
15
+ const then = Date.parse(ts)
16
+ return <span className={className}>
17
+ <TimeAgo date={ts} formatter={(v, unit) => `${v}${timeUnits[unit]}`} now={function () {
18
+ const now = Date.now()
19
+ return then > now ? then : now
20
+ }}/>{suffix}
21
+ </span>
22
+ })
@@ -0,0 +1,23 @@
1
+ import React from 'react'
2
+ import PropTypes from 'prop-types'
3
+ import cn from 'classnames'
4
+ import {normalizeDate, formatDateUTC} from '@stellar-expert/formatter'
5
+ import {BlockSelect} from '../interaction/block-select'
6
+
7
+ export const UtcTimestamp = React.memo(function UtcTimestamp({date, dateOnly, className}) {
8
+ date = normalizeDate(date)
9
+ let formatted = formatDateUTC(date)
10
+ if (dateOnly) {
11
+ formatted = formatted.split(' ')[0]
12
+ } else {
13
+ formatted += ' UTC'
14
+ }
15
+ const localTime = date.toString().replace(/ \(.+\)/, '').replace(/\w+ /, '')
16
+ return <BlockSelect className={cn('condensed nowrap', className)} title={localTime}>{formatted}</BlockSelect>
17
+ })
18
+
19
+ UtcTimestamp.propTypes = {
20
+ date: PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.instanceOf(Date)]).isRequired,
21
+ dateOnly: PropTypes.bool,
22
+ className: PropTypes.string
23
+ }
@@ -0,0 +1,45 @@
1
+ import React from 'react'
2
+ import PropTypes from 'prop-types'
3
+ import cn from 'classnames'
4
+ import {formatPrice} from '@stellar-expert/formatter'
5
+ import './price-dynamic.scss'
6
+
7
+ export const PriceDynamic = React.memo(function PriceDynamic({change, current, prev, standalone, allowZero}) {
8
+ if (change === undefined) {
9
+ if (current === prev || !prev || !current) {
10
+ change = 0
11
+ } else {
12
+ change = 100 * (current - prev) / prev
13
+ }
14
+ }
15
+ if (Math.abs(change) > 10000)
16
+ return null
17
+ change = formatPrice(Math.abs(change), 2) + '%'
18
+ if (change === '0%' && !allowZero)
19
+ return null
20
+ let direction
21
+ if (parseInt(change) > 0) {
22
+ direction = 'positive'
23
+ }
24
+ if (parseInt(change) < 0) {
25
+ direction = 'negative'
26
+ }
27
+ const className = cn('price-change', getChangeDirection(change), {standalone})
28
+ return <span className={className} aria-label={` (${change})`}>{change}</span>
29
+ })
30
+
31
+ function getChangeDirection(change) {
32
+ change = parseInt(change, 10)
33
+ if (change > 0)
34
+ return 'positive'
35
+ if (change < 0)
36
+ return 'negative'
37
+ }
38
+
39
+ PriceDynamic.propTypes = {
40
+ change: PropTypes.number,
41
+ current: PropTypes.number,
42
+ prev: PropTypes.number,
43
+ standalone: PropTypes.bool,
44
+ allowZero: PropTypes.bool
45
+ }
@@ -0,0 +1,36 @@
1
+ @use "../basic-styles/variables";
2
+
3
+ .price-change {
4
+ vertical-align: super;
5
+ line-height: 1;
6
+ font-size: 0.8em;
7
+ margin-right: -0.2em;
8
+
9
+ &:before {
10
+ font-size: .8em;
11
+ vertical-align: bottom;
12
+ padding: 0 .1em;
13
+ line-height: 2.2;
14
+ }
15
+
16
+ &.positive {
17
+ color: var(--color-price-up);
18
+
19
+ &:before {
20
+ content: '\2191';
21
+ }
22
+ }
23
+
24
+ &.negative {
25
+ color: var(--color-price-down);
26
+
27
+ &:before {
28
+ content: '\2193';
29
+ }
30
+ }
31
+
32
+ &.standalone {
33
+ vertical-align: middle;
34
+ font-size: 0.9em;
35
+ }
36
+ }
@@ -0,0 +1,89 @@
1
+ import {useEffect, useState} from 'react'
2
+ import {StrKey} from '@stellar/stellar-base'
3
+ import {stringifyQuery} from '@stellar-expert/navigation'
4
+ import {InMemoryClientCache} from '@stellar-expert/client-cache'
5
+ import {fetchExplorerApi} from '../api/explorer-api-call'
6
+ import {ExplorerBatchInfoLoader} from '../api/explorer-batch-info-loader'
7
+
8
+ const cache = new InMemoryClientCache({})
9
+
10
+ const loader = new ExplorerBatchInfoLoader(batch => {
11
+ return fetchExplorerApi('directory' + stringifyQuery({address: batch}))
12
+ }, entry => {
13
+ cache.set(entry.address, entry)
14
+ return {key: entry.address, info: entry}
15
+ })
16
+
17
+ export async function getDirectoryEntry(address, options) {
18
+ const {forceRefresh = false, extended = false} = options || {}
19
+ //ignore invalid addresses
20
+ if (!address || !StrKey.isValidEd25519PublicKey(address)) return null
21
+ if (extended) {
22
+ return fetchExplorerApi(`directory/${address}` + stringifyQuery({
23
+ extended: true,
24
+ s: Math.random().toString(36).substr(2)
25
+ }))
26
+ .catch(err => {
27
+ console.error(err)
28
+ return null
29
+ })
30
+ }
31
+ //try to load from the shared cache
32
+ if (forceRefresh) {
33
+ const cachedEntry = cache.get(address)
34
+ if (cachedEntry && !cachedEntry.isExpired) return cachedEntry.data
35
+ }
36
+ //load from the server
37
+ return loader.loadEntry(address)
38
+ }
39
+
40
+ /**
41
+ *
42
+ * @param {String} address
43
+ * @param {{[forceRefresh]: Boolean}} [options]
44
+ * @return {String|null}
45
+ */
46
+ export function useDirectory(address, options) {
47
+ const {forceRefresh = false} = options || {}
48
+ const [directoryInfo, setDirectoryInfo] = useState(null)
49
+ let unloaded = false
50
+ useEffect(function () {
51
+ if (!address || !(StrKey.isValidEd25519PublicKey(address) || StrKey.isValidContract(address)))
52
+ return
53
+ if (!forceRefresh) {
54
+ const cachedEntry = cache.get(address)
55
+ if (cachedEntry && !cachedEntry.isStale) {
56
+ if (!cachedEntry.isExpired)
57
+ return setDirectoryInfo(cachedEntry.data) //everything is up to date - no need to re-fetch
58
+ }
59
+ }
60
+ //load from the server
61
+ loader.loadEntry(address)
62
+ .then(di => {
63
+ if (!unloaded) {
64
+ setDirectoryInfo(di)
65
+ }
66
+ })
67
+ return () => {
68
+ unloaded = true
69
+ }
70
+ }, [address, forceRefresh])
71
+ return directoryInfo
72
+ }
73
+
74
+ export function useDirectoryTags() {
75
+ const [tags, setTags] = useState(() => {
76
+ const cachedEntry = cache.get('tags')
77
+ if (cachedEntry && !cachedEntry.isExpired) {
78
+ return cachedEntry.data
79
+ }
80
+ fetchExplorerApi('directory/tags')
81
+ .then(data => {
82
+ cache.set('tags', data, 10 * 60) //10 minutes
83
+ setTags(data)
84
+ })
85
+ .catch(err => console.error(err))
86
+ return []
87
+ })
88
+ return tags
89
+ }