mirror of
https://github.com/Retropex/bitfeed.git
synced 2025-06-02 23:32:31 +02:00
Message sliders & supporters overlay
Donation bar and old donation overlay removed. New dropping/sliding message notification system. Thank you page for sponsors & donors.
This commit is contained in:
parent
8db6fef727
commit
e7dff7024e
@ -83,16 +83,16 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
a {
|
a {
|
||||||
color: rgb(0,100,200);
|
color: var(--palette-x);
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
a:hover {
|
a:hover {
|
||||||
text-decoration: underline;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
a:visited {
|
a:visited {
|
||||||
color: rgb(0,80,160);
|
color: inherit;
|
||||||
}
|
}
|
||||||
|
|
||||||
label {
|
label {
|
||||||
@ -137,6 +137,23 @@ button:focus {
|
|||||||
border-color: #666;
|
border-color: #666;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
button.action-button {
|
||||||
|
background: var(--bold-a);
|
||||||
|
color: white;
|
||||||
|
padding: 0.5em 2em;
|
||||||
|
margin: 1em 2em 0.5em;
|
||||||
|
font-size: 1.1em;
|
||||||
|
font-weight: 600;
|
||||||
|
box-shadow: inset 0px 0px 5px var(--dark-b);
|
||||||
|
text-shadow: 0px 0px 5px var(--dark-b);
|
||||||
|
}
|
||||||
|
button.action-button:hover {
|
||||||
|
box-shadow: inset 0px 0px 2px var(--dark-b);
|
||||||
|
}
|
||||||
|
button.action-button:active {
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
@keyframes spin {
|
@keyframes spin {
|
||||||
from {transform:rotate(0deg);}
|
from {transform:rotate(0deg);}
|
||||||
to {transform:rotate(360deg);}
|
to {transform:rotate(360deg);}
|
||||||
|
@ -1,269 +0,0 @@
|
|||||||
<script>
|
|
||||||
import analytics from '../utils/analytics.js'
|
|
||||||
import { onMount } from 'svelte'
|
|
||||||
import Icon from '../components/Icon.svelte'
|
|
||||||
import config from '../config.js'
|
|
||||||
import { fly, fade } from 'svelte/transition'
|
|
||||||
// import { linear } from 'svelte/easing'
|
|
||||||
import coffeeIcon from '../assets/icon/cib-buy-me-a-coffee.svg'
|
|
||||||
import clipboardIcon from '../assets/icon/cil-clipboard.svg'
|
|
||||||
import qrIcon from '../assets/icon/cil-qr-code.svg'
|
|
||||||
import closeIcon from '../assets/icon/cil-x-circle.svg'
|
|
||||||
import openIcon from '../assets/icon/cil-arrow-circle-bottom.svg'
|
|
||||||
import boltIcon from '../assets/icon/cil-bolt-filled.svg'
|
|
||||||
import { overlay } from '../stores.js'
|
|
||||||
|
|
||||||
let copied = false
|
|
||||||
let addressElement
|
|
||||||
|
|
||||||
let showCopyButton = true
|
|
||||||
let expanded = false
|
|
||||||
let qrHidden = true
|
|
||||||
let qrLocked = false
|
|
||||||
let qrElement
|
|
||||||
|
|
||||||
onMount(() => {
|
|
||||||
showCopyButton = (navigator && navigator.clipboard && navigator.clipboard.writeText) || !!addressElement
|
|
||||||
})
|
|
||||||
|
|
||||||
async function copyAddress () {
|
|
||||||
if (navigator && navigator.clipboard && navigator.clipboard.writeText) {
|
|
||||||
await navigator.clipboard.writeText(config.donationAddress)
|
|
||||||
copied = true
|
|
||||||
setTimeout(() => {
|
|
||||||
copied = false
|
|
||||||
}, 2000)
|
|
||||||
} else if (addressElement) {
|
|
||||||
// fallback
|
|
||||||
const range = document.createRange()
|
|
||||||
range.selectNodeContents(addressElement)
|
|
||||||
const selection = window.getSelection()
|
|
||||||
selection.removeAllRanges()
|
|
||||||
selection.addRange(range)
|
|
||||||
copied = document.execCommand('copy')
|
|
||||||
setTimeout(() => {
|
|
||||||
copied = false
|
|
||||||
selection.removeAllRanges()
|
|
||||||
}, 2000)
|
|
||||||
}
|
|
||||||
analytics.trackEvent('donations', 'on-chain', 'copy-address')
|
|
||||||
}
|
|
||||||
|
|
||||||
function showQR () {
|
|
||||||
qrHidden = false
|
|
||||||
}
|
|
||||||
|
|
||||||
function hideQR () {
|
|
||||||
qrHidden = true
|
|
||||||
}
|
|
||||||
|
|
||||||
function clickaway (event) {
|
|
||||||
if (!qrElement.contains(event.target)) {
|
|
||||||
if (qrLocked) analytics.trackEvent('donations', 'on-chain', 'hide-qr')
|
|
||||||
qrLocked = false
|
|
||||||
window.removeEventListener('click', clickaway)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function toggleQR () {
|
|
||||||
qrLocked = !qrLocked
|
|
||||||
window.addEventListener('click', clickaway)
|
|
||||||
analytics.trackEvent('donations', 'on-chain', qrLocked ? 'show-qr' : 'hide-qr')
|
|
||||||
}
|
|
||||||
|
|
||||||
function toggleExpanded () {
|
|
||||||
expanded = !expanded
|
|
||||||
analytics.trackEvent('donations', 'bar', expanded ? 'open' : 'close')
|
|
||||||
}
|
|
||||||
|
|
||||||
function openLightningOverlay () {
|
|
||||||
expanded = false
|
|
||||||
analytics.trackEvent('donations', 'bar', 'close')
|
|
||||||
$overlay = 'lightning'
|
|
||||||
}
|
|
||||||
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style type="text/scss">
|
|
||||||
.donation-bar {
|
|
||||||
z-index: 100;
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
width: 32rem;
|
|
||||||
min-width: 100px;
|
|
||||||
max-width: calc(100vw - 8.25rem);
|
|
||||||
margin: auto;
|
|
||||||
transition: width 300ms, max-width 300ms;
|
|
||||||
|
|
||||||
.open-close-button {
|
|
||||||
position: absolute;
|
|
||||||
font-size: 1.2rem;
|
|
||||||
bottom: -0.5em;
|
|
||||||
right: 1rem;
|
|
||||||
transform: rotate(0deg);
|
|
||||||
background: var(--palette-c);
|
|
||||||
border-radius: 50%;
|
|
||||||
color: var(--palette-x);
|
|
||||||
cursor: pointer;
|
|
||||||
|
|
||||||
transition: transform 600ms;
|
|
||||||
}
|
|
||||||
|
|
||||||
.donation-content {
|
|
||||||
padding: 5px 5px;
|
|
||||||
|
|
||||||
background: var(--palette-c);
|
|
||||||
color: var(--palette-x);
|
|
||||||
|
|
||||||
border-bottom-left-radius: 5px;
|
|
||||||
border-bottom-right-radius: 5px;
|
|
||||||
|
|
||||||
.main-content {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
|
|
||||||
.coffee-icon {
|
|
||||||
font-size: 2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.address-and-copy {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
align-items: center;
|
|
||||||
flex-shrink: 1;
|
|
||||||
min-width: 4em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.address {
|
|
||||||
font-family: monospace;
|
|
||||||
font-weight: bold;
|
|
||||||
margin: 0 5px;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
}
|
|
||||||
|
|
||||||
.copy-button, .qr-button {
|
|
||||||
position: relative;
|
|
||||||
margin: 0 .25em;
|
|
||||||
font-size: 1.5rem;
|
|
||||||
cursor: pointer;
|
|
||||||
|
|
||||||
.copy-notif {
|
|
||||||
font-size: .7rem;
|
|
||||||
color: var(--palette-x);
|
|
||||||
position: absolute;
|
|
||||||
top: 100%;
|
|
||||||
right: 0;
|
|
||||||
background: var(--palette-d);
|
|
||||||
border: solid 1px var(--palette-x);
|
|
||||||
padding: 2px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.donation-info {
|
|
||||||
text-align: justify;
|
|
||||||
}
|
|
||||||
|
|
||||||
.expandable-content {
|
|
||||||
max-height: 0;
|
|
||||||
transition: max-height 300ms;
|
|
||||||
overflow: hidden;
|
|
||||||
padding: 0 0.5em 0.5em;
|
|
||||||
font-size: 0.8em;
|
|
||||||
|
|
||||||
.lightning-button {
|
|
||||||
background: var(--bold-a);
|
|
||||||
color: white;
|
|
||||||
padding: 5px 8px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.address-qr {
|
|
||||||
display: block;
|
|
||||||
position: absolute;
|
|
||||||
top: 100%;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
margin: auto;
|
|
||||||
max-width: 100vw;
|
|
||||||
max-height: calc(100vh - 120px);
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: 611px) {
|
|
||||||
left: 110px;
|
|
||||||
right: 110px;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 610px) {
|
|
||||||
right: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.expanded {
|
|
||||||
max-width: 100vw;
|
|
||||||
|
|
||||||
.open-close-button {
|
|
||||||
transform: rotate(180deg);
|
|
||||||
}
|
|
||||||
|
|
||||||
.donation-content {
|
|
||||||
.main-content {
|
|
||||||
.address-and-copy {
|
|
||||||
.address {
|
|
||||||
word-break: break-all;
|
|
||||||
overflow: visible;
|
|
||||||
text-align: left;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.expandable-content {
|
|
||||||
max-height: 200px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<div class="donation-bar" transition:fly={{ y: -10 }} class:expanded>
|
|
||||||
<div class="donation-content">
|
|
||||||
<div class="main-content">
|
|
||||||
<span class="coffee-icon">
|
|
||||||
<Icon icon={coffeeIcon} color="var(--bold-a)" inline />
|
|
||||||
</span>
|
|
||||||
<div class="address-and-copy">
|
|
||||||
<span class="address" bind:this={addressElement}>{ config.donationAddress }</span>
|
|
||||||
{#if showCopyButton}
|
|
||||||
<button class="copy-button" on:click={copyAddress} title="Copy address" alt="Copy address">
|
|
||||||
<Icon icon={clipboardIcon} color="var(--palette-x)" />
|
|
||||||
{#if copied }
|
|
||||||
<span class="copy-notif" transition:fly={{ y: -5 }}>Copied to clipboard!</span>
|
|
||||||
{/if}
|
|
||||||
</button>
|
|
||||||
{/if}
|
|
||||||
<div class="qr-button" title="Show QR" on:pointerover={showQR} on:pointerenter={showQR} on:pointerleave={hideQR} on:pointerout={hideQR} on:pointercancel={hideQR} on:click={toggleQR} bind:this={qrElement}>
|
|
||||||
<Icon icon={qrIcon} color="var(--palette-x)" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="expandable-content">
|
|
||||||
<p class="donation-info">
|
|
||||||
Enjoying Bitfeed? Donations keep this site running. On-chain transactions to the donation address above appear highlighted in green.
|
|
||||||
</p>
|
|
||||||
{#if config.lightningEnabled }
|
|
||||||
<button class="lightning-button" on:click={openLightningOverlay} >
|
|
||||||
<Icon icon={boltIcon} color="white" inline />Prefer Lightning?
|
|
||||||
</button>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{#if !qrHidden || qrLocked}
|
|
||||||
<img src="/img/qr.png" alt="" class="address-qr" transition:fade={{ duration: 300 }} >
|
|
||||||
{/if}
|
|
||||||
<div class="open-close-button" on:click={toggleExpanded}>
|
|
||||||
<Icon icon={openIcon} color="var(--palette-x)" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
@ -17,7 +17,7 @@ import clipboardIcon from '../assets/icon/cil-clipboard.svg'
|
|||||||
import twitterIcon from '../assets/icon/cib-twitter.svg'
|
import twitterIcon from '../assets/icon/cib-twitter.svg'
|
||||||
import { fade, fly } from 'svelte/transition'
|
import { fade, fly } from 'svelte/transition'
|
||||||
import { durationFormat } from '../utils/format.js'
|
import { durationFormat } from '../utils/format.js'
|
||||||
import { overlay } from '../stores.js'
|
import { overlay, tiers } from '../stores.js'
|
||||||
import QRCode from 'qrcode'
|
import QRCode from 'qrcode'
|
||||||
|
|
||||||
let tab = 'form' // form | invoice | success
|
let tab = 'form' // form | invoice | success
|
||||||
@ -50,12 +50,11 @@ const invoiceHexPlaceholder = 'lnbcxxxxxxt8l4pp5umz5kyakc0u8z3w2y568entyyq2gafgc
|
|||||||
let qrSrc = null
|
let qrSrc = null
|
||||||
let invoicePayments = []
|
let invoicePayments = []
|
||||||
|
|
||||||
let tierThresholds
|
let tierThresholds = []
|
||||||
let tiers = []
|
|
||||||
|
|
||||||
$: {
|
$: {
|
||||||
if (tierThresholds) {
|
if ($tiers) {
|
||||||
tiers = [
|
tierThresholds = [
|
||||||
{
|
{
|
||||||
title: 'Supporter',
|
title: 'Supporter',
|
||||||
description: "Help to keep the lights on with a small donation",
|
description: "Help to keep the lights on with a small donation",
|
||||||
@ -64,26 +63,26 @@ $: {
|
|||||||
optional: true,
|
optional: true,
|
||||||
min: 0.00005000,
|
min: 0.00005000,
|
||||||
minSats: 5000,
|
minSats: 5000,
|
||||||
max: tierThresholds.hero.min,
|
max: $tiers.hero.min,
|
||||||
maxSats: btcToSats(tierThresholds.hero.min),
|
maxSats: btcToSats($tiers.hero.min),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Community Hero',
|
title: 'Community Hero',
|
||||||
description: "Add your Twitter profile to our Heroes Hall of Fame",
|
description: "Add your Twitter profile to our Supporters page",
|
||||||
emoji: '🦸',
|
emoji: '🦸',
|
||||||
color: 'var(--bold-c)',
|
color: 'var(--bold-c)',
|
||||||
min: tierThresholds.hero.min,
|
min: $tiers.hero.min,
|
||||||
minSats: btcToSats(tierThresholds.hero.min),
|
minSats: btcToSats($tiers.hero.min),
|
||||||
max: tierThresholds.sponsor.min,
|
max: $tiers.sponsor.min,
|
||||||
maxSats: btcToSats(tierThresholds.sponsor.min),
|
maxSats: btcToSats($tiers.sponsor.min),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Enterprise Sponsor',
|
title: 'Enterprise Sponsor',
|
||||||
description: "Display your logo on Bitfeed, with a link to your website",
|
description: "Display your logo on Bitfeed, with a link to your website",
|
||||||
emoji: '🕴️',
|
emoji: '🕴️',
|
||||||
color: 'var(--bold-a)',
|
color: 'var(--bold-a)',
|
||||||
min: tierThresholds.sponsor.min,
|
min: $tiers.sponsor.min,
|
||||||
minSats: btcToSats(tierThresholds.sponsor.min),
|
minSats: btcToSats($tiers.sponsor.min),
|
||||||
max: Infinity,
|
max: Infinity,
|
||||||
maxSats: Infinity,
|
maxSats: Infinity,
|
||||||
}
|
}
|
||||||
@ -207,19 +206,8 @@ onMount(() => {
|
|||||||
console.log('error loading/parsing invoice')
|
console.log('error loading/parsing invoice')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
loadTiers()
|
|
||||||
})
|
})
|
||||||
|
|
||||||
async function loadTiers () {
|
|
||||||
try {
|
|
||||||
const r = await fetch(`${config.donationRoot}/api/sponsorship/tiers.json`)
|
|
||||||
tierThresholds = await r.json()
|
|
||||||
} catch (e) {
|
|
||||||
console.log('failed to load sponsorship tiers')
|
|
||||||
console.log(e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function resetInvoice () {
|
function resetInvoice () {
|
||||||
invoicePaid = false
|
invoicePaid = false
|
||||||
invoiceExpired = false
|
invoiceExpired = false
|
||||||
@ -426,7 +414,7 @@ async function copyInvoice () {
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Overlay name="donation" fullHeight>
|
<Overlay name="donation" fullSize>
|
||||||
<section class="donation-modal">
|
<section class="donation-modal">
|
||||||
<div class="tab-nav">
|
<div class="tab-nav">
|
||||||
<button class="to left" class:disabled={!canTabLeft} on:click={tabLeft}>←</button>
|
<button class="to left" class:disabled={!canTabLeft} on:click={tabLeft}>←</button>
|
||||||
@ -447,7 +435,7 @@ async function copyInvoice () {
|
|||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div class="support-tiers">
|
<div class="support-tiers">
|
||||||
{#each tiers as tier}
|
{#each tierThresholds as tier}
|
||||||
<TierCard {...tier} active={btc >= tier.min && btc < tier.max} on:click={() => { setAmount(tier.min || 0.00005000, true) }} />
|
<TierCard {...tier} active={btc >= tier.min && btc < tier.max} on:click={() => { setAmount(tier.min || 0.00005000, true) }} />
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
@ -456,7 +444,7 @@ async function copyInvoice () {
|
|||||||
|
|
||||||
<div class="choose-amount">
|
<div class="choose-amount">
|
||||||
<div class="amount-slider">
|
<div class="amount-slider">
|
||||||
<SatoshiSlider value={sats} max={btcToSats(1)} thresholds={tiers} logScale on:input={(e) => { setAmount(e.detail)}} />
|
<SatoshiSlider value={sats} max={btcToSats(1)} thresholds={tierThresholds} logScale on:input={(e) => { setAmount(e.detail)}} />
|
||||||
</div>
|
</div>
|
||||||
<div class="amount-input">
|
<div class="amount-input">
|
||||||
{#if unit === 'sats'}
|
{#if unit === 'sats'}
|
||||||
@ -470,8 +458,8 @@ async function copyInvoice () {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if tierThresholds && btc >= tierThresholds.hero.min}
|
{#if $tiers && btc >= $tiers.hero.min}
|
||||||
{#if btc >= tierThresholds.sponsor.min}
|
{#if btc >= $tiers.sponsor.min}
|
||||||
<p class="credit-info">
|
<p class="credit-info">
|
||||||
Enter your email or twitter handle so we can reach you to say thanks and confirm sponsorship details! Or leave these fields blank to donate anonymously.
|
Enter your email or twitter handle so we can reach you to say thanks and confirm sponsorship details! Or leave these fields blank to donate anonymously.
|
||||||
</p>
|
</p>
|
||||||
@ -491,7 +479,7 @@ async function copyInvoice () {
|
|||||||
<input id="twitterHandle" type="text" bind:value={twitter}>
|
<input id="twitterHandle" type="text" bind:value={twitter}>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{#if btc >= tierThresholds.sponsor.min}
|
{#if btc >= $tiers.sponsor.min}
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label for="twitterHandle">Email</label>
|
<label for="twitterHandle">Email</label>
|
||||||
<div class="text-input email-input">
|
<div class="text-input email-input">
|
||||||
@ -752,14 +740,6 @@ async function copyInvoice () {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.action-button {
|
|
||||||
background: var(--bold-a);
|
|
||||||
color: white;
|
|
||||||
padding: 0.5em 2em;
|
|
||||||
margin: 1em 2em 0.5em;
|
|
||||||
font-size: 1.1em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.invoice-area {
|
.invoice-area {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
|
@ -1,359 +0,0 @@
|
|||||||
<script>
|
|
||||||
import analytics from '../utils/analytics.js'
|
|
||||||
import config from '../config.js'
|
|
||||||
import { onMount } from 'svelte'
|
|
||||||
import Overlay from '../components/Overlay.svelte'
|
|
||||||
import Icon from '../components/Icon.svelte'
|
|
||||||
import boltIcon from '../assets/icon/cil-bolt-filled.svg'
|
|
||||||
import tickIcon from '../assets/icon/cil-check-alt.svg'
|
|
||||||
import timerIcon from '../assets/icon/cil-av-timer.svg'
|
|
||||||
import { fade } from 'svelte/transition'
|
|
||||||
import { durationFormat } from '../utils/format.js'
|
|
||||||
import { overlay } from '../stores.js'
|
|
||||||
import QRCode from 'qrcode'
|
|
||||||
|
|
||||||
let amount = 5000
|
|
||||||
let invoice = null
|
|
||||||
let invoicePaid = false
|
|
||||||
let invoiceExpired = false
|
|
||||||
let invoicePoll
|
|
||||||
let pollingEnabled = false
|
|
||||||
|
|
||||||
let invoiceAmountLabel
|
|
||||||
let invoiceExpiryLabel
|
|
||||||
let invoiceHexLabel
|
|
||||||
const invoiceHexPlaceholder = 'lnbcxxxxxxt8l4pp5umz5kyakc0u8z3w2y568entyyq2gafgc3n5a7khdtk5m9fehkxnqdq6gf5hgen9v4jzqer0deshg6t0dccqzpgxqzjcsp5arlylwgraa2u75g4wh40swvxyvt0cpyrmnl4cha40uj5x2fr0t8q9qy9qsqzh3dtfag0ymaf8dpyxrly9p04jwlgdaxkh6g9ysaxyzz7jtrrkpsxv52mlzl6wgn6l6eur9yrl5q2quh5p8kagmng45gqjz9e2c6uxgqx5ezjr'
|
|
||||||
let qrSrc = null
|
|
||||||
$: {
|
|
||||||
if (invoice && invoice.id && invoice.amount && invoice.BOLT11) {
|
|
||||||
invoiceAmountLabel = `${Number.parseInt(invoice.amount) / 1000} sats`
|
|
||||||
invoiceHexLabel = invoice.BOLT11
|
|
||||||
setQR(invoice.BOLT11)
|
|
||||||
} else {
|
|
||||||
stopExpiryTimer()
|
|
||||||
invoiceAmountLabel = `${amount || 5000} sats`
|
|
||||||
invoiceHexLabel = invoiceHexPlaceholder
|
|
||||||
qrSrc = null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function setQR(invoice) {
|
|
||||||
try {
|
|
||||||
qrSrc = await QRCode.toDataURL(invoice.toUpperCase())
|
|
||||||
} catch (err) {
|
|
||||||
console.log('error generating QR code: ', err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let expiryTick
|
|
||||||
let expiresIn = null // time till expiry in seconds
|
|
||||||
$: {
|
|
||||||
invoiceExpiryLabel = expiresIn == null ? '' : durationFormat.format(expiresIn * 1000)
|
|
||||||
}
|
|
||||||
$: {
|
|
||||||
if ($overlay === 'lightning') {
|
|
||||||
startExpiryTimer()
|
|
||||||
stopPollingInvoice()
|
|
||||||
pollingEnabled = true
|
|
||||||
pollInvoice()
|
|
||||||
} else {
|
|
||||||
stopExpiryTimer()
|
|
||||||
stopPollingInvoice()
|
|
||||||
checkResetInvoice()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
function expiryTimer () {
|
|
||||||
if (invoice && invoice.expiresAt) expiresIn = Math.round(((invoice.expiresAt * 1000) - 500000 - Date.now()) / 1000)
|
|
||||||
else expiresIn = null
|
|
||||||
}
|
|
||||||
function stopExpiryTimer () {
|
|
||||||
if (expiryTick) clearInterval(expiryTick)
|
|
||||||
expiryTick = null
|
|
||||||
}
|
|
||||||
function startExpiryTimer () {
|
|
||||||
if (!expiryTick && $overlay === 'lightning' && invoice && invoice.id) {
|
|
||||||
expiryTick = setInterval(expiryTimer, 200)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onMount(() => {
|
|
||||||
// check for existing invoice in local storage:
|
|
||||||
const loadedInvoiceJSON = localStorage.getItem(`lightning-invoice`)
|
|
||||||
localStorage.removeItem('lightning-invoice')
|
|
||||||
if (loadedInvoiceJSON) {
|
|
||||||
try {
|
|
||||||
const loadedInvoice = JSON.parse(loadedInvoiceJSON)
|
|
||||||
if (loadedInvoice && loadedInvoice.id && loadedInvoice.status && loadedInvoice.status === 'Unpaid' && (loadedInvoice.expiresAt * 1000) > Date.now()) {
|
|
||||||
invoice = loadedInvoice
|
|
||||||
processInvoice()
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
console.log('error loading/parsing invoice')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
function resetInvoice () {
|
|
||||||
invoicePaid = false
|
|
||||||
invoiceExpired = false
|
|
||||||
invoice = null
|
|
||||||
qrSrc = null
|
|
||||||
}
|
|
||||||
|
|
||||||
function checkResetInvoice () {
|
|
||||||
if (invoice && invoice.status !== 'Unpaid') resetInvoice()
|
|
||||||
}
|
|
||||||
|
|
||||||
function stopPollingInvoice() {
|
|
||||||
pollingEnabled = false
|
|
||||||
if (invoicePoll) clearTimeout(invoicePoll)
|
|
||||||
invoicePoll = null
|
|
||||||
}
|
|
||||||
|
|
||||||
function processInvoice () {
|
|
||||||
if (invoice) {
|
|
||||||
startExpiryTimer()
|
|
||||||
if (invoice.status === 'Paid') {
|
|
||||||
invoicePaid = true
|
|
||||||
invoiceExpired = false
|
|
||||||
analytics.trackEvent('donations', 'lightning', 'paid', invoice.amount / 1000)
|
|
||||||
localStorage.removeItem('lightning-invoice')
|
|
||||||
} else if (invoice.status === 'Expired' || (invoice.expiresAt * 1000) - 500000 < Date.now()) {
|
|
||||||
invoicePaid = false
|
|
||||||
invoiceExpired = true
|
|
||||||
localStorage.removeItem('lightning-invoice')
|
|
||||||
} else if (invoice.status === 'Unpaid') {
|
|
||||||
invoicePaid = false
|
|
||||||
invoiceExpired = false
|
|
||||||
localStorage.setItem('lightning-invoice', JSON.stringify(invoice))
|
|
||||||
invoicePoll = setTimeout(pollInvoice, 2000)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function pollInvoice () {
|
|
||||||
if (pollingEnabled && invoice && invoice.status === 'Unpaid') {
|
|
||||||
const response = await fetch(`${config.lightningRoot}/api/lightning/invoice/${invoice.id}`, {
|
|
||||||
method: 'GET'
|
|
||||||
})
|
|
||||||
invoice = await response.json()
|
|
||||||
processInvoice()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function generateInvoice () {
|
|
||||||
if (amount) {
|
|
||||||
analytics.trackEvent('donations', 'lightning', 'generate', amount)
|
|
||||||
resetInvoice()
|
|
||||||
const response = await fetch(`${config.lightningRoot}/api/lightning/invoice`, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json'
|
|
||||||
},
|
|
||||||
body: JSON.stringify({ amount })
|
|
||||||
})
|
|
||||||
invoice = await response.json()
|
|
||||||
if (invoice && invoice.amount) analytics.trackEvent('donations', 'lightning', 'generate-success', invoice.amount / 1000)
|
|
||||||
processInvoice()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style type="text/scss">
|
|
||||||
.info {
|
|
||||||
p {
|
|
||||||
text-align: justify;
|
|
||||||
}
|
|
||||||
|
|
||||||
.lightning-form {
|
|
||||||
.sats-input {
|
|
||||||
position: relative;
|
|
||||||
display: inline-block;
|
|
||||||
.units-label {
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
right: 1.4em;
|
|
||||||
bottom: 0;
|
|
||||||
text-align: right;
|
|
||||||
color: var(--dark-c);
|
|
||||||
top: 50%;
|
|
||||||
transform: translateY(-50%);
|
|
||||||
}
|
|
||||||
input {
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.lightning-button {
|
|
||||||
background: var(--bold-a);
|
|
||||||
color: white;
|
|
||||||
padding: 5px 8px;
|
|
||||||
margin: .5em;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.invoice-area {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
align-items: stretch;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.invoice-info {
|
|
||||||
position: relative;
|
|
||||||
padding: 10px;
|
|
||||||
width: 280px;
|
|
||||||
min-width: 200px;
|
|
||||||
margin: .5em;
|
|
||||||
flex: 1;
|
|
||||||
|
|
||||||
.field {
|
|
||||||
word-break: break-all;
|
|
||||||
visibility: hidden;
|
|
||||||
|
|
||||||
.field-label {
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hex {
|
|
||||||
font-family: monospace;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.placeholder-overlay {
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
right: 0;
|
|
||||||
bottom: 0;
|
|
||||||
left: 0;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
border-radius: 10px;
|
|
||||||
background: var(--dark-d);
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
|
|
||||||
.placeholder-label {
|
|
||||||
color: var(--light-a);
|
|
||||||
opacity: 0.5;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&.ready {
|
|
||||||
.field {
|
|
||||||
visibility: visible;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.qr-container {
|
|
||||||
position: relative;
|
|
||||||
flex: 1;
|
|
||||||
width: 280px;
|
|
||||||
min-width: 200px;
|
|
||||||
min-height: 240px;
|
|
||||||
margin: .5em;
|
|
||||||
|
|
||||||
border-radius: 10px;
|
|
||||||
background: var(--dark-d);
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
|
|
||||||
.invoice-status {
|
|
||||||
font-weight: bold;
|
|
||||||
font-size: 1.6em;
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.invoice-icon {
|
|
||||||
font-size: 8em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.qr-image-wrapper {
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
right: 0;
|
|
||||||
bottom: 0;
|
|
||||||
left: 0;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
|
|
||||||
.qr-image {
|
|
||||||
width: 90%;
|
|
||||||
height: 90%;
|
|
||||||
object-fit: contain;
|
|
||||||
object-position: center;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&.paid {
|
|
||||||
background: var(--light-good);
|
|
||||||
}
|
|
||||||
|
|
||||||
&.expired {
|
|
||||||
background: var(--light-unsure);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<Overlay name="lightning">
|
|
||||||
<section class="info">
|
|
||||||
<h2>Donate with Lightning</h2>
|
|
||||||
<p>
|
|
||||||
Enter the amount you would like to donate, then click the button to generate a payment request:
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<div class="lightning-form">
|
|
||||||
<div class="sats-input">
|
|
||||||
<input type="number" bind:value={amount}>
|
|
||||||
<span class="units-label">sats</span>
|
|
||||||
</div>
|
|
||||||
<button class="lightning-button" on:click={generateInvoice} >
|
|
||||||
<Icon icon={boltIcon} color="white" inline />
|
|
||||||
{#if invoice && invoice.id }
|
|
||||||
Generate New Request
|
|
||||||
{:else}
|
|
||||||
Generate Payment Request
|
|
||||||
{/if}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
<div class="invoice-area">
|
|
||||||
<div class="invoice-info" class:ready={invoice && invoice.id}>
|
|
||||||
<p class="field invoice-amount"><span class="field-label">Amount:</span> { invoiceAmountLabel }</p>
|
|
||||||
<p class="field invoice-expires"><span class="field-label">Expiry:</span> { invoiceExpiryLabel }</p>
|
|
||||||
<p class="field invoice"><span class="field-label">Payment request:</span> <span class="hex">{ invoiceHexLabel }</span></p>
|
|
||||||
{#if !invoice || !invoice.id }
|
|
||||||
<div class="placeholder-overlay" transition:fade={{ duration: 300 }}>
|
|
||||||
<p class="placeholder-label">Payment Request</p>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="qr-container" class:paid={invoicePaid} class:expired={invoiceExpired}>
|
|
||||||
{#if invoicePaid }
|
|
||||||
<div class="invoice-icon"><Icon icon={tickIcon} color="white" /></div>
|
|
||||||
<h3 class="invoice-status">Received, Thanks!</h3>
|
|
||||||
{:else if invoiceExpired}
|
|
||||||
<div class="invoice-icon"><Icon icon={timerIcon} color="white" /></div>
|
|
||||||
<h3 class="invoice-status">Invoice Expired</h3>
|
|
||||||
{:else}
|
|
||||||
<div class="invoice-icon"><Icon icon={boltIcon} color="white" /></div>
|
|
||||||
{/if}
|
|
||||||
{#if qrSrc && !invoicePaid && !invoiceExpired}
|
|
||||||
<div class="qr-image-wrapper">
|
|
||||||
<img src={qrSrc} class="qr-image" alt="invoice qr code">
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
</Overlay>
|
|
@ -13,6 +13,19 @@
|
|||||||
padding-bottom: 100%;
|
padding-bottom: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mononaut .backdrop {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: black;
|
||||||
|
border-radius: 50%;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
.mononaut .inner {
|
.mononaut .inner {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
@ -25,7 +38,6 @@
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
||||||
border-radius: 50%;
|
|
||||||
background: linear-gradient(45deg, darkviolet, transparent 100%);
|
background: linear-gradient(45deg, darkviolet, transparent 100%);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -93,6 +105,7 @@
|
|||||||
|
|
||||||
<div class="mononaut">
|
<div class="mononaut">
|
||||||
<div class="aspect">
|
<div class="aspect">
|
||||||
|
<div class="backdrop">
|
||||||
<div class="inner">
|
<div class="inner">
|
||||||
<svg class="avatar"
|
<svg class="avatar"
|
||||||
width="334.47833"
|
width="334.47833"
|
||||||
@ -238,3 +251,4 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
@ -8,7 +8,7 @@ import { fade, fly } from 'svelte/transition'
|
|||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
|
|
||||||
export let name = 'none'
|
export let name = 'none'
|
||||||
export let fullHeight = false
|
export let fullSize = false
|
||||||
|
|
||||||
let open
|
let open
|
||||||
$: {
|
$: {
|
||||||
@ -89,10 +89,12 @@ function close () {
|
|||||||
max-width: 95%;
|
max-width: 95%;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.full-height {
|
&.full-size {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
.overlay-inner {
|
.overlay-inner {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -102,7 +104,7 @@ function close () {
|
|||||||
{#if open}
|
{#if open}
|
||||||
<div class="overlay-wrapper" >
|
<div class="overlay-wrapper" >
|
||||||
<div class="overlay-background" on:click={close} transition:fade={{ duration: 500 }} />
|
<div class="overlay-background" on:click={close} transition:fade={{ duration: 500 }} />
|
||||||
<div class="overlay-outer" class:full-height={fullHeight} transition:fly={{ duration: 500, y: 50 }}>
|
<div class="overlay-outer" class:full-size={fullSize} transition:fly={{ duration: 500, y: 50 }}>
|
||||||
<div class="overlay-inner" id="{name}Overlay">
|
<div class="overlay-inner" id="{name}Overlay">
|
||||||
<slot />
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
|
@ -18,8 +18,8 @@ let settingConfig = {
|
|||||||
showFPS: {
|
showFPS: {
|
||||||
label: 'FPS'
|
label: 'FPS'
|
||||||
},
|
},
|
||||||
showDonation: {
|
showMessages: {
|
||||||
label: 'Donation Info'
|
label: 'Message Bar'
|
||||||
},
|
},
|
||||||
vbytes: {
|
vbytes: {
|
||||||
label: 'Size by',
|
label: 'Size by',
|
||||||
|
@ -13,6 +13,7 @@ import questionIcon from '../assets/icon/help-circle.svg'
|
|||||||
import infoIcon from '../assets/icon/info.svg'
|
import infoIcon from '../assets/icon/info.svg'
|
||||||
import atIcon from '../assets/icon/cil-at.svg'
|
import atIcon from '../assets/icon/cil-at.svg'
|
||||||
import gridIcon from '../assets/icon/grid-icon.svg'
|
import gridIcon from '../assets/icon/grid-icon.svg'
|
||||||
|
import peopleIcon from '../assets/icon/cil-people.svg'
|
||||||
import MempoolLegend from '../components/MempoolLegend.svelte'
|
import MempoolLegend from '../components/MempoolLegend.svelte'
|
||||||
import ContactTab from '../components/ContactTab.svelte'
|
import ContactTab from '../components/ContactTab.svelte'
|
||||||
|
|
||||||
@ -35,6 +36,10 @@ function openAbout () {
|
|||||||
$overlay = 'about'
|
$overlay = 'about'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function openSupporters () {
|
||||||
|
$overlay = 'supporters'
|
||||||
|
}
|
||||||
|
|
||||||
function showBlock () {
|
function showBlock () {
|
||||||
analytics.trackEvent('viz', 'block', 'show')
|
analytics.trackEvent('viz', 'block', 'show')
|
||||||
$blockVisible = true
|
$blockVisible = true
|
||||||
@ -83,6 +88,11 @@ function showBlock () {
|
|||||||
<Icon icon={questionIcon} color="var(--bold-a)" />
|
<Icon icon={questionIcon} color="var(--bold-a)" />
|
||||||
</span>
|
</span>
|
||||||
</SidebarTab>
|
</SidebarTab>
|
||||||
|
<SidebarTab on:click={openSupporters} tooltip="Supporters">
|
||||||
|
<span slot="tab">
|
||||||
|
<Icon icon={peopleIcon} color="var(--bold-a)" />
|
||||||
|
</span>
|
||||||
|
</SidebarTab>
|
||||||
{#if config.dev && config.debug}
|
{#if config.dev && config.debug}
|
||||||
<SidebarTab open={$sidebarToggle === 'dev'} on:click={() => {settings('dev')}} tooltip="Debug">
|
<SidebarTab open={$sidebarToggle === 'dev'} on:click={() => {settings('dev')}} tooltip="Debug">
|
||||||
<span slot="tab">
|
<span slot="tab">
|
||||||
|
126
client/src/components/SupportersOverlay.svelte
Normal file
126
client/src/components/SupportersOverlay.svelte
Normal file
@ -0,0 +1,126 @@
|
|||||||
|
<script>
|
||||||
|
import config from '../config.js'
|
||||||
|
import { onMount } from 'svelte'
|
||||||
|
import Overlay from '../components/Overlay.svelte'
|
||||||
|
import { overlay, tiers, sponsors, heroes } from '../stores.js'
|
||||||
|
|
||||||
|
let displayHeroes = []
|
||||||
|
$: {
|
||||||
|
if ($heroes) {
|
||||||
|
displayHeroes = Object.values($heroes).filter(hero => {
|
||||||
|
return hero && hero.id && hero.img_ext
|
||||||
|
}).map(hero => {
|
||||||
|
return {
|
||||||
|
...hero,
|
||||||
|
img: `${config.donationRoot}/img/avatar/${hero.id}${hero.img_ext}`
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Overlay name="supporters" fullSize>
|
||||||
|
<section class="supporters-modal">
|
||||||
|
<h2>Our Supporters</h2>
|
||||||
|
<p class="info">
|
||||||
|
Bitfeed is only possible thanks to the generosity of our supporters:
|
||||||
|
</p>
|
||||||
|
{#if $sponsors && $sponsors.length}
|
||||||
|
<div class="group">
|
||||||
|
<h3>Enterprise Sponsors</h3>
|
||||||
|
<div class="entries">
|
||||||
|
{#each $sponsors as sponsor}
|
||||||
|
<a class="supporter" target="_blank" href={sponsor.website}>
|
||||||
|
<img src={sponsor.img} alt={sponsor.name}>
|
||||||
|
<span class="label">{ sponsor.name }</span>
|
||||||
|
</a>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button class="action-button" on:click={() => { $overlay = 'donation' }}>
|
||||||
|
Become a Sponsor!
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
|
{#if $heroes && displayHeroes.length}
|
||||||
|
<div class="group">
|
||||||
|
<h3>Community Heroes</h3>
|
||||||
|
<div class="entries">
|
||||||
|
{#each displayHeroes as hero}
|
||||||
|
<a class="supporter hero" target="_blank" href="https://twitter.com/{hero.username}">
|
||||||
|
<img src={hero.img} alt={hero.name}>
|
||||||
|
<span class="label">@{ hero.username }</span>
|
||||||
|
</a>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button class="action-button" on:click={() => { $overlay = 'donation' }}>
|
||||||
|
Become a Community Hero!
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
|
</section>
|
||||||
|
</Overlay>
|
||||||
|
|
||||||
|
<style type="text/scss">
|
||||||
|
.supporters-modal {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
p.info {
|
||||||
|
text-align: center;
|
||||||
|
margin: 0 0 .25em;
|
||||||
|
}
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
margin-top: 2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.group {
|
||||||
|
margin-bottom: 2em;
|
||||||
|
.entries {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: flex-start;
|
||||||
|
|
||||||
|
.supporter {
|
||||||
|
width: 120px;
|
||||||
|
margin: 10px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: flex-start;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
img {
|
||||||
|
width: 72px;
|
||||||
|
height: 72px;
|
||||||
|
border-radius: 50%;
|
||||||
|
object-fit: cover;
|
||||||
|
border: solid 3px transparent;
|
||||||
|
transition: border 200ms;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
img {
|
||||||
|
border: solid 3px var(--bold-a);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.hero {
|
||||||
|
&:hover {
|
||||||
|
img {
|
||||||
|
border: solid 3px var(--bold-b);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.label {
|
||||||
|
max-width: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
font-size: 0.8em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
@ -10,7 +10,8 @@
|
|||||||
import Sidebar from '../components/Sidebar.svelte'
|
import Sidebar from '../components/Sidebar.svelte'
|
||||||
import AboutOverlay from '../components/AboutOverlay.svelte'
|
import AboutOverlay from '../components/AboutOverlay.svelte'
|
||||||
import DonationOverlay from '../components/DonationOverlay.svelte'
|
import DonationOverlay from '../components/DonationOverlay.svelte'
|
||||||
import DonationBar from '../components/DonationBar.svelte'
|
import SupportersOverlay from '../components/SupportersOverlay.svelte'
|
||||||
|
import Alerts from '../components/alert/Alerts.svelte'
|
||||||
import { integerFormat } from '../utils/format.js'
|
import { integerFormat } from '../utils/format.js'
|
||||||
import { exchangeRates, localCurrency, lastBlockId } from '../stores.js'
|
import { exchangeRates, localCurrency, lastBlockId } from '../stores.js'
|
||||||
import { formatCurrency } from '../utils/fx.js'
|
import { formatCurrency } from '../utils/fx.js'
|
||||||
@ -69,7 +70,7 @@
|
|||||||
$devEvents.addManyCallback = fakeTxs
|
$devEvents.addManyCallback = fakeTxs
|
||||||
$devEvents.addBlockCallback = fakeBlock
|
$devEvents.addBlockCallback = fakeBlock
|
||||||
|
|
||||||
if (!$settings.showDonation) $settings.showDonation = true
|
if (!$settings.showMessages) $settings.showMessages = true
|
||||||
})
|
})
|
||||||
|
|
||||||
function resize () {
|
function resize () {
|
||||||
@ -437,17 +438,18 @@
|
|||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{#if $settings.showDonation }
|
|
||||||
<DonationBar />
|
|
||||||
{/if}
|
|
||||||
<div class="spacer" />
|
<div class="spacer" />
|
||||||
|
{#if $settings.showMessages }
|
||||||
|
<Alerts />
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Sidebar />
|
<Sidebar />
|
||||||
|
|
||||||
<AboutOverlay />
|
<AboutOverlay />
|
||||||
{#if config.lightningEnabled }
|
{#if config.donationsEnabled }
|
||||||
<DonationOverlay />
|
<DonationOverlay />
|
||||||
|
<SupportersOverlay />
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if config.dev && config.debug && $devSettings.guides }
|
{#if config.dev && config.debug && $devSettings.guides }
|
||||||
|
199
client/src/components/alert/Alerts.svelte
Normal file
199
client/src/components/alert/Alerts.svelte
Normal file
@ -0,0 +1,199 @@
|
|||||||
|
<script>
|
||||||
|
import { onMount } from 'svelte'
|
||||||
|
import { alerts, heroes, sponsors, overlay, sidebarToggle } from '../../stores.js'
|
||||||
|
import config from '../../config.js'
|
||||||
|
import ByMononaut from './ByMononaut.svelte'
|
||||||
|
import HeroMsg from './Hero.svelte'
|
||||||
|
import SponsoredMsg from './Sponsored.svelte'
|
||||||
|
import GenericAlert from './GenericAlert.svelte'
|
||||||
|
import { fly } from 'svelte/transition'
|
||||||
|
|
||||||
|
const components = {
|
||||||
|
mononaut: ByMononaut,
|
||||||
|
"sponsored-by": SponsoredMsg,
|
||||||
|
// "be-a-hero": BeAHero,
|
||||||
|
"thank-you-hero": HeroMsg,
|
||||||
|
msg: GenericAlert
|
||||||
|
}
|
||||||
|
|
||||||
|
const actions = {
|
||||||
|
support: () => {
|
||||||
|
$overlay = 'donation'
|
||||||
|
},
|
||||||
|
supporters: () => {
|
||||||
|
$overlay = 'supporters'
|
||||||
|
},
|
||||||
|
contact: () => {
|
||||||
|
$sidebarToggle = 'contact'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const sequences = {}
|
||||||
|
let rotating = false
|
||||||
|
|
||||||
|
let processedAlerts = []
|
||||||
|
$: {
|
||||||
|
if ($alerts) {
|
||||||
|
processedAlerts = $alerts.map(processAlert).filter(alert => alert != null)
|
||||||
|
startAlerts()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function processAlert (alert) {
|
||||||
|
if (alert && alert.type && components[alert.type]) {
|
||||||
|
if (!sequences[alert.key]) sequences[alert.key] = 0
|
||||||
|
return {
|
||||||
|
...alert,
|
||||||
|
component: components[alert.type],
|
||||||
|
action: actions[alert.action] || null
|
||||||
|
}
|
||||||
|
} else return null
|
||||||
|
}
|
||||||
|
|
||||||
|
let activeAlerts = [{ key: 'null1' }, { key: 'null2' }]
|
||||||
|
let lastIndex = -1
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
startAlerts()
|
||||||
|
})
|
||||||
|
|
||||||
|
function startAlerts () {
|
||||||
|
if (!rotating && processedAlerts && processedAlerts.length) {
|
||||||
|
rotating = true
|
||||||
|
activeAlerts[0] = processedAlerts[0] || { key: 'null1' }
|
||||||
|
activeAlerts[1] = processedAlerts[1] || { key: 'null2' }
|
||||||
|
lastIndex = processedAlerts[1] ? 1 : 0
|
||||||
|
if (rotateTimer) clearTimeout(rotateTimer)
|
||||||
|
rotateTimer = setTimeout(rotateAlerts, config.alertDuration)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let rotateTimer
|
||||||
|
function rotateAlerts () {
|
||||||
|
if (rotateTimer) clearTimeout(rotateTimer)
|
||||||
|
|
||||||
|
if (processedAlerts && processedAlerts.length > 2) {
|
||||||
|
// find the next alert in the queue
|
||||||
|
let currentIndex = -1
|
||||||
|
if (activeAlerts[1]) {
|
||||||
|
currentIndex = processedAlerts.findIndex(alert => { alert.key === activeAlerts[1].key})
|
||||||
|
}
|
||||||
|
if (currentIndex < 0) currentIndex = lastIndex
|
||||||
|
currentIndex = (currentIndex + 1) % processedAlerts.length
|
||||||
|
// roll over to the next alert if there's a key clash
|
||||||
|
if (processedAlerts[currentIndex].key === activeAlerts[1].key) {
|
||||||
|
currentIndex = (currentIndex + 1) % processedAlerts.length
|
||||||
|
}
|
||||||
|
|
||||||
|
lastIndex = currentIndex
|
||||||
|
let nextAlert = processedAlerts[currentIndex]
|
||||||
|
if (nextAlert)
|
||||||
|
activeAlerts[0] = activeAlerts[1]
|
||||||
|
activeAlerts[1] = { key: 'temp' }
|
||||||
|
setTimeout(() => {
|
||||||
|
activeAlerts[1] = nextAlert
|
||||||
|
sequences[alert.key]++
|
||||||
|
}, 1000)
|
||||||
|
} else if (processedAlerts) {
|
||||||
|
activeAlerts[0] = processedAlerts[0] || { key: 'null1' }
|
||||||
|
activeAlerts[1] = processedAlerts[1] || { key: 'null2' }
|
||||||
|
}
|
||||||
|
|
||||||
|
rotateTimer = setTimeout(rotateAlerts, config.alertDuration)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="alert-bar" transition:fly={{ y: -100 }}>
|
||||||
|
{#each activeAlerts as alert (alert.key)}
|
||||||
|
<div class="alert-wrapper" in:fly|local={{ y: -100 }} out:fly|local={{ x: 400}}>
|
||||||
|
{#if alert && alert.component }
|
||||||
|
{#if alert.href}
|
||||||
|
<a class="alert link" target="_blank" rel="noopener" href={alert.href}>
|
||||||
|
<svelte:component this={alert.component} {...alert} sequence={sequences[alert.key]} />
|
||||||
|
</a>
|
||||||
|
{:else if alert.action}
|
||||||
|
<div class="alert action" on:click={alert.action}>
|
||||||
|
<svelte:component this={alert.component} {...alert} sequence={sequences[alert.key]} />
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<div class="alert">
|
||||||
|
<svelte:component this={alert.component} {...alert} sequence={sequences[alert.key]} />
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style type="text/scss">
|
||||||
|
.alert-bar {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
flex-grow: 1;
|
||||||
|
flex-shrink: 1;
|
||||||
|
height: 3.5em;
|
||||||
|
|
||||||
|
.alert-wrapper {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
right: 0;
|
||||||
|
width: 20em;
|
||||||
|
transform: translateX(-110%);
|
||||||
|
transition: transform 500ms ease-in-out;
|
||||||
|
|
||||||
|
.alert {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
right: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
border-bottom-left-radius: 10px;
|
||||||
|
border-bottom-right-radius: 10px;
|
||||||
|
overflow: hidden;
|
||||||
|
cursor: pointer;
|
||||||
|
background: var(--palette-c);
|
||||||
|
transition: background 200ms;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: var(--palette-d);
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.alert-content) {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
padding: .5em 1em;
|
||||||
|
color: var(--palette-x);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:first-child {
|
||||||
|
transform: translateX(0%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 850px) {
|
||||||
|
.alert-wrapper {
|
||||||
|
transform: translateX(0);
|
||||||
|
|
||||||
|
&:first-child {
|
||||||
|
transform: translateX(110%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 480px) {
|
||||||
|
height: 3em;
|
||||||
|
|
||||||
|
.alert-wrapper {
|
||||||
|
font-size: 0.8em;
|
||||||
|
width: 16em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
41
client/src/components/alert/ByMononaut.svelte
Normal file
41
client/src/components/alert/ByMononaut.svelte
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
<script>
|
||||||
|
import Mononaut from '../Mononaut.svelte'
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="alert-content by-mononaut">
|
||||||
|
<h3>Bitfeed</h3>
|
||||||
|
<p class="by">by mononaut</p>
|
||||||
|
<div class="monkey-avatar">
|
||||||
|
<Mononaut />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style type="text/scss">
|
||||||
|
.by-mononaut {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: space-around;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
text-align: center;
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.by {
|
||||||
|
margin-left: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.monkey-avatar {
|
||||||
|
width: 3em;
|
||||||
|
margin-left: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 480px) {
|
||||||
|
.by {
|
||||||
|
margin-left: 5px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
51
client/src/components/alert/GenericAlert.svelte
Normal file
51
client/src/components/alert/GenericAlert.svelte
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
<script>
|
||||||
|
export let msg
|
||||||
|
export let shortmsg = null
|
||||||
|
export let img
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="alert-content generic-alert">
|
||||||
|
<p class="msg">
|
||||||
|
{@html msg }
|
||||||
|
</p>
|
||||||
|
<p class="shortmsg">
|
||||||
|
{@html shortmsg || msg }
|
||||||
|
</p>
|
||||||
|
{#if img}
|
||||||
|
<img src={img} alt="">
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.generic-alert {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: space-around;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.msg, .shortmsg {
|
||||||
|
margin: 0;
|
||||||
|
font-size: .9em;
|
||||||
|
width: 100%;
|
||||||
|
text-align: center;
|
||||||
|
max-height: 100%;
|
||||||
|
max-width: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
.shortmsg {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
img {
|
||||||
|
height: 2.8em;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 480px) {
|
||||||
|
.shortmsg {
|
||||||
|
display: inline;
|
||||||
|
}
|
||||||
|
.msg {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
83
client/src/components/alert/Hero.svelte
Normal file
83
client/src/components/alert/Hero.svelte
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
<script>
|
||||||
|
import { onMount } from 'svelte'
|
||||||
|
import config from '../../config.js'
|
||||||
|
import { heroes } from '../../stores.js'
|
||||||
|
|
||||||
|
let displayHeroes = []
|
||||||
|
|
||||||
|
function chooseRandomHeroes () {
|
||||||
|
displayHeroes = []
|
||||||
|
const validHeroes = Object.values($heroes).filter(hero => {
|
||||||
|
return hero && hero.id && hero.img_ext
|
||||||
|
})
|
||||||
|
const randomIndex = Math.floor(Math.random() * validHeroes.length)
|
||||||
|
for (let i = 0; i < Math.min(3, validHeroes.length); i++) {
|
||||||
|
const randomHero = validHeroes[(randomIndex + i) % validHeroes.length]
|
||||||
|
displayHeroes.push({
|
||||||
|
...randomHero,
|
||||||
|
img: `${config.donationRoot}/img/avatar/${randomHero.id}${randomHero.img_ext}`
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$: {
|
||||||
|
if ($heroes && !displayHeroes.length) {
|
||||||
|
chooseRandomHeroes()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="alert-content">
|
||||||
|
<p class="msg">Thank you to all of our Community Heroes!</p>
|
||||||
|
<div class="heros">
|
||||||
|
{#each displayHeroes as hero}
|
||||||
|
<img src={hero.img} alt={hero.username} title={hero.name} class="hero">
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style type="text/scss">
|
||||||
|
.alert-content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
color: var(--palette-x);
|
||||||
|
|
||||||
|
.msg {
|
||||||
|
font-size: 0.9em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.heros {
|
||||||
|
height: 2.5em;
|
||||||
|
width: 7.5em;
|
||||||
|
margin-left: 10px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
flex-shrink: 0;
|
||||||
|
|
||||||
|
.hero {
|
||||||
|
width: 2.3em;
|
||||||
|
height: 2.3em;
|
||||||
|
margin: 0 0.1em;
|
||||||
|
border-radius: 50%;
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 480px) {
|
||||||
|
.heros {
|
||||||
|
max-width: 5em;
|
||||||
|
.hero:nth-child(3) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
38
client/src/components/alert/Sponsored.svelte
Normal file
38
client/src/components/alert/Sponsored.svelte
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
<script>
|
||||||
|
export let img
|
||||||
|
export let name
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="alert-content sponsored-by">
|
||||||
|
<p class="msg">Sponsored by</p>
|
||||||
|
<h3>{ name }</h3>
|
||||||
|
{#if img}
|
||||||
|
<img src={img} alt="name" class="logo">
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style type="text/scss">
|
||||||
|
.sponsored-by {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
color: var(--palette-x);
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
text-align: center;
|
||||||
|
margin: 0 10px;
|
||||||
|
flex-grow: 1;
|
||||||
|
flex-shrink: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.msg {
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo {
|
||||||
|
height: 3em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
@ -6,10 +6,8 @@ export default {
|
|||||||
fps: true,
|
fps: true,
|
||||||
websocket_path: '/ws/txs',
|
websocket_path: '/ws/txs',
|
||||||
localSocket: false,
|
localSocket: false,
|
||||||
nofeed: true,
|
nofeed: false,
|
||||||
txDelay: 10000,
|
txDelay: 10000,
|
||||||
blockTimeout: 10000,
|
donationsEnabled: true,
|
||||||
donationAddress: "bc1qthanksv78zs5jnmysvmuuuzj09aklf8jmm49xl",
|
alertDuration: 20000,
|
||||||
donationHash: "5dfb3b419e38a1494f648337ce7052797b6fa4f2",
|
|
||||||
lightningEnabled: true
|
|
||||||
}
|
}
|
||||||
|
@ -20,14 +20,15 @@ export default class BitcoinTx {
|
|||||||
|
|
||||||
this.time = time
|
this.time = time
|
||||||
|
|
||||||
if (config.donationHash && this.outputs) {
|
// Highlight transactions to the static donation address
|
||||||
this.outputs.forEach(output => {
|
// if (config.donationHash && this.outputs) {
|
||||||
if (output.script_pub_key.includes(config.donationHash)) {
|
// this.outputs.forEach(output => {
|
||||||
console.log('donation!', this)
|
// if (output.script_pub_key.includes(config.donationHash)) {
|
||||||
this.highlight = true
|
// console.log('donation!', this)
|
||||||
}
|
// this.highlight = true
|
||||||
})
|
// }
|
||||||
}
|
// })
|
||||||
|
// }
|
||||||
|
|
||||||
// is a coinbase transaction?
|
// is a coinbase transaction?
|
||||||
if (this.inputs && this.inputs.length === 1 && this.inputs[0].prev_txid === "0000000000000000000000000000000000000000000000000000000000000000") {
|
if (this.inputs && this.inputs.length === 1 && this.inputs[0].prev_txid === "0000000000000000000000000000000000000000000000000000000000000000") {
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { writable, derived } from 'svelte/store'
|
import { writable, derived } from 'svelte/store'
|
||||||
export { exchangeRates } from './utils/pollStore.js'
|
import { makePollStore } from './utils/pollStore.js'
|
||||||
import { symbols } from './utils/fx.js'
|
import { symbols } from './utils/fx.js'
|
||||||
import LocaleCurrency from 'locale-currency'
|
import LocaleCurrency from 'locale-currency'
|
||||||
import config from './config.js'
|
import config from './config.js'
|
||||||
@ -42,6 +42,16 @@ function createCachedDict (namespace, defaultValues) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// refresh exchange rates every minute
|
||||||
|
export const exchangeRates = makePollStore('rates', 'https://blockchain.info/ticker', 60000, {})
|
||||||
|
// refresh messages from donation server every hour
|
||||||
|
// export const alerts = makePollStore('alerts', `${config.donationRoot}/api/sponsorship/msgs.json`, 3600000, [])
|
||||||
|
export const alerts = makePollStore('alerts', `${config.donationRoot}/api/sponsorship/msgs.json`, 10000, [])
|
||||||
|
// refresh sponsor data every hour
|
||||||
|
export const heroes = makePollStore('heroes', `${config.donationRoot}/api/sponsorship/heroes.json`, 3600000, null)
|
||||||
|
export const sponsors = makePollStore('sponsors', `${config.donationRoot}/api/sponsorship/sponsors.json`, 3600000, null)
|
||||||
|
export const tiers = makePollStore('tiers', `${config.donationRoot}/api/sponsorship/tiers.json`, 3600000, null)
|
||||||
|
|
||||||
export const darkMode = writable(true)
|
export const darkMode = writable(true)
|
||||||
export const serverConnected = writable(false)
|
export const serverConnected = writable(false)
|
||||||
export const serverDelay = writable(1000)
|
export const serverDelay = writable(1000)
|
||||||
@ -73,7 +83,7 @@ export const settings = createCachedDict('settings', {
|
|||||||
showFX: true,
|
showFX: true,
|
||||||
vbytes: false,
|
vbytes: false,
|
||||||
fancyGraphics: true,
|
fancyGraphics: true,
|
||||||
showDonation: true,
|
showMessages: true,
|
||||||
noTrack: false
|
noTrack: false
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -88,7 +98,7 @@ export const nativeAntialias = writable(false)
|
|||||||
|
|
||||||
const newVisitor = !localStorage.getItem('seen-welcome-msg')
|
const newVisitor = !localStorage.getItem('seen-welcome-msg')
|
||||||
// export const overlay = writable(newVisitor ? 'about' : null)
|
// export const overlay = writable(newVisitor ? 'about' : null)
|
||||||
export const overlay = writable('donation')
|
export const overlay = writable(null)
|
||||||
|
|
||||||
let currencyCode = LocaleCurrency.getCurrency(navigator.language)
|
let currencyCode = LocaleCurrency.getCurrency(navigator.language)
|
||||||
console.log('LOCALE: ', navigator.language, currencyCode)
|
console.log('LOCALE: ', navigator.language, currencyCode)
|
||||||
|
@ -1,19 +1,24 @@
|
|||||||
import { writable } from 'svelte/store'
|
import { writable } from 'svelte/store'
|
||||||
|
|
||||||
function makeRatePollStore () {
|
export function makePollStore (name, url, frequency, initialValue={}, responseHandler) {
|
||||||
let timer
|
let timer
|
||||||
const { subscribe, set, update } = writable({})
|
const { subscribe, set, update } = writable(initialValue)
|
||||||
|
if (!responseHandler) responseHandler = async (response, set) => {
|
||||||
|
try {
|
||||||
|
const data = await response.json()
|
||||||
|
if (data) set(data)
|
||||||
|
} catch (error) {
|
||||||
|
console.log(`failed to parse polled data for ${name}: `, error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const fetcher = () => {
|
const fetcher = () => {
|
||||||
fetch(`https://blockchain.info/ticker?t=${Date.now()}`).then(async response => {
|
fetch(`${url}?t=${Date.now()}`).then(response => { responseHandler(response, set) }).catch(err => {
|
||||||
const rates = await response.json()
|
console.log(`error polling data for ${name}: `, error)
|
||||||
set(rates)
|
|
||||||
}).catch(err => {
|
|
||||||
console.log('error fetching exchange rates: ', err)
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
fetcher()
|
fetcher()
|
||||||
timer = setInterval(fetcher, 60000)
|
timer = setInterval(fetcher, frequency || 60000)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
subscribe,
|
subscribe,
|
||||||
@ -21,5 +26,3 @@ function makeRatePollStore () {
|
|||||||
update
|
update
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const exchangeRates = makeRatePollStore()
|
|
||||||
|
Loading…
Reference in New Issue
Block a user