@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.
- package/.npmrc +1 -0
- package/LICENSE +21 -0
- package/README.md +3 -0
- package/account/account-address.js +127 -0
- package/account/account-address.scss +20 -0
- package/account/available-balance.js +21 -0
- package/account/identicon.js +91 -0
- package/account/identicon.scss +6 -0
- package/account/signer-key.js +65 -0
- package/api/api-cache.js +5 -0
- package/api/explorer-api-call.js +37 -0
- package/api/explorer-api-hooks.js +202 -0
- package/api/explorer-api-paginated-list-hooks.js +439 -0
- package/api/explorer-batch-info-loader.js +85 -0
- package/api/explorer-tx-api.js +29 -0
- package/asset/amount.js +53 -0
- package/asset/asset-icon.js +34 -0
- package/asset/asset-issuer.js +22 -0
- package/asset/asset-link.js +93 -0
- package/asset/asset-link.scss +26 -0
- package/asset/asset-list-hooks.js +60 -0
- package/asset/asset-meta-hooks.js +78 -0
- package/asset/asset-selector.js +72 -0
- package/asset/asset-selector.scss +41 -0
- package/basic-styles/base.scss +53 -0
- package/basic-styles/form.scss +90 -0
- package/basic-styles/grid.scss +388 -0
- package/basic-styles/link.scss +17 -0
- package/basic-styles/lists.scss +68 -0
- package/basic-styles/loader.scss +22 -0
- package/basic-styles/responsive.scss +29 -0
- package/basic-styles/scrollbars.scss +19 -0
- package/basic-styles/spacing.scss +50 -0
- package/basic-styles/table.scss +104 -0
- package/basic-styles/themes.scss +59 -0
- package/basic-styles/typography.scss +191 -0
- package/basic-styles/variables.scss +26 -0
- package/claimable-balance/claimable-balance-claimants.js +12 -0
- package/contract/sc-val.js +70 -0
- package/contract/wasm-section-parser.js +159 -0
- package/contract/wasm-section-reader.js +79 -0
- package/contract/xdr-reader.js +148 -0
- package/controls/active-icon.scss +23 -0
- package/controls/button-group.js +20 -0
- package/controls/button-group.scss +48 -0
- package/controls/button.js +74 -0
- package/controls/button.scss +148 -0
- package/controls/code-block.js +35 -0
- package/controls/code-block.scss +74 -0
- package/controls/dropdown.js +281 -0
- package/controls/dropdown.scss +161 -0
- package/controls/external-link.js +5 -0
- package/controls/info-tooltip.js +17 -0
- package/controls/info-tooltip.scss +4 -0
- package/controls/slider.js +20 -0
- package/controls/slider.scss +22 -0
- package/controls/tabs.js +94 -0
- package/controls/tabs.scss +73 -0
- package/controls/tooltip.js +214 -0
- package/controls/tooltip.scss +97 -0
- package/controls/update-highlighter.js +28 -0
- package/controls/update-highlighter.scss +10 -0
- package/date/date-selector.js +44 -0
- package/date/elapsed-time.js +22 -0
- package/date/utc-timestamp.js +23 -0
- package/dex/price-dynamic.js +45 -0
- package/dex/price-dynamic.scss +36 -0
- package/directory/directory-hooks.js +89 -0
- package/effect/effect-description.js +289 -0
- package/effect/op-effects-view.js +11 -0
- package/effect/op-effects.scss +8 -0
- package/errors/error-boundary.js +98 -0
- package/fonts/font.scss +23 -0
- package/fonts/icons/config.json +1858 -0
- package/fonts/icons/icons-embedded.scss +644 -0
- package/horizon/horizon-account-helpers.js +104 -0
- package/horizon/horizon-client-helpers.js +52 -0
- package/horizon/horizon-ledger-helpers.js +36 -0
- package/horizon/horizon-orderbook-helpers.js +14 -0
- package/horizon/horizon-trades-helper.js +89 -0
- package/horizon/horizon-transaction-helpers.js +36 -0
- package/index.js +85 -0
- package/index.scss +16 -0
- package/interaction/autofocus.js +9 -0
- package/interaction/block-select.js +54 -0
- package/interaction/block-select.scss +24 -0
- package/interaction/copy-to-clipboard.js +19 -0
- package/interaction/inline-progress.js +16 -0
- package/interaction/qr-code.js +45 -0
- package/interaction/responsive.js +21 -0
- package/interaction/spoiler.js +40 -0
- package/interaction/spoiler.scss +9 -0
- package/interaction/theme-selector.js +11 -0
- package/ledger/ledger-entry-href-formatter.js +22 -0
- package/ledger/ledger-entry-link.js +59 -0
- package/meta/page-meta-tags.js +229 -0
- package/module/dynamic-module.js +48 -0
- package/package.json +41 -0
- package/state/on-screen-hooks.js +23 -0
- package/state/page-visibility-helpers.js +17 -0
- package/state/page-visibility-hooks.js +12 -0
- package/state/screen-orientation-hooks.js +16 -0
- package/state/state-hooks.js +77 -0
- package/state/stellar-network-hooks.js +45 -0
- package/state/theme.js +29 -0
- package/stellar/key-type.js +92 -0
- package/stellar/ledger-generic-id.js +40 -0
- package/stellar/signature-hint-utils.js +65 -0
- package/toast/toast-notification-instance.js +43 -0
- package/toast/toast-notification.js +46 -0
- package/toast/toast-notifications-block.js +55 -0
- package/toast/toast-notifications.scss +166 -0
- package/tx/op-accounting-changes.js +27 -0
- package/tx/op-description-view.js +925 -0
- package/tx/op-description.scss +98 -0
- package/tx/op-icon.js +93 -0
- package/tx/parser/op-balance-changes.js +62 -0
- package/tx/parser/op-descriptor.js +49 -0
- package/tx/parser/tx-details-parser.js +75 -0
- package/tx/parser/tx-matcher.js +372 -0
- package/tx/parser/type-filter-matcher.js +127 -0
- package/tx/tx-fee-effect.js +14 -0
- package/tx/tx-list-hooks.js +19 -0
- 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,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
|
+
}
|