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:
Mononaut 2021-12-30 19:19:22 -06:00
parent 8db6fef727
commit e7dff7024e
19 changed files with 789 additions and 842 deletions

View File

@ -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);}

View File

@ -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>

View File

@ -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}>&larr;</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;

View File

@ -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>

View File

@ -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,148 +105,150 @@
<div class="mononaut">
<div class="aspect">
<div class="inner">
<svg class="avatar"
width="334.47833"
height="360.19266"
viewBox="0 0 88.497389 95.30098"
version="1.1"
id="svg8">
<defs
id="defs2">
<linearGradient
id="linearGradient1874">
<stop
style="stop-color:#ffffff;stop-opacity:0.65887851"
offset="0"
id="stop1870" />
<stop
style="stop-color:#a7a7a7;stop-opacity:0;"
offset="1"
id="stop1872" />
</linearGradient>
<linearGradient
xlink:href="#linearGradient1874"
id="linearGradient5760"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(0.88559909,0,0,0.88559909,2.1865141,-1.4202934)"
x1="74.399529"
y1="16.307785"
x2="15.273721"
y2="83.253212" />
</defs>
<g
style="display:inline;opacity:1"
transform="translate(-10.793731,-5.4097061)">
<path
style="display:inline;opacity:1;fill:#576c78;fill-opacity:1;stroke:none;stroke-width:0.71934628;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m 61.651061,20.553894 1.289922,1.959392 C 59.55236,29.73164 61.960527,32.936832 62.291817,37.57986 69.580914,51.34937 46.284973,74.005931 38.794339,77.275048 18.383317,86.182975 8.0562438,56.701838 11.97544,33.820946 13.409936,25.446125 24.242237,4.0376168 61.651061,20.553894 Z"
id="visor-back" />
<path
style="fill:#784421;fill-opacity:1;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="m 40.023612,69.217407 c -1.74228,14.114255 19.884147,12.868589 29.800552,15.568451 l 13.964832,-13.8312 C 77.167852,62.564819 61.700977,53.269993 49.110776,59.796156 Z"
id="monkey-body" />
<g id="monkey">
<g
style="display:inline"
id="monkey-inner"
transform="matrix(0.73793571,0.19032465,-0.19032465,0.73793571,8.5451128,-1.5971534)">
<path
id="ear-right"
d="m 35.094888,56.21721 c -6.89655,2.534431 -14.53992,-2.389386 -17.231469,-4.852391 1.23186,-3.401999 4.604949,-5.92599 8.722879,-7.236581 1.66704,-0.53056 3.45615,-0.862263 5.27469,-0.97286 9.17189,1.090452 10.68783,11.787228 3.2339,13.0061832 z"
style="display:inline;opacity:1;fill:#deaa87;stroke:none;stroke-width:0.21957469px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
<path
id="head"
d="m 30.308872,23.862935 c -7.896397,9.705521 -12.701615,16.830049 -7.269507,28.957925 4.3062,9.614141 -0.284703,11.721588 5.683872,18.251035 5.45108,5.55247 10.178022,5.892849 13.988071,13.594771 11.299136,14.269193 22.409786,3.337588 26.080359,-5.85863 C 81.867477,56.071791 88.32321,34.989715 70.681545,21.166667 55.190177,12.328814 46.139739,12.585053 30.308872,23.862935 Z"
style="display:inline;fill:#784421;fill-opacity:1;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
<g id="ear-left">
<div class="backdrop">
<div class="inner">
<svg class="avatar"
width="334.47833"
height="360.19266"
viewBox="0 0 88.497389 95.30098"
version="1.1"
id="svg8">
<defs
id="defs2">
<linearGradient
id="linearGradient1874">
<stop
style="stop-color:#ffffff;stop-opacity:0.65887851"
offset="0"
id="stop1870" />
<stop
style="stop-color:#a7a7a7;stop-opacity:0;"
offset="1"
id="stop1872" />
</linearGradient>
<linearGradient
xlink:href="#linearGradient1874"
id="linearGradient5760"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(0.88559909,0,0,0.88559909,2.1865141,-1.4202934)"
x1="74.399529"
y1="16.307785"
x2="15.273721"
y2="83.253212" />
</defs>
<g
style="display:inline;opacity:1"
transform="translate(-10.793731,-5.4097061)">
<path
style="display:inline;opacity:1;fill:#576c78;fill-opacity:1;stroke:none;stroke-width:0.71934628;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m 61.651061,20.553894 1.289922,1.959392 C 59.55236,29.73164 61.960527,32.936832 62.291817,37.57986 69.580914,51.34937 46.284973,74.005931 38.794339,77.275048 18.383317,86.182975 8.0562438,56.701838 11.97544,33.820946 13.409936,25.446125 24.242237,4.0376168 61.651061,20.553894 Z"
id="visor-back" />
<path
style="fill:#784421;fill-opacity:1;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="m 40.023612,69.217407 c -1.74228,14.114255 19.884147,12.868589 29.800552,15.568451 l 13.964832,-13.8312 C 77.167852,62.564819 61.700977,53.269993 49.110776,59.796156 Z"
id="monkey-body" />
<g id="monkey">
<g
style="display:inline"
id="monkey-inner"
transform="matrix(0.73793571,0.19032465,-0.19032465,0.73793571,8.5451128,-1.5971534)">
<path
id="ear-left-outer"
d="m 77.833423,50.323879 c 6.47703,-6.03607 5.34152,-16.93279 4.15774,-21.16667 -6.07997,-0.73952 -12.59596,4.00709 -16.44197,10.58333 -3.8176,10.45458 6.85846,17.904101 12.28423,10.58334 z"
style="display:inline;opacity:1;fill:#deaa87;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
<path
id="ear-left-inner"
d="m 76.76206,47.72753 c 4.556293,-4.246097 3.757513,-11.911438 2.924778,-14.889778 -4.276979,-0.520219 -8.860678,2.818803 -11.56617,7.444885 -2.685506,7.354316 4.824612,12.594712 8.641392,7.444893 z"
style="display:inline;opacity:1;fill:#e9c6af;stroke:none;stroke-width:0.18612219px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
</g>
<path
id="face"
d="m 38.742559,49.51488 c 1.45629,-4.066862 6.298841,-7.992738 12.284225,-6.992558 15.425741,4.293009 4.803701,19.409911 -2.976563,18.473584 18.639296,3.573862 22.324617,26.346482 6.174076,27.17782 -12.717835,0.34267 -8.581608,-9.318628 -16.409445,-12.0269 -5.597227,-1.936527 -2.05593,-13.668839 -2.663067,-12.26885 -6.589222,8.759543 -12.047332,-0.849447 -11.528273,-9.921875 1.774561,-2.871137 4.819803,-3.699497 8.882439,-1.039433 4.789983,2.254962 5.346565,-0.781825 6.236608,-3.401788 z"
style="display:inline;fill:#deaa87;fill-opacity:1;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
<path
id="muzzle"
d="M 60.072997,75.352317 C 55.003352,70.027191 55.75107,68.927121 53.34848,66.485233 51.841457,64.88184 47.05277,62.630654 42.069361,63.849024 c -1.699146,0.415416 -4.449014,1.385616 -6.199235,3.299959 -1.682918,1.84073 -1.025774,7.464219 2.777696,9.202331 3.896358,1.78056 4.087632,2.791893 5.88943,6.981181 3.630032,8.440037 20.554285,1.586752 15.535745,-7.980178 z"
style="display:inline;fill:#502d16;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
<path
id="face-inner"
d="m 38.957003,52.607487 c 1.042028,-2.641197 2.728748,-4.7652 10.223515,-5.17104 7.241178,0.814991 8.551291,5.869414 4.817338,8.574224 -3.147594,3.346452 -6.33337,3.393592 -8.935703,2.930689 -3.51048,-0.180022 -6.925621,-0.202977 -8.446902,2.89418 -5.220943,5.911145 -10.282881,0.05394 -10.31162,-5.662332 1.650862,-1.605418 3.81304,-2.437929 7.455186,-1.033305 4.247946,1.104042 4.568382,-0.833998 5.198186,-2.532416 z"
style="display:inline;fill:#e9c6af;stroke:none;stroke-width:0.19504021px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
<path
id="mouth"
d="m 42.472928,76.398309 c -0.995545,-0.575317 -0.253436,-2.698662 1.044456,-2.998078 4.228867,-0.975576 12.597117,-0.526341 13.187645,0.826512 1.454954,3.573662 -4.175521,8.756925 -10.256469,8.652853 -2.374181,0.259391 -1.069921,-4.879679 -3.975632,-6.481287 z"
style="display:inline;fill:#28170b;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
<path
id="tongue"
d="m 46.929215,76.246574 c 0.615399,-1.75537 8.22103,-2.28337 7.216279,4.07586 -0.212053,1.37525 3.358405,1.46548 4.409948,3.34086 1.055887,4.79323 -3.65884,4.36937 -6.94901,2.77293 -4.202979,-2.19163 -5.874033,-5.47644 -4.677217,-10.18965 z"
style="display:inline;opacity:1;fill:#da789c;fill-opacity:1;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
<g id="eye-left" class="monkey-eye">
id="ear-right"
d="m 35.094888,56.21721 c -6.89655,2.534431 -14.53992,-2.389386 -17.231469,-4.852391 1.23186,-3.401999 4.604949,-5.92599 8.722879,-7.236581 1.66704,-0.53056 3.45615,-0.862263 5.27469,-0.97286 9.17189,1.090452 10.68783,11.787228 3.2339,13.0061832 z"
style="display:inline;opacity:1;fill:#deaa87;stroke:none;stroke-width:0.21957469px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
<path
id="pupil-left"
d="m 40.867285,57.273873 c 1.612375,-2.042551 0.701258,-3.847919 2.827551,-5.020606 2.852266,-1.245289 3.874257,0.02912 6.461358,-0.487557 -1.800708,1.927123 -1.087836,3.182768 -3.147949,4.379153 -2.48621,1.342919 -2.404803,0.48141 -6.14096,1.12901 z"
style="display:inline;fill:#28170b;fill-opacity:1;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
id="head"
d="m 30.308872,23.862935 c -7.896397,9.705521 -12.701615,16.830049 -7.269507,28.957925 4.3062,9.614141 -0.284703,11.721588 5.683872,18.251035 5.45108,5.55247 10.178022,5.892849 13.988071,13.594771 11.299136,14.269193 22.409786,3.337588 26.080359,-5.85863 C 81.867477,56.071791 88.32321,34.989715 70.681545,21.166667 55.190177,12.328814 46.139739,12.585053 30.308872,23.862935 Z"
style="display:inline;fill:#784421;fill-opacity:1;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
<g id="ear-left">
<path
id="ear-left-outer"
d="m 77.833423,50.323879 c 6.47703,-6.03607 5.34152,-16.93279 4.15774,-21.16667 -6.07997,-0.73952 -12.59596,4.00709 -16.44197,10.58333 -3.8176,10.45458 6.85846,17.904101 12.28423,10.58334 z"
style="display:inline;opacity:1;fill:#deaa87;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
<path
id="ear-left-inner"
d="m 76.76206,47.72753 c 4.556293,-4.246097 3.757513,-11.911438 2.924778,-14.889778 -4.276979,-0.520219 -8.860678,2.818803 -11.56617,7.444885 -2.685506,7.354316 4.824612,12.594712 8.641392,7.444893 z"
style="display:inline;opacity:1;fill:#e9c6af;stroke:none;stroke-width:0.18612219px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
</g>
<path
id="gleam-left"
d="m 44.577763,52.357009 0.175397,1.00226 1.720546,-0.91874 z"
style="display:inline;opacity:1;fill:#f4e3d7;fill-opacity:1;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
</g>
<g id="eye-right" class="monkey-eye">
id="face"
d="m 38.742559,49.51488 c 1.45629,-4.066862 6.298841,-7.992738 12.284225,-6.992558 15.425741,4.293009 4.803701,19.409911 -2.976563,18.473584 18.639296,3.573862 22.324617,26.346482 6.174076,27.17782 -12.717835,0.34267 -8.581608,-9.318628 -16.409445,-12.0269 -5.597227,-1.936527 -2.05593,-13.668839 -2.663067,-12.26885 -6.589222,8.759543 -12.047332,-0.849447 -11.528273,-9.921875 1.774561,-2.871137 4.819803,-3.699497 8.882439,-1.039433 4.789983,2.254962 5.346565,-0.781825 6.236608,-3.401788 z"
style="display:inline;fill:#deaa87;fill-opacity:1;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
<path
id="pupil-right"
d="m 36.526068,59.323301 c -2.29866,-0.610735 -2.053402,-2.430637 -4.251391,-2.123454 -2.768843,0.651852 -2.383872,2.077275 -4.559515,3.116838 2.373186,0.423104 2.10538,1.799062 4.267942,1.545474 2.554554,-0.379861 1.455015,-0.965477 4.542964,-2.538858 z"
style="display:inline;fill:#28170b;stroke:none;stroke-width:0.24182333px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
id="muzzle"
d="M 60.072997,75.352317 C 55.003352,70.027191 55.75107,68.927121 53.34848,66.485233 51.841457,64.88184 47.05277,62.630654 42.069361,63.849024 c -1.699146,0.415416 -4.449014,1.385616 -6.199235,3.299959 -1.682918,1.84073 -1.025774,7.464219 2.777696,9.202331 3.896358,1.78056 4.087632,2.791893 5.88943,6.981181 3.630032,8.440037 20.554285,1.586752 15.535745,-7.980178 z"
style="display:inline;fill:#502d16;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
<path
id="gleam-right"
d="m 31.608594,58.036779 0.803199,0.56105 0.750048,-0.48428 z"
style="display:inline;opacity:1;fill:#f4e3d7;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
id="face-inner"
d="m 38.957003,52.607487 c 1.042028,-2.641197 2.728748,-4.7652 10.223515,-5.17104 7.241178,0.814991 8.551291,5.869414 4.817338,8.574224 -3.147594,3.346452 -6.33337,3.393592 -8.935703,2.930689 -3.51048,-0.180022 -6.925621,-0.202977 -8.446902,2.89418 -5.220943,5.911145 -10.282881,0.05394 -10.31162,-5.662332 1.650862,-1.605418 3.81304,-2.437929 7.455186,-1.033305 4.247946,1.104042 4.568382,-0.833998 5.198186,-2.532416 z"
style="display:inline;fill:#e9c6af;stroke:none;stroke-width:0.19504021px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
<path
id="mouth"
d="m 42.472928,76.398309 c -0.995545,-0.575317 -0.253436,-2.698662 1.044456,-2.998078 4.228867,-0.975576 12.597117,-0.526341 13.187645,0.826512 1.454954,3.573662 -4.175521,8.756925 -10.256469,8.652853 -2.374181,0.259391 -1.069921,-4.879679 -3.975632,-6.481287 z"
style="display:inline;fill:#28170b;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
<path
id="tongue"
d="m 46.929215,76.246574 c 0.615399,-1.75537 8.22103,-2.28337 7.216279,4.07586 -0.212053,1.37525 3.358405,1.46548 4.409948,3.34086 1.055887,4.79323 -3.65884,4.36937 -6.94901,2.77293 -4.202979,-2.19163 -5.874033,-5.47644 -4.677217,-10.18965 z"
style="display:inline;opacity:1;fill:#da789c;fill-opacity:1;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
<g id="eye-left" class="monkey-eye">
<path
id="pupil-left"
d="m 40.867285,57.273873 c 1.612375,-2.042551 0.701258,-3.847919 2.827551,-5.020606 2.852266,-1.245289 3.874257,0.02912 6.461358,-0.487557 -1.800708,1.927123 -1.087836,3.182768 -3.147949,4.379153 -2.48621,1.342919 -2.404803,0.48141 -6.14096,1.12901 z"
style="display:inline;fill:#28170b;fill-opacity:1;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
<path
id="gleam-left"
d="m 44.577763,52.357009 0.175397,1.00226 1.720546,-0.91874 z"
style="display:inline;opacity:1;fill:#f4e3d7;fill-opacity:1;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
</g>
<g id="eye-right" class="monkey-eye">
<path
id="pupil-right"
d="m 36.526068,59.323301 c -2.29866,-0.610735 -2.053402,-2.430637 -4.251391,-2.123454 -2.768843,0.651852 -2.383872,2.077275 -4.559515,3.116838 2.373186,0.423104 2.10538,1.799062 4.267942,1.545474 2.554554,-0.379861 1.455015,-0.965477 4.542964,-2.538858 z"
style="display:inline;fill:#28170b;stroke:none;stroke-width:0.24182333px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
<path
id="gleam-right"
d="m 31.608594,58.036779 0.803199,0.56105 0.750048,-0.48428 z"
style="display:inline;opacity:1;fill:#f4e3d7;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
</g>
</g>
</g>
<g id="helmet">
<path
style="display:inline;opacity:1;fill:#22676d;fill-opacity:1;stroke:none;stroke-width:0.23431474px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="M 88.570396,58.724343 C 94.62432,54.067131 100.74234,57.883387 98.984914,64.996727 95.801343,75.095656 65.800904,103.41999 49.515946,100.50077 42.891637,97.739253 46.341916,92.001209 46.32058,88.666096 Z"
id="collar-one" />
<path
style="display:inline;opacity:1;fill:#bae5e9;fill-opacity:1;stroke:none;stroke-width:0.23431474px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="M 83.372436,52.659423 C 89.426364,48.002211 95.544387,51.818467 93.786951,58.931806 90.603382,69.030735 60.602943,97.355073 44.317985,94.435853 37.693676,91.674335 41.143955,85.936289 41.122619,82.601171 Z"
id="collar-two" />
<path
style="display:inline;opacity:1;fill:#bae5e9;fill-opacity:1;stroke:none;stroke-width:0.23431474px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="M 28.23215,79.916507 C 40.765843,80.993896 69.099088,55.590613 62.481792,37.486127 59.618529,29.652427 61.621394,25.326412 63.0048283,22.398281 63.451782,21.570257 62.431545,21.170567 61.660924,20.582972 58.721936,18.342009 33.314523,9.1801348 21.414153,19.148253 21.040806,19.46098 16.493514,24.225612 16.466931,23.906261 16.284334,21.712654 15.7061,18.778538 16.842454,17.601749 28.272105,5.7653958 43.244764,5.2528968 51.182613,5.4345318 74.64048,5.9712998 76.979428,18.408564 78.673549,18.329111 92.064018,29.246088 95.017703,40.802051 86.539829,53.0057904 73.279621,74.932862 61.561047,80.045597 49.802622,87.117228 40.441364,90.882866 24.536108,90.048641 21.648484,74.844071 c 2.995365,2.722164 2.788093,4.347644 6.583666,5.072436 z"
id="helmet-case" />
<path
style="display:inline;opacity:1;fill:#22676d;fill-opacity:1;stroke:none;stroke-width:0.23431474px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="m 38.50793,85.407931 c 1.842704,-0.411794 2.387542,-0.346584 3.341095,-0.04751 4.084771,1.281151 4.96339,0.781298 6.513681,0.03974 7.412447,-4.138988 13.715592,-9.704221 19.57263,-15.84302 l 0.241658,-11.116412 c -0.0621,-0.767793 -0.0058,-1.485505 0.329786,-2.085218 l 9.16256,-15.933883 c 1.483253,-4.949488 3.702339,-12.396219 3.313375,-17.72015 -0.162002,-2.217387 -1.329754,-1.991515 -2.30917,-4.372385 13.390473,10.916977 16.344158,22.47294 7.866281,34.728794 -13.26021,21.874955 -24.978782,26.987686 -36.737206,34.059324 -9.361257,3.765637 -18.488123,1.257736 -21.626797,-1.059526 4.264979,0.370603 6.29984,0.400653 10.332107,-0.649754 z"
id="helmet-base" />
<ellipse
style="display:inline;opacity:1;fill:#133a3e;fill-opacity:1;stroke:none;stroke-width:0.24935082;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="helmet-joint"
cx="53.400845"
cy="67.588089"
rx="6.4156117"
ry="9.9880085"
transform="matrix(0.88495557,-0.46567547,0.48925229,0.8721423,0,0)" />
<path
style="display:inline;opacity:0.98000004;fill:url(#linearGradient5760);fill-opacity:1;stroke:#000000;stroke-width:0.71934628;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m 61.651061,20.553894 1.289918,1.959392 C 59.552359,29.73164 61.960527,32.936833 62.291817,37.57986 69.580918,51.349372 46.284972,74.005931 38.794337,77.275049 18.383317,86.182977 8.0562438,56.70184 11.97544,33.820947 13.409936,25.446126 24.242237,4.0376178 61.651061,20.553894 Z"
id="visor" />
<path
style="display:inline;opacity:0.753;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.23431474;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m 35.84597,19.072302 c 7.774683,-2.280832 13.824223,-0.609922 19.290532,2.95867 l 1.301816,4.023793 c -8.1156,-3.761618 -15.343244,-5.578561 -23.314324,-3.432059 z"
id="helmet-gleam" />
</g>
</g>
<g id="helmet">
<path
style="display:inline;opacity:1;fill:#22676d;fill-opacity:1;stroke:none;stroke-width:0.23431474px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="M 88.570396,58.724343 C 94.62432,54.067131 100.74234,57.883387 98.984914,64.996727 95.801343,75.095656 65.800904,103.41999 49.515946,100.50077 42.891637,97.739253 46.341916,92.001209 46.32058,88.666096 Z"
id="collar-one" />
<path
style="display:inline;opacity:1;fill:#bae5e9;fill-opacity:1;stroke:none;stroke-width:0.23431474px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="M 83.372436,52.659423 C 89.426364,48.002211 95.544387,51.818467 93.786951,58.931806 90.603382,69.030735 60.602943,97.355073 44.317985,94.435853 37.693676,91.674335 41.143955,85.936289 41.122619,82.601171 Z"
id="collar-two" />
<path
style="display:inline;opacity:1;fill:#bae5e9;fill-opacity:1;stroke:none;stroke-width:0.23431474px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="M 28.23215,79.916507 C 40.765843,80.993896 69.099088,55.590613 62.481792,37.486127 59.618529,29.652427 61.621394,25.326412 63.0048283,22.398281 63.451782,21.570257 62.431545,21.170567 61.660924,20.582972 58.721936,18.342009 33.314523,9.1801348 21.414153,19.148253 21.040806,19.46098 16.493514,24.225612 16.466931,23.906261 16.284334,21.712654 15.7061,18.778538 16.842454,17.601749 28.272105,5.7653958 43.244764,5.2528968 51.182613,5.4345318 74.64048,5.9712998 76.979428,18.408564 78.673549,18.329111 92.064018,29.246088 95.017703,40.802051 86.539829,53.0057904 73.279621,74.932862 61.561047,80.045597 49.802622,87.117228 40.441364,90.882866 24.536108,90.048641 21.648484,74.844071 c 2.995365,2.722164 2.788093,4.347644 6.583666,5.072436 z"
id="helmet-case" />
<path
style="display:inline;opacity:1;fill:#22676d;fill-opacity:1;stroke:none;stroke-width:0.23431474px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="m 38.50793,85.407931 c 1.842704,-0.411794 2.387542,-0.346584 3.341095,-0.04751 4.084771,1.281151 4.96339,0.781298 6.513681,0.03974 7.412447,-4.138988 13.715592,-9.704221 19.57263,-15.84302 l 0.241658,-11.116412 c -0.0621,-0.767793 -0.0058,-1.485505 0.329786,-2.085218 l 9.16256,-15.933883 c 1.483253,-4.949488 3.702339,-12.396219 3.313375,-17.72015 -0.162002,-2.217387 -1.329754,-1.991515 -2.30917,-4.372385 13.390473,10.916977 16.344158,22.47294 7.866281,34.728794 -13.26021,21.874955 -24.978782,26.987686 -36.737206,34.059324 -9.361257,3.765637 -18.488123,1.257736 -21.626797,-1.059526 4.264979,0.370603 6.29984,0.400653 10.332107,-0.649754 z"
id="helmet-base" />
<ellipse
style="display:inline;opacity:1;fill:#133a3e;fill-opacity:1;stroke:none;stroke-width:0.24935082;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="helmet-joint"
cx="53.400845"
cy="67.588089"
rx="6.4156117"
ry="9.9880085"
transform="matrix(0.88495557,-0.46567547,0.48925229,0.8721423,0,0)" />
<path
style="display:inline;opacity:0.98000004;fill:url(#linearGradient5760);fill-opacity:1;stroke:#000000;stroke-width:0.71934628;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m 61.651061,20.553894 1.289918,1.959392 C 59.552359,29.73164 61.960527,32.936833 62.291817,37.57986 69.580918,51.349372 46.284972,74.005931 38.794337,77.275049 18.383317,86.182977 8.0562438,56.70184 11.97544,33.820947 13.409936,25.446126 24.242237,4.0376178 61.651061,20.553894 Z"
id="visor" />
<path
style="display:inline;opacity:0.753;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.23431474;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m 35.84597,19.072302 c 7.774683,-2.280832 13.824223,-0.609922 19.290532,2.95867 l 1.301816,4.023793 c -8.1156,-3.761618 -15.343244,-5.578561 -23.314324,-3.432059 z"
id="helmet-gleam" />
</g>
</g>
</svg>
</svg>
</div>
</div>
</div>
</div>

View File

@ -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>

View File

@ -18,8 +18,8 @@ let settingConfig = {
showFPS: {
label: 'FPS'
},
showDonation: {
label: 'Donation Info'
showMessages: {
label: 'Message Bar'
},
vbytes: {
label: 'Size by',

View File

@ -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">

View 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>

View File

@ -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 }

View 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>

View 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>

View 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>

View 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>

View 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>

View File

@ -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,
}

View File

@ -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") {

View File

@ -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)

View File

@ -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()