kopia lustrzana https://github.com/bugout-dev/moonstream
Merge branch 'main' into whale-watch
commit
828c6b1e61
|
@ -1,7 +1,9 @@
|
|||
name: Lint Moonstream backend
|
||||
|
||||
on:
|
||||
push:
|
||||
pull_request:
|
||||
branches:
|
||||
- "main"
|
||||
paths:
|
||||
- "backend/**"
|
||||
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
name: Lint Moonstream crawlers
|
||||
|
||||
on:
|
||||
push:
|
||||
pull_request:
|
||||
branches:
|
||||
- "main"
|
||||
paths:
|
||||
- "crawlers/**"
|
||||
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
name: Lint Moonstream db
|
||||
|
||||
on:
|
||||
push:
|
||||
pull_request:
|
||||
branches:
|
||||
- "main"
|
||||
paths:
|
||||
- "db/**"
|
||||
|
||||
|
|
|
@ -1,9 +1,6 @@
|
|||
name: Build Moonstream frontend
|
||||
|
||||
on:
|
||||
push:
|
||||
paths:
|
||||
- "frontend/**"
|
||||
pull_request:
|
||||
branches:
|
||||
- "main"
|
||||
|
|
|
@ -160,3 +160,17 @@ class TxinfoEthereumBlockchainResponse(BaseModel):
|
|||
smart_contract_address: Optional[str] = None
|
||||
abi: Optional[ContractABI] = None
|
||||
errors: List[str] = Field(default_factory=list)
|
||||
|
||||
|
||||
class AddressLabelResponse(BaseModel):
|
||||
label: str
|
||||
label_data: Optional[Dict[str, Any]] = None
|
||||
|
||||
|
||||
class AddressLabelsResponse(BaseModel):
|
||||
address: str
|
||||
labels: List[AddressLabelResponse] = Field(default_factory=list)
|
||||
|
||||
|
||||
class AddressListLabelsResponse(BaseModel):
|
||||
addresses: List[AddressLabelsResponse] = Field(default_factory=list)
|
||||
|
|
|
@ -6,33 +6,26 @@ transactions, etc.) with side information and return objects that are better sui
|
|||
end users.
|
||||
"""
|
||||
import logging
|
||||
from typing import Any, Dict
|
||||
from typing import Dict, Optional
|
||||
|
||||
from fastapi import (
|
||||
FastAPI,
|
||||
Depends,
|
||||
)
|
||||
from fastapi import FastAPI, Depends, HTTPException, Query
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from moonstreamdb.db import yield_db_session
|
||||
from moonstreamdb.models import EthereumAddress
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from ..abi_decoder import decode_abi
|
||||
from ..data import TxinfoEthereumBlockchainRequest, TxinfoEthereumBlockchainResponse
|
||||
from .. import actions
|
||||
from .. import data
|
||||
from ..middleware import BroodAuthMiddleware
|
||||
from ..settings import (
|
||||
DOCS_TARGET_PATH,
|
||||
ORIGINS,
|
||||
DOCS_PATHS,
|
||||
bugout_client as bc,
|
||||
)
|
||||
from ..settings import DOCS_TARGET_PATH, ORIGINS, DOCS_PATHS
|
||||
from ..version import MOONSTREAM_VERSION
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
tags_metadata = [
|
||||
{"name": "users", "description": "Operations with users."},
|
||||
{"name": "tokens", "description": "Operations with user tokens."},
|
||||
{"name": "txinfo", "description": "Ethereum transactions info."},
|
||||
{"name": "address info", "description": "Addresses public information."},
|
||||
]
|
||||
|
||||
app = FastAPI(
|
||||
|
@ -63,13 +56,13 @@ app.add_middleware(BroodAuthMiddleware, whitelist=whitelist_paths)
|
|||
@app.post(
|
||||
"/ethereum_blockchain",
|
||||
tags=["txinfo"],
|
||||
response_model=TxinfoEthereumBlockchainResponse,
|
||||
response_model=data.TxinfoEthereumBlockchainResponse,
|
||||
)
|
||||
async def txinfo_ethereum_blockchain_handler(
|
||||
txinfo_request: TxinfoEthereumBlockchainRequest,
|
||||
txinfo_request: data.TxinfoEthereumBlockchainRequest,
|
||||
db_session: Session = Depends(yield_db_session),
|
||||
) -> TxinfoEthereumBlockchainResponse:
|
||||
response = TxinfoEthereumBlockchainResponse(tx=txinfo_request.tx)
|
||||
) -> data.TxinfoEthereumBlockchainResponse:
|
||||
response = data.TxinfoEthereumBlockchainResponse(tx=txinfo_request.tx)
|
||||
if txinfo_request.tx.input is not None:
|
||||
try:
|
||||
response.abi = decode_abi(txinfo_request.tx.input, db_session)
|
||||
|
@ -92,3 +85,31 @@ async def txinfo_ethereum_blockchain_handler(
|
|||
response.is_smart_contract_call = True
|
||||
|
||||
return response
|
||||
|
||||
|
||||
@app.get(
|
||||
"/addresses", tags=["address info"], response_model=data.AddressListLabelsResponse
|
||||
)
|
||||
async def addresses_labels_handler(
|
||||
addresses: Optional[str] = Query(None),
|
||||
start: Optional[int] = Query(0),
|
||||
limit: Optional[int] = Query(100),
|
||||
db_session: Session = Depends(yield_db_session),
|
||||
) -> data.AddressListLabelsResponse:
|
||||
"""
|
||||
Fetch labels with additional public information
|
||||
about known addresses.
|
||||
"""
|
||||
if limit > 100:
|
||||
raise HTTPException(
|
||||
status_code=406, detail="The limit cannot exceed 100 addresses"
|
||||
)
|
||||
try:
|
||||
addresses = actions.get_address_labels(
|
||||
db_session=db_session, start=start, limit=limit, addresses=addresses
|
||||
)
|
||||
except Exception as err:
|
||||
logger.error(f"Unable to get info about Ethereum addresses {err}")
|
||||
raise HTTPException(status_code=500)
|
||||
|
||||
return addresses
|
||||
|
|
|
@ -0,0 +1,192 @@
|
|||
import argparse
|
||||
import boto3
|
||||
import csv
|
||||
import codecs
|
||||
import json
|
||||
import os
|
||||
from sqlalchemy.orm import Session
|
||||
from moonstreamdb.db import yield_db_session_ctx
|
||||
import sys
|
||||
import time
|
||||
from datetime import datetime
|
||||
from typing import Any, List, Optional, Tuple, Dict
|
||||
from dataclasses import dataclass
|
||||
from sqlalchemy.sql.expression import label, text
|
||||
from .version import MOONCRAWL_VERSION
|
||||
from moonstreamdb.models import EthereumAddress, EthereumLabel
|
||||
import requests
|
||||
|
||||
from .settings import MOONSTREAM_ETHERSCAN_TOKEN
|
||||
|
||||
if MOONSTREAM_ETHERSCAN_TOKEN is None:
|
||||
raise Exception("MOONSTREAM_ETHERSCAN_TOKEN environment variable must be set")
|
||||
|
||||
|
||||
BASE_API_URL = "https://api.etherscan.io/api?module=contract&action=getsourcecode"
|
||||
|
||||
ETHERSCAN_SMARTCONTRACTS_LABEL_NAME = "etherscan_smartcontract"
|
||||
|
||||
bucket = os.environ.get("AWS_S3_SMARTCONTRACT_BUCKET")
|
||||
if bucket is None:
|
||||
raise ValueError("AWS_S3_SMARTCONTRACT_BUCKET must be set")
|
||||
|
||||
|
||||
@dataclass
|
||||
class VerifiedSmartContract:
|
||||
name: str
|
||||
address: str
|
||||
tx_hash: str
|
||||
|
||||
|
||||
def push_to_bucket(contract_data: Dict[str, Any], contract_file: str):
|
||||
result_bytes = json.dumps(contract_data).encode("utf-8")
|
||||
result_key = contract_file
|
||||
|
||||
s3 = boto3.client("s3")
|
||||
s3.put_object(
|
||||
Body=result_bytes,
|
||||
Bucket=bucket,
|
||||
Key=result_key,
|
||||
ContentType="application/json",
|
||||
Metadata={"source": "etherscan", "crawler_version": MOONCRAWL_VERSION},
|
||||
)
|
||||
|
||||
|
||||
def get_address_id(db_session: Session, contract_address: str) -> int:
|
||||
"""
|
||||
Searches for given address in EthereumAddress table,
|
||||
If doesn't find one, creates new.
|
||||
Returns id of address
|
||||
"""
|
||||
query = db_session.query(EthereumAddress.id).filter(
|
||||
EthereumAddress.address == contract_address
|
||||
)
|
||||
id = query.one_or_none()
|
||||
if id is not None:
|
||||
return id[0]
|
||||
|
||||
latest_address_id = (
|
||||
db_session.query(EthereumAddress.id).order_by(text("id desc")).limit(1).one()
|
||||
)[0]
|
||||
|
||||
id = latest_address_id + 1
|
||||
smart_contract = EthereumAddress(
|
||||
id=id,
|
||||
address=contract_address,
|
||||
)
|
||||
try:
|
||||
db_session.add(smart_contract)
|
||||
db_session.commit()
|
||||
except:
|
||||
db_session.rollback()
|
||||
return id
|
||||
|
||||
|
||||
def crawl_step(db_session: Session, contract: VerifiedSmartContract, crawl_url: str):
|
||||
attempt = 0
|
||||
current_interval = 2
|
||||
success = False
|
||||
|
||||
response: Optional[requests.Response] = None
|
||||
while (not success) and attempt < 3:
|
||||
attempt += 1
|
||||
try:
|
||||
response = requests.get(crawl_url)
|
||||
response.raise_for_status()
|
||||
success = True
|
||||
except:
|
||||
current_interval *= 2
|
||||
time.sleep(current_interval)
|
||||
|
||||
if response is None:
|
||||
print(f"Could not process URL: {crawl_url}", file=sys.stderr)
|
||||
return None
|
||||
page = response.json()
|
||||
result = page["result"][0]
|
||||
contract_info = {
|
||||
"data": result,
|
||||
"crawl_version": MOONCRAWL_VERSION,
|
||||
"crawled_at": f"{datetime.now()}",
|
||||
}
|
||||
object_key = f"/etherscan/v1/{contract.address}.json"
|
||||
push_to_bucket(contract_info, object_key)
|
||||
|
||||
eth_address_id = get_address_id(db_session, contract.address)
|
||||
|
||||
eth_label = EthereumLabel(
|
||||
label=ETHERSCAN_SMARTCONTRACTS_LABEL_NAME,
|
||||
address_id=eth_address_id,
|
||||
label_data={
|
||||
"object_uri": f"s3://{bucket}/{object_key}",
|
||||
"name": contract.name,
|
||||
"tx_hash": contract.tx_hash,
|
||||
},
|
||||
)
|
||||
try:
|
||||
db_session.add(eth_label)
|
||||
db_session.commit()
|
||||
except:
|
||||
db_session.rollback()
|
||||
|
||||
|
||||
def crawl(
|
||||
db_session: Session,
|
||||
smart_contracts: List[VerifiedSmartContract],
|
||||
interval: float,
|
||||
start_step=0,
|
||||
):
|
||||
for i in range(start_step, len(smart_contracts)):
|
||||
contract = smart_contracts[i]
|
||||
print(f"Crawling {i}/{len(smart_contracts)} : {contract.address}")
|
||||
query_url = (
|
||||
BASE_API_URL
|
||||
+ f"&address={contract.address}&apikey={MOONSTREAM_ETHERSCAN_TOKEN}"
|
||||
)
|
||||
crawl_step(db_session, contract, query_url)
|
||||
time.sleep(interval)
|
||||
|
||||
|
||||
def load_smart_contracts() -> List[VerifiedSmartContract]:
|
||||
smart_contracts: List[VerifiedSmartContract] = []
|
||||
s3 = boto3.client("s3")
|
||||
data = s3.get_object(Bucket=bucket, Key="util/verified-contractaddress.csv")
|
||||
for row in csv.DictReader(codecs.getreader("utf-8")(data["Body"])):
|
||||
smart_contracts.append(
|
||||
VerifiedSmartContract(
|
||||
tx_hash=row["Txhash"],
|
||||
address=row["ContractAddress"],
|
||||
name=row["ContractName"],
|
||||
)
|
||||
)
|
||||
return smart_contracts
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Crawls smart contract sources from etherscan.io"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--interval",
|
||||
type=float,
|
||||
default=0.2,
|
||||
help="Number of seconds to wait between requests to the etherscan.io (default: 0.2)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--offset",
|
||||
type=int,
|
||||
default=0,
|
||||
help="Number of smart contract to skip for crawling from smart contracts .csv file",
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
with yield_db_session_ctx() as db_session:
|
||||
crawl(
|
||||
db_session,
|
||||
load_smart_contracts(),
|
||||
interval=args.interval,
|
||||
start_step=args.offset,
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
|
@ -1,12 +1,12 @@
|
|||
import argparse
|
||||
import json
|
||||
import os
|
||||
import time
|
||||
|
||||
import requests
|
||||
from sqlalchemy import text
|
||||
|
||||
from moonstreamdb.db import yield_db_session_ctx
|
||||
from moonstreamdb.models import EthereumAddress
|
||||
from moonstreamdb.models import EthereumAddress, EthereumLabel
|
||||
|
||||
COINMARKETCAP_API_KEY = os.environ.get("COINMARKETCAP_API_KEY")
|
||||
if COINMARKETCAP_API_KEY is None:
|
||||
|
@ -18,7 +18,7 @@ CRAWL_ORIGINS = {
|
|||
}
|
||||
|
||||
|
||||
def identities_cmc_handler(args: argparse.Namespace) -> None:
|
||||
def identities_cmc_add_handler(args: argparse.Namespace) -> None:
|
||||
"""
|
||||
Parse metadata for Ethereum tokens.
|
||||
"""
|
||||
|
@ -37,7 +37,11 @@ def identities_cmc_handler(args: argparse.Namespace) -> None:
|
|||
limit_n = 5000
|
||||
|
||||
while True:
|
||||
params = {"start": start_n, "limit": limit_n}
|
||||
params = {
|
||||
"start": start_n,
|
||||
"limit": limit_n,
|
||||
"listing_status": args.listing_status,
|
||||
}
|
||||
try:
|
||||
r = requests.get(url=url, headers=headers, params=params)
|
||||
r.raise_for_status()
|
||||
|
@ -50,20 +54,36 @@ def identities_cmc_handler(args: argparse.Namespace) -> None:
|
|||
break
|
||||
|
||||
with yield_db_session_ctx() as db_session:
|
||||
for crypto in response["data"]:
|
||||
if crypto["platform"] is not None:
|
||||
latest_address = (
|
||||
db_session.query(EthereumAddress.id)
|
||||
.order_by(text("id desc"))
|
||||
.limit(1)
|
||||
.one()
|
||||
)[0]
|
||||
for coin in response["data"]:
|
||||
if coin["platform"] is not None:
|
||||
if (
|
||||
crypto["platform"]["id"] == 1027
|
||||
and crypto["platform"]["token_address"] is not None
|
||||
coin["platform"]["id"] == 1027
|
||||
and coin["platform"]["token_address"] is not None
|
||||
):
|
||||
|
||||
latest_address += 1
|
||||
eth_token_id = latest_address
|
||||
eth_token = EthereumAddress(
|
||||
address=crypto["platform"]["token_address"],
|
||||
name=crypto["name"],
|
||||
symbol=crypto["symbol"],
|
||||
id=eth_token_id,
|
||||
address=coin["platform"]["token_address"],
|
||||
)
|
||||
db_session.add(eth_token)
|
||||
print(f"Added {crypto['name']} token")
|
||||
eth_token_label = EthereumLabel(
|
||||
label="coinmarketcap_token",
|
||||
address_id=eth_token_id,
|
||||
label_data={
|
||||
"name": coin["name"],
|
||||
"symbol": coin["symbol"],
|
||||
"coinmarketcap_url": f'https://coinmarketcap.com/currencies/{coin["slug"]}',
|
||||
},
|
||||
)
|
||||
db_session.add(eth_token_label)
|
||||
print(f"Added {coin['name']} token")
|
||||
|
||||
db_session.commit()
|
||||
start_n += limit_n
|
||||
|
@ -79,21 +99,31 @@ def main():
|
|||
|
||||
parser_cmc = subcommands.add_parser("cmc", description="Coinmarketcap commands")
|
||||
parser_cmc.set_defaults(func=lambda _: parser_cmc.print_help())
|
||||
subcommands_parser_cmc = parser_cmc.add_subparsers(
|
||||
description="Ethereum blocks commands"
|
||||
)
|
||||
parser_cmc.add_argument(
|
||||
"-s",
|
||||
"--sandbox",
|
||||
action="store_true",
|
||||
help="Target to sandbox API",
|
||||
)
|
||||
parser_cmc.set_defaults(func=identities_cmc_handler)
|
||||
|
||||
parser_label_cloud = subcommands.add_parser(
|
||||
"label_cloud", description="Etherscan label cloud commands"
|
||||
subcommands_parser_cmc = parser_cmc.add_subparsers(
|
||||
description="Ethereum blocks commands"
|
||||
)
|
||||
parser_label_cloud.set_defaults(func=identities_get_handler)
|
||||
parser_cmc_add = subcommands_parser_cmc.add_parser(
|
||||
"add", description="Add additional information about Ethereum addresses"
|
||||
)
|
||||
parser_cmc_add.add_argument(
|
||||
"-s",
|
||||
"--listing_status",
|
||||
default="active,inactive,untracked",
|
||||
help="Listing status of coin, by default all of them: active,inactive,untracked",
|
||||
)
|
||||
parser_cmc_add.set_defaults(func=identities_cmc_add_handler)
|
||||
|
||||
# parser_label_cloud = subcommands.add_parser(
|
||||
# "label_cloud", description="Etherscan label cloud commands"
|
||||
# )
|
||||
# parser_label_cloud.set_defaults(func=identities_get_handler)
|
||||
|
||||
args = parser.parse_args()
|
||||
args.func(args)
|
||||
|
|
|
@ -11,3 +11,6 @@ except:
|
|||
raise Exception(
|
||||
f"Could not parse MOONSTREAM_CRAWL_WORKERS as int: {MOONSTREAM_CRAWL_WORKERS_RAW}"
|
||||
)
|
||||
|
||||
|
||||
MOONSTREAM_ETHERSCAN_TOKEN = os.environ.get("MOONSTREAM_ETHERSCAN_TOKEN")
|
||||
|
|
|
@ -2,4 +2,4 @@
|
|||
Moonstream crawlers version.
|
||||
"""
|
||||
|
||||
MOONCRAWL_VERSION = "0.0.2"
|
||||
MOONCRAWL_VERSION = "0.0.3"
|
||||
|
|
|
@ -2,4 +2,8 @@
|
|||
export MOONSTREAM_IPC_PATH=null
|
||||
export MOONSTREAM_CRAWL_WORKERS=4
|
||||
export MOONSTREAM_DB_URI="postgresql://<username>:<password>@<db_host>:<db_port>/<db_name>"
|
||||
|
||||
export MOONSTREAM_ETHERSCAN_TOKEN="TOKEN"
|
||||
export AWS_S3_SMARTCONTRACT_BUCKET=""
|
||||
export MOONSTREAM_HUMBUG_TOKEN="<Token for crawlers store data via Humbug>"
|
||||
export COINMARKETCAP_API_KEY="<API key to parse conmarketcap>"
|
||||
|
|
|
@ -38,6 +38,7 @@ setup(
|
|||
"requests",
|
||||
"tqdm",
|
||||
"web3",
|
||||
"boto3",
|
||||
],
|
||||
extras_require={"dev": ["black", "mypy", "types-requests"]},
|
||||
entry_points={
|
||||
|
@ -45,6 +46,7 @@ setup(
|
|||
"ethcrawler=mooncrawl.ethcrawler:main",
|
||||
"esd=mooncrawl.esd:main",
|
||||
"identity=mooncrawl.identity:main",
|
||||
"etherscan=mooncrawl.etherscan:main",
|
||||
]
|
||||
},
|
||||
)
|
||||
|
|
|
@ -0,0 +1,32 @@
|
|||
"""Unique const for addr
|
||||
|
||||
Revision ID: ea8185bd24c7
|
||||
Revises: 40871a7807f6
|
||||
Create Date: 2021-08-18 09:41:00.512462
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = 'ea8185bd24c7'
|
||||
down_revision = '40871a7807f6'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_index('ix_ethereum_addresses_address', table_name='ethereum_addresses')
|
||||
op.create_index(op.f('ix_ethereum_addresses_address'), 'ethereum_addresses', ['address'], unique=True)
|
||||
op.create_unique_constraint(op.f('uq_ethereum_labels_id'), 'ethereum_labels', ['id'])
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_constraint(op.f('uq_ethereum_labels_id'), 'ethereum_labels', type_='unique')
|
||||
op.drop_index(op.f('ix_ethereum_addresses_address'), table_name='ethereum_addresses')
|
||||
op.create_index('ix_ethereum_addresses_address', 'ethereum_addresses', ['address'], unique=False)
|
||||
# ### end Alembic commands ###
|
|
@ -113,7 +113,7 @@ class EthereumAddress(Base): # type: ignore
|
|||
nullable=True,
|
||||
index=True,
|
||||
)
|
||||
address = Column(VARCHAR(256), nullable=False, index=True)
|
||||
address = Column(VARCHAR(256), nullable=False, unique=True, index=True)
|
||||
created_at = Column(
|
||||
DateTime(timezone=True), server_default=utcnow(), nullable=False
|
||||
)
|
||||
|
|
|
@ -39,7 +39,6 @@ export default function CachingApp({ Component, pageProps }) {
|
|||
router.events.on("routeChangeComplete", handleStop);
|
||||
router.events.on("routeChangeError", handleStop);
|
||||
|
||||
console.log("_app", router.asPath);
|
||||
return () => {
|
||||
router.events.off("routeChangeStart", handleStart);
|
||||
router.events.off("routeChangeComplete", handleStop);
|
||||
|
@ -49,8 +48,6 @@ export default function CachingApp({ Component, pageProps }) {
|
|||
const getLayout =
|
||||
Component.getLayout || ((page) => <DefaultLayout>{page}</DefaultLayout>);
|
||||
|
||||
console.log("_app loaded", router.asPath);
|
||||
|
||||
return (
|
||||
<>
|
||||
<style global jsx>{`
|
||||
|
|
|
@ -10,7 +10,9 @@ import {
|
|||
UnorderedList,
|
||||
ListItem,
|
||||
} from "@chakra-ui/react";
|
||||
import RouteButton from "../../src/components/RouteButton";
|
||||
const Entry = () => {
|
||||
console.count("render stream!");
|
||||
const ui = useContext(UIContext);
|
||||
|
||||
useEffect(() => {
|
||||
|
@ -49,6 +51,14 @@ const Entry = () => {
|
|||
subscription screen
|
||||
</ListItem>
|
||||
</UnorderedList>
|
||||
<RouteButton
|
||||
variant="solid"
|
||||
size="md"
|
||||
colorScheme="suggested"
|
||||
href="/welcome"
|
||||
>
|
||||
Learn how to use moonstream
|
||||
</RouteButton>
|
||||
</Stack>
|
||||
</>
|
||||
</Box>
|
||||
|
|
|
@ -15,7 +15,7 @@ import {
|
|||
ModalOverlay,
|
||||
ModalContent,
|
||||
} from "@chakra-ui/react";
|
||||
import NewSubscription from "../src/components/NewSubscription";
|
||||
import NewSubscription from "../src/components/NewModalSubscripton";
|
||||
import { AiOutlinePlusCircle } from "react-icons/ai";
|
||||
|
||||
const Subscriptions = () => {
|
||||
|
|
|
@ -0,0 +1,468 @@
|
|||
import React, { useContext, useEffect, useRef } from "react";
|
||||
import { getLayout } from "../src/layouts/AppLayout";
|
||||
import UIContext from "../src/core/providers/UIProvider/context";
|
||||
import {
|
||||
Heading,
|
||||
Text,
|
||||
Button,
|
||||
Stack,
|
||||
ButtonGroup,
|
||||
Spacer,
|
||||
Radio,
|
||||
RadioGroup,
|
||||
UnorderedList,
|
||||
ListItem,
|
||||
Fade,
|
||||
chakra,
|
||||
useBoolean,
|
||||
Flex,
|
||||
IconButton,
|
||||
Tooltip,
|
||||
} from "@chakra-ui/react";
|
||||
import StepProgress from "../src/components/StepProgress";
|
||||
import { ArrowLeftIcon, ArrowRightIcon } from "@chakra-ui/icons";
|
||||
import Scrollable from "../src/components/Scrollable";
|
||||
import AnalyticsContext from "../src/core/providers/AnalyticsProvider/context";
|
||||
import NewSubscription from "../src/components/NewSubscription";
|
||||
import StreamEntry from "../src/components/StreamEntry";
|
||||
import SubscriptionsList from "../src/components/SubscriptionsList";
|
||||
import { useSubscriptions } from "../src/core/hooks";
|
||||
import router from "next/router";
|
||||
import { FaFilter } from "react-icons/fa";
|
||||
|
||||
const Welcome = () => {
|
||||
console.count("render welcome!");
|
||||
const { subscriptionsCache } = useSubscriptions();
|
||||
const ui = useContext(UIContext);
|
||||
const { mixpanel, isLoaded, MIXPANEL_PROPS } = useContext(AnalyticsContext);
|
||||
const [profile, setProfile] = React.useState();
|
||||
const [showSubscriptionForm, setShowSubscriptionForm] = useBoolean(true);
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof window !== "undefined") {
|
||||
document.title = `Welcome to moonstream.to!`;
|
||||
}
|
||||
}, []);
|
||||
|
||||
const progressButtonCallback = (index) => {
|
||||
ui.setOnboardingStep(index);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (profile && isLoaded) {
|
||||
mixpanel.people.set({
|
||||
[`${MIXPANEL_PROPS.USER_SPECIALITY}`]: profile,
|
||||
});
|
||||
}
|
||||
}, [profile, MIXPANEL_PROPS, isLoaded, mixpanel]);
|
||||
|
||||
const SubscriptonCreatedCallback = () => {
|
||||
setShowSubscriptionForm.off();
|
||||
};
|
||||
|
||||
const scrollRef = useRef();
|
||||
const handleNextClick = () => {
|
||||
if (ui.onboardingStep < ui.onboardingSteps.length - 1) {
|
||||
ui.setOnboardingStep(ui.onboardingStep + 1);
|
||||
scrollRef?.current?.scrollIntoView();
|
||||
} else {
|
||||
ui.setisOnboardingComplete(true);
|
||||
router.push("/stream");
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Scrollable>
|
||||
<Stack px="7%" pt={4} w="100%" spacing={4} ref={scrollRef}>
|
||||
<StepProgress
|
||||
numSteps={ui.onboardingSteps.length}
|
||||
currentStep={ui.onboardingStep}
|
||||
colorScheme="primary"
|
||||
buttonCallback={progressButtonCallback}
|
||||
buttonTitles={[
|
||||
"Moonstream basics",
|
||||
"Setup subscriptions",
|
||||
"How to read stream",
|
||||
]}
|
||||
style="arrows"
|
||||
/>
|
||||
|
||||
{ui.onboardingStep === 0 && (
|
||||
<Fade in>
|
||||
<Stack spacing={4}>
|
||||
<Stack
|
||||
px={12}
|
||||
// mt={24}
|
||||
bgColor="gray.50"
|
||||
borderRadius="xl"
|
||||
boxShadow="xl"
|
||||
py={4}
|
||||
>
|
||||
<Heading as="h4" size="md">
|
||||
Greetings traveller!
|
||||
</Heading>
|
||||
<Text fontWeight="semibold" pl={2}>
|
||||
{" "}
|
||||
We are very excited to see you onboard!
|
||||
</Text>
|
||||
|
||||
<Text fontWeight="semibold" pl={2}>
|
||||
Moonstream is a product which helps anyone participate in
|
||||
decentralized finance. From the most sophisticated flash
|
||||
arbitrageurs to people looking for yield from currency that
|
||||
would otherwise lie dormant in their exchange accounts.
|
||||
</Text>
|
||||
<Text fontWeight="semibold" pl={2}>
|
||||
Moonstream is ment to give you critical insights needed to
|
||||
succeed in your crypto quest!
|
||||
</Text>
|
||||
</Stack>
|
||||
<Stack
|
||||
px={12}
|
||||
// mt={24}
|
||||
bgColor="gray.50"
|
||||
borderRadius="xl"
|
||||
boxShadow="xl"
|
||||
py={4}
|
||||
>
|
||||
<Heading as="h4" size="md">
|
||||
How does Moonstream work?
|
||||
</Heading>
|
||||
<chakra.span fontWeight="semibold" pl={2}>
|
||||
<Text fontWeight="bold" display="inline">
|
||||
We run nodes
|
||||
</Text>{" "}
|
||||
- Now get most precise and accurate data you can just query
|
||||
our database. You {`don't`} need to maintain your node, and
|
||||
still have data that miners have access to!
|
||||
</chakra.span>
|
||||
<chakra.span fontWeight="semibold" pl={2}>
|
||||
<Text fontWeight="bold" display="inline">
|
||||
We crawl data
|
||||
</Text>{" "}
|
||||
We analyze millions of transactions, data, smart contract code
|
||||
to link all them together
|
||||
</chakra.span>
|
||||
|
||||
<chakra.span fontWeight="semibold" pl={2}>
|
||||
<Text fontWeight="bold" display="inline">
|
||||
We provide data
|
||||
</Text>{" "}
|
||||
We allow you to fetch data trough the website or trough API
|
||||
</chakra.span>
|
||||
|
||||
<chakra.span fontWeight="semibold" pl={2}>
|
||||
<Text fontWeight="bold" display="inline">
|
||||
We analyze data
|
||||
</Text>{" "}
|
||||
We find most interesting stuff and show it to you!
|
||||
</chakra.span>
|
||||
</Stack>
|
||||
<Stack
|
||||
px={12}
|
||||
// mt={24}
|
||||
bgColor="gray.50"
|
||||
borderRadius="xl"
|
||||
boxShadow="xl"
|
||||
py={4}
|
||||
>
|
||||
<Heading as="h4" size="md">
|
||||
UI 101?
|
||||
</Heading>
|
||||
<Text fontWeight="semibold" pl={2}>
|
||||
On the left side corner there is sidebar that you can use to
|
||||
navigate across the website. There are following views you can
|
||||
navigate to:
|
||||
</Text>
|
||||
<chakra.span fontWeight="semibold" pl={2}>
|
||||
<Text fontWeight="bold" display="inline">
|
||||
Subscriptions
|
||||
</Text>{" "}
|
||||
- Use this screen to set up addresses you would like to
|
||||
monitor to.{" "}
|
||||
<i>
|
||||
NB: Without setting up subscriptions moonstream will have
|
||||
quite empty feel!{" "}
|
||||
</i>{" "}
|
||||
No worries, we will help you to set up your subscriptions in
|
||||
the next steps!
|
||||
</chakra.span>
|
||||
<chakra.span fontWeight="semibold" pl={2}>
|
||||
<Text fontWeight="bold" display="inline">
|
||||
Stream
|
||||
</Text>{" "}
|
||||
This view is somewhat similar to a bank statement where you
|
||||
can define time range and see what happened in that time over
|
||||
your subscriptions. In next steps we will show how you can
|
||||
apply filters to it!
|
||||
</chakra.span>
|
||||
|
||||
<chakra.span fontWeight="semibold" pl={2}>
|
||||
<Text fontWeight="bold" display="inline">
|
||||
Stream Entry
|
||||
</Text>{" "}
|
||||
You can see detailed view of stream cards with very specific
|
||||
and essential data, like methods called in smart contracts
|
||||
etc!!
|
||||
</chakra.span>
|
||||
|
||||
<chakra.span fontWeight="semibold" pl={2}>
|
||||
<Text fontWeight="bold" display="inline">
|
||||
Analytics
|
||||
</Text>{" "}
|
||||
This section is under construction yet. Soon you will be able
|
||||
to create your dashboard with data of your interest. We will
|
||||
really appretiate if you visit that section, and fill up form
|
||||
describing what analytical tools you would love to see there!
|
||||
</chakra.span>
|
||||
</Stack>
|
||||
|
||||
<Stack
|
||||
px={12}
|
||||
// mt={24}
|
||||
bgColor="gray.50"
|
||||
borderRadius="xl"
|
||||
boxShadow="xl"
|
||||
py={4}
|
||||
>
|
||||
<Heading as="h4" size="md">
|
||||
Tell us more about your needs?
|
||||
</Heading>
|
||||
<Text fontWeight="semibold" pl={2}>
|
||||
In order to fetch best possible experience, we would like to
|
||||
know some details about you
|
||||
</Text>
|
||||
<Text fontWeight="semibold" pl={2}>
|
||||
Please tell us what profile describes you best?{" "}
|
||||
<i>
|
||||
This is purely analytical data, you can change it anytime
|
||||
later
|
||||
</i>
|
||||
</Text>
|
||||
<RadioGroup
|
||||
onChange={setProfile}
|
||||
value={profile}
|
||||
fontWeight="bold"
|
||||
>
|
||||
<Stack direction="row" justifyContent="space-evenly">
|
||||
<Radio value="trader">I am trading crypto currency</Radio>
|
||||
<Radio value="fund">I represent investment fund</Radio>
|
||||
<Radio value="developer">I am developer</Radio>
|
||||
</Stack>
|
||||
</RadioGroup>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Fade>
|
||||
)}
|
||||
{ui.onboardingStep === 1 && (
|
||||
<Fade in>
|
||||
<Stack px="7%">
|
||||
<Stack
|
||||
px={12}
|
||||
// mt={24}
|
||||
bgColor="gray.50"
|
||||
borderRadius="xl"
|
||||
boxShadow="xl"
|
||||
py={4}
|
||||
my={2}
|
||||
>
|
||||
<Heading as="h4" size="md">
|
||||
Subscriptions
|
||||
</Heading>
|
||||
<chakra.span fontWeight="semibold" pl={2}>
|
||||
Subscriptions is essential tool of Moonstream. We gather data
|
||||
for you based on addresses you have subscribed for.
|
||||
<br />
|
||||
Subscribe to any addres which you are interested in and they
|
||||
will become part of your stream!
|
||||
<UnorderedList>
|
||||
<ListItem>
|
||||
Color - you can define color to easily identify this
|
||||
subscription in your stream
|
||||
</ListItem>
|
||||
<ListItem>Address - thing you subscribe to</ListItem>
|
||||
<ListItem>
|
||||
Label - Its good idea to use human readible name that
|
||||
represents address
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
Source - In Alpha we support only Ethereum blockchain,
|
||||
more sources are coming soon!
|
||||
</ListItem>
|
||||
</UnorderedList>
|
||||
</chakra.span>
|
||||
</Stack>
|
||||
{subscriptionsCache.data.subscriptions.length > 0 &&
|
||||
!subscriptionsCache.isLoading && (
|
||||
<>
|
||||
<Heading>
|
||||
{" "}
|
||||
You already have some subscriptions set up
|
||||
</Heading>
|
||||
</>
|
||||
)}
|
||||
<SubscriptionsList />
|
||||
{showSubscriptionForm && (
|
||||
<>
|
||||
<Heading pt={12}>{`Let's add new subscription!`}</Heading>
|
||||
|
||||
<NewSubscription
|
||||
isFreeOption={true}
|
||||
onClose={SubscriptonCreatedCallback}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{!showSubscriptionForm && (
|
||||
<Button
|
||||
colorScheme="suggested"
|
||||
variant="solid"
|
||||
onClick={() => setShowSubscriptionForm.on()}
|
||||
>
|
||||
Add another subscription
|
||||
</Button>
|
||||
)}
|
||||
</Stack>
|
||||
</Fade>
|
||||
)}
|
||||
{ui.onboardingStep === 2 && (
|
||||
<Fade in>
|
||||
<Stack>
|
||||
<Stack
|
||||
px={12}
|
||||
// mt={24}
|
||||
bgColor="gray.50"
|
||||
borderRadius="xl"
|
||||
boxShadow="xl"
|
||||
py={4}
|
||||
my={2}
|
||||
>
|
||||
<Heading as="h4" size="md">
|
||||
Stream
|
||||
</Heading>
|
||||
<chakra.span fontWeight="semibold" pl={2}>
|
||||
We are almost done!
|
||||
<br />
|
||||
{`Stream is where you can read data you've subscribed for. Here
|
||||
you have different cards for different subscription types.`}
|
||||
<br />
|
||||
If card has some extra details - there will be orange button
|
||||
on right hand side inviting you to see more!
|
||||
<br />
|
||||
Below is typical card for ethereum blockchain event. Useful
|
||||
information right on the card:
|
||||
<UnorderedList py={2}>
|
||||
<ListItem>Hash - unique ID of the event </ListItem>
|
||||
<ListItem>
|
||||
From - sender address. If it is one of your subscription
|
||||
addresses - will appear in color and with label{" "}
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
To - receiver address. If it is one of your subscription
|
||||
addresses - will appear in color and with label{" "}
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
Nonce - Counter how many transactions address has sent. It
|
||||
also determines sequence of transaction!{" "}
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
Gas Price - this is how much ether is being paid per gas
|
||||
unit
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
Gas - Ammount of gas this event consumes
|
||||
</ListItem>
|
||||
</UnorderedList>
|
||||
</chakra.span>
|
||||
</Stack>
|
||||
<Stack
|
||||
pb={ui.isMobileView ? 24 : 8}
|
||||
w={ui.isMobileView ? "100%" : "calc(100% - 300px)"}
|
||||
alignSelf="center"
|
||||
>
|
||||
<Flex h="3rem" w="100%" bgColor="gray.100" alignItems="center">
|
||||
<Flex maxW="90%"></Flex>
|
||||
<Spacer />
|
||||
<Tooltip
|
||||
variant="onboarding"
|
||||
placement={ui.isMobileView ? "bottom" : "right"}
|
||||
label="Filtering menu"
|
||||
isOpen={true}
|
||||
maxW="150px"
|
||||
hasArrow
|
||||
>
|
||||
<IconButton
|
||||
mr={4}
|
||||
// onClick={onOpen}
|
||||
colorScheme="primary"
|
||||
variant="ghost"
|
||||
icon={<FaFilter />}
|
||||
/>
|
||||
</Tooltip>
|
||||
</Flex>
|
||||
<StreamEntry
|
||||
mt={20}
|
||||
entry={{
|
||||
from_address: "this is address from",
|
||||
to_address: "this is to address",
|
||||
hash: "this is hash",
|
||||
}}
|
||||
showOnboardingTooltips={true}
|
||||
/>
|
||||
</Stack>
|
||||
<Stack
|
||||
px={12}
|
||||
// mt={24}
|
||||
bgColor="gray.50"
|
||||
borderRadius="xl"
|
||||
boxShadow="xl"
|
||||
py={4}
|
||||
my={2}
|
||||
>
|
||||
<Heading as="h4" size="md">
|
||||
Applying filters
|
||||
</Heading>
|
||||
<chakra.span fontWeight="semibold" pl={2}>
|
||||
You can apply various filters by clicking filter menu button
|
||||
<br />
|
||||
{`Right now you can use it to select address from and to, we are adding more complex queries soon, stay tuna! `}
|
||||
<br />
|
||||
</chakra.span>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Fade>
|
||||
)}
|
||||
|
||||
<ButtonGroup>
|
||||
<Button
|
||||
colorScheme="secondary"
|
||||
leftIcon={<ArrowLeftIcon />}
|
||||
variant="outline"
|
||||
hidden={ui.onboardingStep === 0}
|
||||
disabled={ui.onboardingStep === 0}
|
||||
onClick={() => {
|
||||
ui.setOnboardingStep(ui.onboardingStep - 1);
|
||||
scrollRef?.current?.scrollIntoView();
|
||||
}}
|
||||
>
|
||||
Previous
|
||||
</Button>
|
||||
<Spacer />
|
||||
<Button
|
||||
colorScheme="secondary"
|
||||
variant="solid"
|
||||
rightIcon={<ArrowRightIcon />}
|
||||
// hidden={!(ui.onboardingStep < ui.onboardingSteps.length - 1)}
|
||||
// disabled={!(ui.onboardingStep < ui.onboardingSteps.length - 1)}
|
||||
onClick={() => handleNextClick()}
|
||||
>
|
||||
{ui.onboardingStep < ui.onboardingSteps.length - 1
|
||||
? `Next`
|
||||
: `Finish `}
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
</Stack>
|
||||
</Scrollable>
|
||||
);
|
||||
};
|
||||
Welcome.getLayout = getLayout;
|
||||
export default Welcome;
|
|
@ -1,13 +1,38 @@
|
|||
const Spinner = {
|
||||
baseStyle: {
|
||||
color: "primary.400",
|
||||
thickness: "4px",
|
||||
speed: "1.5s",
|
||||
my: 8,
|
||||
const baseStyle = {
|
||||
color: "primary.400",
|
||||
thickness: "4px",
|
||||
speed: "1.5s",
|
||||
my: 8,
|
||||
};
|
||||
const variants = {
|
||||
basic: { thickness: "4px", speed: "1.5s" },
|
||||
};
|
||||
|
||||
const sizes = {
|
||||
xs: {
|
||||
"--spinner-size": "0.75rem",
|
||||
},
|
||||
variants: {
|
||||
basic: { thickness: "4px", speed: "1.5s" },
|
||||
sm: {
|
||||
"--spinner-size": "1rem",
|
||||
},
|
||||
md: {
|
||||
"--spinner-size": "1.5rem",
|
||||
},
|
||||
lg: {
|
||||
"--spinner-size": "2rem",
|
||||
},
|
||||
xl: {
|
||||
"--spinner-size": "3rem",
|
||||
},
|
||||
};
|
||||
|
||||
export default Spinner;
|
||||
const defaultProps = {
|
||||
size: "md",
|
||||
};
|
||||
|
||||
export default {
|
||||
baseStyle,
|
||||
sizes,
|
||||
defaultProps,
|
||||
variants,
|
||||
};
|
||||
|
|
|
@ -0,0 +1,46 @@
|
|||
import { mode } from "@chakra-ui/theme-tools";
|
||||
|
||||
const baseStyle = (props) => {
|
||||
const bg = mode("gray.700", "gray.300")(props);
|
||||
return {
|
||||
"--tooltip-bg": `colors.${bg}`,
|
||||
px: "8px",
|
||||
py: "2px",
|
||||
bg: "var(--tooltip-bg)",
|
||||
"--popper-arrow-bg": "var(--tooltip-bg)",
|
||||
color: mode("whiteAlpha.900", "gray.900")(props),
|
||||
borderRadius: "sm",
|
||||
fontWeight: "medium",
|
||||
fontSize: "sm",
|
||||
boxShadow: "md",
|
||||
maxW: "320px",
|
||||
zIndex: "tooltip",
|
||||
};
|
||||
};
|
||||
|
||||
const variantOnboarding = (props) => {
|
||||
const bg = mode("secondary.700", "secondary.300")(props);
|
||||
return {
|
||||
"--tooltip-bg": `colors.${bg}`,
|
||||
px: "8px",
|
||||
py: "2px",
|
||||
bg: "var(--tooltip-bg)",
|
||||
"--popper-arrow-bg": "var(--tooltip-bg)",
|
||||
color: mode("whiteAlpha.900", "gray.900")(props),
|
||||
borderRadius: "md",
|
||||
fontWeight: "medium",
|
||||
fontSize: "sm",
|
||||
boxShadow: "md",
|
||||
maxW: "320px",
|
||||
zIndex: "tooltip",
|
||||
};
|
||||
};
|
||||
|
||||
const variants = {
|
||||
onboarding: variantOnboarding,
|
||||
};
|
||||
|
||||
export default {
|
||||
baseStyle,
|
||||
variants,
|
||||
};
|
|
@ -8,6 +8,8 @@ import NumberInput from "./NumberInput";
|
|||
import Badge from "./Badge";
|
||||
import Checkbox from "./Checkbox";
|
||||
import Table from "./Table";
|
||||
import Tooltip from "./Tooltip";
|
||||
import Spinner from "./Spinner";
|
||||
import { createBreakpoints } from "@chakra-ui/theme-tools";
|
||||
|
||||
const breakpointsCustom = createBreakpoints({
|
||||
|
@ -53,6 +55,8 @@ const theme = extendTheme({
|
|||
Badge,
|
||||
Checkbox,
|
||||
Table,
|
||||
Spinner,
|
||||
Tooltip,
|
||||
},
|
||||
|
||||
fonts: {
|
||||
|
|
|
@ -15,6 +15,7 @@ import {
|
|||
PopoverCloseButton,
|
||||
useBreakpointValue,
|
||||
Spacer,
|
||||
ButtonGroup,
|
||||
} from "@chakra-ui/react";
|
||||
import {
|
||||
HamburgerIcon,
|
||||
|
@ -26,6 +27,7 @@ import { MdTimeline } from "react-icons/md";
|
|||
import useRouter from "../core/hooks/useRouter";
|
||||
import UIContext from "../core/providers/UIProvider/context";
|
||||
import AccountIconButton from "./AccountIconButton";
|
||||
import RouteButton from "./RouteButton";
|
||||
|
||||
const AppNavbar = () => {
|
||||
const ui = useContext(UIContext);
|
||||
|
@ -95,6 +97,14 @@ const AppNavbar = () => {
|
|||
<Flex width="100%" px={2}>
|
||||
<Spacer />
|
||||
<Flex placeSelf="flex-end">
|
||||
<ButtonGroup spacing={4}>
|
||||
{/* <RouteButton variant="link" href="/docs">
|
||||
Docs
|
||||
</RouteButton> */}
|
||||
<RouteButton variant="link" href="/welcome">
|
||||
Learn how to
|
||||
</RouteButton>
|
||||
</ButtonGroup>
|
||||
<SupportPopover />
|
||||
<AccountIconButton
|
||||
colorScheme="primary"
|
||||
|
|
|
@ -204,7 +204,6 @@ const EntriesNavigation = () => {
|
|||
};
|
||||
|
||||
const dropFilterArrayItem = (idx) => {
|
||||
console.log("dropFilterArrayItem", idx, filterState);
|
||||
const oldArray = [...filterState];
|
||||
const newArray = oldArray.filter(function (ele) {
|
||||
return ele != oldArray[idx];
|
||||
|
@ -242,7 +241,6 @@ const EntriesNavigation = () => {
|
|||
};
|
||||
|
||||
const handleFilterStateCallback = (props) => {
|
||||
console.log("handleFilterStateCallback", props);
|
||||
const currentFilterState = [...filterState];
|
||||
currentFilterState.push({ ...props });
|
||||
|
||||
|
@ -529,6 +527,7 @@ const EntriesNavigation = () => {
|
|||
?.sort((a, b) => b.timestamp - a.timestamp) // TODO(Andrey) improve that for bi chunks of data sorting can take time
|
||||
.map((entry, idx) => (
|
||||
<StreamEntry
|
||||
showOnboardingTooltips={false}
|
||||
key={`entry-list-${idx}`}
|
||||
entry={entry}
|
||||
disableDelete={!canDelete}
|
||||
|
|
|
@ -13,6 +13,7 @@ import { HamburgerIcon } from "@chakra-ui/icons";
|
|||
import useModals from "../core/hooks/useModals";
|
||||
import UIContext from "../core/providers/UIProvider/context";
|
||||
import ChakraAccountIconButton from "./AccountIconButton";
|
||||
import RouteButton from "./RouteButton";
|
||||
|
||||
const LandingNavbar = () => {
|
||||
const ui = useContext(UIContext);
|
||||
|
@ -52,6 +53,17 @@ const LandingNavbar = () => {
|
|||
spacing={4}
|
||||
pr={16}
|
||||
>
|
||||
{ui.isLoggedIn && (
|
||||
<ButtonGroup spacing={4}>
|
||||
{/* <RouteButton variant="link" href="/docs">
|
||||
Docs
|
||||
</RouteButton> */}
|
||||
<RouteButton variant="link" href="/welcome">
|
||||
Learn how to
|
||||
</RouteButton>
|
||||
</ButtonGroup>
|
||||
)}
|
||||
|
||||
{ui.isLoggedIn && (
|
||||
<RouterLink href="/stream" passHref>
|
||||
<Button
|
||||
|
|
|
@ -0,0 +1,177 @@
|
|||
import React, { useState, useEffect } from "react";
|
||||
import { useSubscriptions } from "../core/hooks";
|
||||
import {
|
||||
Input,
|
||||
Stack,
|
||||
Text,
|
||||
HStack,
|
||||
useRadioGroup,
|
||||
FormControl,
|
||||
FormErrorMessage,
|
||||
ModalBody,
|
||||
ModalCloseButton,
|
||||
ModalHeader,
|
||||
Button,
|
||||
ModalFooter,
|
||||
Spinner,
|
||||
IconButton,
|
||||
} from "@chakra-ui/react";
|
||||
import RadioCard from "./RadioCard";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { GithubPicker } from "react-color";
|
||||
import { BiRefresh } from "react-icons/bi";
|
||||
import { makeColor } from "../core/utils/makeColor";
|
||||
const NewSubscription = ({ isFreeOption, onClose }) => {
|
||||
const [color, setColor] = useState(makeColor());
|
||||
const { typesCache, createSubscription } = useSubscriptions();
|
||||
const { handleSubmit, errors, register } = useForm({});
|
||||
const [radioState, setRadioState] = useState("ethereum_blockchain");
|
||||
let { getRootProps, getRadioProps } = useRadioGroup({
|
||||
name: "type",
|
||||
defaultValue: radioState,
|
||||
onChange: setRadioState,
|
||||
});
|
||||
|
||||
const group = getRootProps();
|
||||
|
||||
useEffect(() => {
|
||||
if (createSubscription.isSuccess) {
|
||||
onClose();
|
||||
}
|
||||
}, [createSubscription.isSuccess, onClose]);
|
||||
|
||||
if (typesCache.isLoading) return <Spinner />;
|
||||
|
||||
const createSubscriptionWrap = (props) => {
|
||||
createSubscription.mutate({
|
||||
...props,
|
||||
color: color,
|
||||
type: isFreeOption ? "free" : radioState,
|
||||
});
|
||||
};
|
||||
|
||||
const handleChangeColorComplete = (color) => {
|
||||
setColor(color.hex);
|
||||
};
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit(createSubscriptionWrap)}>
|
||||
<ModalHeader>Subscribe to a new address</ModalHeader>
|
||||
<ModalCloseButton />
|
||||
<ModalBody>
|
||||
<FormControl isInvalid={errors.label}>
|
||||
<Input
|
||||
my={2}
|
||||
type="text"
|
||||
autoComplete="off"
|
||||
placeholder="Enter label"
|
||||
name="label"
|
||||
ref={register({ required: "label is required!" })}
|
||||
></Input>
|
||||
<FormErrorMessage color="unsafe.400" pl="1">
|
||||
{errors.label && errors.label.message}
|
||||
</FormErrorMessage>
|
||||
</FormControl>
|
||||
<FormControl isInvalid={errors.address}>
|
||||
<Input
|
||||
type="text"
|
||||
autoComplete="off"
|
||||
my={2}
|
||||
placeholder="Enter address"
|
||||
name="address"
|
||||
ref={register({ required: "address is required!" })}
|
||||
></Input>
|
||||
<FormErrorMessage color="unsafe.400" pl="1">
|
||||
{errors.address && errors.address.message}
|
||||
</FormErrorMessage>
|
||||
</FormControl>
|
||||
<Stack my={16} direction="column">
|
||||
<Text fontWeight="600">
|
||||
{isFreeOption
|
||||
? `Free subscription is only availible Ethereum blockchain source`
|
||||
: `On which source?`}
|
||||
</Text>
|
||||
|
||||
<FormControl isInvalid={errors.subscription_type}>
|
||||
<HStack {...group} alignItems="stretch">
|
||||
{typesCache.data.subscriptions.map((type) => {
|
||||
const radio = getRadioProps({
|
||||
value: type.id,
|
||||
isDisabled:
|
||||
!type.active ||
|
||||
(isFreeOption &&
|
||||
type.subscription_type !== "ethereum_blockchain"),
|
||||
});
|
||||
return (
|
||||
<RadioCard key={`subscription_type_${type.id}`} {...radio}>
|
||||
{type.name}
|
||||
</RadioCard>
|
||||
);
|
||||
})}
|
||||
</HStack>
|
||||
<Input
|
||||
type="hidden"
|
||||
placeholder="subscription_type"
|
||||
name="subscription_type"
|
||||
ref={register({ required: "select type" })}
|
||||
value={radioState}
|
||||
onChange={() => null}
|
||||
></Input>
|
||||
<FormErrorMessage color="unsafe.400" pl="1">
|
||||
{errors.subscription_type && errors.subscription_type.message}
|
||||
</FormErrorMessage>
|
||||
</FormControl>
|
||||
</Stack>
|
||||
<FormControl isInvalid={errors.color}>
|
||||
<Stack direction="row" pb={2}>
|
||||
<Text fontWeight="600" alignSelf="center">
|
||||
Label color
|
||||
</Text>{" "}
|
||||
<IconButton
|
||||
size="md"
|
||||
// colorScheme="primary"
|
||||
color={"white.100"}
|
||||
_hover={{ bgColor: { color } }}
|
||||
bgColor={color}
|
||||
variant="outline"
|
||||
onClick={() => setColor(makeColor())}
|
||||
icon={<BiRefresh />}
|
||||
/>
|
||||
<Input
|
||||
type="input"
|
||||
placeholder="color"
|
||||
name="color"
|
||||
ref={register({ required: "color is required!" })}
|
||||
value={color}
|
||||
onChange={() => null}
|
||||
w="200px"
|
||||
></Input>
|
||||
</Stack>
|
||||
|
||||
<GithubPicker
|
||||
// color={this.state.background}
|
||||
onChangeComplete={handleChangeColorComplete}
|
||||
/>
|
||||
|
||||
<FormErrorMessage color="unsafe.400" pl="1">
|
||||
{errors.color && errors.color.message}
|
||||
</FormErrorMessage>
|
||||
</FormControl>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button
|
||||
type="submit"
|
||||
colorScheme="suggested"
|
||||
isLoading={createSubscription.isLoading}
|
||||
>
|
||||
Confirm
|
||||
</Button>
|
||||
<Button colorScheme="gray" onClick={onClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
export default NewSubscription;
|
|
@ -1,4 +1,4 @@
|
|||
import React, { useState, useEffect } from "react";
|
||||
import React, { useState, useEffect, useCallback } from "react";
|
||||
import { useSubscriptions } from "../core/hooks";
|
||||
import {
|
||||
Input,
|
||||
|
@ -8,23 +8,24 @@ import {
|
|||
useRadioGroup,
|
||||
FormControl,
|
||||
FormErrorMessage,
|
||||
ModalBody,
|
||||
ModalCloseButton,
|
||||
ModalHeader,
|
||||
Button,
|
||||
ModalFooter,
|
||||
Spinner,
|
||||
IconButton,
|
||||
ButtonGroup,
|
||||
Spacer,
|
||||
Flex,
|
||||
} from "@chakra-ui/react";
|
||||
import RadioCard from "./RadioCard";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { GithubPicker } from "react-color";
|
||||
// import { useForm } from "react-hook-form";
|
||||
import { CirclePicker } from "react-color";
|
||||
import { BiRefresh } from "react-icons/bi";
|
||||
import { makeColor } from "../core/utils/makeColor";
|
||||
const NewSubscription = ({ isFreeOption, onClose }) => {
|
||||
import { useForm } from "react-hook-form";
|
||||
const _NewSubscription = ({ isFreeOption, onClose, setIsLoading }) => {
|
||||
const [color, setColor] = useState(makeColor());
|
||||
const { typesCache, createSubscription } = useSubscriptions();
|
||||
const { handleSubmit, errors, register } = useForm({});
|
||||
const { typesCache, createSubscription } = useSubscriptions();
|
||||
// const { handleSubmit, errors, register } = useForm({});
|
||||
const [radioState, setRadioState] = useState("ethereum_blockchain");
|
||||
let { getRootProps, getRadioProps } = useRadioGroup({
|
||||
name: "type",
|
||||
|
@ -34,96 +35,105 @@ const NewSubscription = ({ isFreeOption, onClose }) => {
|
|||
|
||||
const group = getRootProps();
|
||||
|
||||
useEffect(() => {
|
||||
if (setIsLoading) {
|
||||
setIsLoading(createSubscription.isLoading);
|
||||
}
|
||||
}, [createSubscription.isLoading, setIsLoading]);
|
||||
|
||||
useEffect(() => {
|
||||
if (createSubscription.isSuccess) {
|
||||
onClose();
|
||||
}
|
||||
}, [createSubscription.isSuccess, onClose]);
|
||||
|
||||
if (typesCache.isLoading) return <Spinner />;
|
||||
const createSubscriptionWrapper = useCallback(
|
||||
(props) => {
|
||||
createSubscription.mutate({
|
||||
...props,
|
||||
color: color,
|
||||
type: isFreeOption ? 0 : radioState,
|
||||
});
|
||||
},
|
||||
[createSubscription, isFreeOption, color, radioState]
|
||||
);
|
||||
|
||||
const createSubscriptionWrap = (props) => {
|
||||
createSubscription.mutate({
|
||||
...props,
|
||||
color: color,
|
||||
type: isFreeOption ? "free" : radioState,
|
||||
});
|
||||
};
|
||||
if (typesCache.isLoading) return <Spinner />;
|
||||
|
||||
const handleChangeColorComplete = (color) => {
|
||||
setColor(color.hex);
|
||||
};
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit(createSubscriptionWrap)}>
|
||||
<ModalHeader>Subscribe to a new address</ModalHeader>
|
||||
<ModalCloseButton />
|
||||
<ModalBody>
|
||||
<FormControl isInvalid={errors.label}>
|
||||
<Input
|
||||
my={2}
|
||||
type="text"
|
||||
autoComplete="off"
|
||||
placeholder="Enter label"
|
||||
name="label"
|
||||
ref={register({ required: "label is required!" })}
|
||||
></Input>
|
||||
<FormErrorMessage color="unsafe.400" pl="1">
|
||||
{errors.label && errors.label.message}
|
||||
</FormErrorMessage>
|
||||
</FormControl>
|
||||
<FormControl isInvalid={errors.address}>
|
||||
<Input
|
||||
type="text"
|
||||
autoComplete="off"
|
||||
my={2}
|
||||
placeholder="Enter address"
|
||||
name="address"
|
||||
ref={register({ required: "address is required!" })}
|
||||
></Input>
|
||||
<FormErrorMessage color="unsafe.400" pl="1">
|
||||
{errors.address && errors.address.message}
|
||||
</FormErrorMessage>
|
||||
</FormControl>
|
||||
<Stack my={16} direction="column">
|
||||
<Text fontWeight="600">
|
||||
{isFreeOption
|
||||
? `Free subscription is only availible Ethereum blockchain source`
|
||||
: `On which source?`}
|
||||
</Text>
|
||||
if (!errors) return "";
|
||||
|
||||
<FormControl isInvalid={errors.subscription_type}>
|
||||
<HStack {...group} alignItems="stretch">
|
||||
{typesCache.data.subscriptions.map((type) => {
|
||||
const radio = getRadioProps({
|
||||
value: type.id,
|
||||
isDisabled:
|
||||
!type.active ||
|
||||
(isFreeOption &&
|
||||
type.subscription_type !== "ethereum_blockchain"),
|
||||
});
|
||||
return (
|
||||
<RadioCard key={`subscription_type_${type.id}`} {...radio}>
|
||||
{type.name}
|
||||
</RadioCard>
|
||||
);
|
||||
})}
|
||||
</HStack>
|
||||
<Input
|
||||
type="hidden"
|
||||
placeholder="subscription_type"
|
||||
name="subscription_type"
|
||||
ref={register({ required: "select type" })}
|
||||
value={radioState}
|
||||
onChange={() => null}
|
||||
></Input>
|
||||
<FormErrorMessage color="unsafe.400" pl="1">
|
||||
{errors.subscription_type && errors.subscription_type.message}
|
||||
</FormErrorMessage>
|
||||
</FormControl>
|
||||
</Stack>
|
||||
<FormControl isInvalid={errors.color}>
|
||||
<Stack direction="row" pb={2}>
|
||||
return (
|
||||
<form onSubmit={handleSubmit(createSubscriptionWrapper)}>
|
||||
<FormControl isInvalid={errors?.label}>
|
||||
<Input
|
||||
my={2}
|
||||
type="text"
|
||||
autoComplete="off"
|
||||
placeholder="Meaningful name of your subscription"
|
||||
name="label"
|
||||
ref={register({ required: "label is required!" })}
|
||||
></Input>
|
||||
<FormErrorMessage color="unsafe.400" pl="1">
|
||||
{errors?.label && errors?.label.message}
|
||||
</FormErrorMessage>
|
||||
</FormControl>
|
||||
<FormControl isInvalid={errors?.address}>
|
||||
<Input
|
||||
type="text"
|
||||
autoComplete="off"
|
||||
my={2}
|
||||
placeholder="Address to subscribe to"
|
||||
name="address"
|
||||
ref={register({ required: "address is required!" })}
|
||||
></Input>
|
||||
<FormErrorMessage color="unsafe.400" pl="1">
|
||||
{errors?.address && errors?.address.message}
|
||||
</FormErrorMessage>
|
||||
</FormControl>
|
||||
<Stack my={4} direction="column">
|
||||
{/* <Text fontWeight="600">
|
||||
{isFreeOption
|
||||
? `Right now you can subscribe only to ethereum blockchain`
|
||||
: `On which source?`}
|
||||
</Text> */}
|
||||
|
||||
<FormControl isInvalid={errors?.subscription_type}>
|
||||
<HStack {...group} alignItems="stretch">
|
||||
{typesCache.data.subscriptions.map((type) => {
|
||||
const radio = getRadioProps({
|
||||
value: type.id,
|
||||
isDisabled:
|
||||
!type.active ||
|
||||
(isFreeOption &&
|
||||
type.subscription_type !== "ethereum_blockchain"),
|
||||
});
|
||||
return (
|
||||
<RadioCard key={`subscription_type_${type.id}`} {...radio}>
|
||||
{type.name}
|
||||
</RadioCard>
|
||||
);
|
||||
})}
|
||||
</HStack>
|
||||
<Input
|
||||
type="hidden"
|
||||
placeholder="subscription_type"
|
||||
name="subscription_type"
|
||||
ref={register({ required: "select type" })}
|
||||
value={radioState}
|
||||
onChange={() => null}
|
||||
></Input>
|
||||
<FormErrorMessage color="unsafe.400" pl="1">
|
||||
{errors?.subscription_type && errors?.subscription_type.message}
|
||||
</FormErrorMessage>
|
||||
</FormControl>
|
||||
</Stack>
|
||||
<FormControl isInvalid={errors?.color}>
|
||||
<Flex direction="row" pb={2} flexWrap="wrap">
|
||||
<Stack pt={2} direction="row" h="min-content">
|
||||
<Text fontWeight="600" alignSelf="center">
|
||||
Label color
|
||||
</Text>{" "}
|
||||
|
@ -147,18 +157,21 @@ const NewSubscription = ({ isFreeOption, onClose }) => {
|
|||
w="200px"
|
||||
></Input>
|
||||
</Stack>
|
||||
<Flex p={2}>
|
||||
<CirclePicker
|
||||
onChangeComplete={handleChangeColorComplete}
|
||||
circleSpacing={1}
|
||||
circleSize={24}
|
||||
/>
|
||||
</Flex>
|
||||
</Flex>
|
||||
|
||||
<GithubPicker
|
||||
// color={this.state.background}
|
||||
onChangeComplete={handleChangeColorComplete}
|
||||
/>
|
||||
<FormErrorMessage color="unsafe.400" pl="1">
|
||||
{errors?.color && errors?.color.message}
|
||||
</FormErrorMessage>
|
||||
</FormControl>
|
||||
|
||||
<FormErrorMessage color="unsafe.400" pl="1">
|
||||
{errors.color && errors.color.message}
|
||||
</FormErrorMessage>
|
||||
</FormControl>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<ButtonGroup direction="row" justifyContent="space-evenly">
|
||||
<Button
|
||||
type="submit"
|
||||
colorScheme="suggested"
|
||||
|
@ -166,12 +179,13 @@ const NewSubscription = ({ isFreeOption, onClose }) => {
|
|||
>
|
||||
Confirm
|
||||
</Button>
|
||||
<Spacer />
|
||||
<Button colorScheme="gray" onClick={onClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</ButtonGroup>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
export default NewSubscription;
|
||||
export default _NewSubscription;
|
||||
|
|
|
@ -0,0 +1,17 @@
|
|||
import React from "react";
|
||||
import { chakra, Button, Link } from "@chakra-ui/react";
|
||||
import NextLink from "next/link";
|
||||
|
||||
const _RouteButton = (props) => {
|
||||
return (
|
||||
<NextLink href={props.href} passHref>
|
||||
<Button as={Link} {...props}>
|
||||
{props.children}
|
||||
</Button>
|
||||
</NextLink>
|
||||
);
|
||||
};
|
||||
|
||||
const RouteButton = chakra(_RouteButton, "button");
|
||||
|
||||
export default RouteButton;
|
|
@ -14,6 +14,7 @@ import React from "react";
|
|||
import { HamburgerIcon, ArrowLeftIcon, ArrowRightIcon } from "@chakra-ui/icons";
|
||||
import { MdTimeline, MdSettings } from "react-icons/md";
|
||||
import { ImStatsBars } from "react-icons/im";
|
||||
import { HiAcademicCap } from "react-icons/hi";
|
||||
|
||||
const Sidebar = () => {
|
||||
const ui = useContext(UIContext);
|
||||
|
@ -79,6 +80,13 @@ const Sidebar = () => {
|
|||
<RouterLink href="/subscriptions">Subscriptions </RouterLink>
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
{ui.isMobileView && (
|
||||
<Menu iconShape="square">
|
||||
<MenuItem icon={<HiAcademicCap />}>
|
||||
<RouterLink href="/welcome">Learn how to</RouterLink>
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
)}
|
||||
</SidebarContent>
|
||||
)}
|
||||
{!ui.isLoggedIn && (
|
||||
|
|
|
@ -36,17 +36,16 @@ const SteamEntryDetails = () => {
|
|||
>
|
||||
<HStack id="EntryHeader" width="100%" m={0}>
|
||||
<Heading
|
||||
overflow="hidden"
|
||||
width={entry?.context_url ? "calc(100% - 28px)" : "100%"}
|
||||
minH="36px"
|
||||
style={{ marginLeft: "0" }}
|
||||
m={0}
|
||||
p={0}
|
||||
fontWeight="600"
|
||||
fontSize="1.5rem"
|
||||
fontSize="md"
|
||||
textAlign="left"
|
||||
>
|
||||
{entry && entry.tx.hash}
|
||||
{entry && `Hash: ${entry.tx.hash}`}
|
||||
</Heading>
|
||||
</HStack>
|
||||
</Skeleton>
|
||||
|
|
|
@ -0,0 +1,68 @@
|
|||
import React, { useContext } from "react";
|
||||
import { Box, Button, Progress, ButtonGroup } from "@chakra-ui/react";
|
||||
import _ from "lodash";
|
||||
import UIContext from "../core/providers/UIProvider/context";
|
||||
const StepProgress = ({
|
||||
numSteps,
|
||||
currentStep,
|
||||
colorScheme,
|
||||
buttonCallback,
|
||||
buttonTitles,
|
||||
}) => {
|
||||
const ui = useContext(UIContext);
|
||||
return (
|
||||
<Box w="100%" h="auto" pos="relative">
|
||||
<ButtonGroup
|
||||
display="inline-flex"
|
||||
flexDirection="row"
|
||||
justifyContent="space-between"
|
||||
w="100%"
|
||||
m={0}
|
||||
p={0}
|
||||
spacing={0}
|
||||
>
|
||||
{_.times(numSteps, (i) => {
|
||||
const setActive = i === parseInt(currentStep) ? true : false;
|
||||
return (
|
||||
<Button
|
||||
key={`${i}-progress-steps`}
|
||||
size={ui.isMobileView ? "md" : "sm"}
|
||||
borderRadius={ui.isMobileView ? "full" : "md"}
|
||||
// size="sm"
|
||||
// bgColor={`${colorScheme}.200`}
|
||||
_active={{ bgColor: `${colorScheme}.1200` }}
|
||||
zIndex={1}
|
||||
m={0}
|
||||
colorScheme={colorScheme}
|
||||
isActive={setActive}
|
||||
onClick={() => buttonCallback(i)}
|
||||
>
|
||||
{ui.isMobileView && i + 1}
|
||||
{!ui.isMobileView && buttonTitles[i]}
|
||||
</Button>
|
||||
);
|
||||
})}
|
||||
</ButtonGroup>
|
||||
<Progress
|
||||
position="absolute"
|
||||
top="50%"
|
||||
transform="translateY(-50%)"
|
||||
h={2}
|
||||
w="full"
|
||||
// hasStripe
|
||||
// isAnimated
|
||||
max={numSteps - 1}
|
||||
min={0}
|
||||
value={currentStep}
|
||||
/>
|
||||
{/* <Flex
|
||||
h="1rem"
|
||||
flexGrow={1}
|
||||
flexBasis="10px"
|
||||
backgroundColor={`${colorScheme}.300`}
|
||||
/> */}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default StepProgress;
|
|
@ -11,6 +11,7 @@ import {
|
|||
useMediaQuery,
|
||||
Spacer,
|
||||
Spinner,
|
||||
chakra,
|
||||
} from "@chakra-ui/react";
|
||||
import moment from "moment";
|
||||
import { ArrowRightIcon } from "@chakra-ui/icons";
|
||||
|
@ -18,7 +19,7 @@ import UIContext from "../core/providers/UIProvider/context";
|
|||
import { useToast } from "../core/hooks";
|
||||
import { useSubscriptions } from "../core/hooks";
|
||||
|
||||
const StreamEntry = ({ entry }) => {
|
||||
const StreamEntry = ({ entry, showOnboardingTooltips, className }) => {
|
||||
const { subscriptionsCache } = useSubscriptions();
|
||||
const ui = useContext(UIContext);
|
||||
const [copyString, setCopyString] = useState(false);
|
||||
|
@ -49,6 +50,7 @@ const StreamEntry = ({ entry }) => {
|
|||
|
||||
return (
|
||||
<Flex
|
||||
className={className}
|
||||
p={0}
|
||||
m={1}
|
||||
mr={2}
|
||||
|
@ -89,35 +91,44 @@ const StreamEntry = ({ entry }) => {
|
|||
overflowX="hidden"
|
||||
overflowY="visible"
|
||||
>
|
||||
<Stack
|
||||
className="title"
|
||||
direction="row"
|
||||
w="100%"
|
||||
h="1.6rem"
|
||||
minH="1.6rem"
|
||||
textAlign="center"
|
||||
spacing={0}
|
||||
alignItems="center"
|
||||
bgColor="gray.300"
|
||||
<Tooltip
|
||||
hasArrow
|
||||
isOpen={showOnboardingTooltips}
|
||||
// shouldWrapChildren
|
||||
label="Top of card describes type of event. Ethereum blockchain in this case. It as unique hash shown here"
|
||||
variant="onboarding"
|
||||
placement="top"
|
||||
>
|
||||
<Image
|
||||
boxSize="16px"
|
||||
src={
|
||||
"https://upload.wikimedia.org/wikipedia/commons/0/05/Ethereum_logo_2014.svg"
|
||||
}
|
||||
/>
|
||||
<Heading px={1} size="xs">
|
||||
Hash
|
||||
</Heading>
|
||||
<Spacer />
|
||||
<Text
|
||||
isTruncated
|
||||
onClick={() => setCopyString(entry.hash)}
|
||||
pr={12}
|
||||
<Stack
|
||||
className="title"
|
||||
direction="row"
|
||||
w="100%"
|
||||
h="1.6rem"
|
||||
minH="1.6rem"
|
||||
textAlign="center"
|
||||
spacing={0}
|
||||
alignItems="center"
|
||||
bgColor="gray.300"
|
||||
>
|
||||
{entry.hash}
|
||||
</Text>
|
||||
</Stack>
|
||||
<Image
|
||||
boxSize="16px"
|
||||
src={
|
||||
"https://upload.wikimedia.org/wikipedia/commons/0/05/Ethereum_logo_2014.svg"
|
||||
}
|
||||
/>
|
||||
<Heading px={1} size="xs">
|
||||
Hash
|
||||
</Heading>
|
||||
<Spacer />
|
||||
<Text
|
||||
isTruncated
|
||||
onClick={() => setCopyString(entry.hash)}
|
||||
pr={12}
|
||||
>
|
||||
{entry.hash}
|
||||
</Text>
|
||||
</Stack>
|
||||
</Tooltip>
|
||||
<Stack
|
||||
className="CardAddressesRow"
|
||||
direction={showFullView ? "row" : "column"}
|
||||
|
@ -143,16 +154,25 @@ const StreamEntry = ({ entry }) => {
|
|||
placeContent="center"
|
||||
spacing={0}
|
||||
>
|
||||
<Text
|
||||
bgColor="gray.600"
|
||||
h="100%"
|
||||
fontSize="sm"
|
||||
py="2px"
|
||||
px={2}
|
||||
w={showFullView ? null : "120px"}
|
||||
<Tooltip
|
||||
hasArrow
|
||||
isOpen={showOnboardingTooltips && !ui.isMobileView}
|
||||
label="From and to addresses, clicking names will copy address to clipboard!"
|
||||
variant="onboarding"
|
||||
placement={ui.isMobileView ? "bottom" : "left"}
|
||||
maxW="150px"
|
||||
>
|
||||
From:
|
||||
</Text>
|
||||
<Text
|
||||
bgColor="gray.600"
|
||||
h="100%"
|
||||
fontSize="sm"
|
||||
py="2px"
|
||||
px={2}
|
||||
w={showFullView ? null : "120px"}
|
||||
>
|
||||
From:
|
||||
</Text>
|
||||
</Tooltip>
|
||||
<Tooltip label={entry.from_address} aria-label="From:">
|
||||
<Text
|
||||
mx={0}
|
||||
|
@ -168,6 +188,7 @@ const StreamEntry = ({ entry }) => {
|
|||
</Text>
|
||||
</Tooltip>
|
||||
</Stack>
|
||||
|
||||
<Stack
|
||||
overflow="hidden"
|
||||
textOverflow="ellipsis"
|
||||
|
@ -322,22 +343,33 @@ const StreamEntry = ({ entry }) => {
|
|||
</Stack>
|
||||
)}
|
||||
<Flex>
|
||||
<IconButton
|
||||
m={0}
|
||||
onClick={() => ui.setCurrentTransaction(entry)}
|
||||
h="full"
|
||||
// minH="24px"
|
||||
borderLeftRadius={0}
|
||||
variant="solid"
|
||||
px={0}
|
||||
minW="24px"
|
||||
colorScheme="secondary"
|
||||
icon={<ArrowRightIcon w="24px" />}
|
||||
/>
|
||||
<Tooltip
|
||||
hasArrow
|
||||
isOpen={showOnboardingTooltips}
|
||||
placement={ui.isMobileView ? "bottom" : "right"}
|
||||
label="Clicking side arrow will bring up detailed view"
|
||||
variant="onboarding"
|
||||
maxW="150px"
|
||||
>
|
||||
<IconButton
|
||||
m={0}
|
||||
onClick={() => ui.setCurrentTransaction(entry)}
|
||||
h="full"
|
||||
// minH="24px"
|
||||
borderLeftRadius={0}
|
||||
variant="solid"
|
||||
px={0}
|
||||
minW="24px"
|
||||
colorScheme="secondary"
|
||||
icon={<ArrowRightIcon w="24px" />}
|
||||
/>
|
||||
</Tooltip>
|
||||
</Flex>
|
||||
</Stack>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
export default StreamEntry;
|
||||
const StreamChakraEntry = chakra(StreamEntry);
|
||||
|
||||
export default StreamChakraEntry;
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import React from "react";
|
||||
import { Skeleton, IconButton } from "@chakra-ui/react";
|
||||
import { Skeleton, IconButton, Container } from "@chakra-ui/react";
|
||||
import {
|
||||
Table,
|
||||
Th,
|
||||
|
@ -12,6 +12,7 @@ import {
|
|||
EditableInput,
|
||||
Image,
|
||||
EditablePreview,
|
||||
Button,
|
||||
} from "@chakra-ui/react";
|
||||
import { DeleteIcon } from "@chakra-ui/icons";
|
||||
import moment from "moment";
|
||||
|
@ -20,7 +21,7 @@ import { useSubscriptions } from "../core/hooks";
|
|||
import ConfirmationRequest from "./ConfirmationRequest";
|
||||
import ColorSelector from "./ColorSelector";
|
||||
|
||||
const SubscriptionsList = () => {
|
||||
const SubscriptionsList = ({ emptyCTA }) => {
|
||||
const { subscriptionsCache, updateSubscription, deleteSubscription } =
|
||||
useSubscriptions();
|
||||
|
||||
|
@ -31,7 +32,10 @@ const SubscriptionsList = () => {
|
|||
updateSubscription.mutate(data);
|
||||
};
|
||||
|
||||
if (subscriptionsCache.data) {
|
||||
if (
|
||||
subscriptionsCache.data &&
|
||||
subscriptionsCache.data.subscriptions.length > 0
|
||||
) {
|
||||
return (
|
||||
<Table
|
||||
borderColor="gray.200"
|
||||
|
@ -138,6 +142,16 @@ const SubscriptionsList = () => {
|
|||
</Tbody>
|
||||
</Table>
|
||||
);
|
||||
} else if (
|
||||
subscriptionsCache.data &&
|
||||
subscriptionsCache.data.subscriptions.length === 0
|
||||
) {
|
||||
return (
|
||||
<Container>
|
||||
{` You don't have any subscriptions at the moment.`}
|
||||
{emptyCTA && <Button variant="suggested">Create one</Button>}
|
||||
</Container>
|
||||
);
|
||||
} else if (subscriptionsCache.isLoading) {
|
||||
return <Skeleton />;
|
||||
} else {
|
||||
|
|
|
@ -16,7 +16,6 @@ const toEth = (wei) => {
|
|||
const TxABI = (props) => {
|
||||
const byteCode = props.byteCode;
|
||||
const abi = props.abi;
|
||||
console.log(abi.functions);
|
||||
return (
|
||||
<VStack spacing={3}>
|
||||
<br />
|
||||
|
@ -58,25 +57,29 @@ const TxInfo = (props) => {
|
|||
return (
|
||||
<Box boxShadow="xs" p="6" rounded="md">
|
||||
<StatGroup>
|
||||
<Stat>
|
||||
<Stat px={2}>
|
||||
<StatLabel>Value</StatLabel>
|
||||
<StatNumber>{toEth(transaction.tx.value)} eth</StatNumber>
|
||||
<StatNumber fontSize="md">
|
||||
{toEth(transaction.tx.value)} eth
|
||||
</StatNumber>
|
||||
<StatHelpText>amount of ETH to transfer</StatHelpText>
|
||||
</Stat>
|
||||
<Stat>
|
||||
<StatLabel>Gas limit</StatLabel>
|
||||
<StatNumber>{transaction.tx.gas}</StatNumber>
|
||||
<StatNumber fontSize="md">{transaction.tx.gas}</StatNumber>
|
||||
<StatHelpText>Maximum amount of gas </StatHelpText>
|
||||
<StatHelpText>provided for the transaction</StatHelpText>
|
||||
</Stat>
|
||||
<Stat>
|
||||
<StatLabel>Gas price</StatLabel>
|
||||
<StatNumber>{toEth(transaction.tx.gasPrice)} eth</StatNumber>
|
||||
<StatNumber fontSize="md">
|
||||
{toEth(transaction.tx.gasPrice)} eth
|
||||
</StatNumber>
|
||||
<StatHelpText>the fee the sender pays per unit of gas</StatHelpText>
|
||||
</Stat>
|
||||
</StatGroup>
|
||||
|
||||
<Table variant="simple">
|
||||
<Table variant="simple" size="sm">
|
||||
<Tbody>
|
||||
{Object.keys(transaction.tx)
|
||||
.filter(dont_display)
|
||||
|
@ -85,7 +88,7 @@ const TxInfo = (props) => {
|
|||
transaction.tx[key] != undefined && (
|
||||
<Tr key={key}>
|
||||
<Td>{key}</Td>
|
||||
<Td>{transaction.tx[key]}</Td>
|
||||
<Td wordBreak="break-all">{transaction.tx[key]}</Td>
|
||||
</Tr>
|
||||
)
|
||||
)}
|
||||
|
|
|
@ -1,9 +1,11 @@
|
|||
import { useEffect } from "react";
|
||||
import { useContext } from "react";
|
||||
import { useMutation } from "react-query";
|
||||
import { AuthService } from "../services";
|
||||
import { useUser, useToast, useInviteAccept, useRouter, useAnalytics } from ".";
|
||||
import UIContext from "../providers/UIProvider/context";
|
||||
|
||||
const useSignUp = (source) => {
|
||||
const ui = useContext(UIContext);
|
||||
const router = useRouter();
|
||||
const { getUser } = useUser();
|
||||
const toast = useToast();
|
||||
|
@ -30,28 +32,16 @@ const useSignUp = (source) => {
|
|||
{ full_url: router.nextRouter.asPath, code: source }
|
||||
);
|
||||
}
|
||||
getUser();
|
||||
ui.setisOnboardingComplete(false);
|
||||
ui.setOnboardingState({ welcome: 0, subscriptions: 0, stream: 0 });
|
||||
router.push("/welcome", undefined, { shallow: false });
|
||||
},
|
||||
onError: (error) => {
|
||||
toast(error, "error");
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (!data) {
|
||||
return;
|
||||
}
|
||||
|
||||
getUser();
|
||||
|
||||
const requested_pricing = window.sessionStorage.getItem(
|
||||
"requested_pricing_plan"
|
||||
);
|
||||
const redirectURL = requested_pricing ? "/subscriptions" : "/stream";
|
||||
|
||||
router.push(redirectURL);
|
||||
window.sessionStorage.clear("requested_pricing");
|
||||
}, [data, getUser, router]);
|
||||
|
||||
return {
|
||||
signUp,
|
||||
isLoading,
|
||||
|
|
|
@ -5,6 +5,7 @@ export const MIXPANEL_PROPS = {
|
|||
PRICING_PLAN: "Pricing Plan",
|
||||
TOGGLE_LEFT_SIDEBAR: "Toggle left sidebar",
|
||||
BUTTON_NAME: "Button",
|
||||
USER_SPECIALITY: "user speciality",
|
||||
};
|
||||
|
||||
export const MIXPANEL_EVENTS = {
|
||||
|
|
|
@ -17,15 +17,7 @@ const UIProvider = ({ children }) => {
|
|||
xl: false,
|
||||
"2xl": false,
|
||||
});
|
||||
|
||||
const currentBreakpoint = useBreakpointValue({
|
||||
base: 0,
|
||||
sm: 1,
|
||||
md: 2,
|
||||
lg: 3,
|
||||
xl: 4,
|
||||
"2xl": 5,
|
||||
});
|
||||
// const isMobileView = true;
|
||||
|
||||
const { modal, toggleModal } = useContext(ModalContext);
|
||||
const [searchTerm, setSearchTerm] = useQuery("q", "", true, false);
|
||||
|
@ -44,7 +36,8 @@ const UIProvider = ({ children }) => {
|
|||
router.nextRouter.asPath.includes("/stream") ||
|
||||
router.nextRouter.asPath.includes("/account") ||
|
||||
router.nextRouter.asPath.includes("/subscriptions") ||
|
||||
router.nextRouter.asPath.includes("/analytics")
|
||||
router.nextRouter.asPath.includes("/analytics") ||
|
||||
router.nextRouter.asPath.includes("/welcome")
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
|
@ -81,7 +74,8 @@ const UIProvider = ({ children }) => {
|
|||
router.nextRouter.asPath.includes("/stream") ||
|
||||
router.nextRouter.asPath.includes("/account") ||
|
||||
router.nextRouter.asPath.includes("/subscriptions") ||
|
||||
router.nextRouter.asPath.includes("/analytics")
|
||||
router.nextRouter.asPath.includes("/analytics") ||
|
||||
router.nextRouter.asPath.includes("/welcome")
|
||||
);
|
||||
}, [router.nextRouter.asPath, user]);
|
||||
|
||||
|
@ -119,7 +113,7 @@ const UIProvider = ({ children }) => {
|
|||
//Sidebar is visible at at breakpoint value less then 2
|
||||
//Sidebar is visible always in appView
|
||||
useEffect(() => {
|
||||
if (currentBreakpoint < 2) {
|
||||
if (isMobileView) {
|
||||
setSidebarVisible(true);
|
||||
setSidebarCollapsed(false);
|
||||
} else {
|
||||
|
@ -130,7 +124,7 @@ const UIProvider = ({ children }) => {
|
|||
}
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [currentBreakpoint, isAppView]);
|
||||
}, [isMobileView, isAppView]);
|
||||
|
||||
// *********** Entries layout states **********************
|
||||
|
||||
|
@ -159,6 +153,81 @@ const UIProvider = ({ children }) => {
|
|||
);
|
||||
}, [isEntryDetailView, isMobileView]);
|
||||
|
||||
// *********** Onboarding state **********************
|
||||
|
||||
const onboardingSteps = [
|
||||
{
|
||||
step: "welcome",
|
||||
description: "Basics of how Moonstream works",
|
||||
},
|
||||
{
|
||||
step: "subscriptions",
|
||||
description: "Learn how to subscribe to blockchain events",
|
||||
},
|
||||
{
|
||||
step: "stream",
|
||||
description: "Learn how to use your Moonstream to analyze blah blah blah",
|
||||
},
|
||||
];
|
||||
|
||||
const [onboardingState, setOnboardingState] = useStorage(
|
||||
window.localStorage,
|
||||
"onboardingState",
|
||||
{
|
||||
welcome: 0,
|
||||
subscriptions: 0,
|
||||
stream: 0,
|
||||
}
|
||||
);
|
||||
|
||||
const [onboardingStep, setOnboardingStep] = useState(() => {
|
||||
const step = onboardingSteps.findIndex(
|
||||
(event) => onboardingState[event.step] === 0
|
||||
);
|
||||
if (step === -1 && isOnboardingComplete) return onboardingSteps.length - 1;
|
||||
else if (step === -1 && !isOnboardingComplete) return 0;
|
||||
else return step;
|
||||
});
|
||||
|
||||
const [isOnboardingComplete, setisOnboardingComplete] = useStorage(
|
||||
window.localStorage,
|
||||
"isOnboardingComplete",
|
||||
isLoggedIn ? true : false
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (isLoggedIn && !isOnboardingComplete) {
|
||||
router.replace("/welcome");
|
||||
}
|
||||
// eslint-disable-next-line
|
||||
}, [isLoggedIn, isOnboardingComplete]);
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
onboardingSteps.findIndex(
|
||||
(event) => onboardingState[event.step] === 0
|
||||
) === -1
|
||||
) {
|
||||
setisOnboardingComplete(true);
|
||||
}
|
||||
//eslint-disable-next-line
|
||||
}, [onboardingState]);
|
||||
|
||||
useEffect(() => {
|
||||
if (router.nextRouter.pathname === "/welcome") {
|
||||
const newOnboardingState = {
|
||||
...onboardingState,
|
||||
[`${onboardingSteps[onboardingStep].step}`]:
|
||||
onboardingState[onboardingSteps[onboardingStep].step] + 1,
|
||||
};
|
||||
|
||||
setOnboardingState(newOnboardingState);
|
||||
}
|
||||
// eslint-disable-next-line
|
||||
}, [onboardingStep, router.nextRouter.pathname]);
|
||||
|
||||
// const ONBOARDING_STEP_NUM = steps.length;
|
||||
|
||||
// ********************************************************
|
||||
|
||||
return (
|
||||
|
@ -188,6 +257,12 @@ const UIProvider = ({ children }) => {
|
|||
currentTransaction,
|
||||
setCurrentTransaction,
|
||||
isEntryDetailView,
|
||||
onboardingStep,
|
||||
setOnboardingStep,
|
||||
isOnboardingComplete,
|
||||
setisOnboardingComplete,
|
||||
onboardingSteps,
|
||||
setOnboardingState,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
|
|
Ładowanie…
Reference in New Issue