Extract market place (#78)
* chore: remove marketplace components * feat: add static marketplace * feat: add entry point for static marketplace * doc: add comment * chore: include nostr-bundle.js
1
static/market/assets/ErrorNotFound.db627eb7.js
Normal file
|
|
@ -0,0 +1 @@
|
|||
import{_ as t,y as o,A as s,aU as a,D as e,E as n,aQ as r}from"./index.725caa24.js";const c=o({name:"ErrorNotFound"}),l={class:"fullscreen bg-blue text-white text-center q-pa-md flex flex-center"},d=e("div",{style:{"font-size":"30vh"}}," 404 ",-1),i=e("div",{class:"text-h2",style:{opacity:".4"}}," Oops. Nothing here... ",-1);function _(p,f,u,h,x,m){return s(),a("div",l,[e("div",null,[d,i,n(r,{class:"q-mt-xl",color:"white","text-color":"blue",unelevated:"",to:"/",label:"Go Home","no-caps":""})])])}var N=t(c,[["render",_]]);export{N as default};
|
||||
BIN
static/market/assets/KFOkCnqEu92Fr1MmgVxIIzQ.34e9582c.woff
Normal file
BIN
static/market/assets/KFOlCnqEu92Fr1MmEU9fBBc-.9ce7f3ac.woff
Normal file
BIN
static/market/assets/KFOlCnqEu92Fr1MmSU5fBBc-.bf14c7d7.woff
Normal file
BIN
static/market/assets/KFOlCnqEu92Fr1MmWUlfBBc-.e0fd57c0.woff
Normal file
BIN
static/market/assets/KFOlCnqEu92Fr1MmYUtfBBc-.f6537e32.woff
Normal file
BIN
static/market/assets/KFOmCnqEu92Fr1Mu4mxM.f2abf7fb.woff
Normal file
1
static/market/assets/MainLayout.421c5479.js
Normal file
18
static/market/assets/MarketPage.2aa781b5.js
Normal file
1
static/market/assets/QResizeObserver.bcb70109.js
Normal file
|
|
@ -0,0 +1 @@
|
|||
import{r as g,s as z,o as c,c as y,f,n as w,F as v,h as R,g as O,k as b}from"./index.725caa24.js";function x(){const r=g(!z.value);return r.value===!1&&c(()=>{r.value=!0}),r}const m=typeof ResizeObserver!="undefined",h=m===!0?{}:{style:"display:block;position:absolute;top:0;left:0;right:0;bottom:0;height:100%;width:100%;overflow:hidden;pointer-events:none;z-index:-1;",url:"about:blank"};var L=y({name:"QResizeObserver",props:{debounce:{type:[String,Number],default:100}},emits:["resize"],setup(r,{emit:p}){let i=null,t,o={width:-1,height:-1};function s(e){e===!0||r.debounce===0||r.debounce==="0"?u():i===null&&(i=setTimeout(u,r.debounce))}function u(){if(i!==null&&(clearTimeout(i),i=null),t){const{offsetWidth:e,offsetHeight:n}=t;(e!==o.width||n!==o.height)&&(o={width:e,height:n},p("resize",o))}}const{proxy:a}=O();if(m===!0){let e;const n=l=>{t=a.$el.parentNode,t?(e=new ResizeObserver(s),e.observe(t),u()):l!==!0&&v(()=>{n(!0)})};return c(()=>{n()}),f(()=>{i!==null&&clearTimeout(i),e!==void 0&&(e.disconnect!==void 0?e.disconnect():t&&e.unobserve(t))}),w}else{let l=function(){i!==null&&(clearTimeout(i),i=null),n!==void 0&&(n.removeEventListener!==void 0&&n.removeEventListener("resize",s,b.passive),n=void 0)},d=function(){l(),t&&t.contentDocument&&(n=t.contentDocument.defaultView,n.addEventListener("resize",s,b.passive),u())};const e=x();let n;return c(()=>{v(()=>{t=a.$el,t&&d()})}),f(l),a.trigger=s,()=>{if(e.value===!0)return R("object",{style:h.style,tabindex:-1,type:"text/html",data:h.url,"aria-hidden":"true",onLoad:d})}}}});export{L as Q};
|
||||
BIN
static/market/assets/flUhRq6tzZclQEJ-Vdg-IuiaDsNa.fd84f88b.woff
Normal file
1
static/market/assets/index.5f2eed21.css
Normal file
5
static/market/assets/index.725caa24.js
Normal file
BIN
static/market/favicon.ico
Normal file
|
After Width: | Height: | Size: 245 KiB |
BIN
static/market/icons/favicon-128x128.png
Normal file
|
After Width: | Height: | Size: 47 KiB |
BIN
static/market/icons/favicon-16x16.png
Normal file
|
After Width: | Height: | Size: 967 B |
BIN
static/market/icons/favicon-32x32.png
Normal file
|
After Width: | Height: | Size: 3.4 KiB |
BIN
static/market/icons/favicon-64x64.png
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
static/market/icons/favicon-96x96.png
Normal file
|
After Width: | Height: | Size: 27 KiB |
BIN
static/market/images/bitcoin-shop.png
Normal file
|
After Width: | Height: | Size: 5.9 KiB |
BIN
static/market/images/blank-avatar.webp
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
static/market/images/nostr-avatar.png
Normal file
|
After Width: | Height: | Size: 784 KiB |
BIN
static/market/images/nostr-cover.png
Normal file
|
After Width: | Height: | Size: 1.1 MiB |
BIN
static/market/images/placeholder.png
Normal file
|
After Width: | Height: | Size: 2.8 KiB |
27
static/market/index.html
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<title>Nostr Market App</title>
|
||||
<meta charset=utf-8>
|
||||
<meta name=description content="A Nostr marketplace">
|
||||
<meta name=format-detection content="telephone=no">
|
||||
<meta name=msapplication-tap-highlight content=no>
|
||||
<meta name=viewport content="user-scalable=no,initial-scale=1,maximum-scale=1,minimum-scale=1,width=device-width">
|
||||
<script src="/nostrmarket/static/market/js/nostr.bundle.js"></script>
|
||||
<script src="/nostrmarket/static/market/js/bolt11-decoder.js"></script>
|
||||
<script src="/nostrmarket/static/market/js/utils.js"></script>
|
||||
<link rel=icon type=image/png sizes=128x128 href="/nostrmarket/static/market/icons/favicon-128x128.png">
|
||||
<link rel=icon type=image/png sizes=96x96 href="/nostrmarket/static/market/icons/favicon-96x96.png">
|
||||
<link rel=icon type=image/png sizes=32x32 href="/nostrmarket/static/market/icons/favicon-32x32.png">
|
||||
<link rel=icon type=image/png sizes=16x16 href="/nostrmarket/static/market/icons/favicon-16x16.png">
|
||||
<link rel=icon type=image/ico href="/nostrmarket/static/market/favicon.ico">
|
||||
<script type="module" crossorigin src="/nostrmarket/static/market/assets/index.725caa24.js"></script>
|
||||
<link rel="stylesheet" href="/nostrmarket/static/market/assets/index.5f2eed21.css">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id=q-app></div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
347
static/market/js/bolt11-decoder.js
Normal file
|
|
@ -0,0 +1,347 @@
|
|||
//TODO - A reader MUST check that the signature is valid (see the n tagged field)
|
||||
//TODO - Tagged part of type f: the fallback on-chain address should be decoded into an address format
|
||||
//TODO - A reader MUST check that the SHA-2 256 in the h field exactly matches the hashed description.
|
||||
//TODO - A reader MUST use the n field to validate the signature instead of performing signature recovery if a valid n field is provided.
|
||||
|
||||
function decode(paymentRequest) {
|
||||
let input = paymentRequest.toLowerCase()
|
||||
let splitPosition = input.lastIndexOf('1')
|
||||
let humanReadablePart = input.substring(0, splitPosition)
|
||||
let data = input.substring(splitPosition + 1, input.length - 6)
|
||||
let checksum = input.substring(input.length - 6, input.length)
|
||||
if (
|
||||
!verify_checksum(humanReadablePart, bech32ToFiveBitArray(data + checksum))
|
||||
) {
|
||||
throw 'Malformed request: checksum is incorrect' // A reader MUST fail if the checksum is incorrect.
|
||||
}
|
||||
return {
|
||||
human_readable_part: decodeHumanReadablePart(humanReadablePart),
|
||||
data: decodeData(data, humanReadablePart),
|
||||
checksum: checksum
|
||||
}
|
||||
}
|
||||
|
||||
function decodeHumanReadablePart(humanReadablePart) {
|
||||
let prefixes = ['lnbc', 'lntb', 'lnbcrt', 'lnsb']
|
||||
let prefix
|
||||
prefixes.forEach(value => {
|
||||
if (humanReadablePart.substring(0, value.length) === value) {
|
||||
prefix = value
|
||||
}
|
||||
})
|
||||
if (prefix == null) throw 'Malformed request: unknown prefix' // A reader MUST fail if it does not understand the prefix.
|
||||
let amount = decodeAmount(
|
||||
humanReadablePart.substring(prefix.length, humanReadablePart.length)
|
||||
)
|
||||
return {
|
||||
prefix: prefix,
|
||||
amount: amount
|
||||
}
|
||||
}
|
||||
|
||||
function decodeData(data, humanReadablePart) {
|
||||
let date32 = data.substring(0, 7)
|
||||
let dateEpoch = bech32ToInt(date32)
|
||||
let signature = data.substring(data.length - 104, data.length)
|
||||
let tagData = data.substring(7, data.length - 104)
|
||||
let decodedTags = decodeTags(tagData)
|
||||
let value = bech32ToFiveBitArray(date32 + tagData)
|
||||
value = fiveBitArrayTo8BitArray(value, true)
|
||||
value = textToHexString(humanReadablePart).concat(byteArrayToHexString(value))
|
||||
return {
|
||||
time_stamp: dateEpoch,
|
||||
tags: decodedTags,
|
||||
signature: decodeSignature(signature),
|
||||
signing_data: value
|
||||
}
|
||||
}
|
||||
|
||||
function decodeSignature(signature) {
|
||||
let data = fiveBitArrayTo8BitArray(bech32ToFiveBitArray(signature))
|
||||
let recoveryFlag = data[data.length - 1]
|
||||
let r = byteArrayToHexString(data.slice(0, 32))
|
||||
let s = byteArrayToHexString(data.slice(32, data.length - 1))
|
||||
return {
|
||||
r: r,
|
||||
s: s,
|
||||
recovery_flag: recoveryFlag
|
||||
}
|
||||
}
|
||||
|
||||
function decodeAmount(str) {
|
||||
let multiplier = str.charAt(str.length - 1)
|
||||
let amount = str.substring(0, str.length - 1)
|
||||
if (amount.substring(0, 1) === '0') {
|
||||
throw 'Malformed request: amount cannot contain leading zeros'
|
||||
}
|
||||
amount = Number(amount)
|
||||
if (amount < 0 || !Number.isInteger(amount)) {
|
||||
throw 'Malformed request: amount must be a positive decimal integer' // A reader SHOULD fail if amount contains a non-digit
|
||||
}
|
||||
|
||||
switch (multiplier) {
|
||||
case '':
|
||||
return 'Any amount' // A reader SHOULD indicate if amount is unspecified
|
||||
case 'p':
|
||||
return amount / 10
|
||||
case 'n':
|
||||
return amount * 100
|
||||
case 'u':
|
||||
return amount * 100000
|
||||
case 'm':
|
||||
return amount * 100000000
|
||||
default:
|
||||
// A reader SHOULD fail if amount is followed by anything except a defined multiplier.
|
||||
throw 'Malformed request: undefined amount multiplier'
|
||||
}
|
||||
}
|
||||
|
||||
function decodeTags(tagData) {
|
||||
let tags = extractTags(tagData)
|
||||
let decodedTags = []
|
||||
tags.forEach(value =>
|
||||
decodedTags.push(decodeTag(value.type, value.length, value.data))
|
||||
)
|
||||
return decodedTags
|
||||
}
|
||||
|
||||
function extractTags(str) {
|
||||
let tags = []
|
||||
while (str.length > 0) {
|
||||
let type = str.charAt(0)
|
||||
let dataLength = bech32ToInt(str.substring(1, 3))
|
||||
let data = str.substring(3, dataLength + 3)
|
||||
tags.push({
|
||||
type: type,
|
||||
length: dataLength,
|
||||
data: data
|
||||
})
|
||||
str = str.substring(3 + dataLength, str.length)
|
||||
}
|
||||
return tags
|
||||
}
|
||||
|
||||
function decodeTag(type, length, data) {
|
||||
switch (type) {
|
||||
case 'p':
|
||||
if (length !== 52) break // A reader MUST skip over a 'p' field that does not have data_length 52
|
||||
return {
|
||||
type: type,
|
||||
length: length,
|
||||
description: 'payment_hash',
|
||||
value: byteArrayToHexString(
|
||||
fiveBitArrayTo8BitArray(bech32ToFiveBitArray(data))
|
||||
)
|
||||
}
|
||||
case 'd':
|
||||
return {
|
||||
type: type,
|
||||
length: length,
|
||||
description: 'description',
|
||||
value: bech32ToUTF8String(data)
|
||||
}
|
||||
case 'n':
|
||||
if (length !== 53) break // A reader MUST skip over a 'n' field that does not have data_length 53
|
||||
return {
|
||||
type: type,
|
||||
length: length,
|
||||
description: 'payee_public_key',
|
||||
value: byteArrayToHexString(
|
||||
fiveBitArrayTo8BitArray(bech32ToFiveBitArray(data))
|
||||
)
|
||||
}
|
||||
case 'h':
|
||||
if (length !== 52) break // A reader MUST skip over a 'h' field that does not have data_length 52
|
||||
return {
|
||||
type: type,
|
||||
length: length,
|
||||
description: 'description_hash',
|
||||
value: data
|
||||
}
|
||||
case 'x':
|
||||
return {
|
||||
type: type,
|
||||
length: length,
|
||||
description: 'expiry',
|
||||
value: bech32ToInt(data)
|
||||
}
|
||||
case 'c':
|
||||
return {
|
||||
type: type,
|
||||
length: length,
|
||||
description: 'min_final_cltv_expiry',
|
||||
value: bech32ToInt(data)
|
||||
}
|
||||
case 'f':
|
||||
let version = bech32ToFiveBitArray(data.charAt(0))[0]
|
||||
if (version < 0 || version > 18) break // a reader MUST skip over an f field with unknown version.
|
||||
data = data.substring(1, data.length)
|
||||
return {
|
||||
type: type,
|
||||
length: length,
|
||||
description: 'fallback_address',
|
||||
value: {
|
||||
version: version,
|
||||
fallback_address: data
|
||||
}
|
||||
}
|
||||
case 'r':
|
||||
data = fiveBitArrayTo8BitArray(bech32ToFiveBitArray(data))
|
||||
let pubkey = data.slice(0, 33)
|
||||
let shortChannelId = data.slice(33, 41)
|
||||
let feeBaseMsat = data.slice(41, 45)
|
||||
let feeProportionalMillionths = data.slice(45, 49)
|
||||
let cltvExpiryDelta = data.slice(49, 51)
|
||||
return {
|
||||
type: type,
|
||||
length: length,
|
||||
description: 'routing_information',
|
||||
value: {
|
||||
public_key: byteArrayToHexString(pubkey),
|
||||
short_channel_id: byteArrayToHexString(shortChannelId),
|
||||
fee_base_msat: byteArrayToInt(feeBaseMsat),
|
||||
fee_proportional_millionths: byteArrayToInt(
|
||||
feeProportionalMillionths
|
||||
),
|
||||
cltv_expiry_delta: byteArrayToInt(cltvExpiryDelta)
|
||||
}
|
||||
}
|
||||
default:
|
||||
// reader MUST skip over unknown fields
|
||||
}
|
||||
}
|
||||
|
||||
function polymod(values) {
|
||||
let GEN = [0x3b6a57b2, 0x26508e6d, 0x1ea119fa, 0x3d4233dd, 0x2a1462b3]
|
||||
let chk = 1
|
||||
values.forEach(value => {
|
||||
let b = chk >> 25
|
||||
chk = ((chk & 0x1ffffff) << 5) ^ value
|
||||
for (let i = 0; i < 5; i++) {
|
||||
if (((b >> i) & 1) === 1) {
|
||||
chk ^= GEN[i]
|
||||
} else {
|
||||
chk ^= 0
|
||||
}
|
||||
}
|
||||
})
|
||||
return chk
|
||||
}
|
||||
|
||||
function expand(str) {
|
||||
let array = []
|
||||
for (let i = 0; i < str.length; i++) {
|
||||
array.push(str.charCodeAt(i) >> 5)
|
||||
}
|
||||
array.push(0)
|
||||
for (let i = 0; i < str.length; i++) {
|
||||
array.push(str.charCodeAt(i) & 31)
|
||||
}
|
||||
return array
|
||||
}
|
||||
|
||||
function verify_checksum(hrp, data) {
|
||||
hrp = expand(hrp)
|
||||
let all = hrp.concat(data)
|
||||
let bool = polymod(all)
|
||||
return bool === 1
|
||||
}
|
||||
|
||||
const bech32CharValues = 'qpzry9x8gf2tvdw0s3jn54khce6mua7l'
|
||||
|
||||
function byteArrayToInt(byteArray) {
|
||||
let value = 0
|
||||
for (let i = 0; i < byteArray.length; ++i) {
|
||||
value = (value << 8) + byteArray[i]
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
function bech32ToInt(str) {
|
||||
let sum = 0
|
||||
for (let i = 0; i < str.length; i++) {
|
||||
sum = sum * 32
|
||||
sum = sum + bech32CharValues.indexOf(str.charAt(i))
|
||||
}
|
||||
return sum
|
||||
}
|
||||
|
||||
function bech32ToFiveBitArray(str) {
|
||||
let array = []
|
||||
for (let i = 0; i < str.length; i++) {
|
||||
array.push(bech32CharValues.indexOf(str.charAt(i)))
|
||||
}
|
||||
return array
|
||||
}
|
||||
|
||||
function fiveBitArrayTo8BitArray(int5Array, includeOverflow) {
|
||||
let count = 0
|
||||
let buffer = 0
|
||||
let byteArray = []
|
||||
int5Array.forEach(value => {
|
||||
buffer = (buffer << 5) + value
|
||||
count += 5
|
||||
if (count >= 8) {
|
||||
byteArray.push((buffer >> (count - 8)) & 255)
|
||||
count -= 8
|
||||
}
|
||||
})
|
||||
if (includeOverflow && count > 0) {
|
||||
byteArray.push((buffer << (8 - count)) & 255)
|
||||
}
|
||||
return byteArray
|
||||
}
|
||||
|
||||
function bech32ToUTF8String(str) {
|
||||
let int5Array = bech32ToFiveBitArray(str)
|
||||
let byteArray = fiveBitArrayTo8BitArray(int5Array)
|
||||
|
||||
let utf8String = ''
|
||||
for (let i = 0; i < byteArray.length; i++) {
|
||||
utf8String += '%' + ('0' + byteArray[i].toString(16)).slice(-2)
|
||||
}
|
||||
return decodeURIComponent(utf8String)
|
||||
}
|
||||
|
||||
function byteArrayToHexString(byteArray) {
|
||||
return Array.prototype.map
|
||||
.call(byteArray, function (byte) {
|
||||
return ('0' + (byte & 0xff).toString(16)).slice(-2)
|
||||
})
|
||||
.join('')
|
||||
}
|
||||
|
||||
function textToHexString(text) {
|
||||
let hexString = ''
|
||||
for (let i = 0; i < text.length; i++) {
|
||||
hexString += text.charCodeAt(i).toString(16)
|
||||
}
|
||||
return hexString
|
||||
}
|
||||
|
||||
function epochToDate(int) {
|
||||
let date = new Date(int * 1000)
|
||||
return date.toUTCString()
|
||||
}
|
||||
|
||||
function isEmptyOrSpaces(str) {
|
||||
return str === null || str.match(/^ *$/) !== null
|
||||
}
|
||||
|
||||
function toFixed(x) {
|
||||
if (Math.abs(x) < 1.0) {
|
||||
var e = parseInt(x.toString().split('e-')[1])
|
||||
if (e) {
|
||||
x *= Math.pow(10, e - 1)
|
||||
x = '0.' + new Array(e).join('0') + x.toString().substring(2)
|
||||
}
|
||||
} else {
|
||||
var e = parseInt(x.toString().split('+')[1])
|
||||
if (e > 20) {
|
||||
e -= 20
|
||||
x /= Math.pow(10, e)
|
||||
x += new Array(e + 1).join('0')
|
||||
}
|
||||
}
|
||||
return x
|
||||
}
|
||||
8705
static/market/js/nostr.bundle.js
Normal file
167
static/market/js/utils.js
Normal file
|
|
@ -0,0 +1,167 @@
|
|||
var NostrTools = window.NostrTools
|
||||
|
||||
var defaultRelays = [
|
||||
'wss://relay.damus.io',
|
||||
'wss://relay.snort.social',
|
||||
'wss://nostr-pub.wellorder.net',
|
||||
'wss://nostr.zebedee.cloud',
|
||||
'wss://nostr.walletofsatoshi.com'
|
||||
]
|
||||
var eventToObj = event => {
|
||||
try {
|
||||
event.content = JSON.parse(event.content) || null
|
||||
} catch {
|
||||
event.content = null
|
||||
}
|
||||
|
||||
|
||||
return {
|
||||
...event,
|
||||
...Object.values(event.tags).reduce((acc, tag) => {
|
||||
let [key, value] = tag
|
||||
if (key == 't') {
|
||||
return { ...acc, [key]: [...(acc[key] || []), value] }
|
||||
} else {
|
||||
return { ...acc, [key]: value }
|
||||
}
|
||||
}, {})
|
||||
}
|
||||
}
|
||||
|
||||
function confirm(message) {
|
||||
return {
|
||||
message,
|
||||
ok: {
|
||||
flat: true,
|
||||
color: 'primary'
|
||||
},
|
||||
cancel: {
|
||||
flat: true,
|
||||
color: 'grey'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
async function hash(string) {
|
||||
const utf8 = new TextEncoder().encode(string)
|
||||
const hashBuffer = await crypto.subtle.digest('SHA-256', utf8)
|
||||
const hashArray = Array.from(new Uint8Array(hashBuffer))
|
||||
const hashHex = hashArray
|
||||
.map(bytes => bytes.toString(16).padStart(2, '0'))
|
||||
.join('')
|
||||
return hashHex
|
||||
}
|
||||
|
||||
function isJson(str) {
|
||||
if (typeof str !== 'string') {
|
||||
return false
|
||||
}
|
||||
try {
|
||||
JSON.parse(str)
|
||||
return true
|
||||
} catch (error) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
function formatSat(value) {
|
||||
return new Intl.NumberFormat(window.LOCALE).format(value)
|
||||
}
|
||||
|
||||
function satOrBtc(val, showUnit = true, showSats = false) {
|
||||
const value = showSats
|
||||
? formatSat(val)
|
||||
: val == 0
|
||||
? 0.0
|
||||
: (val / 100000000).toFixed(8)
|
||||
if (!showUnit) return value
|
||||
return showSats ? value + ' sat' : value + ' BTC'
|
||||
}
|
||||
|
||||
function timeFromNow(time) {
|
||||
// Get timestamps
|
||||
let unixTime = new Date(time).getTime()
|
||||
if (!unixTime) return
|
||||
let now = new Date().getTime()
|
||||
|
||||
// Calculate difference
|
||||
let difference = unixTime / 1000 - now / 1000
|
||||
|
||||
// Setup return object
|
||||
let tfn = {}
|
||||
|
||||
// Check if time is in the past, present, or future
|
||||
tfn.when = 'now'
|
||||
if (difference > 0) {
|
||||
tfn.when = 'future'
|
||||
} else if (difference < -1) {
|
||||
tfn.when = 'past'
|
||||
}
|
||||
|
||||
// Convert difference to absolute
|
||||
difference = Math.abs(difference)
|
||||
|
||||
// Calculate time unit
|
||||
if (difference / (60 * 60 * 24 * 365) > 1) {
|
||||
// Years
|
||||
tfn.unitOfTime = 'years'
|
||||
tfn.time = Math.floor(difference / (60 * 60 * 24 * 365))
|
||||
} else if (difference / (60 * 60 * 24 * 45) > 1) {
|
||||
// Months
|
||||
tfn.unitOfTime = 'months'
|
||||
tfn.time = Math.floor(difference / (60 * 60 * 24 * 45))
|
||||
} else if (difference / (60 * 60 * 24) > 1) {
|
||||
// Days
|
||||
tfn.unitOfTime = 'days'
|
||||
tfn.time = Math.floor(difference / (60 * 60 * 24))
|
||||
} else if (difference / (60 * 60) > 1) {
|
||||
// Hours
|
||||
tfn.unitOfTime = 'hours'
|
||||
tfn.time = Math.floor(difference / (60 * 60))
|
||||
} else if (difference / 60 > 1) {
|
||||
// Minutes
|
||||
tfn.unitOfTime = 'minutes'
|
||||
tfn.time = Math.floor(difference / 60)
|
||||
} else {
|
||||
// Seconds
|
||||
tfn.unitOfTime = 'seconds'
|
||||
tfn.time = Math.floor(difference)
|
||||
}
|
||||
|
||||
// Return time from now data
|
||||
return `${tfn.time} ${tfn.unitOfTime}`
|
||||
}
|
||||
|
||||
function isValidImageUrl(string) {
|
||||
let url
|
||||
try {
|
||||
url = new URL(string)
|
||||
} catch (_) {
|
||||
return false
|
||||
}
|
||||
return url.protocol === 'http:' || url.protocol === 'https:'
|
||||
}
|
||||
|
||||
function isValidKey(key, prefix = 'n') {
|
||||
try {
|
||||
if (key && key.startsWith(prefix)) {
|
||||
let { _, data } = NostrTools.nip19.decode(key)
|
||||
key = data
|
||||
}
|
||||
return isValidKeyHex(key)
|
||||
} catch (error) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
function isValidKeyHex(key) {
|
||||
return key?.toLowerCase()?.match(/^[0-9a-f]{64}$/)
|
||||
}
|
||||
|
||||
function formatCurrency(value, currency) {
|
||||
return new Intl.NumberFormat(window.LOCALE, {
|
||||
style: 'currency',
|
||||
currency: currency
|
||||
}).format(value)
|
||||
}
|
||||