From f2be36a3e95b5a5d3bc9aa0794d70bc64ec9fe09 Mon Sep 17 00:00:00 2001 From: Iveta Date: Fri, 9 Jan 2026 09:38:50 -0500 Subject: [PATCH 01/11] Functional --- .eslintrc.js | 22 +- .gitignore | 6 +- .prettierignore | 2 +- Dockerfile | 10 +- Procfile | 2 +- backend/app.ts | 5 - backend/ledgers.ts | 6 +- common/lumens.d.ts | 1 + common/lumens.js | 15 + common/lumens.mjs | 249 + frontend/app.js | 8 - .../{AccountBadge.js => AccountBadge.jsx} | 0 .../{AccountBalance.js => AccountBalance.jsx} | 2 +- .../{AmountWidget.js => AmountWidget.jsx} | 0 frontend/components/{App.js => App.jsx} | 63 +- frontend/components/{AppBar.js => AppBar.jsx} | 0 .../{AssetLink.js => AssetLink.jsx} | 2 +- frontend/components/D3BarChart.jsx | 150 + frontend/components/D3BarChartNoXLabels.jsx | 158 + ...nsChart.js => FailedTransactionsChart.jsx} | 30 +- .../components/{FeeStats.js => FeeStats.jsx} | 4 +- .../{Incidents.js => Incidents.jsx} | 0 ...dgerCloseChart.js => LedgerCloseChart.jsx} | 24 +- ...ityPoolBadge.js => LiquidityPoolBadge.jsx} | 0 .../{ListAccounts.js => ListAccounts.jsx} | 2 +- ...nsCirculating.js => LumensCirculating.jsx} | 5 +- ...nsDistributed.js => LumensDistributed.jsx} | 5 +- ...irculating.js => LumensNonCirculating.jsx} | 5 +- .../{NetworkStatus.js => NetworkStatus.jsx} | 0 ...s => PublicNetworkLedgersHistoryChart.jsx} | 32 +- ...centOperations.js => RecentOperations.jsx} | 6 +- ...aintenance.js => ScheduledMaintenance.jsx} | 0 .../{TotalCoins.js => TotalCoins.jsx} | 5 +- ...sactionsChart.js => TransactionsChart.jsx} | 30 +- frontend/main.jsx | 10 + frontend/scss/index.scss | 2 +- gulpfile.babel.js | 132 - frontend/index.html => index.html | 8 +- package-lock.json | 9458 +++++++++++++ package.json | 58 +- test/mocha.opts | 1 - test/test-setup.ts | 3 - test/tests/integration/backend.ts | 165 +- test/tests/unit/backend.ts | 14 +- tsconfig.json | 26 +- vite.config.js | 33 + yarn.lock | 11374 ++++------------ 47 files changed, 13492 insertions(+), 8641 deletions(-) create mode 100644 common/lumens.mjs delete mode 100644 frontend/app.js rename frontend/components/{AccountBadge.js => AccountBadge.jsx} (100%) rename frontend/components/{AccountBalance.js => AccountBalance.jsx} (95%) rename frontend/components/{AmountWidget.js => AmountWidget.jsx} (100%) rename frontend/components/{App.js => App.jsx} (86%) rename frontend/components/{AppBar.js => AppBar.jsx} (100%) rename frontend/components/{AssetLink.js => AssetLink.jsx} (88%) create mode 100644 frontend/components/D3BarChart.jsx create mode 100644 frontend/components/D3BarChartNoXLabels.jsx rename frontend/components/{FailedTransactionsChart.js => FailedTransactionsChart.jsx} (80%) rename frontend/components/{FeeStats.js => FeeStats.jsx} (97%) rename frontend/components/{Incidents.js => Incidents.jsx} (100%) rename frontend/components/{LedgerCloseChart.js => LedgerCloseChart.jsx} (85%) rename frontend/components/{LiquidityPoolBadge.js => LiquidityPoolBadge.jsx} (100%) rename frontend/components/{ListAccounts.js => ListAccounts.jsx} (98%) rename frontend/components/{LumensCirculating.js => LumensCirculating.jsx} (85%) rename frontend/components/{LumensDistributed.js => LumensDistributed.jsx} (85%) rename frontend/components/{LumensNonCirculating.js => LumensNonCirculating.jsx} (83%) rename frontend/components/{NetworkStatus.js => NetworkStatus.jsx} (100%) rename frontend/components/{PublicNetworkLedgersHistoryChart.js => PublicNetworkLedgersHistoryChart.jsx} (71%) rename frontend/components/{RecentOperations.js => RecentOperations.jsx} (98%) rename frontend/components/{ScheduledMaintenance.js => ScheduledMaintenance.jsx} (100%) rename frontend/components/{TotalCoins.js => TotalCoins.jsx} (83%) rename frontend/components/{TransactionsChart.js => TransactionsChart.jsx} (80%) create mode 100644 frontend/main.jsx delete mode 100644 gulpfile.babel.js rename frontend/index.html => index.html (92%) create mode 100644 package-lock.json delete mode 100644 test/mocha.opts delete mode 100644 test/test-setup.ts create mode 100644 vite.config.js diff --git a/.eslintrc.js b/.eslintrc.js index fd789b60..e718e3e0 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -1,10 +1,24 @@ module.exports = { - extends: ["@stellar/eslint-config"], + env: { + browser: true, + es2021: true, + node: true, + }, + extends: ["eslint:recommended", "prettier"], + parser: "@typescript-eslint/parser", + parserOptions: { + ecmaVersion: 12, + sourceType: "module", + }, + plugins: ["@typescript-eslint"], rules: { "no-console": "off", - "import/no-unresolved": "off", - "no-await-in-loop": "off", "no-constant-condition": "off", - "@typescript-eslint/naming-convention": ["warn"], + "@typescript-eslint/no-unused-vars": ["warn", { argsIgnorePattern: "^_" }], + "no-unused-vars": ["error", { argsIgnorePattern: "^_" }], + "prefer-const": "warn", + "no-var": "warn", + eqeqeq: "warn", }, + ignorePatterns: ["node_modules/", "dist/", "*.min.js"], }; diff --git a/.gitignore b/.gitignore index 9bd8a66e..9e180427 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,10 @@ /node_modules -/.tmp /dist *.eslintcache *service-account.json *.env +/.vscode +/.idea +/.kiro +/.cursor +dump.rdb diff --git a/.prettierignore b/.prettierignore index d36977dc..8b137891 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1 +1 @@ -.tmp + diff --git a/Dockerfile b/Dockerfile index 133f538e..6266d562 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM ubuntu:22.04 +FROM ubuntu:24.04 MAINTAINER SDF Ops Team @@ -8,11 +8,9 @@ WORKDIR /app/src RUN apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install --no-install-recommends -y \ gpg curl ca-certificates git apt-transport-https && \ curl -sSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key|gpg --dearmor >/etc/apt/trusted.gpg.d/nodesource-key.gpg && \ - echo "deb https://deb.nodesource.com/node_16.x nodistro main" | tee /etc/apt/sources.list.d/nodesource.list && \ - curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg |gpg --dearmor >/etc/apt/trusted.gpg.d/yarnpkg.gpg && \ - echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list && \ - apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y nodejs yarn && \ - yarn install && /app/src/node_modules/gulp/bin/gulp.js build + echo "deb https://deb.nodesource.com/node_22.x nodistro main" | tee /etc/apt/sources.list.d/nodesource.list && \ + apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y nodejs && \ + npm ci --legacy-peer-deps && npm run build ENV PORT=80 UPDATE_DATA=false EXPOSE 80 diff --git a/Procfile b/Procfile index e1d4131b..f9a9b5c6 100644 --- a/Procfile +++ b/Procfile @@ -1 +1 @@ -web: node app.js +web: npx tsx backend/app.ts diff --git a/backend/app.ts b/backend/app.ts index fdaf3ba4..18c4e095 100644 --- a/backend/app.ts +++ b/backend/app.ts @@ -1,8 +1,3 @@ -// need to manually import regeneratorRuntime for babel w/ async -// https://github.com/babel/babel/issues/9849#issuecomment-487040428 -// require("regenerator-runtime/runtime"); -import "regenerator-runtime/runtime"; - import "dotenv/config"; // Run backend with cache updates. diff --git a/backend/ledgers.ts b/backend/ledgers.ts index 54328f6c..fcf06fd7 100644 --- a/backend/ledgers.ts +++ b/backend/ledgers.ts @@ -1,4 +1,4 @@ -import stellarSdk from "stellar-sdk"; +import { Horizon } from "@stellar/stellar-sdk"; import { findIndex } from "lodash"; import { Response, NextFunction } from "express"; @@ -64,7 +64,7 @@ export async function updateLedgers() { await catchup(REDIS_LEDGER_KEY, pagingToken, REDIS_PAGING_TOKEN_KEY, 0); - const horizon = new stellarSdk.Server("https://horizon.stellar.org"); + const horizon = new Horizon.Server("https://horizon.stellar.org"); horizon .ledgers() .cursor(CURSOR_NOW) @@ -82,7 +82,7 @@ export async function catchup( pagingTokenKey: string, limit: number, // if 0, catchup until now ) { - const horizon = new stellarSdk.Server("https://horizon.stellar.org"); + const horizon = new Horizon.Server("https://horizon.stellar.org"); let ledgers: LedgerRecord[] = []; let total = 0; let pagingToken = pagingTokenStart; diff --git a/common/lumens.d.ts b/common/lumens.d.ts index 04b573a4..46f01a99 100644 --- a/common/lumens.d.ts +++ b/common/lumens.d.ts @@ -8,6 +8,7 @@ export function directDevelopmentAll(): Promise; export function distributionEcosystemSupport(): Promise; export function distributionUseCaseInvestment(): Promise; export function distributionUserAcquisition(): Promise; +export function distributionAll(): Promise; export function getUpgradeReserve(): string; export function sdfAccounts(): Promise; export function totalSupply(): Promise; diff --git a/common/lumens.js b/common/lumens.js index a1b32585..9cecafe2 100644 --- a/common/lumens.js +++ b/common/lumens.js @@ -187,6 +187,21 @@ function distributionUserAcquisition() { ]); } +exports.distributionAll = distributionAll; +function distributionAll() { + return Promise.all([ + distributionEcosystemSupport(), + distributionUseCaseInvestment(), + distributionUserAcquisition(), + directDevelopmentAll(), + ]).then((results) => { + return results.reduce( + (sum, balance) => new BigNumber(sum).plus(balance), + new BigNumber(0), + ); + }); +} + exports.getUpgradeReserve = getUpgradeReserve; function getUpgradeReserve() { return getLumenBalance(horizonLiveURL, networkUpgradeReserveAccount); diff --git a/common/lumens.mjs b/common/lumens.mjs new file mode 100644 index 00000000..76ba2e70 --- /dev/null +++ b/common/lumens.mjs @@ -0,0 +1,249 @@ +// ES module version of lumens.js for frontend use +import axios from "axios"; +import BigNumber from "bignumber.js"; +import map from "lodash/map"; +import reduce from "lodash/reduce"; +import find from "lodash/find"; + +const horizonLiveURL = "https://horizon.stellar.org"; + +const voidAccount = "GALAXYVOIDAOPZTDLHILAJQKCVVFMD4IKLXLSZV5YHO7VY74IWZILUTO"; +const networkUpgradeReserveAccount = + "GBEZOC5U4TVH7ZY5N3FLYHTCZSI6VFGTULG7PBITLF5ZEBPJXFT46YZM"; +const accounts = { + // escrowJan2021: "GBA6XT7YBQOERXT656T74LYUVJ6MEIOC5EUETGAQNHQHEPUFPKCW5GYM", + escrowJan2022: "GD2D6JG6D3V52ZMPIYSVHYFKVNIMXGYVLYJQ3HYHG5YDPGJ3DCRGPLTP", + escrowJan2023: "GA2VRL65L3ZFEDDJ357RGI3MAOKPJZ2Z3IJTPSC24I4KDTNFSVEQURRA", + developerSupportHot: + "GCKJZ2YVECFGLUDJ5T7NZMJPPWERBNYHCXT2MZPXKELFHUSYQR5TVHJQ", + developerSupportHot2: + "GC3ITNZSVVPOWZ5BU7S64XKNI5VPTRSBEXXLS67V4K6LEUETWBMTE7IH", + directDevelopment: "GB6NVEN5HSUBKMYCE5ZOWSK5K23TBWRUQLZY3KNMXUZ3AQ2ESC4MY4AQ", + // directDevelopmentHot1: + // "GCEZYB47RSSSR6RMHQDTBWL4L6RY5CY2SPJU3QHP3YPB6ALPVRLPN7OQ", + directDevelopmentHot2: + "GATL3ETTZ3XDGFXX2ELPIKCZL7S5D2HY3VK4T7LRPD6DW5JOLAEZSZBA", + // directDevelopmentHot3: + // "GCVLWV5B3L3YE6DSCCMHLCK7QIB365NYOLQLW3ZKHI5XINNMRLJ6YHVX", + directDevelopmentHot4: + "GAKGC35HMNB7A3Q2V5SQU6VJC2JFTZB6I7ZW77SJSMRCOX2ZFBGJOCHH", + directDevelopmentHot5: + "GAPV2C4BTHXPL2IVYDXJ5PUU7Q3LAXU7OAQDP7KVYHLCNM2JTAJNOQQI", + infrastructureGrants: + "GCVJDBALC2RQFLD2HYGQGWNFZBCOD2CPOTN3LE7FWRZ44H2WRAVZLFCU", + currencySupport: "GAMGGUQKKJ637ILVDOSCT5X7HYSZDUPGXSUW67B2UKMG2HEN5TPWN3LQ", + currencySupportHot: + "GANII5Y2LABEBK74NWNKS4NREX2T52YTBGQDRDKVBFRIIF5VE4ORYOVY", + enterpriseFund: "GDUY7J7A33TQWOSOQGDO776GGLM3UQERL4J3SPT56F6YS4ID7MLDERI4", + newProducts: "GCPWKVQNLDPD4RNP5CAXME4BEDTKSSYRR4MMEL4KG65NEGCOGNJW7QI2", + inAppDistribution: "GDKIJJIKXLOM2NRMPNQZUUYK24ZPVFC6426GZAEP3KUK6KEJLACCWNMX", + inAppDistributionHot: + "GAX3BRBNB5WTJ2GNEFFH7A4CZKT2FORYABDDBZR5FIIT3P7FLS2EFOZZ", + inAppDistributionHot2: + "GDWXQOTIIDO2EUK4DIGIBLEHLME2IAJRNU6JDFS5B2ZTND65P7J36WQZ", + marketingSupport: "GBEVKAYIPWC5AQT6D4N7FC3XGKRRBMPCAMTO3QZWMHHACLHTMAHAM2TP", + marketingSupportHot: + "GBI5PADO5TEDY3R6WFAO2HEKBTTZS4LGR77XM4AHGN52H45ENBWGDFOH", +}; + +const ORIGINAL_SUPPLY_AMOUNT = "100000000000"; + +export { ORIGINAL_SUPPLY_AMOUNT }; + +export function getLumenBalance(horizonURL, accountId) { + return axios + .get(`${horizonURL}/accounts/${accountId}`) + .then((response) => { + var xlmBalance = find( + response.data.balances, + (b) => b.asset_type == "native", + ); + return xlmBalance.balance; + }) + .catch((error) => { + if ( + error.response && + (error.response.status == 404 || error.response.status == 400) + ) { + console.warn( + `Account ${accountId} not found or invalid (${error.response.status}), treating as 0 balance`, + ); + return "0.0"; // consider the balance of an account zero if the account does not exist, has been deleted, or is invalid + } else { + console.error( + `Error fetching balance for account ${accountId}:`, + error, + ); + throw error; // something else happened, and at this point we shouldn't trust the computed balance + } + }); +} + +function sumRelevantAccounts(accounts) { + return Promise.all( + accounts.map((acct) => getLumenBalance(horizonLiveURL, acct)), + ).then((data) => + data + .reduce( + (sum, currentBalance) => new BigNumber(currentBalance).plus(sum), + new BigNumber(0), + ) + .toString(), + ); +} + +export function totalLumens(horizonURL) { + return axios + .get(`${horizonURL}/ledgers/?order=desc&limit=1`) + .then((response) => { + return response.data._embedded.records[0].total_coins; + }); +} + +export function inflationLumens() { + return Promise.all([ + totalLumens(horizonLiveURL), + ORIGINAL_SUPPLY_AMOUNT, + ]).then((result) => { + let [totalLumens, originalSupply] = result; + return new BigNumber(totalLumens).minus(originalSupply); + }); +} + +export function feePool() { + return axios + .get(`${horizonLiveURL}/ledgers/?order=desc&limit=1`) + .then((response) => { + return response.data._embedded.records[0].fee_pool; + }); +} + +export function burnedLumens() { + return axios + .get(`${horizonLiveURL}/accounts/${voidAccount}`) + .then((response) => { + var xlmBalance = find( + response.data.balances, + (b) => b.asset_type == "native", + ); + return xlmBalance.balance; + }); +} + +export function directDevelopmentAll() { + const { + directDevelopment, + // directDevelopmentHot1, + directDevelopmentHot2, + // directDevelopmentHot3, + directDevelopmentHot4, + directDevelopmentHot5, + } = accounts; + return sumRelevantAccounts([ + directDevelopment, + // directDevelopmentHot1, + directDevelopmentHot2, + // directDevelopmentHot3, + directDevelopmentHot4, + directDevelopmentHot5, + ]); +} + +export function distributionEcosystemSupport() { + const { + infrastructureGrants, + currencySupport, + currencySupportHot, + developerSupportHot, + developerSupportHot2, + } = accounts; + return sumRelevantAccounts([ + infrastructureGrants, + currencySupport, + currencySupportHot, + developerSupportHot, + developerSupportHot2, + ]); +} + +export function distributionUseCaseInvestment() { + const { enterpriseFund, newProducts } = accounts; + return sumRelevantAccounts([enterpriseFund, newProducts]); +} + +export function distributionUserAcquisition() { + const { + inAppDistribution, + inAppDistributionHot, + inAppDistributionHot2, + marketingSupport, + marketingSupportHot, + } = accounts; + + return sumRelevantAccounts([ + inAppDistribution, + inAppDistributionHot, + inAppDistributionHot2, + marketingSupport, + marketingSupportHot, + ]); +} + +export function distributionAll() { + return Promise.all([ + distributionEcosystemSupport(), + distributionUseCaseInvestment(), + distributionUserAcquisition(), + directDevelopmentAll(), + ]).then((results) => { + return results.reduce( + (sum, balance) => new BigNumber(sum).plus(balance), + new BigNumber(0), + ); + }); +} + +export function getUpgradeReserve() { + return getLumenBalance(horizonLiveURL, networkUpgradeReserveAccount); +} + +export function sdfAccounts() { + var balanceMap = map(accounts, (id) => getLumenBalance(horizonLiveURL, id)); + return Promise.all(balanceMap).then((balances) => { + return reduce( + balances, + (sum, balance) => sum.plus(balance), + new BigNumber(0), + ); + }); +} + +export function totalSupply() { + return Promise.all([inflationLumens(), burnedLumens()]).then((result) => { + let [inflationLumens, burnedLumens] = result; + + return new BigNumber(ORIGINAL_SUPPLY_AMOUNT) + .plus(inflationLumens) + .minus(burnedLumens); + }); +} + +export function noncirculatingSupply() { + return Promise.all([getUpgradeReserve(), feePool(), sdfAccounts()]).then( + (balances) => { + return reduce( + balances, + (sum, balance) => sum.plus(balance), + new BigNumber(0), + ); + }, + ); +} + +export function circulatingSupply() { + return Promise.all([totalSupply(), noncirculatingSupply()]).then((result) => { + let [totalLumens, noncirculatingSupply] = result; + + return new BigNumber(totalLumens).minus(noncirculatingSupply); + }); +} diff --git a/frontend/app.js b/frontend/app.js deleted file mode 100644 index 3c396e91..00000000 --- a/frontend/app.js +++ /dev/null @@ -1,8 +0,0 @@ -import React from "react"; -import ReactDOM from "react-dom"; -import App from "./components/App.js"; - -require("./index.html"); -require("./scss/index.scss"); - -ReactDOM.render(, document.getElementById("app")); diff --git a/frontend/components/AccountBadge.js b/frontend/components/AccountBadge.jsx similarity index 100% rename from frontend/components/AccountBadge.js rename to frontend/components/AccountBadge.jsx diff --git a/frontend/components/AccountBalance.js b/frontend/components/AccountBalance.jsx similarity index 95% rename from frontend/components/AccountBalance.js rename to frontend/components/AccountBalance.jsx index 5b665201..9cc57492 100644 --- a/frontend/components/AccountBalance.js +++ b/frontend/components/AccountBalance.jsx @@ -1,5 +1,5 @@ import React from "react"; -import AmountWidget from "./AmountWidget"; +import AmountWidget from "./AmountWidget.jsx"; import Panel from "muicss/lib/react/panel"; import axios from "axios"; import find from "lodash/find"; diff --git a/frontend/components/AmountWidget.js b/frontend/components/AmountWidget.jsx similarity index 100% rename from frontend/components/AmountWidget.js rename to frontend/components/AmountWidget.jsx diff --git a/frontend/components/App.js b/frontend/components/App.jsx similarity index 86% rename from frontend/components/App.js rename to frontend/components/App.jsx index 5c160dc3..689f70ed 100644 --- a/frontend/components/App.js +++ b/frontend/components/App.jsx @@ -3,24 +3,25 @@ import Panel from "muicss/lib/react/panel"; import { EventEmitter } from "fbemitter"; import axios from "axios"; import moment from "moment"; -import { Server } from "stellar-sdk"; +import * as StellarSdk from "@stellar/stellar-sdk"; -import AppBar from "./AppBar"; -import AccountBalance from "./AccountBalance"; -import FeeStats from "./FeeStats"; -import NetworkStatus from "./NetworkStatus"; -import Incidents from "./Incidents"; -import LedgerCloseChart from "./LedgerCloseChart"; -import LumensCirculating from "./LumensCirculating"; -import LumensNonCirculating from "./LumensNonCirculating"; -import PublicNetworkLedgersHistoryChart from "./PublicNetworkLedgersHistoryChart"; -import RecentOperations from "./RecentOperations"; -import TotalCoins from "./TotalCoins"; -import TransactionsChart from "./TransactionsChart"; -import FailedTransactionsChart from "./FailedTransactionsChart"; +import AppBar from "./AppBar.jsx"; +import AccountBalance from "./AccountBalance.jsx"; +import FeeStats from "./FeeStats.jsx"; +import NetworkStatus from "./NetworkStatus.jsx"; +import Incidents from "./Incidents.jsx"; +// D3 components - now updated to D3 v7 +import LedgerCloseChart from "./LedgerCloseChart.jsx"; +import PublicNetworkLedgersHistoryChart from "./PublicNetworkLedgersHistoryChart.jsx"; +import TransactionsChart from "./TransactionsChart.jsx"; +import FailedTransactionsChart from "./FailedTransactionsChart.jsx"; +import LumensCirculating from "./LumensCirculating.jsx"; +import LumensNonCirculating from "./LumensNonCirculating.jsx"; +import RecentOperations from "./RecentOperations.jsx"; +import TotalCoins from "./TotalCoins.jsx"; import { LIVE_NEW_LEDGER, TEST_NEW_LEDGER } from "../events"; import { setTimeOffset } from "../common/time"; -import { ScheduledMaintenance } from "./ScheduledMaintenance"; +import { ScheduledMaintenance } from "./ScheduledMaintenance.jsx"; import sanitizeHtml from "../utilities/sanitizeHtml.js"; const horizonLive = "https://horizon.stellar.org"; @@ -134,17 +135,29 @@ export default class App extends React.Component { streamLedgers(horizonURL, eventName) { // Get last ledger - axios.get(`${horizonURL}/ledgers?order=desc&limit=1`).then((response) => { - let lastLedger = response.data._embedded.records[0]; + axios + .get(`${horizonURL}/ledgers?order=desc&limit=1`) + .then((response) => { + let lastLedger = response.data._embedded.records[0]; - new Server(horizonURL) - .ledgers() - .cursor(lastLedger.paging_token) - .limit(200) - .stream({ - onmessage: (ledger) => this.emitter.emit(eventName, ledger), - }); - }); + new StellarSdk.Horizon.Server(horizonURL) + .ledgers() + .cursor(lastLedger.paging_token) + .limit(200) + .stream({ + onmessage: (ledger) => this.emitter.emit(eventName, ledger), + onerror: (error) => { + console.warn("Stellar streaming error:", error); + // Retry after 5 seconds + setTimeout(() => this.streamLedgers(horizonURL, eventName), 5000); + }, + }); + }) + .catch((error) => { + console.warn("Failed to get initial ledger:", error); + // Retry after 5 seconds + setTimeout(() => this.streamLedgers(horizonURL, eventName), 5000); + }); } turnOffForceTheme() { diff --git a/frontend/components/AppBar.js b/frontend/components/AppBar.jsx similarity index 100% rename from frontend/components/AppBar.js rename to frontend/components/AppBar.jsx diff --git a/frontend/components/AssetLink.js b/frontend/components/AssetLink.jsx similarity index 88% rename from frontend/components/AssetLink.js rename to frontend/components/AssetLink.jsx index f73cee5e..d3108788 100644 --- a/frontend/components/AssetLink.js +++ b/frontend/components/AssetLink.jsx @@ -1,5 +1,5 @@ import React from "react"; -import AccountBadge from "./AccountBadge"; +import AccountBadge from "./AccountBadge.jsx"; export default class AssetLink extends React.Component { render() { diff --git a/frontend/components/D3BarChart.jsx b/frontend/components/D3BarChart.jsx new file mode 100644 index 00000000..56fdbe22 --- /dev/null +++ b/frontend/components/D3BarChart.jsx @@ -0,0 +1,150 @@ +import React, { useEffect, useRef } from "react"; +import * as d3 from "d3"; + +export default function D3BarChart({ + data, + width = 400, + height = 120, + margin = { top: 10, right: 10, bottom: 30, left: 50 }, + colorScale, + tickFormat, +}) { + const svgRef = useRef(); + + useEffect(() => { + if (!data || data.length === 0) return; + + const svg = d3.select(svgRef.current); + svg.selectAll("*").remove(); // Clear previous render + + const innerWidth = width - margin.left - margin.right; + const innerHeight = height - margin.top - margin.bottom; + + // Flatten all values from all series for domain calculation + const allValues = data.flatMap((series) => series.values); + const xValues = allValues.map((d) => d.x); + const yValues = allValues.map((d) => d.y); + + // Create scales - use scalePoint for fixed spacing instead of scaleBand + const xScale = d3 + .scalePoint() + .domain(xValues) + .range([0, innerWidth]) + .padding(1.0); // Double the gap - more space from Y-axis + + const yScale = d3 + .scaleLinear() + .domain([0, d3.max(yValues)]) + .range([innerHeight, 0]); + + // Fixed bar dimensions + const fixedBarWidth = 5; // Fixed 5px bar width + const fixedGapWidth = 3; // Fixed 3px gap between bars + + // Create color scale - match original react-d3-components colors + const colors = + colorScale || + d3 + .scaleOrdinal() + .range([ + "#1f77b4", + "#ff7f0e", + "#2ca02c", + "#d62728", + "#9467bd", + "#8c564b", + "#e377c2", + "#7f7f7f", + "#bcbd22", + "#17becf", + ]); + + // Create main group + const g = svg + .attr("width", width) + .attr("height", height) + .append("g") + .attr("transform", `translate(${margin.left},${margin.top})`); + + // Calculate bar width - always use fixed width for all bars + const barWidth = fixedBarWidth; // All bars are exactly 5px wide + + // Add bars for each series + data.forEach((series, seriesIndex) => { + g.selectAll(`.bar-${seriesIndex}`) + .data(series.values) + .enter() + .append("rect") + .attr("class", `bar-${seriesIndex}`) + .attr("x", (d) => xScale(d.x) - fixedBarWidth / 2) // Center the bar on the scale point + .attr("y", (d) => yScale(d.y)) + .attr("width", barWidth) // Always 5px wide + .attr("height", (d) => innerHeight - yScale(d.y)) + .attr("fill", colors(seriesIndex)) + .style("shape-rendering", "crispEdges"); // Crisp edges like original + }); + + // Add x-axis with styling to match original + const xAxis = d3.axisBottom(xScale).tickSize(6).tickPadding(3); + + const xAxisGroup = g + .append("g") + .attr("class", "axis") + .attr("transform", `translate(0,${innerHeight})`) + .call(xAxis); + + // Style x-axis to match original + xAxisGroup + .selectAll("text") + .style("font-size", "10px") + .style("font-family", "sans-serif") + .style("fill", "#000"); + + xAxisGroup + .selectAll("line") + .style("stroke", "#000") + .style("shape-rendering", "crispEdges"); + + xAxisGroup + .select(".domain") + .style("stroke", "#000") + .style("shape-rendering", "crispEdges"); + + // Add y-axis with styling to match original + const yAxis = d3 + .axisLeft(yScale) + .tickSize(6) + .tickPadding(3) + .ticks(Math.min(7, Math.floor(d3.max(yValues)))) // Limit to max 7 ticks or the max value, whichever is smaller + .tickValues( + d3.range(0, Math.ceil(d3.max(yValues)) + 1).filter((d) => d % 1 === 0), + ); // Only integer values + + if (tickFormat) { + yAxis.tickFormat(tickFormat); + } else { + yAxis.tickFormat(d3.format("d")); // Format as integers (1, 2, 3) instead of decimals (1.0, 2.0) + } + + const yAxisGroup = g.append("g").attr("class", "axis").call(yAxis); + + // Style y-axis to match original + yAxisGroup + .selectAll("text") + .style("font-size", "10px") + .style("font-family", "sans-serif") + .style("fill", "#000"); + + yAxisGroup + .selectAll("line") + .style("stroke", "#000") + .style("shape-rendering", "crispEdges"); + + yAxisGroup + .select(".domain") + .style("stroke", "#000") + .style("shape-rendering", "crispEdges"); + }, [data, width, height, margin, colorScale, tickFormat]); + + return ; +} diff --git a/frontend/components/D3BarChartNoXLabels.jsx b/frontend/components/D3BarChartNoXLabels.jsx new file mode 100644 index 00000000..428e9086 --- /dev/null +++ b/frontend/components/D3BarChartNoXLabels.jsx @@ -0,0 +1,158 @@ +import React, { useEffect, useRef } from "react"; +import * as d3 from "d3"; + +export default function D3BarChartNoXLabels({ + data, + width = 400, + height = 120, + margin = { top: 10, right: 10, bottom: 8, left: 50 }, // Reduced bottom margin since no X labels + colorScale, + tickFormat, + yAxisMax = 450, // Maximum Y value + yAxisStep = 50, // Y axis increment step +}) { + const svgRef = useRef(); + + useEffect(() => { + if (!data || data.length === 0) return; + + const svg = d3.select(svgRef.current); + svg.selectAll("*").remove(); // Clear previous render + + const innerWidth = width - margin.left - margin.right; + const innerHeight = height - margin.top - margin.bottom; + + // Flatten all values from all series for domain calculation + const allValues = data.flatMap((series) => series.values); + const xValues = allValues.map((d) => d.x); + const yValues = allValues.map((d) => d.y); + + // Create scales - use scalePoint for fixed spacing instead of scaleBand + const xScale = d3 + .scalePoint() + .domain(xValues) + .range([0, innerWidth]) + .padding(1.0); // Double the gap - more space from Y-axis + + const yScale = d3 + .scaleLinear() + .domain([0, yAxisMax]) // Use fixed max instead of data max + .range([innerHeight, 0]); + + // Fixed bar dimensions + const fixedBarWidth = 5; // Fixed 5px bar width + const fixedGapWidth = 3; // Fixed 3px gap between bars + + // Create color scale - match original react-d3-components colors + const colors = + colorScale || + d3 + .scaleOrdinal() + .range([ + "#1f77b4", + "#ff7f0e", + "#2ca02c", + "#d62728", + "#9467bd", + "#8c564b", + "#e377c2", + "#7f7f7f", + "#bcbd22", + "#17becf", + ]); + + // Create main group + const g = svg + .attr("width", width) + .attr("height", height) + .append("g") + .attr("transform", `translate(${margin.left},${margin.top})`); + + // Calculate bar width - always use fixed width for all bars + const barWidth = fixedBarWidth; // All bars are exactly 5px wide + + // Add bars for each series + data.forEach((series, seriesIndex) => { + g.selectAll(`.bar-${seriesIndex}`) + .data(series.values) + .enter() + .append("rect") + .attr("class", `bar-${seriesIndex}`) + .attr("x", (d) => xScale(d.x) - fixedBarWidth / 2) // Center the bar on the scale point + .attr("y", (d) => yScale(d.y)) + .attr("width", barWidth) // Always 5px wide + .attr("height", (d) => innerHeight - yScale(d.y)) + .attr("fill", colors(seriesIndex)) + .style("shape-rendering", "crispEdges"); // Crisp edges like original + }); + + // Add x-axis with NO labels + const xAxis = d3 + .axisBottom(xScale) + .tickSize(6) + .tickPadding(3) + .tickFormat(""); // No labels + + const xAxisGroup = g + .append("g") + .attr("class", "axis") + .attr("transform", `translate(0,${innerHeight})`) + .call(xAxis); + + // Style x-axis lines only (no text) + xAxisGroup + .selectAll("line") + .style("stroke", "#000") + .style("shape-rendering", "crispEdges"); + + xAxisGroup + .select(".domain") + .style("stroke", "#000") + .style("shape-rendering", "crispEdges"); + + // Add y-axis with custom ticks + const yAxisTicks = d3.range(0, yAxisMax + yAxisStep, yAxisStep); // [0, 50, 100, 150, ..., yAxisMax] + + const yAxis = d3 + .axisLeft(yScale) + .tickSize(6) + .tickPadding(3) + .tickValues(yAxisTicks); + + if (tickFormat) { + yAxis.tickFormat(tickFormat); + } else { + yAxis.tickFormat(d3.format("d")); // Format as integers + } + + const yAxisGroup = g.append("g").attr("class", "axis").call(yAxis); + + // Style y-axis to match original + yAxisGroup + .selectAll("text") + .style("font-size", "10px") + .style("font-family", "sans-serif") + .style("fill", "#000"); + + yAxisGroup + .selectAll("line") + .style("stroke", "#000") + .style("shape-rendering", "crispEdges"); + + yAxisGroup + .select(".domain") + .style("stroke", "#000") + .style("shape-rendering", "crispEdges"); + }, [ + data, + width, + height, + margin, + colorScale, + tickFormat, + yAxisMax, + yAxisStep, + ]); + + return ; +} diff --git a/frontend/components/FailedTransactionsChart.js b/frontend/components/FailedTransactionsChart.jsx similarity index 80% rename from frontend/components/FailedTransactionsChart.js rename to frontend/components/FailedTransactionsChart.jsx index a68141be..3fc4aaa4 100644 --- a/frontend/components/FailedTransactionsChart.js +++ b/frontend/components/FailedTransactionsChart.jsx @@ -1,8 +1,8 @@ import React from "react"; import Panel from "muicss/lib/react/panel"; import axios from "axios"; -import { scale, format } from "d3"; -import BarChart from "react-d3-components/lib/BarChart"; +import * as d3 from "d3"; +import D3BarChartNoXLabels from "./D3BarChartNoXLabels.jsx"; import clone from "lodash/clone"; import each from "lodash/each"; @@ -10,7 +10,21 @@ export default class FailedTransactionsChart extends React.Component { constructor(props) { super(props); this.panel = null; - this.colorScale = scale.category10(); + // Use the same colors as the original react-d3-components + this.colorScale = d3 + .scaleOrdinal() + .range([ + "#1f77b4", + "#ff7f0e", + "#2ca02c", + "#d62728", + "#9467bd", + "#8c564b", + "#e377c2", + "#7f7f7f", + "#bcbd22", + "#17becf", + ]); this.state = { loading: true, chartWidth: 400, @@ -88,11 +102,11 @@ export default class FailedTransactionsChart extends React.Component { >
- + Successful {" "} &{" "} - Failed{" "} + Failed{" "} Txs in the last {this.props.limit} ledgers: {this.props.network} API @@ -101,13 +115,15 @@ export default class FailedTransactionsChart extends React.Component { {this.state.loading ? ( "Loading..." ) : ( - )} diff --git a/frontend/components/FeeStats.js b/frontend/components/FeeStats.jsx similarity index 97% rename from frontend/components/FeeStats.js rename to frontend/components/FeeStats.jsx index 83128cf5..cb3302bc 100644 --- a/frontend/components/FeeStats.js +++ b/frontend/components/FeeStats.jsx @@ -6,8 +6,8 @@ import clone from "lodash/clone"; import each from "lodash/each"; import defaults from "lodash/defaults"; import get from "lodash/get"; -import AccountBadge from "./AccountBadge"; -import AssetLink from "./AssetLink"; +import AccountBadge from "./AccountBadge.jsx"; +import AssetLink from "./AssetLink.jsx"; import BigNumber from "bignumber.js"; import { ago } from "../common/time"; diff --git a/frontend/components/Incidents.js b/frontend/components/Incidents.jsx similarity index 100% rename from frontend/components/Incidents.js rename to frontend/components/Incidents.jsx diff --git a/frontend/components/LedgerCloseChart.js b/frontend/components/LedgerCloseChart.jsx similarity index 85% rename from frontend/components/LedgerCloseChart.js rename to frontend/components/LedgerCloseChart.jsx index 0ebe3d8e..97f64c0a 100644 --- a/frontend/components/LedgerCloseChart.js +++ b/frontend/components/LedgerCloseChart.jsx @@ -1,8 +1,8 @@ import React from "react"; import Panel from "muicss/lib/react/panel"; import axios from "axios"; -import { scale } from "d3"; -import BarChart from "react-d3-components/lib/BarChart"; +import * as d3 from "d3"; +import D3BarChart from "./D3BarChart.jsx"; import each from "lodash/each"; import clone from "lodash/clone"; @@ -10,7 +10,21 @@ export default class LedgerChartClose extends React.Component { constructor(props) { super(props); this.panel = null; - this.colorScale = scale.category10(); + // Use the same colors as the original react-d3-components + this.colorScale = d3 + .scaleOrdinal() + .range([ + "#1f77b4", + "#ff7f0e", + "#2ca02c", + "#d62728", + "#9467bd", + "#8c564b", + "#e377c2", + "#7f7f7f", + "#bcbd22", + "#17becf", + ]); this.state = { loading: true, chartWidth: 400, @@ -94,12 +108,12 @@ export default class LedgerChartClose extends React.Component { {this.state.loading ? ( "Loading..." ) : ( - )} diff --git a/frontend/components/LiquidityPoolBadge.js b/frontend/components/LiquidityPoolBadge.jsx similarity index 100% rename from frontend/components/LiquidityPoolBadge.js rename to frontend/components/LiquidityPoolBadge.jsx diff --git a/frontend/components/ListAccounts.js b/frontend/components/ListAccounts.jsx similarity index 98% rename from frontend/components/ListAccounts.js rename to frontend/components/ListAccounts.jsx index d3001343..3c96f0a8 100644 --- a/frontend/components/ListAccounts.js +++ b/frontend/components/ListAccounts.jsx @@ -4,7 +4,7 @@ import axios from "axios"; import clone from "lodash/clone"; import find from "lodash/find"; import reduce from "lodash/reduce"; -import AccountBadge from "./AccountBadge"; +import AccountBadge from "./AccountBadge.jsx"; import BigNumber from "bignumber.js"; export default class ListAccounts extends React.Component { diff --git a/frontend/components/LumensCirculating.js b/frontend/components/LumensCirculating.jsx similarity index 85% rename from frontend/components/LumensCirculating.js rename to frontend/components/LumensCirculating.jsx index 51a28912..a1b8114d 100644 --- a/frontend/components/LumensCirculating.js +++ b/frontend/components/LumensCirculating.jsx @@ -1,8 +1,9 @@ import React from "react"; -import AmountWidget from "./AmountWidget"; +import AmountWidget from "./AmountWidget.jsx"; import BigNumber from "bignumber.js"; import Panel from "muicss/lib/react/panel"; -import { circulatingSupply } from "../../common/lumens.js"; +import * as lumens from "../../common/lumens.mjs"; +const { circulatingSupply } = lumens; export default class LumensCirculating extends AmountWidget { constructor(props) { diff --git a/frontend/components/LumensDistributed.js b/frontend/components/LumensDistributed.jsx similarity index 85% rename from frontend/components/LumensDistributed.js rename to frontend/components/LumensDistributed.jsx index 775c38be..68e958ec 100644 --- a/frontend/components/LumensDistributed.js +++ b/frontend/components/LumensDistributed.jsx @@ -1,10 +1,11 @@ import React from "react"; -import AmountWidget from "./AmountWidget"; +import AmountWidget from "./AmountWidget.jsx"; import Panel from "muicss/lib/react/panel"; import BigNumber from "bignumber.js"; import axios from "axios"; import find from "lodash/find"; -import { distributionAll } from "../../common/lumens.js"; +import * as lumens from "../../common/lumens.mjs"; +const { distributionAll } = lumens; export default class LumensDistributed extends AmountWidget { constructor(props) { diff --git a/frontend/components/LumensNonCirculating.js b/frontend/components/LumensNonCirculating.jsx similarity index 83% rename from frontend/components/LumensNonCirculating.js rename to frontend/components/LumensNonCirculating.jsx index cf258429..e41a43cc 100644 --- a/frontend/components/LumensNonCirculating.js +++ b/frontend/components/LumensNonCirculating.jsx @@ -1,7 +1,8 @@ import React from "react"; -import AmountWidget from "./AmountWidget"; +import AmountWidget from "./AmountWidget.jsx"; import Panel from "muicss/lib/react/panel"; -import { noncirculatingSupply } from "../../common/lumens.js"; +import * as lumens from "../../common/lumens.mjs"; +const { noncirculatingSupply } = lumens; export default class LumensNonCirculating extends AmountWidget { constructor(props) { diff --git a/frontend/components/NetworkStatus.js b/frontend/components/NetworkStatus.jsx similarity index 100% rename from frontend/components/NetworkStatus.js rename to frontend/components/NetworkStatus.jsx diff --git a/frontend/components/PublicNetworkLedgersHistoryChart.js b/frontend/components/PublicNetworkLedgersHistoryChart.jsx similarity index 71% rename from frontend/components/PublicNetworkLedgersHistoryChart.js rename to frontend/components/PublicNetworkLedgersHistoryChart.jsx index bc91dab7..d20c02c5 100644 --- a/frontend/components/PublicNetworkLedgersHistoryChart.js +++ b/frontend/components/PublicNetworkLedgersHistoryChart.jsx @@ -1,8 +1,8 @@ import React from "react"; import Panel from "muicss/lib/react/panel"; import axios from "axios"; -import { scale, format } from "d3"; -import BarChart from "react-d3-components/lib/BarChart"; +import * as d3 from "d3"; +import D3BarChartNoXLabels from "./D3BarChartNoXLabels.jsx"; import clone from "lodash/clone"; import each from "lodash/each"; @@ -10,7 +10,21 @@ export default class PublicNetworkLedgersHistoryChart extends React.Component { constructor(props) { super(props); this.panel = null; - this.colorScale = scale.category10(); + // Use the same colors as the original react-d3-components + this.colorScale = d3 + .scaleOrdinal() + .range([ + "#1f77b4", + "#ff7f0e", + "#2ca02c", + "#d62728", + "#9467bd", + "#8c564b", + "#e377c2", + "#7f7f7f", + "#bcbd22", + "#17becf", + ]); this.state = { loading: true, chartWidth: 400, @@ -62,20 +76,22 @@ export default class PublicNetworkLedgersHistoryChart extends React.Component { >
- Txs &{" "} - Ops in + Txs &{" "} + Ops in the last 30 days: Live Network
{this.state.loading ? ( "Loading..." ) : ( - )}
diff --git a/frontend/components/RecentOperations.js b/frontend/components/RecentOperations.jsx similarity index 98% rename from frontend/components/RecentOperations.js rename to frontend/components/RecentOperations.jsx index a82600ab..42bc8e8a 100644 --- a/frontend/components/RecentOperations.js +++ b/frontend/components/RecentOperations.jsx @@ -4,9 +4,9 @@ import axios from "axios"; import moment from "moment"; import each from "lodash/each"; import defaults from "lodash/defaults"; -import AccountBadge from "./AccountBadge"; -import LiquidityPoolBadge from "./LiquidityPoolBadge"; -import AssetLink from "./AssetLink"; +import AccountBadge from "./AccountBadge.jsx"; +import LiquidityPoolBadge from "./LiquidityPoolBadge.jsx"; +import AssetLink from "./AssetLink.jsx"; import BigNumber from "bignumber.js"; import { ago } from "../common/time"; diff --git a/frontend/components/ScheduledMaintenance.js b/frontend/components/ScheduledMaintenance.jsx similarity index 100% rename from frontend/components/ScheduledMaintenance.js rename to frontend/components/ScheduledMaintenance.jsx diff --git a/frontend/components/TotalCoins.js b/frontend/components/TotalCoins.jsx similarity index 83% rename from frontend/components/TotalCoins.js rename to frontend/components/TotalCoins.jsx index f3771000..630126d8 100644 --- a/frontend/components/TotalCoins.js +++ b/frontend/components/TotalCoins.jsx @@ -1,6 +1,7 @@ import React from "react"; -import AmountWidget from "./AmountWidget"; -import { totalSupply } from "../../common/lumens.js"; +import AmountWidget from "./AmountWidget.jsx"; +import * as lumens from "../../common/lumens.mjs"; +const { totalSupply } = lumens; export default class TotalCoins extends AmountWidget { constructor(props) { diff --git a/frontend/components/TransactionsChart.js b/frontend/components/TransactionsChart.jsx similarity index 80% rename from frontend/components/TransactionsChart.js rename to frontend/components/TransactionsChart.jsx index 1b747108..f6d53bc1 100644 --- a/frontend/components/TransactionsChart.js +++ b/frontend/components/TransactionsChart.jsx @@ -1,8 +1,8 @@ import React from "react"; import Panel from "muicss/lib/react/panel"; import axios from "axios"; -import { scale, format } from "d3"; -import BarChart from "react-d3-components/lib/BarChart"; +import * as d3 from "d3"; +import D3BarChartNoXLabels from "./D3BarChartNoXLabels.jsx"; import clone from "lodash/clone"; import each from "lodash/each"; @@ -10,7 +10,21 @@ export default class TransactionsChart extends React.Component { constructor(props) { super(props); this.panel = null; - this.colorScale = scale.category10(); + // Use the same colors as the original react-d3-components + this.colorScale = d3 + .scaleOrdinal() + .range([ + "#1f77b4", + "#ff7f0e", + "#2ca02c", + "#d62728", + "#9467bd", + "#8c564b", + "#e377c2", + "#7f7f7f", + "#bcbd22", + "#17becf", + ]); this.state = { loading: true, chartWidth: 400, @@ -89,8 +103,8 @@ export default class TransactionsChart extends React.Component {
Successful{" "} - Txs &{" "} - Ops in + Txs &{" "} + Ops in the last {this.props.limit} ledgers: {this.props.network} API @@ -99,13 +113,15 @@ export default class TransactionsChart extends React.Component { {this.state.loading ? ( "Loading..." ) : ( - )} diff --git a/frontend/main.jsx b/frontend/main.jsx new file mode 100644 index 00000000..0f971c0e --- /dev/null +++ b/frontend/main.jsx @@ -0,0 +1,10 @@ +import React from "react"; +import { createRoot } from "react-dom/client"; +import App from "./components/App.jsx"; + +// Add SCSS import +import "./scss/index.scss"; + +const container = document.getElementById("app"); +const root = createRoot(container); +root.render(); diff --git a/frontend/scss/index.scss b/frontend/scss/index.scss index 8d2ac09f..3619c6bc 100644 --- a/frontend/scss/index.scss +++ b/frontend/scss/index.scss @@ -1,3 +1,3 @@ -@import "../../node_modules/bourbon/app/assets/stylesheets/_bourbon"; +@import "bourbon"; @import "_main"; @import "_force"; diff --git a/gulpfile.babel.js b/gulpfile.babel.js deleted file mode 100644 index 44b4f8dc..00000000 --- a/gulpfile.babel.js +++ /dev/null @@ -1,132 +0,0 @@ -"use strict"; - -var _ = require("lodash"); -var bs = require("browser-sync").create(); -var gulp = require("gulp"); -var path = require("path"); -var webpack = require("webpack"); -var ExtractTextPlugin = require("extract-text-webpack-plugin"); - -var webpackOptions = { - entry: { - app: "./frontend/app.js", - vendor: [ - "react", - "react-dom", - "muicss", - "stellar-sdk", - "axios", - "d3", - "fbemitter", - ], - }, - devtool: "source-map", - resolve: { - root: [path.resolve("frontend"), path.resolve("common")], - modulesDirectories: ["node_modules"], - }, - module: { - loaders: [ - { - test: /\.js$/, - exclude: /node_modules/, - loader: "babel-loader", - query: { presets: ["es2015", "react"] }, - }, - { test: /\.json$/, loader: "json-loader" }, - { test: /\.html$/, loader: "file?name=[name].html" }, - { - test: /\.scss$/, - loader: ExtractTextPlugin.extract( - "style-loader", - "css-loader!sass-loader", - ), - }, - ], - }, - plugins: [ - new webpack.IgnorePlugin(/ed25519/), - new ExtractTextPlugin("style.css"), - ], -}; - -const develop = function (done) { - var options = merge(webpackOptions, { - output: { - filename: "[name].js", - path: "./.tmp", - }, - plugins: [new webpack.optimize.CommonsChunkPlugin("vendor", "vendor.js")], - }); - - var watchOptions = { - aggregateTimeout: 300, - }; - - var bsInitialized = false; - - var compiler = webpack(options); - compiler.purgeInputFileSystem(); - compiler.watch(watchOptions, function (error, stats) { - if (!bsInitialized) { - gulp.watch(".tmp/**/*").on("change", bs.reload); - bs.init({ - port: 3000, - online: false, - notify: false, - server: "./.tmp", - socket: { - domain: "localhost:3000", - }, - }); - bsInitialized = true; - } - console.log( - stats.toString({ - hash: false, - version: false, - timings: true, - chunks: false, - colors: true, - }), - ); - }); -}; - -const build = function (done) { - var options = merge(webpackOptions, { - bail: true, - output: { - // TODO chunkhash - filename: "[name].js", //"[name]-[chunkhash].js", - path: "./dist", - }, - plugins: [ - new webpack.optimize.CommonsChunkPlugin("vendor", "vendor.js"), - new webpack.optimize.DedupePlugin(), - new webpack.optimize.OccurenceOrderPlugin(), - new webpack.DefinePlugin({ - "process.env": { - NODE_ENV: JSON.stringify("production"), - }, - }), - new webpack.optimize.UglifyJsPlugin(), - ], - }); - - var compiler = webpack(options); - compiler.purgeInputFileSystem(); - compiler.run(done); -}; - -function merge(object1, object2) { - return _.mergeWith(object1, object2, function (a, b) { - if (_.isArray(a)) { - return a.concat(b); - } - }); -} - -gulp.task("develop", develop); -gulp.task("build", build); -gulp.task("default", develop); diff --git a/frontend/index.html b/index.html similarity index 92% rename from frontend/index.html rename to index.html index 5cecba75..0036dd93 100644 --- a/frontend/index.html +++ b/index.html @@ -71,20 +71,18 @@ type="text/css" media="screen" /> -
- - +