mirror of
https://github.com/Retropex/bitfeed.git
synced 2025-05-28 04:52:29 +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 {
|
||||
color: rgb(0,100,200);
|
||||
color: var(--palette-x);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
text-decoration: underline;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
a:visited {
|
||||
color: rgb(0,80,160);
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
label {
|
||||
@ -137,6 +137,23 @@ button:focus {
|
||||
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 {
|
||||
from {transform:rotate(0deg);}
|
||||
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 { fade, fly } from 'svelte/transition'
|
||||
import { durationFormat } from '../utils/format.js'
|
||||
import { overlay } from '../stores.js'
|
||||
import { overlay, tiers } from '../stores.js'
|
||||
import QRCode from 'qrcode'
|
||||
|
||||
let tab = 'form' // form | invoice | success
|
||||
@ -50,12 +50,11 @@ const invoiceHexPlaceholder = 'lnbcxxxxxxt8l4pp5umz5kyakc0u8z3w2y568entyyq2gafgc
|
||||
let qrSrc = null
|
||||
let invoicePayments = []
|
||||
|
||||
let tierThresholds
|
||||
let tiers = []
|
||||
let tierThresholds = []
|
||||
|
||||
$: {
|
||||
if (tierThresholds) {
|
||||
tiers = [
|
||||
if ($tiers) {
|
||||
tierThresholds = [
|
||||
{
|
||||
title: 'Supporter',
|
||||
description: "Help to keep the lights on with a small donation",
|
||||
@ -64,26 +63,26 @@ $: {
|
||||
optional: true,
|
||||
min: 0.00005000,
|
||||
minSats: 5000,
|
||||
max: tierThresholds.hero.min,
|
||||
maxSats: btcToSats(tierThresholds.hero.min),
|
||||
max: $tiers.hero.min,
|
||||
maxSats: btcToSats($tiers.hero.min),
|
||||
},
|
||||
{
|
||||
title: 'Community Hero',
|
||||
description: "Add your Twitter profile to our Heroes Hall of Fame",
|
||||
description: "Add your Twitter profile to our Supporters page",
|
||||
emoji: '🦸',
|
||||
color: 'var(--bold-c)',
|
||||
min: tierThresholds.hero.min,
|
||||
minSats: btcToSats(tierThresholds.hero.min),
|
||||
max: tierThresholds.sponsor.min,
|
||||
maxSats: btcToSats(tierThresholds.sponsor.min),
|
||||
min: $tiers.hero.min,
|
||||
minSats: btcToSats($tiers.hero.min),
|
||||
max: $tiers.sponsor.min,
|
||||
maxSats: btcToSats($tiers.sponsor.min),
|
||||
},
|
||||
{
|
||||
title: 'Enterprise Sponsor',
|
||||
description: "Display your logo on Bitfeed, with a link to your website",
|
||||
emoji: '🕴️',
|
||||
color: 'var(--bold-a)',
|
||||
min: tierThresholds.sponsor.min,
|
||||
minSats: btcToSats(tierThresholds.sponsor.min),
|
||||
min: $tiers.sponsor.min,
|
||||
minSats: btcToSats($tiers.sponsor.min),
|
||||
max: Infinity,
|
||||
maxSats: Infinity,
|
||||
}
|
||||
@ -207,19 +206,8 @@ onMount(() => {
|
||||
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 () {
|
||||
invoicePaid = false
|
||||
invoiceExpired = false
|
||||
@ -426,7 +414,7 @@ async function copyInvoice () {
|
||||
}
|
||||
</script>
|
||||
|
||||
<Overlay name="donation" fullHeight>
|
||||
<Overlay name="donation" fullSize>
|
||||
<section class="donation-modal">
|
||||
<div class="tab-nav">
|
||||
<button class="to left" class:disabled={!canTabLeft} on:click={tabLeft}>←</button>
|
||||
@ -447,7 +435,7 @@ async function copyInvoice () {
|
||||
</p>
|
||||
|
||||
<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) }} />
|
||||
{/each}
|
||||
</div>
|
||||
@ -456,7 +444,7 @@ async function copyInvoice () {
|
||||
|
||||
<div class="choose-amount">
|
||||
<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 class="amount-input">
|
||||
{#if unit === 'sats'}
|
||||
@ -470,8 +458,8 @@ async function copyInvoice () {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if tierThresholds && btc >= tierThresholds.hero.min}
|
||||
{#if btc >= tierThresholds.sponsor.min}
|
||||
{#if $tiers && btc >= $tiers.hero.min}
|
||||
{#if btc >= $tiers.sponsor.min}
|
||||
<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.
|
||||
</p>
|
||||
@ -491,7 +479,7 @@ async function copyInvoice () {
|
||||
<input id="twitterHandle" type="text" bind:value={twitter}>
|
||||
</div>
|
||||
</div>
|
||||
{#if btc >= tierThresholds.sponsor.min}
|
||||
{#if btc >= $tiers.sponsor.min}
|
||||
<div class="field">
|
||||
<label for="twitterHandle">Email</label>
|
||||
<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 {
|
||||
display: flex;
|
||||
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%;
|
||||
}
|
||||
|
||||
.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 {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
@ -25,7 +38,6 @@
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(45deg, darkviolet, transparent 100%);
|
||||
}
|
||||
|
||||
@ -93,6 +105,7 @@
|
||||
|
||||
<div class="mononaut">
|
||||
<div class="aspect">
|
||||
<div class="backdrop">
|
||||
<div class="inner">
|
||||
<svg class="avatar"
|
||||
width="334.47833"
|
||||
@ -237,4 +250,5 @@
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -8,7 +8,7 @@ import { fade, fly } from 'svelte/transition'
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
export let name = 'none'
|
||||
export let fullHeight = false
|
||||
export let fullSize = false
|
||||
|
||||
let open
|
||||
$: {
|
||||
@ -89,10 +89,12 @@ function close () {
|
||||
max-width: 95%;
|
||||
}
|
||||
|
||||
&.full-height {
|
||||
&.full-size {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
.overlay-inner {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -102,7 +104,7 @@ function close () {
|
||||
{#if open}
|
||||
<div class="overlay-wrapper" >
|
||||
<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">
|
||||
<slot />
|
||||
</div>
|
||||
|
@ -18,8 +18,8 @@ let settingConfig = {
|
||||
showFPS: {
|
||||
label: 'FPS'
|
||||
},
|
||||
showDonation: {
|
||||
label: 'Donation Info'
|
||||
showMessages: {
|
||||
label: 'Message Bar'
|
||||
},
|
||||
vbytes: {
|
||||
label: 'Size by',
|
||||
|
@ -13,6 +13,7 @@ import questionIcon from '../assets/icon/help-circle.svg'
|
||||
import infoIcon from '../assets/icon/info.svg'
|
||||
import atIcon from '../assets/icon/cil-at.svg'
|
||||
import gridIcon from '../assets/icon/grid-icon.svg'
|
||||
import peopleIcon from '../assets/icon/cil-people.svg'
|
||||
import MempoolLegend from '../components/MempoolLegend.svelte'
|
||||
import ContactTab from '../components/ContactTab.svelte'
|
||||
|
||||
@ -35,6 +36,10 @@ function openAbout () {
|
||||
$overlay = 'about'
|
||||
}
|
||||
|
||||
function openSupporters () {
|
||||
$overlay = 'supporters'
|
||||
}
|
||||
|
||||
function showBlock () {
|
||||
analytics.trackEvent('viz', 'block', 'show')
|
||||
$blockVisible = true
|
||||
@ -83,6 +88,11 @@ function showBlock () {
|
||||
<Icon icon={questionIcon} color="var(--bold-a)" />
|
||||
</span>
|
||||
</SidebarTab>
|
||||
<SidebarTab on:click={openSupporters} tooltip="Supporters">
|
||||
<span slot="tab">
|
||||
<Icon icon={peopleIcon} color="var(--bold-a)" />
|
||||
</span>
|
||||
</SidebarTab>
|
||||
{#if config.dev && config.debug}
|
||||
<SidebarTab open={$sidebarToggle === 'dev'} on:click={() => {settings('dev')}} tooltip="Debug">
|
||||
<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 AboutOverlay from '../components/AboutOverlay.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 { exchangeRates, localCurrency, lastBlockId } from '../stores.js'
|
||||
import { formatCurrency } from '../utils/fx.js'
|
||||
@ -69,7 +70,7 @@
|
||||
$devEvents.addManyCallback = fakeTxs
|
||||
$devEvents.addBlockCallback = fakeBlock
|
||||
|
||||
if (!$settings.showDonation) $settings.showDonation = true
|
||||
if (!$settings.showMessages) $settings.showMessages = true
|
||||
})
|
||||
|
||||
function resize () {
|
||||
@ -437,17 +438,18 @@
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{#if $settings.showDonation }
|
||||
<DonationBar />
|
||||
{/if}
|
||||
<div class="spacer" />
|
||||
{#if $settings.showMessages }
|
||||
<Alerts />
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<Sidebar />
|
||||
|
||||
<AboutOverlay />
|
||||
{#if config.lightningEnabled }
|
||||
{#if config.donationsEnabled }
|
||||
<DonationOverlay />
|
||||
<SupportersOverlay />
|
||||
{/if}
|
||||
|
||||
{#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,
|
||||
websocket_path: '/ws/txs',
|
||||
localSocket: false,
|
||||
nofeed: true,
|
||||
nofeed: false,
|
||||
txDelay: 10000,
|
||||
blockTimeout: 10000,
|
||||
donationAddress: "bc1qthanksv78zs5jnmysvmuuuzj09aklf8jmm49xl",
|
||||
donationHash: "5dfb3b419e38a1494f648337ce7052797b6fa4f2",
|
||||
lightningEnabled: true
|
||||
donationsEnabled: true,
|
||||
alertDuration: 20000,
|
||||
}
|
||||
|
@ -20,14 +20,15 @@ export default class BitcoinTx {
|
||||
|
||||
this.time = time
|
||||
|
||||
if (config.donationHash && this.outputs) {
|
||||
this.outputs.forEach(output => {
|
||||
if (output.script_pub_key.includes(config.donationHash)) {
|
||||
console.log('donation!', this)
|
||||
this.highlight = true
|
||||
}
|
||||
})
|
||||
}
|
||||
// Highlight transactions to the static donation address
|
||||
// if (config.donationHash && this.outputs) {
|
||||
// this.outputs.forEach(output => {
|
||||
// if (output.script_pub_key.includes(config.donationHash)) {
|
||||
// console.log('donation!', this)
|
||||
// this.highlight = true
|
||||
// }
|
||||
// })
|
||||
// }
|
||||
|
||||
// is a coinbase transaction?
|
||||
if (this.inputs && this.inputs.length === 1 && this.inputs[0].prev_txid === "0000000000000000000000000000000000000000000000000000000000000000") {
|
||||
|
@ -1,5 +1,5 @@
|
||||
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 LocaleCurrency from 'locale-currency'
|
||||
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 serverConnected = writable(false)
|
||||
export const serverDelay = writable(1000)
|
||||
@ -73,7 +83,7 @@ export const settings = createCachedDict('settings', {
|
||||
showFX: true,
|
||||
vbytes: false,
|
||||
fancyGraphics: true,
|
||||
showDonation: true,
|
||||
showMessages: true,
|
||||
noTrack: false
|
||||
})
|
||||
|
||||
@ -88,7 +98,7 @@ export const nativeAntialias = writable(false)
|
||||
|
||||
const newVisitor = !localStorage.getItem('seen-welcome-msg')
|
||||
// export const overlay = writable(newVisitor ? 'about' : null)
|
||||
export const overlay = writable('donation')
|
||||
export const overlay = writable(null)
|
||||
|
||||
let currencyCode = LocaleCurrency.getCurrency(navigator.language)
|
||||
console.log('LOCALE: ', navigator.language, currencyCode)
|
||||
|
@ -1,19 +1,24 @@
|
||||
import { writable } from 'svelte/store'
|
||||
|
||||
function makeRatePollStore () {
|
||||
export function makePollStore (name, url, frequency, initialValue={}, responseHandler) {
|
||||
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 = () => {
|
||||
fetch(`https://blockchain.info/ticker?t=${Date.now()}`).then(async response => {
|
||||
const rates = await response.json()
|
||||
set(rates)
|
||||
}).catch(err => {
|
||||
console.log('error fetching exchange rates: ', err)
|
||||
fetch(`${url}?t=${Date.now()}`).then(response => { responseHandler(response, set) }).catch(err => {
|
||||
console.log(`error polling data for ${name}: `, error)
|
||||
})
|
||||
}
|
||||
fetcher()
|
||||
timer = setInterval(fetcher, 60000)
|
||||
timer = setInterval(fetcher, frequency || 60000)
|
||||
|
||||
return {
|
||||
subscribe,
|
||||
@ -21,5 +26,3 @@ function makeRatePollStore () {
|
||||
update
|
||||
}
|
||||
}
|
||||
|
||||
export const exchangeRates = makeRatePollStore()
|
||||
|
Loading…
Reference in New Issue
Block a user