├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── CODEOWNERS ├── LICENSE ├── PULL_REQUEST_TEMPLATE.md ├── README.md ├── Setup.hs ├── cardano-sl-explorer.cabal ├── frontend ├── .gitignore ├── .gitmodules ├── README.md ├── bower.json ├── debug │ └── socket │ │ ├── .gitignore │ │ ├── index.js │ │ ├── package.json │ │ └── yarn.lock ├── default.nix ├── package.json ├── postcss.config.js ├── purescript-derive-lenses.nix ├── scripts │ ├── build-frontend-simple.sh │ ├── build.sh │ ├── generate-backend-lenses.sh │ ├── generate-frontend-lenses.sh │ └── generate-lenses.sh ├── src │ ├── Control │ │ └── SocketIO │ │ │ ├── Client.js │ │ │ └── Client.purs │ ├── Data │ │ └── Time │ │ │ ├── NominalDiffTime.Test.purs │ │ │ └── NominalDiffTime.purs │ ├── Explorer │ │ ├── Api │ │ │ ├── Helper.purs │ │ │ ├── Http.purs │ │ │ ├── Socket.Test.purs │ │ │ ├── Socket.purs │ │ │ └── Types.purs │ │ ├── I18n │ │ │ ├── DE.purs │ │ │ ├── EN.purs │ │ │ ├── JP.purs │ │ │ ├── Lang.js │ │ │ ├── Lang.purs │ │ │ └── Types.purs │ │ ├── Images.purs │ │ ├── Routes.Test.purs │ │ ├── Routes.purs │ │ ├── State.purs │ │ ├── Types │ │ │ ├── Actions.purs │ │ │ ├── App.purs │ │ │ └── State.purs │ │ ├── Update.Test.purs │ │ ├── Update.purs │ │ ├── Util │ │ │ ├── Config.Test.purs │ │ │ ├── Config.js │ │ │ ├── Config.purs │ │ │ ├── DOM.js │ │ │ ├── DOM.purs │ │ │ ├── Data.Test.purs │ │ │ ├── Data.purs │ │ │ ├── Factory.purs │ │ │ ├── QRCode.js │ │ │ ├── QRCode.purs │ │ │ ├── String.Test.purs │ │ │ ├── String.js │ │ │ ├── String.purs │ │ │ ├── Time.Test.purs │ │ │ └── Time.purs │ │ └── View │ │ │ ├── Address.purs │ │ │ ├── Block.purs │ │ │ ├── Blocks.purs │ │ │ ├── CSS.Test.purs │ │ │ ├── CSS.purs │ │ │ ├── Calculator.purs │ │ │ ├── Common.Test.purs │ │ │ ├── Common.purs │ │ │ ├── Dashboard │ │ │ ├── Api.purs │ │ │ ├── Blocks.purs │ │ │ ├── Dashboard.purs │ │ │ ├── Hero.purs │ │ │ ├── Lenses.purs │ │ │ ├── Network.purs │ │ │ ├── Offer.purs │ │ │ ├── Shared.purs │ │ │ ├── Transactions.purs │ │ │ ├── Types.purs │ │ │ ├── api.css │ │ │ ├── dashboard.css │ │ │ ├── hero.css │ │ │ └── transactions.css │ │ │ ├── Footer.purs │ │ │ ├── GenesisBlock.purs │ │ │ ├── Header.purs │ │ │ ├── Layout.purs │ │ │ ├── NotFound.purs │ │ │ ├── Playground.purs │ │ │ ├── Search.purs │ │ │ ├── Transaction.purs │ │ │ ├── Types.purs │ │ │ ├── address.css │ │ │ ├── block.css │ │ │ ├── blocks.css │ │ │ ├── calculator.css │ │ │ ├── common.css │ │ │ ├── footer.css │ │ │ ├── genesis.css │ │ │ ├── header.css │ │ │ ├── layout.css │ │ │ ├── notfound.css │ │ │ ├── search.css │ │ │ └── transaction.css │ ├── Lib │ │ ├── BigNumber │ │ │ ├── BigNumber.Test.purs │ │ │ ├── BigNumber.js │ │ │ └── BigNumber.purs │ │ └── Waypoints │ │ │ ├── Waypoints.js │ │ │ └── Waypoints.purs │ ├── Main.Test.purs │ ├── Main.purs │ ├── Test │ │ └── MockFactory.purs │ ├── global.css │ ├── index.css │ ├── index.js │ └── index.tpl.html ├── static │ ├── fonts │ │ ├── Montserrat-ExtraBold.otf │ │ ├── Montserrat-ExtraLight.otf │ │ ├── Montserrat-Medium.otf │ │ ├── Montserrat-SemiBold.otf │ │ ├── Montserrat-Thin.otf │ │ ├── MontserratAlternates-Black.otf │ │ ├── MontserratAlternates-Bold.otf │ │ ├── MontserratAlternates-ExtraBold.otf │ │ ├── MontserratAlternates-ExtraLight.otf │ │ ├── MontserratAlternates-Light.otf │ │ ├── MontserratAlternates-Medium.otf │ │ ├── MontserratAlternates-Regular.otf │ │ ├── MontserratAlternates-SemiBold.otf │ │ ├── MontserratAlternates-Thin.otf │ │ ├── montserrat-black_[allfont.ru].ttf │ │ ├── montserrat-bold_[allfont.ru].ttf │ │ ├── montserrat-hairline_[allfont.ru].ttf │ │ ├── montserrat-light_[allfont.ru].ttf │ │ └── montserrat_[allfont.ru].ttf │ └── images │ │ ├── ada-currency-symbol-dark.svg │ │ ├── ada-currency-symbol.svg │ │ ├── arrow-bottom.svg │ │ ├── arrow-right.svg │ │ ├── arrow-up.svg │ │ ├── cardano-logo-name.svg │ │ ├── cardano-logo.svg │ │ ├── header_300517.jpg │ │ ├── icon-cross.svg │ │ ├── icon-menu.svg │ │ ├── icon-search.svg │ │ ├── image-404.svg │ │ ├── iohk-logo.svg │ │ ├── transaction-arrow-blue.svg │ │ ├── triangle-right.svg │ │ └── usd-currency-symbol-dark.svg ├── test │ ├── rename-placeholders.js │ └── setup.js ├── webpack.config.babel.js └── yarn.lock ├── log-config.yaml ├── scripts ├── build.sh ├── build │ ├── cardano-sl.sh │ └── clean.sh ├── fetch_dependencies.sh ├── generate_cabal2nix.sh ├── launch │ ├── qa.sh │ └── staging.sh └── run.sh ├── shell.nix ├── src ├── Pos │ └── Explorer │ │ ├── Aeson │ │ └── ClientTypes.hs │ │ ├── Socket.hs │ │ ├── Socket │ │ ├── App.hs │ │ ├── Holder.hs │ │ ├── Methods.hs │ │ └── Util.hs │ │ ├── Web.hs │ │ └── Web │ │ ├── Api.hs │ │ ├── ClientTypes.hs │ │ ├── Error.hs │ │ ├── Server.hs │ │ ├── TestServer.hs │ │ └── Transform.hs ├── documentation │ ├── Description.hs │ └── Main.hs ├── explorer │ ├── ExplorerOptions.hs │ ├── Main.hs │ ├── Params.hs │ └── Secrets.hs ├── mock │ └── Main.hs └── purescript │ ├── Main.hs │ ├── PSOptions.hs │ └── PSTypes.hs ├── stack.yaml ├── start-dev.sh ├── test ├── Spec.hs ├── Test.hs └── Test │ └── Pos │ └── Explorer │ └── Web │ └── ServerSpec.hs └── update_explorer_web_api_docs.sh /.gitignore: -------------------------------------------------------------------------------- 1 | dist 2 | dist-* 3 | cabal-dev 4 | *.o 5 | *.hi 6 | *.chi 7 | *.chs.h 8 | *.dyn_o 9 | *.dyn_hi 10 | .hpc 11 | .hsenv 12 | .cabal-sandbox/ 13 | cabal.sandbox.config 14 | *.prof 15 | *.aux 16 | *.hp 17 | *.eventlog 18 | .stack-work/ 19 | cabal.project.local 20 | .dir-locals.el 21 | node-db/* 22 | *.key 23 | *.lock 24 | *.dump 25 | *.log 26 | wallet-db/* 27 | logs/* 28 | explorer.log.* 29 | explorer-node.log.* 30 | socket-io.log.* 31 | 32 | # Build script 33 | b 34 | 35 | # Intellij Idea & intellij-haskell 36 | .idea 37 | .ideaHaskellLib 38 | *.iml 39 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: nix 2 | sudo: true 3 | env: 4 | - NIX_PATH=nixpkgs=https://github.com/NixOS/nixpkgs/archive/cb90e6a0361554d01b7a576af6c6fae4c28d7513.tar.gz 5 | install: 6 | - sudo mount -o remount,exec,size=4G,mode=755 /run/user || true 7 | script: 8 | - pushd frontend 9 | - "./scripts/build.sh" 10 | - nix-shell --run "yarn test" 11 | - popd 12 | - "./update_explorer_web_api_docs.sh" 13 | notifications: 14 | email: false 15 | slack: 16 | secure: OdwiFeu7kwochdkIbVz5ZIlTa1AdItO9EFYPVHKoqGOy8s/6kkmMlb0XH4w8ZvKMSDVE2tuXBKz2cVqu0k1A3Nml/q8LF0a2zYagkpAUe4uEHBD7jTO8RCLzABw06unlIyOuNRApTUH97Dq/GftNwunkCyeY7DgzNoxm9mHB53BHaF2A/eHw6oHX3MQwGZvjuqZAT2wx7UbxEISWVWx18ZPTjCJsrDR7dwwB07IhA8oYUNk9nqE94OplLofUafBDRneDs5kEQoVmxomt6U44BIQs7acUGDvSjnmxhgEbq8rge7N5I3i0REtNQCOBeWThGkNY9H3fZfqSTeVQa7/xig8Vxu7+6G+QfOxvCp8PZP3sDACSXJvy/6gUN/sU6q8lZweuAm9BddULof6jjMhPcohRf/hu5/NmSFqYo33wZWvZaBrijBrpPOooU/16W+SbK6kKVuv8YxhnQ64wwKAlnGUcFCEcZwb4EZsMDho1VMO002yC5WmnB3+N/n3i9ncTaT1aVnXZYvkH971AOQ3uMLdICJpMPmZ5ykl8KmfL27fUwxngQzkARdioCt/WEsCLPUJP0YBfjK5GLCZdeLobAeHoE7aKp2XCIwwvoaLwo2z96HSq6OoWBZC8z60Y6ttbEwSFBe/kYU55UYCbZi4bDrtZbpiVp66QxCL9k/xv65o= 17 | on_success: never 18 | on_failure: always 19 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 0.1.0 (April 24, 2017) 4 | 5 | ### Features: 6 | 7 | * Use `cardano-sl` version `0.4.3` 8 | * Dashboard to display last `epochs`, `slots` and `transactions` 9 | * Detail page of a `transaction` 10 | * Detail page of a `slot` 11 | * Detail page of an `address` 12 | * Search for `address`, `transaction` and `time` (`epoch` and / or `slot`) 13 | * Support of `English` and `German` 14 | * Mobile first CSS 15 | 16 | ## 0.2.0 (July 7-10, 2017) 17 | 18 | * Auto-generated documentation available at https://cardanodocs.com/technical/explorer/api/ 19 | * Upgraded the client-side with the new version of the libraries, now using purescript 0.11.5 20 | * Added `paging` for `blocks`, simplified API 21 | * Added and improved socket.io, simplified events 22 | * `Japanese` translations 23 | * ADA formatting using `lovelaces` 24 | * Speed optimizations for `block` and `epoch` fetching 25 | * Corrected mobile issues and improved mobile experience 26 | * Added Waypoint header for better UX -------------------------------------------------------------------------------- /CODEOWNERS: -------------------------------------------------------------------------------- 1 | # Unless a later match takes precedence, @akegalj will be requested 2 | # for review when someone opens a pull request. 3 | 4 | * @akegalj 5 | 6 | # Order is important; the last matching pattern takes the most precedence. 7 | 8 | *.purs @sectore 9 | frontend/ @sectore 10 | 11 | src/ @ksaric 12 | src/Pos/Explorer/Socket/ @martoon 13 | test/ @ksaric 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016 IOHK 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 7 | of the Software, and to permit persons to whom the Software is furnished to 8 | do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | 21 | -------------------------------------------------------------------------------- /PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | # PR 2 | 3 | --- 4 | 5 | Youtrack issue URL - **LINK** 6 | 7 | --- 8 | 9 | Short description - **DESCRIBE** 10 | 11 | --- 12 | 13 | How did I test this new functionality/bug fix? 14 | 15 | 1. branch I used 16 | 2. how did I run the project - was it on _DEV_, _TESTNET_, something else? 17 | 3. steps to reproduce the new behaviour, if I have additional tests, please write them down 18 | 19 | --- 20 | 21 | - [ ] PR was tested, and is linted. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # `cardano-sl-explorer` 2 | 3 | This repository is deprecated. Explorer development is moved to https://github.com/input-output-hk/cardano-explorer. 4 | -------------------------------------------------------------------------------- /Setup.hs: -------------------------------------------------------------------------------- 1 | import Distribution.Simple 2 | main = defaultMain 3 | -------------------------------------------------------------------------------- /frontend/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | bower_components 3 | dist 4 | output 5 | .DS_Store 6 | typings 7 | npm-debug.log 8 | package-lock.json 9 | .psc-ide-port 10 | .psci_modules 11 | # generated backend types + lenses 12 | src/Generated/* 13 | # generated lenses 14 | src/Data/Time/Lenses/* 15 | src/Explorer/I18n/Lenses.purs 16 | src/Explorer/Lenses/* 17 | src/Explorer/View/Lenses/* 18 | -------------------------------------------------------------------------------- /frontend/.gitmodules: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/input-output-hk/cardano-sl-explorer/605ffe82a174d9da863b3f09b510f5585d67d608/frontend/.gitmodules -------------------------------------------------------------------------------- /frontend/bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cardano-sl-explorer", 3 | "description": "Frontend of 'cardano-sl' explorer", 4 | "main": "index.js", 5 | "authors": [], 6 | "keywords": [], 7 | "moduleType": [], 8 | "homepage": "", 9 | "private": true, 10 | "ignore": [ 11 | "**/.*", 12 | "node_modules", 13 | "bower_components", 14 | "test", 15 | "tests" 16 | ], 17 | "dependencies": { 18 | "purescript-pux": "10.0.0", 19 | "purescript-maybe": "3.0.0", 20 | "purescript-affjax": "4.0.0", 21 | "purescript-maps": "3.3.1", 22 | "purescript-profunctor-lenses": "3.2.0", 23 | "purescript-argonaut-generic-codecs": "6.0.3", 24 | "purescript-formatters": "1.0.1", 25 | "purescript-remotedata": "2.1.0", 26 | "purescript-parsing": "4.2.2", 27 | "purescript-now": "v3.0.0", 28 | "purescript-strings": "3.2.1" 29 | }, 30 | "devDependencies": { 31 | "purescript-debug": "3.0.0", 32 | "purescript-console": "3.0.0", 33 | "purescript-psci-support": "3.0.0", 34 | "purescript-spec": "1.0.0" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /frontend/debug/socket/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log 3 | -------------------------------------------------------------------------------- /frontend/debug/socket/index.js: -------------------------------------------------------------------------------- 1 | 2 | var express = require('express'); 3 | var app = express(); 4 | var server = require('http').createServer(app); 5 | var io = require('socket.io')(server); 6 | var port = process.env.PORT || 9999; 7 | var randomstring = require("randomstring"); 8 | 9 | // -------------------------- 10 | // express 11 | // -------------------------- 12 | 13 | server.listen(port, function () { 14 | console.log('Server listening at port %d', port); 15 | }); 16 | 17 | firstRun = true; 18 | 19 | // -------------------------- 20 | // socket 21 | // -------------------------- 22 | 23 | io.on('connection', function (socket) { 24 | 25 | if(firstRun) { 26 | // push blocks 27 | var blocksCount = 0; 28 | var sendBlocksId = setInterval(function() { 29 | blocksCount++; 30 | socket.broadcast.emit('latestBlocks', { "Right": randomBlocks(100) }); 31 | if(blocksCount>10) { 32 | clearInterval(sendBlocksId); 33 | } 34 | }, randomNumber(450, 1000)); 35 | 36 | // push transactions 37 | var txCount = 0; 38 | var sendTxId = setInterval(function() { 39 | txCount++; 40 | socket.broadcast.emit('latestTransactions', { "Right": randomTxs(5) }); 41 | if(txCount>20) { 42 | clearInterval(sendTxId); 43 | } 44 | }, randomNumber(750, 1200)); 45 | } 46 | 47 | firstRun = false; 48 | }); 49 | 50 | // -------------------------- 51 | // mock blocks 52 | // -------------------------- 53 | 54 | var blockId = 0; 55 | var randomBlock = function() { 56 | // Encoded CBlockEntry 57 | // @see src/Generated/Pos/Explorer/Web/ClientTypes.purs 58 | return { cbeBlkHash: randomNumber(1, 50000).toString() 59 | , cbeHeight: randomNumber(1, 50000) 60 | , cbeRelayedBy: randomStringFromList([null, "KNCMiner", "BMinor", "CMinor"]) 61 | , cbeSize: randomNumber(1, 10000) 62 | , cbeTimeIssued: blockId++ 63 | , cbeTxNum: randomNumber(1, 10000) 64 | , cbeTotalSent: { getCoin: randomNumber(1, 1000000) } 65 | } 66 | } 67 | 68 | 69 | var randomBlocks = function (max) { 70 | var maxRandom = randomNumber(1, max) 71 | , i = 0 72 | , blocks = []; 73 | for(i; i < maxRandom; i++ ) { 74 | blocks[i] = randomBlock(); 75 | } 76 | return blocks; 77 | } 78 | 79 | // -------------------------- 80 | // mock transactions 81 | // -------------------------- 82 | 83 | var txId = 0; 84 | var randomTx = function() { 85 | // Encoded CTxEntry 86 | // @see src/Generated/Pos/Explorer/Web/ClientTypes.purs 87 | return { cteId: randomstring.generate(63) 88 | , cteTimeIssued: txId++ 89 | , cteAmount: { getCoin: randomNumber(1, 1000000) } 90 | } 91 | } 92 | 93 | var randomTxs = function (max) { 94 | var maxR = randomNumber(1, max) 95 | , i = 0 96 | , txs = []; 97 | for(i; i < maxR; i++ ) { 98 | txs[i] = randomTx(); 99 | } 100 | return txs; 101 | } 102 | 103 | 104 | // -------------------------- 105 | // helper 106 | // -------------------------- 107 | 108 | 109 | var randomNumber = function(from, to) { 110 | return Math.floor(Math.random() * ((to-from)+1) + from); 111 | } 112 | 113 | var randomStringFromList = function(list) { 114 | if (list !== undefined || list.length > 0) { 115 | return list[randomNumber(0, list.length - 1 )]; 116 | } else { 117 | return ''; 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /frontend/debug/socket/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "", 3 | "version": "0.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "author": "", 7 | "private": true, 8 | "license": "", 9 | "scripts": { 10 | "start": "PORT=9999 node index.js" 11 | }, 12 | "dependencies": { 13 | "express": "^4.14.1", 14 | "randomstring": "^1.1.5", 15 | "socket.io": "^1.7.2" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /frontend/default.nix: -------------------------------------------------------------------------------- 1 | if builtins.compareVersions "1.11.7" builtins.nixVersion == 1 then 2 | abort '' 3 | This project requires Nix >= 1.11.7, please upgrade: 4 | 5 | curl https://nixos.org/nix/install | sh 6 | '' 7 | else 8 | 9 | with (import (fetchTarball https://github.com/NixOS/nixpkgs/archive/cb90e6a0361554d01b7a576af6c6fae4c28d7513.tar.gz) {}); 10 | 11 | # https://github.com/paf31/purescript-derive-lenses/issues/12 12 | # cabal2nix https://github.com/paf31/purescript-derive-lenses.git > purescript-derive-lenses.nix 13 | 14 | let 15 | hspkgs = pkgs.haskell.packages.ghc802.override { 16 | overrides = self: super: { 17 | purescript-derive-lenses = hspkgs.callPackage ./purescript-derive-lenses.nix {}; 18 | }; 19 | }; 20 | in stdenv.mkDerivation { 21 | name = "explorer-bridge"; 22 | 23 | buildInputs = with hspkgs; [ nodejs yarn nodePackages.bower purescript purescript-derive-lenses ]; 24 | 25 | src = null; 26 | } 27 | -------------------------------------------------------------------------------- /frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cardano-sl-explorer", 3 | "version": "0.2.0", 4 | "description": "Frontend of 'cardano-sl' explorer", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "npm run server:dev", 8 | "postinstall": "bower cache clean && bower install", 9 | "preinstall": "which psc || which purs || npm install purescript@0.11.5", 10 | "clean:output": "./node_modules/.bin/rimraf output", 11 | "server:webpack": "./node_modules/.bin/webpack-dev-server --config webpack.config.babel.js --progress", 12 | "server:dev": "npm run clean:output && NODE_ENV=development npm run server:webpack", 13 | "server:prod": "npm run clean:output && NODE_ENV=production npm run server:webpack", 14 | "build:prod": "./node_modules/.bin/rimraf dist && mkdir dist && NODE_ENV=production ./node_modules/.bin/webpack --config webpack.config.babel.js", 15 | "test": "npm run clean:output && npm run test:all", 16 | "test:rename-placeholders": "node 'test/rename-placeholders.js'", 17 | "test:all": "purs compile 'bower_components/purescript-*/src/**/*.purs' 'src/**/*.purs' -o output && npm run test:rename-placeholders && NODE_PATH=./output node -e 'require(\"./test/setup\").setup(); require(\"Main.Test\").main();'" 18 | }, 19 | "repository": { 20 | "type": "git", 21 | "url": "git+https://github.com/input-output-hk/cardano-sl-explorer-frontend.git" 22 | }, 23 | "keywords": [], 24 | "author": {}, 25 | "contributors": [], 26 | "license": "MIT", 27 | "bugs": { 28 | "url": "https://github.com/input-output-hk/cardano-sl-explorer-frontend/issues" 29 | }, 30 | "homepage": "https://github.com/input-output-hk/cardano-sl-explorer-frontend#readme", 31 | "dependencies": { 32 | "animate.css": "^3.5.2", 33 | "babel-polyfill": "^6.23.0", 34 | "bignumber.js": "^4.0.2", 35 | "js-polyfills": "^0.1.33", 36 | "react": "^15.6.1", 37 | "react-dom": "^15.6.1", 38 | "socket.io-client": "^2.0.3", 39 | "waypoints": "^4.0.1" 40 | }, 41 | "devDependencies": { 42 | "autoprefixer": "^7.1.2", 43 | "babel-core": "^6.25.0", 44 | "babel-loader": "^7.1.1", 45 | "babel-plugin-transform-es2015-destructuring": "^6.23.0", 46 | "babel-preset-es2015": "^6.24.1", 47 | "babel-preset-react": "^6.24.1", 48 | "babel-preset-stage-2": "^6.24.1", 49 | "bower": "^1.8.0", 50 | "copy-webpack-plugin": "^4.0.1", 51 | "css-loader": "^0.28.4", 52 | "cssnano": "^3.10.0", 53 | "extract-text-webpack-plugin": "^3.0.0", 54 | "file-loader": "^0.11.2", 55 | "git-revision-webpack-plugin": "^2.5.1", 56 | "html-webpack-plugin": "^2.29.0", 57 | "jsdom": "^11.1.0", 58 | "lost": "^8.1.0", 59 | "module-alias": "^2.0.0", 60 | "postcss-button": "^0.1.19", 61 | "postcss-color-function": "^4.0.0", 62 | "postcss-css-reset": "^1.0.2", 63 | "postcss-cssnext": "^3.0.2", 64 | "postcss-custom-media": "^6.0.0", 65 | "postcss-custom-properties": "^6.1.0", 66 | "postcss-discard-comments": "^2.0.4", 67 | "postcss-extend": "^1.0.5", 68 | "postcss-flexbox": "^1.0.3", 69 | "postcss-import": "^10.0.0", 70 | "postcss-inline-svg": "^3.0.0", 71 | "postcss-loader": "^2.0.6", 72 | "postcss-media-minmax": "^3.0.0", 73 | "postcss-neat": "^2.5.2", 74 | "postcss-nested": "^2.0.2", 75 | "postcss-svgo": "^2.1.6", 76 | "purescript-psa": "^0.5.1", 77 | "purs-loader": "^3.0.0", 78 | "replace-in-file": "^2.5.3", 79 | "rimraf": "^2.6.1", 80 | "source-map-loader": "^0.2.1", 81 | "style-loader": "^0.18.2", 82 | "url-loader": "^0.5.9", 83 | "webpack": "^3.2.0", 84 | "webpack-dev-server": "^2.5.1" 85 | }, 86 | "babel": { 87 | "presets": [ 88 | "es2015", 89 | "stage-2", 90 | "react" 91 | ] 92 | }, 93 | "_moduleAliases": { 94 | "@noframework.waypoints": "node_modules/waypoints/lib/noframework.waypoints.js" 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /frontend/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: [ 3 | require('postcss-import'), 4 | require('postcss-css-reset'), 5 | require('postcss-custom-properties'), 6 | require('postcss-nested'), 7 | require('postcss-extend'), 8 | require('postcss-color-function'), 9 | require('postcss-button'), 10 | require('postcss-inline-svg'), 11 | require('postcss-svgo'), 12 | require('postcss-flexbox'), 13 | require('postcss-neat')({ 14 | neatMaxWidth: '1200px' 15 | }), 16 | require('lost'), 17 | require('postcss-custom-media'), 18 | require('postcss-media-minmax'), 19 | require('postcss-cssnext')({ 20 | warnForDuplicates: false, 21 | browsers: [ 22 | 'last 2 versions', 23 | 'ie >= 10' 24 | ] 25 | }), 26 | require('postcss-discard-comments'), 27 | require('cssnano')({ 28 | preset: ['default', { 29 | autoprefixer: false, // already prefixed w/ cssnext 30 | sourcemap: false, 31 | discardComments: { 32 | removeAll: true, 33 | }, 34 | }] 35 | }) 36 | ] 37 | }; 38 | -------------------------------------------------------------------------------- /frontend/purescript-derive-lenses.nix: -------------------------------------------------------------------------------- 1 | { mkDerivation, array, base, boxes, fetchgit, optparse-generic 2 | , purescript, split, stdenv, text 3 | }: 4 | mkDerivation { 5 | pname = "purescript-derive-lenses"; 6 | version = "0.10.5.0"; 7 | src = fetchgit { 8 | url = "https://github.com/paf31/purescript-derive-lenses.git"; 9 | sha256 = "0p53kdw1bcqpl0sls4c7ds0zf9ks5ivdgv23vbhdfcfl0bf107na"; 10 | rev = "02457e610789263326b936ebdfa72edbb6599094"; 11 | }; 12 | isLibrary = false; 13 | isExecutable = true; 14 | executableHaskellDepends = [ 15 | array base boxes optparse-generic purescript split text 16 | ]; 17 | description = "A tool to derive lenses for PureScript data types"; 18 | license = stdenv.lib.licenses.mit; 19 | } 20 | -------------------------------------------------------------------------------- /frontend/scripts/build-frontend-simple.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -e 4 | 5 | if ! which purescript-derive-lenses >/dev/null; then 6 | echo "Installing purescript-derive-lenses..." 7 | rm -rf purescript-derive-lenses 8 | git clone https://github.com/paf31/purescript-derive-lenses 9 | cd purescript-derive-lenses 10 | git checkout 02457e6 # TODO: I am not sure which version should we use? @jens? 11 | stack install --install-ghc 12 | cd .. 13 | rm -rf purescript-derive-lenses 14 | fi 15 | 16 | rm -rf frontend/src/Generated 17 | stack exec -- cardano-explorer-hs2purs --bridge-path ./frontend/src/Generated 18 | 19 | cd frontend 20 | rm -rf output node_modules bower_components 21 | yarn install # or use npm install 22 | 23 | scripts/generate-backend-lenses.sh 24 | scripts/generate-frontend-lenses.sh 25 | 26 | yarn start # or use npm start 27 | 28 | # open http://localhost:3100/ 29 | -------------------------------------------------------------------------------- /frontend/scripts/build.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env nix-shell 2 | #! nix-shell -j 4 -i bash -p stack git 3 | #! nix-shell -I nixpkgs=https://github.com/NixOS/nixpkgs/archive/cb90e6a0361554d01b7a576af6c6fae4c28d7513.tar.gz 4 | 5 | set -xe 6 | 7 | if [ -n "$NIX_SSL_CERT_FILE" ]; then 8 | export SSL_CERT_FILE=$NIX_SSL_CERT_FILE 9 | fi 10 | 11 | pushd .. 12 | if [ -n "$EXPLORER_NIX_FILE" ]; then 13 | $(nix-build -A cardano-sl-explorer-static $EXPLORER_NIX_FILE)/bin/cardano-explorer-hs2purs --bridge-path frontend/src/Generated/ 14 | else 15 | stack --nix install happy --fast --ghc-options="-j +RTS -A128m -n2m -RTS" 16 | stack --nix build --fast --ghc-options="-j +RTS -A128m -n2m -RTS" 17 | stack --nix exec -- cardano-explorer-hs2purs --bridge-path frontend/src/Generated/ 18 | fi 19 | popd 20 | nix-shell --run "rm -rf .psci_modules/ .pulp-cache/ node_modules/ bower_components/ output/" 21 | nix-shell --run "yarn install" 22 | nix-shell --run ./scripts/generate-backend-lenses.sh 23 | nix-shell --run ./scripts/generate-frontend-lenses.sh 24 | nix-shell --run "yarn ${1:-build:prod}" 25 | echo "Done." 26 | -------------------------------------------------------------------------------- /frontend/scripts/generate-backend-lenses.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -e 4 | 5 | DIR_GENERATED_WEB=src/Generated/Pos/Explorer/Web 6 | 7 | mkdir -p $DIR_GENERATED_WEB/Lenses 8 | 9 | purescript-derive-lenses \ 10 | < $DIR_GENERATED_WEB/ClientTypes.purs \ 11 | --moduleName Pos.Explorer.Web.Lenses.ClientTypes \ 12 | --moduleImports "import Data.Maybe" \ 13 | --moduleImports "import Data.Tuple" \ 14 | --moduleImports "import Data.Time.NominalDiffTime (NominalDiffTime(..))" \ 15 | > $DIR_GENERATED_WEB/Lenses/ClientTypes.purs 16 | 17 | 18 | DIR_GENERATED_TYPES=src/Generated/Pos/Core 19 | 20 | mkdir -p $DIR_GENERATED_TYPES/Lenses 21 | 22 | purescript-derive-lenses \ 23 | < $DIR_GENERATED_TYPES/Types.purs \ 24 | --moduleName Pos.Core.Lenses.Types \ 25 | > $DIR_GENERATED_TYPES/Lenses/Types.purs 26 | -------------------------------------------------------------------------------- /frontend/scripts/generate-frontend-lenses.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -e 4 | 5 | # - - - - - - - - - - - 6 | # Types 7 | # - - - - - - - - - - - 8 | 9 | DIR_TYPES=src/Explorer/Types 10 | DIR_TYPES_LENSES=src/Explorer/Lenses 11 | 12 | mkdir -p $DIR_TYPES_LENSES 13 | 14 | purescript-derive-lenses \ 15 | < $DIR_TYPES/State.purs \ 16 | --moduleName Explorer.Lenses.State \ 17 | --moduleImports "import Explorer.Api.Types (SocketSubscription, SocketSubscriptionData)" \ 18 | --moduleImports "import Waypoints (Waypoint)" \ 19 | --moduleImports "import Explorer.Routes (Route)" \ 20 | > $DIR_TYPES_LENSES/State.purs 21 | 22 | # - - - - - - - - - - - 23 | # I18n 24 | # - - - - - - - - - - - 25 | 26 | DIR_I18N=src/Explorer/I18n 27 | 28 | purescript-derive-lenses \ 29 | < $DIR_I18N/Types.purs \ 30 | --moduleName Explorer.I18n.Lenses \ 31 | > $DIR_I18N/Lenses.purs 32 | 33 | # - - - - - - - - - - - 34 | # Data.Time 35 | # - - - - - - - - - - - 36 | 37 | DIR_TIME=src/Data/Time 38 | DIR_TIME_LENSES=$DIR_TIME/Lenses 39 | 40 | mkdir -p $DIR_TIME_LENSES 41 | 42 | purescript-derive-lenses \ 43 | < $DIR_TIME/NominalDiffTime.purs \ 44 | --moduleName Data.Time.NominalDiffTime.Lenses \ 45 | --moduleImports "import Data.Time.Duration (Seconds (..))" \ 46 | > $DIR_TIME_LENSES/NominalDiffTime.purs 47 | 48 | # - - - - - - - - - - - 49 | # View 50 | # - - - - - - - - - - - 51 | 52 | DIR_VIEW=src/Explorer/View 53 | DIR_VIEW_LENSES=$DIR_VIEW/Lenses 54 | 55 | mkdir -p $DIR_VIEW_LENSES 56 | 57 | purescript-derive-lenses \ 58 | < $DIR_VIEW/Types.purs \ 59 | --moduleName Explorer.View.Lenses \ 60 | --moduleImports "import Data.Time.NominalDiffTime (NominalDiffTime(..))" \ 61 | --moduleImports "import Data.Maybe (Maybe)" \ 62 | --moduleImports "import Data.Tuple (Tuple)" \ 63 | --moduleImports "import Pos.Explorer.Web.ClientTypes (CCoin, CAddress, CTxId)" \ 64 | > $DIR_VIEW_LENSES/View.purs 65 | -------------------------------------------------------------------------------- /frontend/scripts/generate-lenses.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | #BACKEND 4 | 5 | set -e 6 | 7 | DIR_GENERATED_WEB=src/Generated/Pos/Explorer/Web 8 | 9 | mkdir -p $DIR_GENERATED_WEB/Lenses 10 | 11 | purescript-derive-lenses \ 12 | < $DIR_GENERATED_WEB/ClientTypes.purs \ 13 | --moduleName Pos.Explorer.Web.Lenses.ClientTypes \ 14 | --moduleImports "import Data.Maybe" \ 15 | --moduleImports "import Data.Tuple" \ 16 | --moduleImports "import Data.Time.NominalDiffTime (NominalDiffTime(..))" \ 17 | > $DIR_GENERATED_WEB/Lenses/ClientTypes.purs 18 | 19 | 20 | DIR_GENERATED_TYPES=src/Generated/Pos/Core 21 | 22 | mkdir -p $DIR_GENERATED_TYPES/Lenses 23 | 24 | purescript-derive-lenses \ 25 | < $DIR_GENERATED_TYPES/Types.purs \ 26 | --moduleName Pos.Core.Lenses.Types \ 27 | > $DIR_GENERATED_TYPES/Lenses/Types.purs 28 | 29 | #FRONTEND 30 | 31 | set -e 32 | 33 | # - - - - - - - - - - - 34 | # Types 35 | # - - - - - - - - - - - 36 | 37 | DIR_TYPES=src/Explorer/Types 38 | DIR_TYPES_LENSES=src/Explorer/Lenses 39 | 40 | mkdir -p $DIR_TYPES_LENSES 41 | 42 | purescript-derive-lenses \ 43 | < $DIR_TYPES/State.purs \ 44 | --moduleName Explorer.Lenses.State \ 45 | --moduleImports "import Pos.Explorer.Socket.Methods (Subscription)" \ 46 | > $DIR_TYPES_LENSES/State.purs 47 | 48 | # - - - - - - - - - - - 49 | # I18n 50 | # - - - - - - - - - - - 51 | 52 | DIR_I18N=src/Explorer/I18n 53 | 54 | purescript-derive-lenses \ 55 | < $DIR_I18N/Types.purs \ 56 | --moduleName Explorer.I18n.Lenses \ 57 | > $DIR_I18N/Lenses.purs 58 | 59 | # - - - - - - - - - - - 60 | # Data.Time 61 | # - - - - - - - - - - - 62 | 63 | DIR_TIME=src/Data/Time 64 | DIR_TIME_LENSES=$DIR_TIME/Lenses 65 | 66 | mkdir -p $DIR_TIME_LENSES 67 | 68 | purescript-derive-lenses \ 69 | < $DIR_TIME/NominalDiffTime.purs \ 70 | --moduleName Data.Time.NominalDiffTime.Lenses \ 71 | --moduleImports "import Data.Time.Duration (Seconds (..))" \ 72 | > $DIR_TIME_LENSES/NominalDiffTime.purs 73 | 74 | # - - - - - - - - - - - 75 | # View 76 | # - - - - - - - - - - - 77 | 78 | DIR_VIEW=src/Explorer/View 79 | DIR_VIEW_LENSES=$DIR_VIEW/Lenses 80 | 81 | mkdir -p $DIR_VIEW_LENSES 82 | 83 | purescript-derive-lenses \ 84 | < $DIR_VIEW/Types.purs \ 85 | --moduleName Explorer.View.Lenses \ 86 | --moduleImports "import Data.Time.NominalDiffTime (NominalDiffTime(..))" \ 87 | --moduleImports "import Data.Maybe (Maybe)" \ 88 | --moduleImports "import Data.Tuple (Tuple)" \ 89 | --moduleImports "import Pos.Explorer.Web.ClientTypes (CCoin, CAddress, CTxId)" \ 90 | > $DIR_VIEW_LENSES/View.purs 91 | -------------------------------------------------------------------------------- /frontend/src/Control/SocketIO/Client.js: -------------------------------------------------------------------------------- 1 | 2 | exports.connectImpl = function (url) { 3 | return function() { 4 | const s = require('socket.io-client'); 5 | return s(url); 6 | }; 7 | } 8 | 9 | exports.emitImpl = function(socket, eventName) { 10 | // console.log("emit eventName ", eventName); 11 | return function() { 12 | socket.emit(eventName); 13 | }; 14 | } 15 | 16 | exports.emitDataImpl = function(socket, eventName, data) { 17 | // console.log("emit eventName ", eventName); 18 | return function() { 19 | socket.emit(eventName, data); 20 | }; 21 | } 22 | 23 | exports.onImpl = function(socket, eventName, callback) { 24 | // console.log("on eventName ", eventName); 25 | return function() { 26 | socket.on(eventName, function(data) { 27 | callback(data)(); 28 | }); 29 | }; 30 | } 31 | -------------------------------------------------------------------------------- /frontend/src/Control/SocketIO/Client.purs: -------------------------------------------------------------------------------- 1 | module Control.SocketIO.Client where 2 | 3 | import Prelude 4 | import Control.Monad.Eff (kind Effect, Eff) 5 | import Data.Function.Uncurried (Fn2, Fn3, runFn1, runFn2, runFn3) 6 | 7 | foreign import data SocketIO :: Effect 8 | foreign import data Socket :: Type 9 | 10 | type Host = String 11 | type Event = String 12 | 13 | type EventHandler a eff = a -> Eff (socket :: SocketIO | eff) Unit 14 | 15 | foreign import connectImpl :: forall eff. Host -> (Eff (socket :: SocketIO | eff) Socket) 16 | foreign import emitImpl :: forall eff. Fn2 Socket Event (Eff (socket :: SocketIO | eff) Unit) 17 | foreign import emitDataImpl :: forall d eff. Fn3 Socket Event d (Eff (socket :: SocketIO | eff) Unit) 18 | foreign import onImpl :: forall a eff. Fn3 Socket Event (EventHandler a eff) (Eff (socket :: SocketIO | eff) Unit) 19 | 20 | connect :: forall eff. Host -> Eff (socket :: SocketIO | eff) Socket 21 | connect host = runFn1 connectImpl host 22 | 23 | -- | Emits an event without data 24 | emit :: forall eff. Socket -> Event -> Eff (socket :: SocketIO | eff) Unit 25 | emit socket event = runFn2 emitImpl socket event 26 | 27 | -- | Emits an event and data 28 | emitData :: forall d eff. Socket -> Event -> d -> Eff (socket :: SocketIO | eff) Unit 29 | emitData socket event dataObj = runFn3 emitDataImpl socket event dataObj 30 | 31 | on :: forall a eff. Socket -> Event -> (EventHandler a eff) -> Eff (socket :: SocketIO | eff) Unit 32 | on socket event callback = runFn3 onImpl socket event callback 33 | -------------------------------------------------------------------------------- /frontend/src/Data/Time/NominalDiffTime.Test.purs: -------------------------------------------------------------------------------- 1 | module Data.Time.Test where 2 | 3 | import Prelude 4 | import Control.Monad.Aff (Aff) 5 | import Control.Monad.State (StateT) 6 | import Data.Identity (Identity) 7 | import Data.Time.Duration (Seconds(..)) 8 | import Data.Time.NominalDiffTime (NominalDiffTime(..), mkTime, unwrapSeconds) 9 | import Test.Spec (Group, describe, it) 10 | import Test.Spec.Assertions (shouldEqual) 11 | 12 | testNominalDiffTime :: forall r. StateT (Array (Group (Aff r Unit))) Identity Unit 13 | testNominalDiffTime = 14 | describe "Data.Time.NominalDiffTime" do 15 | describe "mkTime" do 16 | it "makes an instance of NominalDiffTime" 17 | let seconds = 12.0 18 | time = mkTime seconds 19 | in time `shouldEqual` (NominalDiffTime $ Seconds seconds) 20 | describe "unwrapSeconds" do 21 | it "unwraps value of Seconds" 22 | let seconds = 12.0 23 | time = mkTime seconds 24 | in unwrapSeconds time `shouldEqual` seconds 25 | -------------------------------------------------------------------------------- /frontend/src/Data/Time/NominalDiffTime.purs: -------------------------------------------------------------------------------- 1 | module Data.Time.NominalDiffTime 2 | ( NominalDiffTime (..) 3 | , mkTime 4 | , unwrapSeconds 5 | ) where 6 | 7 | import Prelude 8 | import Data.Generic (class Generic, gShow) 9 | import Data.Newtype (class Newtype, unwrap) 10 | import Data.Time.Duration (Seconds(..)) 11 | 12 | newtype NominalDiffTime = NominalDiffTime Seconds 13 | 14 | derive instance newtypeNominalDiffTime :: Newtype NominalDiffTime _ 15 | derive instance genericNominalDiffTime :: Generic NominalDiffTime 16 | derive instance eqNominalDiffTime :: Eq NominalDiffTime 17 | derive instance ordNominalDiffTime :: Ord NominalDiffTime 18 | instance showNominalDiffTime :: Show NominalDiffTime where 19 | show = gShow 20 | 21 | mkTime :: Number -> NominalDiffTime 22 | mkTime = NominalDiffTime <<< Seconds 23 | 24 | unwrapSeconds :: NominalDiffTime -> Number 25 | unwrapSeconds = unwrap <<< unwrap 26 | -------------------------------------------------------------------------------- /frontend/src/Explorer/Api/Helper.purs: -------------------------------------------------------------------------------- 1 | module Explorer.Api.Helper where 2 | 3 | import Prelude 4 | import Control.Monad.Eff.Exception (Error, error) 5 | import Data.Argonaut.Core (Json) 6 | import Data.Argonaut.Generic.Aeson (userDecoding, userEncoding) 7 | import Data.Argonaut.Generic.Decode (genericDecodeJson) 8 | import Data.Argonaut.Generic.Encode (genericEncodeJson) 9 | import Data.Argonaut.Generic.Options (Options(..), SumEncoding(..)) 10 | import Data.Argonaut.Generic.Util (stripModulePath) 11 | import Data.Bifunctor (bimap) 12 | import Data.Either (Either(..), either) 13 | import Data.Generic (class Generic) 14 | import Explorer.Api.Types (EndpointError(..)) 15 | 16 | 17 | -- custom encoding 18 | 19 | sumEncoding :: SumEncoding 20 | sumEncoding = 21 | TaggedObject 22 | { tagFieldName: "tag" 23 | , contentsFieldName: "contents" 24 | , unpackRecords: false 25 | } 26 | 27 | options :: Options 28 | options = 29 | Options 30 | { constructorTagModifier: stripModulePath 31 | , allNullaryToStringTag: true 32 | , sumEncoding 33 | , flattenContentsArray: true 34 | , encodeSingleConstructors: false 35 | , userEncoding 36 | , userDecoding 37 | , fieldLabelModifier: id 38 | , omitNothingFields: false 39 | } 40 | 41 | 42 | -- | Encodes JSON using custom Options provided by `options` 43 | encodeJson :: forall a. (Generic a) => a -> Json 44 | encodeJson = genericEncodeJson options 45 | 46 | -- | Decodes JSON using custom Options provided by `options` 47 | decodeJson :: forall a. (Generic a) => Json -> Either String a 48 | decodeJson = genericDecodeJson options 49 | 50 | -- decode result of `http` or `socket` endpoints 51 | 52 | -- | Converts a JSONDecodingError into an error 53 | mkJSONError :: String -> Error 54 | mkJSONError = error <<< show <<< JSONDecodingError 55 | 56 | -- | Decodes result considering JSON and Server errors 57 | decodeResult :: forall a. Generic a => Json -> Either Error a 58 | decodeResult = either (Left <<< mkJSONError) (bimap mkServerError id) <<< decodeJson 59 | where 60 | mkServerError = error <<< show <<< ServerError 61 | 62 | -- | Decodes result considering JSON errors, only 63 | decodeResult' :: forall a. Generic a => Json -> Either Error a 64 | decodeResult' = either (Left <<< mkJSONError) pure <<< decodeJson 65 | -------------------------------------------------------------------------------- /frontend/src/Explorer/Api/Socket.Test.purs: -------------------------------------------------------------------------------- 1 | module Explorer.Api.Socket.Test where 2 | 3 | import Prelude 4 | 5 | import Control.Monad.Aff (Aff) 6 | import Control.Monad.State (StateT) 7 | import Data.Identity (Identity) 8 | import Explorer.Api.Socket (toEvent) 9 | import Pos.Explorer.Socket.Methods (ClientEvent(..), ServerEvent(..)) 10 | import Test.Spec (Group, describe, it) 11 | import Test.Spec.Assertions (shouldEqual) 12 | 13 | 14 | testApiSocket :: forall r. StateT (Array (Group (Aff r Unit))) Identity Unit 15 | testApiSocket = 16 | describe "Explorer.Api.Socket" do 17 | describe "toEvent" do 18 | it "converts a ClientEvent to an 'event' string" do 19 | (toEvent CallMe) `shouldEqual` "CallMe" 20 | it "converts a ServerEvent to an 'event' string " do 21 | (toEvent AddrUpdated) `shouldEqual` "AddrUpdated" 22 | -------------------------------------------------------------------------------- /frontend/src/Explorer/Api/Socket.purs: -------------------------------------------------------------------------------- 1 | module Explorer.Api.Socket where 2 | 3 | import Prelude 4 | import Control.Monad.Eff (Eff) 5 | import Control.SocketIO.Client (Event, Host) 6 | import Data.Argonaut.Core (Json) 7 | import Data.Array (foldl, last) 8 | import Data.Foreign (Foreign) 9 | import Data.Generic (class Generic, gShow) 10 | import Data.Maybe (fromMaybe) 11 | import Data.String (Pattern(..), Replacement(..), replaceAll, split, trim) 12 | import Explorer.Api.Helper (decodeResult') 13 | import Explorer.Types.Actions (Action(..), ActionChannel) 14 | import Explorer.Util.Config (Protocol(..)) 15 | import Pos.Explorer.Socket.Methods (ClientEvent, ServerEvent) 16 | import Signal.Channel (CHANNEL, send) 17 | 18 | 19 | -- | We need to have socket.io on port 8110 when testing (without `https`). 20 | -- When we deploy on production we use nginx forwarding, so we send it to the 21 | -- default port (443) and nginx forwards it to 8110 on the server. 22 | mkSocketHost :: Protocol -> String -> Host 23 | mkSocketHost Http hostname = "http://" <> hostname <> ":8110" 24 | mkSocketHost Https hostname = "https://" <> hostname 25 | 26 | -- events 27 | 28 | class SocketEvent a where 29 | toEvent :: a -> String 30 | 31 | instance socketEventServerEvent :: SocketEvent ClientEvent where 32 | toEvent = showEvent 33 | 34 | instance socketEventClientEvent :: SocketEvent ServerEvent where 35 | toEvent = showEvent 36 | 37 | -- | Helper to grab event names from ClientEvent | ServerEvent 38 | -- | _Note_: We can't create a generic show instances from these types, 39 | -- | because they are generated by purescript-bridge 40 | showEvent :: forall b. (Generic b) => b -> String 41 | showEvent t = 42 | trim <<< getModuleNames <<< split (Pattern " ") $ gShow t 43 | where 44 | getModuleNames m = foldl (\acc mn -> acc <> " " <> getModuleName mn) "" m 45 | getModuleName n = fromMaybe "" <<< last $ split (Pattern ".") n 46 | 47 | 48 | -- | Helper function to remove """ from event names 49 | cleanEventName :: Event -> Event 50 | cleanEventName = replaceAll (Pattern "\"") (Replacement "") 51 | 52 | connectEvent :: Event 53 | connectEvent = "connect" 54 | 55 | closeEvent :: Event 56 | closeEvent = "close" 57 | 58 | -- event handler 59 | 60 | connectHandler :: forall eff. ActionChannel -> Foreign 61 | -> Eff (channel :: CHANNEL | eff) Unit 62 | connectHandler channel _ = 63 | send channel $ SocketConnected true 64 | 65 | closeHandler :: forall eff. ActionChannel -> Foreign 66 | -> Eff (channel :: CHANNEL | eff) Unit 67 | closeHandler channel _ = 68 | send channel $ SocketConnected false 69 | 70 | addressTxsUpdatedEventHandler :: forall eff. ActionChannel -> Json 71 | -> Eff (channel :: CHANNEL | eff) Unit 72 | addressTxsUpdatedEventHandler channel json = 73 | let result = decodeResult' json in 74 | send channel $ SocketAddressTxsUpdated result 75 | 76 | blocksPageUpdatedEventHandler :: forall eff. ActionChannel -> Json 77 | -> Eff (channel :: CHANNEL | eff) Unit 78 | blocksPageUpdatedEventHandler channel json = 79 | let result = decodeResult' json in 80 | send channel $ SocketBlocksPageUpdated result 81 | 82 | txUpdatedHandler :: forall eff. ActionChannel -> Json 83 | -> Eff (channel :: CHANNEL | eff) Unit 84 | txUpdatedHandler channel json = 85 | let result = decodeResult' json in 86 | send channel $ SocketTxUpdated result 87 | 88 | txsUpdatedHandler :: forall eff. ActionChannel -> Json 89 | -> Eff (channel :: CHANNEL | eff) Unit 90 | txsUpdatedHandler channel json = 91 | let result = decodeResult' json in 92 | send channel $ SocketTxsUpdated result 93 | 94 | 95 | callYouEventHandler :: forall eff. ActionChannel -> Foreign -> Eff eff Unit 96 | callYouEventHandler channel _ = 97 | -- just an empty callback to be connected with socket.io 98 | pure unit 99 | -------------------------------------------------------------------------------- /frontend/src/Explorer/Api/Types.purs: -------------------------------------------------------------------------------- 1 | module Explorer.Api.Types where 2 | 3 | import Prelude 4 | 5 | import Data.Argonaut.Core (Json) 6 | import Data.Generic (class Generic, gEq, gShow) 7 | import Data.Newtype (class Newtype) 8 | import Network.HTTP.Affjax (AffjaxResponse) 9 | import Pos.Explorer.Socket.Methods (Subscription) 10 | import Pos.Explorer.Web.ClientTypes (CAddress, CTxId) 11 | import Pos.Explorer.Web.Error (ExplorerError) 12 | 13 | type Endpoint = String 14 | 15 | data EndpointError 16 | = HTTPStatusError (AffjaxResponse Json) 17 | | JSONDecodingError String 18 | | ServerError ExplorerError 19 | 20 | instance showEndpointError :: Show EndpointError where 21 | show (HTTPStatusError res) = 22 | "HTTPStatusError: " <> show res.status <> " msg: " <> show res.response 23 | show (JSONDecodingError e) = 24 | "JSONDecodingError: " <> gShow e 25 | show (ServerError e) = 26 | "ServerError: " <> gShow e 27 | 28 | newtype RequestLimit = RequestLimit Int 29 | newtype RequestOffset = RequestOffset Int 30 | 31 | -- Wrapper of 'Subscription' built by 'purescript bridge' 32 | -- needed to derive generice instances of it 33 | newtype SocketSubscription = SocketSubscription Subscription 34 | derive instance gSocketSubscription :: Generic SocketSubscription 35 | derive instance newtypeSocketSubscription :: Newtype SocketSubscription _ 36 | instance eqSocketSubscription :: Eq SocketSubscription where 37 | eq = gEq 38 | 39 | newtype SocketOffset = SocketOffset Int 40 | derive instance gSocketOffset :: Generic SocketOffset 41 | 42 | -- | Types of socket data, which can be emitted to back end 43 | -- | It can be extended if we will have any other data 44 | data SocketSubscriptionData 45 | = SocketNoData -- no data sending to backend 46 | | SocketOffsetData SocketOffset -- sending value of `SocketOffset` 47 | | SocketCAddressData CAddress -- sending value of `CAddress` 48 | | SocketCTxSummaryData CTxId -- subscribe to `CTxSummary` by sending `CTxId` 49 | 50 | derive instance gSocketSubscriptionData :: Generic SocketSubscriptionData 51 | instance eqSocketSubscriptionData :: Eq SocketSubscriptionData where 52 | eq = gEq 53 | -------------------------------------------------------------------------------- /frontend/src/Explorer/I18n/Lang.js: -------------------------------------------------------------------------------- 1 | 2 | // -- Based on https://github.com/input-output-hk/vending-application/blob/master/web-client/src/Data/I18N.js 3 | 4 | exports.detectLocaleImpl = function() { 5 | return window.navigator.languages ? 6 | window.navigator.languages[0] : 7 | window.navigator.language || window.navigator.userLanguage; 8 | } 9 | -------------------------------------------------------------------------------- /frontend/src/Explorer/I18n/Lang.purs: -------------------------------------------------------------------------------- 1 | -- | Based on https://github.com/input-output-hk/vending-application/blob/master/web-client/src/Data/I18N.purs 2 | module Explorer.I18n.Lang where 3 | 4 | import Prelude 5 | import Control.Monad.Eff (Eff) 6 | import DOM (DOM) 7 | import Data.Lens (Lens', view) 8 | import Data.Maybe (Maybe(..)) 9 | import Data.String (take) 10 | import Explorer.I18n.DE (translation) as DE 11 | import Explorer.I18n.EN (translation) as EN 12 | import Explorer.I18n.JP (translation) as JP 13 | import Explorer.I18n.Types (Translation) 14 | 15 | foreign import detectLocaleImpl :: forall e. Eff (dom :: DOM | e) String 16 | 17 | detectLocale :: forall e. Eff (dom :: DOM | e) (Maybe Language) 18 | detectLocale = readLanguage <<< take 2 <$> detectLocaleImpl 19 | 20 | type I18nLens = Lens' Translation String 21 | 22 | translate :: I18nLens -> Language -> String 23 | translate lens = view lens <<< getTranslation 24 | 25 | -- | ISO 639-1 https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes 26 | readLanguage :: String -> Maybe Language 27 | readLanguage "en" = Just English 28 | readLanguage "English" = Just English 29 | readLanguage "jp" = Just Japanese 30 | readLanguage "日本語" = Just Japanese 31 | readLanguage "de" = Just German 32 | readLanguage "Deutsch" = Just German 33 | readLanguage _ = Nothing 34 | 35 | getTranslation :: Language -> Translation 36 | getTranslation English = EN.translation 37 | getTranslation Japanese = JP.translation 38 | getTranslation German = DE.translation 39 | 40 | data Language 41 | = English 42 | | Japanese 43 | | German 44 | 45 | instance showLanguage :: Show Language where 46 | show = languageNativeName 47 | 48 | derive instance eqLanguage :: Eq Language 49 | 50 | -- | ISO 639 https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes 51 | languageNativeName :: Language -> String 52 | languageNativeName English = "English" 53 | languageNativeName Japanese = "日本語" 54 | languageNativeName German = "Deutsch" 55 | -------------------------------------------------------------------------------- /frontend/src/Explorer/Images.purs: -------------------------------------------------------------------------------- 1 | module Explorer.Images where 2 | 3 | import Prelude ((<>)) 4 | 5 | imagePath :: String -> String 6 | imagePath = (<>) "/images/" 7 | 8 | examplePath :: String 9 | examplePath = imagePath "any-image.jpg" 10 | -------------------------------------------------------------------------------- /frontend/src/Explorer/Routes.Test.purs: -------------------------------------------------------------------------------- 1 | module Explorer.Routes.Test where 2 | 3 | import Prelude 4 | import Control.Monad.Aff (Aff) 5 | import Control.Monad.State (StateT) 6 | import Data.Identity (Identity) 7 | import Explorer.Routes (Route(..), match, paramToString, toUrl) 8 | import Explorer.Util.Factory (mkCAddress, mkCHash, mkCTxId, mkEpochIndex, mkLocalSlotIndex) 9 | import Test.Spec (Group, describe, it) 10 | import Test.Spec.Assertions (shouldEqual) 11 | 12 | testRoutes :: forall r. StateT (Array (Group (Aff r Unit))) Identity Unit 13 | testRoutes = 14 | describe "Explorer.Routes" do 15 | describe "type class RouteParams" do 16 | it "stringified param of an EpochIndex" 17 | let index = mkEpochIndex 101 18 | result = paramToString index 19 | in result `shouldEqual` "101" 20 | it "stringified param of an SlotIndex" 21 | let index = mkLocalSlotIndex 102 22 | result = paramToString index 23 | in result `shouldEqual` "102" 24 | describe "toUrl" do 25 | it "parses url of Dashboard" 26 | let result = toUrl Dashboard 27 | in result `shouldEqual` "/" 28 | it "parses url of Tx" 29 | let result = toUrl <<< Tx $ mkCTxId "1" 30 | in result `shouldEqual` "/tx/1" 31 | it "parses url of Address" 32 | let result = toUrl <<< Address $ mkCAddress "1" 33 | in result `shouldEqual` "/address/1" 34 | it "parses url of Epoch" 35 | let result = toUrl <<< Epoch $ mkEpochIndex 1 36 | in result `shouldEqual` "/epoch/1" 37 | it "parses url of EpochSlot" 38 | let result = toUrl $ EpochSlot (mkEpochIndex 1) (mkLocalSlotIndex 2) 39 | in result `shouldEqual` "/epoch/1/slot/2" 40 | it "parses url of Calculator" 41 | let result = toUrl Calculator 42 | in result `shouldEqual` "/calculator" 43 | it "parses url of Block" 44 | let result = toUrl <<< Block $ mkCHash "1" 45 | in result `shouldEqual` "/slot/1" 46 | it "parses url of NotFound" 47 | let result = toUrl NotFound 48 | in result `shouldEqual` "/404" 49 | describe "match" do 50 | it "Dashboard" 51 | let url = "/" 52 | result = match url 53 | in toUrl result `shouldEqual` url 54 | it "Tx" 55 | let url = "/tx/1" 56 | result = match url 57 | in toUrl result `shouldEqual` url 58 | it "Address" 59 | let url = "/address/1" 60 | result = match url 61 | in toUrl result `shouldEqual` url 62 | it "Epoch" 63 | let url = "/epoch/1" 64 | result = match url 65 | in toUrl result `shouldEqual` url 66 | it "EpochSlot" 67 | let url = "/epoch/1/slot/2" 68 | result = match url 69 | in toUrl result `shouldEqual` url 70 | it "Calculator" 71 | let url = "/calculator" 72 | result = match url 73 | in (toUrl result) `shouldEqual` url 74 | it "Block" 75 | let url = "/slot/1" 76 | result = match url 77 | in toUrl result `shouldEqual` url 78 | it "NotFound" 79 | let url = "/xyz" 80 | result = match url 81 | in toUrl result `shouldEqual` "/404" 82 | -------------------------------------------------------------------------------- /frontend/src/Explorer/State.purs: -------------------------------------------------------------------------------- 1 | module Explorer.State where 2 | 3 | import Prelude 4 | 5 | import DOM.Node.Types (ElementId(..)) 6 | import Data.DateTime.Instant (instant, toDateTime) 7 | import Data.Maybe (Maybe(..), fromJust) 8 | import Data.Time.Duration (Milliseconds(..)) 9 | import Data.Tuple (Tuple(..)) 10 | import Explorer.Api.Types (SocketSubscription, SocketSubscriptionData) 11 | import Explorer.I18n.Lang (Language(..)) 12 | import Explorer.Routes (Route(..)) 13 | import Explorer.Types.State (DashboardAPICode(..), GenesisBlockPagination(..), PageNumber(..), Search(..), SearchEpochSlotQuery, SocketSubscriptionItem(..), State) 14 | import Explorer.Util.Config (SyncAction(..)) 15 | import Explorer.Util.Factory (mkCAddress) 16 | import Network.RemoteData (RemoteData(..)) 17 | import Partial.Unsafe (unsafePartial) 18 | 19 | initialState :: State 20 | initialState = 21 | { lang: English 22 | , route: Dashboard 23 | , socket: 24 | { connected: false 25 | , connection: Nothing 26 | , subscriptions: [] 27 | } 28 | , syncAction: SyncBySocket 29 | -- , syncAction: SyncByPolling 30 | , viewStates: 31 | { globalViewState: 32 | { gViewMobileMenuOpenend: false 33 | , gViewSearchInputFocused: false 34 | , gViewSelectedSearch: SearchAddress 35 | , gViewSearchQuery: emptySearchQuery 36 | , gViewSearchTimeQuery: emptySearchTimeQuery 37 | , gWaypoints: [] 38 | } 39 | , dashboard: 40 | { dbViewBlocksExpanded: false 41 | , dbViewBlockPagination: PageNumber minPagination 42 | , dbViewMaxBlockPagination: NotAsked 43 | , dbViewLoadingBlockPagination: false 44 | , dbViewBlockPaginationEditable: false 45 | , dbViewTxsExpanded: false 46 | , dbViewSelectedApiCode: Curl 47 | } 48 | , addressDetail: 49 | { addressTxPagination: PageNumber minPagination 50 | , addressTxPaginationEditable: false 51 | } 52 | , blockDetail: 53 | { blockTxPagination: PageNumber minPagination 54 | , blockTxPaginationEditable: false 55 | } 56 | , blocksViewState: 57 | { blsViewPagination: PageNumber minPagination 58 | , blsViewPaginationEditable: false 59 | } 60 | , genesisBlockViewState: 61 | { gblAddressInfosPagination: GBPaginateAllAddresses 62 | , gblAddressInfosPageNumber: PageNumber minPagination 63 | , gblAddressInfosMaxPageNumber: PageNumber minPagination 64 | , gblAddressInfosPaginationEditable: false 65 | , gblLoadingAddressInfosPagination: false 66 | } 67 | } 68 | , latestBlocks: NotAsked 69 | , currentBlockSummary: NotAsked 70 | , currentBlockTxs: NotAsked 71 | , latestTransactions: NotAsked 72 | , currentTxSummary: NotAsked 73 | , currentCAddress: mkCAddress "" 74 | , currentAddressSummary: NotAsked 75 | , currentBlocksResult: NotAsked 76 | , currentCGenesisSummary: NotAsked 77 | , currentCGenesisAddressInfos: NotAsked 78 | , errors: [] 79 | , loading: false 80 | , now: toDateTime $ unsafePartial $ fromJust $ instant $ Milliseconds 0.0 81 | , testnet: false 82 | } 83 | 84 | -- all constants are following here: 85 | 86 | emptySearchQuery :: String 87 | emptySearchQuery = "" 88 | 89 | emptySearchTimeQuery :: SearchEpochSlotQuery 90 | emptySearchTimeQuery = Tuple Nothing Nothing 91 | 92 | maxSlotInEpoch :: Int 93 | maxSlotInEpoch = 21600 94 | 95 | minPagination :: Int 96 | minPagination = 1 -- Note: We do start with 1 (not 0) 97 | 98 | addressQRImageId :: String 99 | addressQRImageId = "qr_image_id" 100 | 101 | heroSearchContainerId :: ElementId 102 | heroSearchContainerId = ElementId "heroSearchContainerId" 103 | 104 | headerSearchContainerId :: ElementId 105 | headerSearchContainerId = ElementId "headerSearchContainerId" 106 | 107 | mobileMenuSearchContainerId :: ElementId 108 | mobileMenuSearchContainerId = ElementId "mobileMenuSearchContainerId" 109 | 110 | mkSocketSubscriptionItem :: SocketSubscription -> SocketSubscriptionData -> SocketSubscriptionItem 111 | mkSocketSubscriptionItem socketSub socketSubData = SocketSubscriptionItem 112 | { socketSub 113 | , socketSubData 114 | } 115 | -------------------------------------------------------------------------------- /frontend/src/Explorer/Types/App.purs: -------------------------------------------------------------------------------- 1 | module Explorer.Types.App where 2 | 3 | import Control.Monad.Eff.Console (CONSOLE) 4 | import Control.Monad.Eff.Now (NOW) 5 | import Control.SocketIO.Client (SocketIO) 6 | import DOM (DOM) 7 | import DOM.HTML.Types (HISTORY) 8 | import Network.HTTP.Affjax (AJAX) 9 | import Waypoints (WAYPOINT) 10 | 11 | type AppEffects eff = 12 | ( dom :: DOM 13 | , ajax :: AJAX 14 | , socket :: SocketIO 15 | , now :: NOW 16 | , waypoint :: WAYPOINT 17 | , history :: HISTORY 18 | , console :: CONSOLE 19 | | eff 20 | ) 21 | -------------------------------------------------------------------------------- /frontend/src/Explorer/Util/Config.Test.purs: -------------------------------------------------------------------------------- 1 | module Explorer.Util.Config.Test where 2 | 3 | import Prelude 4 | import Control.Monad.Aff (Aff) 5 | import Control.Monad.State (StateT) 6 | import Data.Identity (Identity) 7 | import Explorer.Util.Config (SyncAction(..), isTestnet, syncByPolling, syncBySocket) 8 | import Test.Spec (Group, describe, it) 9 | import Test.Spec.Assertions (shouldEqual) 10 | 11 | 12 | testConfigUtil :: forall r. StateT (Array (Group (Aff r Unit))) Identity Unit 13 | testConfigUtil = 14 | describe "Explorer.Util.Config" do 15 | describe "syncBySocket" do 16 | it "should be true" do 17 | (syncBySocket SyncBySocket) `shouldEqual` true 18 | it "should be false" do 19 | (syncBySocket SyncByPolling) `shouldEqual` false 20 | describe "syncByPolling" do 21 | it "should be true" do 22 | (syncByPolling SyncByPolling) `shouldEqual` true 23 | it "should be false" do 24 | (syncByPolling SyncBySocket) `shouldEqual` false 25 | describe "isTestnet" do 26 | it "should be true" do 27 | (isTestnet "https://www.testnet.cardanoexplorer.com/") `shouldEqual` true 28 | it "should be false" do 29 | (isTestnet "https://www.cardanoexplorer.com") `shouldEqual` false 30 | -------------------------------------------------------------------------------- /frontend/src/Explorer/Util/Config.js: -------------------------------------------------------------------------------- 1 | exports.versionImpl = $VERSION; // set by webpack 2 | exports.commitHashImpl = $COMMIT_HASH; // set by webpack 3 | exports.isProductionImpl = $PRODUCTION; // set by webpack 4 | -------------------------------------------------------------------------------- /frontend/src/Explorer/Util/Config.purs: -------------------------------------------------------------------------------- 1 | module Explorer.Util.Config where 2 | 3 | import Prelude 4 | 5 | import Control.Monad.Eff (Eff) 6 | import DOM (DOM) 7 | import DOM.HTML (window) 8 | import DOM.HTML.Location (hostname) as L 9 | import DOM.HTML.Window (location) 10 | import Data.Generic (class Generic, gEq, gShow) 11 | import Data.Maybe (isJust) 12 | import Data.String.Regex (search) 13 | import Data.String.Regex.Flags (noFlags) 14 | import Data.String.Regex.Unsafe (unsafeRegex) 15 | 16 | foreign import versionImpl :: String 17 | 18 | version :: String 19 | version = versionImpl 20 | 21 | testNetVersion :: String 22 | testNetVersion = "0.5" 23 | 24 | foreign import commitHashImpl :: String 25 | 26 | commitHash :: String 27 | commitHash = commitHashImpl 28 | 29 | foreign import isProductionImpl :: Boolean 30 | 31 | isProduction :: Boolean 32 | isProduction = isProductionImpl 33 | 34 | hostname :: forall eff. Eff (dom :: DOM | eff) String 35 | hostname = window >>= location >>= L.hostname 36 | 37 | data Protocol = Http | Https 38 | 39 | secureProtocol :: Boolean -> Protocol 40 | secureProtocol true = Https 41 | secureProtocol false = Http 42 | 43 | data SyncAction = SyncByPolling | SyncBySocket 44 | derive instance gSyncAction :: Generic SyncAction 45 | instance eqSyncAction :: Eq SyncAction where 46 | eq = gEq 47 | instance showSyncAction :: Show SyncAction where 48 | show = gShow 49 | 50 | syncBySocket :: SyncAction -> Boolean 51 | syncBySocket = (==) SyncBySocket 52 | 53 | syncByPolling :: SyncAction -> Boolean 54 | syncByPolling = (==) SyncByPolling 55 | 56 | isTestnet :: String -> Boolean 57 | isTestnet location = 58 | isJust $ search (unsafeRegex "testnet\\." noFlags) location 59 | -------------------------------------------------------------------------------- /frontend/src/Explorer/Util/DOM.js: -------------------------------------------------------------------------------- 1 | 2 | exports.scrollTopImpl = function() { 3 | return window.scrollTo(0, 0); 4 | } 5 | 6 | exports.classList = function (element) { 7 | return function () { 8 | return element.classList; 9 | }; 10 | }; 11 | 12 | exports.addClassImpl = function(clazzList, clazz) { 13 | return clazzList.add(clazz); 14 | } 15 | 16 | exports.removeClassImpl = function(clazzList, clazz) { 17 | return clazzList.remove(clazz); 18 | } 19 | -------------------------------------------------------------------------------- /frontend/src/Explorer/Util/DOM.purs: -------------------------------------------------------------------------------- 1 | module Explorer.Util.DOM 2 | ( addClass 3 | , addClassToElement 4 | , classList 5 | , enterKeyPressed 6 | , eventToKeyPressed 7 | , findElementById 8 | , removeClass 9 | , removeClassFromElement 10 | , scrollTop 11 | , nodeToHTMLElement 12 | , nodeToHTMLInputElement 13 | ) where 14 | 15 | import Prelude 16 | 17 | import Control.Monad.Eff (Eff) 18 | import Control.Monad.Eff.Uncurried (EffFn2, runEffFn2) 19 | import Control.Monad.Except (runExcept) 20 | import DOM (DOM) 21 | import DOM.Event.KeyboardEvent (eventToKeyboardEvent, key) 22 | import DOM.HTML (window) 23 | import DOM.HTML.Types (HTMLElement, HTMLInputElement, htmlDocumentToNonElementParentNode) 24 | import DOM.HTML.Window (document) 25 | import DOM.Node.NonElementParentNode (getElementById) 26 | import DOM.Node.Types (DOMTokenList, Element, ElementId, Node) 27 | import Data.Either (either) 28 | import Data.Maybe (Maybe(..)) 29 | import Pux.DOM.Events (DOMEvent) 30 | import Unsafe.Coerce (unsafeCoerce) 31 | 32 | foreign import scrollTopImpl :: forall eff. Eff (dom :: DOM | eff) Unit 33 | 34 | -- | Helper function to scroll to top of a HTML page 35 | scrollTop :: forall eff. Eff (dom :: DOM | eff) Unit 36 | scrollTop = scrollTopImpl 37 | 38 | -- | Converts a `Node` to `HTMLInputElement` 39 | nodeToHTMLInputElement :: Node -> HTMLInputElement 40 | nodeToHTMLInputElement = unsafeCoerce 41 | 42 | -- | Converts a `Node` to `HTMLElement` 43 | nodeToHTMLElement :: Node -> HTMLElement 44 | nodeToHTMLElement = unsafeCoerce 45 | 46 | -- | Helper function get an `getElementById` from document 47 | findElementById :: forall eff. ElementId -> Eff (dom :: DOM | eff) (Maybe Element) 48 | findElementById id' = do 49 | el <- window >>= 50 | document >>= 51 | getElementById id' <<< htmlDocumentToNonElementParentNode 52 | pure el 53 | 54 | -- | Returns a `classList` from `Element` 55 | foreign import classList :: forall eff. Element -> Eff (dom :: DOM | eff) DOMTokenList 56 | 57 | 58 | foreign import addClassImpl :: forall eff. EffFn2 (dom :: DOM | eff) DOMTokenList String Unit 59 | -- | Adds a single `CSS` class to a `classList` 60 | addClass :: forall eff. DOMTokenList -> String -> Eff (dom :: DOM | eff) Unit 61 | addClass = runEffFn2 addClassImpl 62 | 63 | 64 | foreign import removeClassImpl :: forall eff. EffFn2 (dom :: DOM | eff) DOMTokenList String Unit 65 | -- | Removes a single `CSS` class from a `classList` 66 | removeClass :: forall eff. DOMTokenList -> String -> Eff (dom :: DOM | eff) Unit 67 | removeClass = runEffFn2 removeClassImpl 68 | 69 | -- | Helper function to add a single `CSS` class to a `classList` of an HTML element 70 | addClassToElement :: forall eff. ElementId -> String -> Eff (dom :: DOM | eff) Unit 71 | addClassToElement elemId clazz = do 72 | el <- findElementById elemId 73 | case el of 74 | Just el' -> do 75 | cL <- classList el' 76 | addClass cL clazz 77 | Nothing -> 78 | pure unit 79 | 80 | -- | Helper function to remove a single `CSS` class from a `classList` of an HTML element 81 | removeClassFromElement :: forall eff. ElementId -> String -> Eff (dom :: DOM | eff) Unit 82 | removeClassFromElement elemId clazz = do 83 | el <- findElementById elemId 84 | case el of 85 | Just el' -> do 86 | cL <- classList el' 87 | removeClass cL clazz 88 | Nothing -> 89 | pure unit 90 | 91 | eventToKeyPressed :: DOMEvent -> String 92 | eventToKeyPressed ev = either (const "") key $ runExcept $ eventToKeyboardEvent ev 93 | 94 | enterKeyPressed :: DOMEvent -> Boolean 95 | enterKeyPressed event = 96 | (eventToKeyPressed event) == "Enter" 97 | -------------------------------------------------------------------------------- /frontend/src/Explorer/Util/Data.purs: -------------------------------------------------------------------------------- 1 | module Explorer.Util.Data where 2 | 3 | import Prelude 4 | import Data.Array (reverse, sortBy, unionBy) 5 | import Data.Lens ((^.)) 6 | import Data.Maybe (fromMaybe) 7 | import Data.Time.NominalDiffTime (mkTime, unwrapSeconds) 8 | import Explorer.State (maxSlotInEpoch) 9 | import Explorer.Types.State (CBlockEntries, CTxEntries) 10 | import Pos.Explorer.Web.ClientTypes (CBlockEntry(..), CTxEntry(..)) 11 | import Pos.Explorer.Web.Lenses.ClientTypes (_CBlockEntry, _CHash, _CTxEntry, _CTxId, cbeBlkHash, cbeEpoch, cbeSlot, cbeTimeIssued, cteId, cteTimeIssued) 12 | 13 | -- | Sort a list of CBlockEntry by time in an ascending (up) order 14 | sortBlocksByTime :: CBlockEntries -> CBlockEntries 15 | sortBlocksByTime blocks = 16 | sortBy (comparing time) blocks 17 | where 18 | time :: CBlockEntry -> Number 19 | time (CBlockEntry entry) = 20 | unwrapSeconds <<< fromMaybe (mkTime 0.0) $ entry ^. cbeTimeIssued 21 | 22 | -- | Sort a list of CBlockEntry by time in a descending (down) order 23 | sortBlocksByTime' :: CBlockEntries -> CBlockEntries 24 | sortBlocksByTime' = 25 | reverse <<< sortBlocksByTime 26 | 27 | 28 | -- | Sort CBlockEntries by epochs and slots an ascending (up) order 29 | sortBlocksByEpochSlot :: CBlockEntries -> CBlockEntries 30 | sortBlocksByEpochSlot blocks = 31 | sortBy (comparing epochsAndSlots) blocks 32 | where 33 | epochsAndSlots :: CBlockEntry -> Int 34 | epochsAndSlots (CBlockEntry entry) = 35 | ((entry ^. cbeEpoch) * maxSlotInEpoch) + (entry ^. cbeSlot) 36 | 37 | -- | Sort CBlockEntries by epochs and slots a descending (down) order 38 | sortBlocksByEpochSlot' :: CBlockEntries -> CBlockEntries 39 | sortBlocksByEpochSlot' = 40 | reverse <<< sortBlocksByEpochSlot 41 | 42 | 43 | -- | Helper to remove duplicates of blocks by comparing its CHash 44 | -- | 45 | -- | _Note:_ To "union" current with new blocks we have to compare CBlockEntry 46 | -- | Because we don't have an Eq instance of generated CBlockEntry's 47 | -- | As a workaround we do have to compare CBlockEntry by its hash 48 | unionBlocks :: CBlockEntries -> CBlockEntries -> CBlockEntries 49 | unionBlocks blocksA blocksB = 50 | unionBy (\b1 b2 -> getHash b1 == getHash b2) blocksA blocksB 51 | where 52 | getHash :: CBlockEntry -> String 53 | getHash block = block ^. (_CBlockEntry <<< cbeBlkHash <<< _CHash) 54 | 55 | 56 | 57 | -- | Sort a list of `CTxEntries` by time in an ascending (up) order 58 | sortTxsByTime :: CTxEntries -> CTxEntries 59 | sortTxsByTime txs = 60 | sortBy (comparing time) txs 61 | where 62 | time :: CTxEntry -> Number 63 | time (CTxEntry entry) = 64 | unwrapSeconds $ entry ^. cteTimeIssued 65 | 66 | -- | Sort a list of `CTxEntry` by time in a descending (down) order 67 | sortTxsByTime' :: CTxEntries -> CTxEntries 68 | sortTxsByTime' = 69 | reverse <<< sortTxsByTime 70 | 71 | -- | Helper to remove duplicates of blocks by comparing its CHash 72 | -- | 73 | -- | Note: To "union" current with new `txs` we have to compare CTxEntry 74 | -- | Because we don't have an Eq instance of generated CTxEntry's 75 | -- | As a workaround we do have to compare CTxEntry by its id 76 | unionTxs :: CTxEntries -> CTxEntries -> CTxEntries 77 | unionTxs = 78 | unionBy (\tx1 tx2 -> getId tx1 == getId tx2) 79 | where 80 | getId tx = tx ^. (_CTxEntry <<< cteId <<< _CTxId <<< _CHash) 81 | -------------------------------------------------------------------------------- /frontend/src/Explorer/Util/Factory.purs: -------------------------------------------------------------------------------- 1 | module Explorer.Util.Factory where 2 | 3 | import Prelude 4 | import Pos.Core.Types (EpochIndex(..), LocalSlotIndex(..)) 5 | import Pos.Explorer.Web.ClientTypes (CAddress(..), CCoin(..), CHash(..), CTxId(..)) 6 | 7 | 8 | mkCHash :: String -> CHash 9 | mkCHash = CHash 10 | 11 | mkCTxId :: String -> CTxId 12 | mkCTxId = 13 | CTxId <<< mkCHash 14 | 15 | mkCoin :: String -> CCoin 16 | mkCoin coin = 17 | CCoin {getCoin: coin} 18 | 19 | mkCAddress :: String -> CAddress 20 | mkCAddress = CAddress 21 | 22 | mkEpochIndex :: Int -> EpochIndex 23 | mkEpochIndex index = EpochIndex {getEpochIndex: index} 24 | 25 | mkLocalSlotIndex :: Int -> LocalSlotIndex 26 | mkLocalSlotIndex index = LocalSlotIndex {getSlotIndex: index} 27 | -------------------------------------------------------------------------------- /frontend/src/Explorer/Util/QRCode.purs: -------------------------------------------------------------------------------- 1 | module Explorer.Util.QrCode 2 | ( mkQRCode 3 | , generateQrCode 4 | , QRCode 5 | ) where 6 | 7 | import DOM (DOM) 8 | import Control.Monad.Eff (Eff) 9 | import Control.Monad.Eff.Uncurried (EffFn1, EffFn2, runEffFn1, runEffFn2) 10 | import Prelude (Unit, bind, pure, unit) 11 | 12 | data QRCode 13 | 14 | type Config = 15 | { text :: String 16 | , width :: Int 17 | , height :: Int 18 | , colorDark :: String 19 | , colorLight :: String 20 | , typeNumber :: Int 21 | } 22 | 23 | defaultConfig :: Config 24 | defaultConfig = 25 | { text: "" 26 | , width: 256 27 | , height: 256 28 | , colorDark: "#000000" 29 | , colorLight: "#FFFFFF" 30 | , typeNumber: 4 31 | } 32 | 33 | foreign import clearDomNodeImpl :: forall eff. EffFn1 (dom :: DOM | eff) String Unit 34 | foreign import mkQRCodeImpl :: forall eff. EffFn2 (dom :: DOM | eff) String Config QRCode 35 | foreign import clearImpl :: forall eff. EffFn1 (dom :: DOM | eff) QRCode QRCode 36 | 37 | 38 | clearDomNode :: forall eff. String -> Eff (dom :: DOM | eff) Unit 39 | clearDomNode = runEffFn1 clearDomNodeImpl 40 | 41 | mkQRCode :: forall eff. String -> Config -> Eff (dom :: DOM | eff) QRCode 42 | mkQRCode = runEffFn2 mkQRCodeImpl 43 | 44 | clear :: forall eff. QRCode -> Eff (dom :: DOM | eff) QRCode 45 | clear = runEffFn1 clearImpl 46 | 47 | generateQrCode :: forall eff. String -> String -> Eff (dom :: DOM | eff) Unit 48 | generateQrCode text id = do 49 | _ <- clearDomNode id 50 | qrCode <- mkQRCode id defaultConfig { text = text, width = 96, height = 96 } 51 | pure unit 52 | -------------------------------------------------------------------------------- /frontend/src/Explorer/Util/String.Test.purs: -------------------------------------------------------------------------------- 1 | module Explorer.Util.String.Test where 2 | 3 | import Prelude 4 | import Control.Monad.Aff (Aff) 5 | import Control.Monad.State (StateT) 6 | import Data.Either (Either(..)) 7 | import Data.Identity (Identity) 8 | import Data.Maybe (Maybe(..)) 9 | import Data.Tuple (Tuple(..)) 10 | import Explorer.I18n.Lang (Language(..)) 11 | import Explorer.Util.Factory (mkCoin) 12 | import Explorer.Util.String (formatADA, substitute, parseSearchEpoch) 13 | import Test.Spec (Group, describe, it) 14 | import Test.Spec.Assertions (shouldEqual) 15 | 16 | testStringUtil :: forall r. StateT (Array (Group (Aff r Unit))) Identity Unit 17 | testStringUtil = 18 | describe "Explorer.Util.String" do 19 | describe "substitute" do 20 | it "takes all two arguments" do 21 | let result = substitute "Hello {0}, what's going on {1}?" ["Jane", "today"] 22 | expected = "Hello Jane, what's going on today?" 23 | result `shouldEqual` expected 24 | it "takes the third argument only" do 25 | let result = substitute "Hello {2}" ["foo", "bar", "baz"] 26 | expected = "Hello baz" 27 | result `shouldEqual` expected 28 | it "ignores all extra arguments" do 29 | let result = substitute "Hello {0}, what's going on?" ["foo", "bar", "baz"] 30 | expected = "Hello foo, what's going on?" 31 | result `shouldEqual` expected 32 | it "ignores placeholder if an argument is missing" do 33 | let result = substitute "Hello {0}, let's go now" [] 34 | expected = "Hello , let's go now" 35 | result `shouldEqual` expected 36 | describe "query parser" do 37 | it "parses an epoch" do 38 | let result = Right $ Tuple (Just 3) Nothing 39 | expected = parseSearchEpoch "3" 40 | result `shouldEqual` expected 41 | it "parses an epoch and raises error" do 42 | let result = Right $ Tuple Nothing Nothing 43 | expected = parseSearchEpoch "d" 44 | result `shouldEqual` expected 45 | it "parses an epoch and a slot" do 46 | let result = Right $ Tuple (Just 583) (Just 12) 47 | expected = parseSearchEpoch "583,12" 48 | result `shouldEqual` expected 49 | it "parses an epoch and a slot and raises error" do 50 | let result = Right $ Tuple (Just 583) Nothing 51 | expected = parseSearchEpoch "583,aa" 52 | result `shouldEqual` expected 53 | describe "formatADA" do 54 | it "formats big number of lovelaces using EN format" do 55 | let result = formatADA (mkCoin "123456789123456789") English 56 | result `shouldEqual` "123,456,789,123.456789" 57 | it "formats zero lovelaces using EN format" do 58 | let result = formatADA (mkCoin "0") English 59 | result `shouldEqual` "0.000000" 60 | it "formats big number of lovelaces using DE format" do 61 | let result = formatADA (mkCoin "123456789123456789") German 62 | result `shouldEqual` "123.456.789.123,456789" 63 | it "formats zero lovelaces using DE format" do 64 | let result = formatADA (mkCoin "0") German 65 | result `shouldEqual` "0,000000" 66 | it "formats big number of lovelaces using JP format" do 67 | let result = formatADA (mkCoin "123456789123456789") Japanese 68 | result `shouldEqual` "123,456,789,123.456789" 69 | it "formats zero lovelaces using JP format" do 70 | let result = formatADA (mkCoin "0") Japanese 71 | result `shouldEqual` "0.000000" 72 | -------------------------------------------------------------------------------- /frontend/src/Explorer/Util/String.js: -------------------------------------------------------------------------------- 1 | 2 | // Based on sformat by underscore.string 3 | // https://github.com/prantlf/underscore.string/blob/sformat/lib/underscore.string.js#L192 4 | exports.substituteImpl = function(str) { 5 | return function(args) { 6 | return str.replace(/\{\{|\}\}|\{(\d+)\}/g, function (match, group) { 7 | var value = args[parseInt(group, 10)]; 8 | return value ? value.toString() : ""; 9 | }); 10 | }; 11 | }; 12 | -------------------------------------------------------------------------------- /frontend/src/Explorer/Util/String.purs: -------------------------------------------------------------------------------- 1 | module Explorer.Util.String 2 | ( substitute 3 | , parseSearchEpoch 4 | , formatADA 5 | ) where 6 | 7 | import BigNumber (BigNumberFormat(..), defaultFormat, dividedByInt, toFormat, fromString) as BN 8 | import Control.Alt ((<|>)) 9 | import Control.Monad.Eff.Unsafe (unsafePerformEff) 10 | import Data.Array (many) 11 | import Data.Either (Either) 12 | import Data.Int (fromString) 13 | import Data.Lens ((^.)) 14 | import Data.Maybe (Maybe(..)) 15 | import Data.Newtype (unwrap) 16 | import Data.String (fromCharArray) 17 | import Data.Tuple (Tuple(..)) 18 | import Explorer.I18n.Lang (Language, translate) 19 | import Explorer.I18n.Lenses (common, cDecimalSeparator, cGroupSeparator) as I18nL 20 | import Explorer.Types.State (SearchEpochSlotQuery) 21 | import Pos.Explorer.Web.ClientTypes (CCoin) 22 | import Pos.Explorer.Web.Lenses.ClientTypes (getCoin, _CCoin) 23 | import Text.Parsing.Parser (Parser, ParseError, runParser) 24 | import Text.Parsing.Parser.Combinators (try) 25 | import Text.Parsing.Parser.String (char) 26 | import Text.Parsing.Parser.Token (digit) 27 | import Prelude hiding (between,when) 28 | 29 | foreign import substituteImpl :: String -> Array String -> String 30 | 31 | -- | Substitutes `{0}` placeholders of a string 32 | -- | * `substitute "Hello {0}, what's going on {1}" ["Jane", "today"]` 33 | -- | * `-- output: "Hello Jane, what's going on today"` 34 | substitute :: String -> Array String -> String 35 | substitute = substituteImpl 36 | 37 | -- | A simple parser for the epoch: 38 | -- | ```purescript 39 | -- | parseSearchEpochSlotQuery "256" 40 | -- | ``` 41 | parseSearchEpochQuery :: Parser String SearchEpochSlotQuery 42 | parseSearchEpochQuery = do 43 | epoch <- many digit >>= pure <<< fromString <<< fromCharArray 44 | pure $ Tuple epoch Nothing 45 | 46 | -- | A simple parser for the epoch and slot: 47 | -- | ```purescript 48 | -- | parseSearchEpochSlotQuery "256,12" 49 | -- | ``` 50 | parseSearchEpochSlotQuery :: Parser String SearchEpochSlotQuery 51 | parseSearchEpochSlotQuery = do 52 | epoch <- many digit >>= pure <<< fromString <<< fromCharArray 53 | _ <- char ',' 54 | slot <- many digit >>= pure <<< fromString <<< fromCharArray 55 | pure $ Tuple epoch slot 56 | 57 | -- | Combine both parsers 58 | parseEpochOrEpochSlot :: Parser String SearchEpochSlotQuery 59 | parseEpochOrEpochSlot = try parseSearchEpochSlotQuery <|> parseSearchEpochQuery 60 | 61 | -- | Run the actual parser 62 | parseSearchEpoch :: String -> Either ParseError SearchEpochSlotQuery 63 | parseSearchEpoch input = runParser input parseEpochOrEpochSlot 64 | 65 | formatADA :: CCoin -> Language -> String 66 | formatADA coin lang = 67 | case BN.fromString $ coin ^. (_CCoin <<< getCoin) of 68 | Nothing -> "" 69 | Just bigNumber -> do 70 | let bigNumberADA = BN.dividedByInt bigNumber lovelacesADA 71 | unsafePerformEff $ BN.toFormat bigNumberADA newFormat decimalPlacesADA 72 | where 73 | decimalPlacesADA = 6 74 | lovelacesADA = 1000000 75 | newFormat = BN.BigNumberFormat $ (unwrap BN.defaultFormat) 76 | { decimalSeparator = translate (I18nL.common <<< I18nL.cDecimalSeparator) lang 77 | , groupSeparator = translate (I18nL.common <<< I18nL.cGroupSeparator) lang 78 | } 79 | -------------------------------------------------------------------------------- /frontend/src/Explorer/Util/Time.Test.purs: -------------------------------------------------------------------------------- 1 | module Explorer.Util.Time.Test where 2 | 3 | import Prelude 4 | import Control.Monad.Aff (Aff) 5 | import Control.Monad.State (StateT) 6 | import Data.Identity (Identity) 7 | import Data.Maybe (fromMaybe) 8 | import Data.Time.Duration (Days(Days), Milliseconds(Milliseconds), Minutes(Minutes), Seconds(Seconds)) 9 | import Data.Time.NominalDiffTime (mkTime) 10 | import Explorer.I18n.Lang (Language(..)) 11 | import Explorer.Util.Time (prettyDuration, prettyDate) 12 | import Test.Spec (Group, describe, it) 13 | import Test.Spec.Assertions (shouldEqual) 14 | 15 | testPrettyDuration :: forall r. StateT (Array (Group (Aff r Unit))) Identity Unit 16 | testPrettyDuration = 17 | describe "Explorer.Util.Time.Test" do 18 | describe "prettyDuration" do 19 | describe "short durations" do 20 | it "a milisecond should return less than a minute" do 21 | let result = prettyDuration English (Milliseconds 1.0) 22 | expected = "< 1 Minutes" 23 | result `shouldEqual` expected 24 | it "a second should return less than a minute" do 25 | let result = prettyDuration English (Seconds 1.0) 26 | expected = "< 1 Minutes" 27 | result `shouldEqual` expected 28 | it "59 seconds should return less than a minute" do 29 | let result = prettyDuration English (Seconds 59.0) 30 | expected = "< 1 Minutes" 31 | result `shouldEqual` expected 32 | describe "durations" do 33 | it "30 minutes should return 30 minutes" do 34 | let result = prettyDuration English (Minutes 30.0) 35 | expected = "30 Minutes" 36 | result `shouldEqual` expected 37 | describe "longer durations" do 38 | it "5 days should return 5 days" do 39 | let result = prettyDuration English (Days 5.0) 40 | expected = "5 Days" 41 | result `shouldEqual` expected 42 | describe "prettyDate" do 43 | it "format date of DD.MM.YYYY HH:mm,ss" do 44 | let result = prettyDate "DD.MM.YYYY HH:mm,ss" $ mkTime 1490098347.0 45 | "21.03.2017 12:12,27" `shouldEqual` fromMaybe "failed" result 46 | it "format date of MM/DD/YYYY HH:mm,ss" do 47 | let result = prettyDate "DD.MM.YYYY HH:mm.ss" $ mkTime 1490098689.0 48 | "21.03.2017 12:18.09" `shouldEqual` fromMaybe "failed" result 49 | -------------------------------------------------------------------------------- /frontend/src/Explorer/Util/Time.purs: -------------------------------------------------------------------------------- 1 | module Explorer.Util.Time ( 2 | prettyDuration 3 | , nominalDiffTimeToDateTime 4 | , prettyDate 5 | ) where 6 | 7 | import Prelude 8 | import Data.DateTime (DateTime) 9 | import Data.DateTime.Instant (instant, toDateTime) 10 | import Data.Either (either) 11 | import Data.Formatter.DateTime (formatDateTime) 12 | import Data.Int (floor, toNumber) 13 | import Data.Maybe (Maybe(..)) 14 | import Data.Newtype (un) 15 | import Data.String (joinWith, trim) 16 | import Data.Time.Duration (class Duration, Days(..), Hours(..), Minutes(..), convertDuration, fromDuration) 17 | import Data.Time.NominalDiffTime (NominalDiffTime(..)) 18 | import Data.Tuple (uncurry, Tuple(..)) 19 | import Explorer.I18n.Lang (Language, translate) 20 | import Explorer.I18n.Lenses (common, cDays, cHours, cMinutes) as I18nL 21 | 22 | -- show readable duration 23 | prettyDuration :: forall a. Duration a => Language -> a -> String 24 | prettyDuration lang dur | convertDuration dur < Minutes 1.0 = "< 1 " <> translate (I18nL.common <<< I18nL.cMinutes) lang 25 | | otherwise = trim $ joinWith " " $ map (uncurry showIfNonZero) 26 | [ Tuple d translationDays 27 | , Tuple h translationHours 28 | , Tuple m translationMinutes 29 | ] 30 | where 31 | translationMinutes = translate (I18nL.common <<< I18nL.cMinutes) lang 32 | translationHours = translate (I18nL.common <<< I18nL.cHours) lang 33 | translationDays = translate (I18nL.common <<< I18nL.cDays) lang 34 | m = floor $ un Minutes (convertDuration dur `sub` convertDuration (Days $ toNumber d) `sub` convertDuration (Hours $ toNumber h)) 35 | h = floor $ un Hours (convertDuration dur `sub` convertDuration (Days $ toNumber d)) 36 | d = floor $ un Days (convertDuration dur) 37 | showIfNonZero nu st = 38 | if nu == 0 39 | then "" 40 | else show nu <> " " <> st 41 | 42 | nominalDiffTimeToDateTime :: NominalDiffTime -> Maybe DateTime 43 | nominalDiffTimeToDateTime (NominalDiffTime s) = 44 | map toDateTime <<< instant $ fromDuration s 45 | 46 | 47 | type TimeFormat = String 48 | 49 | prettyDate' :: TimeFormat -> DateTime -> Maybe String 50 | prettyDate' format dateTime = 51 | either (const Nothing) Just $ formatDateTime format dateTime 52 | 53 | prettyDate :: TimeFormat -> NominalDiffTime -> Maybe String 54 | prettyDate format time = do 55 | dateTime <- nominalDiffTimeToDateTime time 56 | pretty <- prettyDate' format dateTime 57 | pure pretty 58 | -------------------------------------------------------------------------------- /frontend/src/Explorer/View/CSS.Test.purs: -------------------------------------------------------------------------------- 1 | module Explorer.View.CSS.Test where 2 | 3 | import Prelude 4 | import Control.Monad.Aff (Aff) 5 | import Control.Monad.State (StateT) 6 | import Data.Identity (Identity) 7 | import Explorer.Routes (Route(..)) 8 | import Explorer.Util.Factory (mkCAddress, mkCHash, mkCTxId, mkEpochIndex, mkLocalSlotIndex) 9 | import Explorer.View.CSS (route) 10 | import Test.Spec (Group, describe, it) 11 | import Test.Spec.Assertions (shouldEqual) 12 | 13 | testCSS :: forall r. StateT (Array (Group (Aff r Unit))) Identity Unit 14 | testCSS = 15 | describe "Explorer.View.CSS" do 16 | describe "creates css classes of each routes" do 17 | it "Dashboard" 18 | let result = route Dashboard 19 | in result `shouldEqual` "explorer-route-dashboard" 20 | it "Tx" 21 | let result = route <<< Tx $ mkCTxId "0" 22 | in result `shouldEqual` "explorer-route-tx" 23 | it "Address" 24 | let result = route <<< Address $ mkCAddress "0" 25 | in result `shouldEqual` "explorer-route-address" 26 | it "EpochSlot" 27 | let result = route $ EpochSlot (mkEpochIndex 0) (mkLocalSlotIndex 0) 28 | in result `shouldEqual` "explorer-route-epoch-slot" 29 | it "Epoch" 30 | let result = route <<< Epoch $ mkEpochIndex 0 31 | in result `shouldEqual` "explorer-route-epoch" 32 | it "Calculator" 33 | let result = route Calculator 34 | in result `shouldEqual` "explorer-route-calculator" 35 | it "Block" 36 | let result = route <<< Block $ mkCHash "0" 37 | in result `shouldEqual` "explorer-route-slot" 38 | it "Genesis" 39 | let result = route GenesisBlock 40 | in result `shouldEqual` "explorer-route-genesis" 41 | it "Playground" 42 | let result = route Playground 43 | in result `shouldEqual` "explorer-route-playground" 44 | -------------------------------------------------------------------------------- /frontend/src/Explorer/View/Calculator.purs: -------------------------------------------------------------------------------- 1 | module Explorer.View.Calculator where 2 | 3 | import Prelude 4 | 5 | import Explorer.I18n.Lang (translate) 6 | import Explorer.I18n.Lenses (cBack, cCalculator, common) as I18nL 7 | import Explorer.Types.State (State) 8 | import Explorer.Types.Actions (Action(..)) 9 | import Explorer.Routes (Route(Dashboard), toUrl) 10 | import Explorer.View.Common (placeholderView) 11 | 12 | import Text.Smolder.HTML (div, a) as S 13 | import Text.Smolder.HTML.Attributes (className, href) as S 14 | import Text.Smolder.Markup (text) as S 15 | import Text.Smolder.Markup ((#!), (!)) 16 | 17 | import Pux.DOM.Events (onClick) as P 18 | import Pux.DOM.HTML (HTML) as P 19 | 20 | calculatorView :: State -> P.HTML Action 21 | calculatorView state = 22 | S.div ! S.className "explorer-calculator" 23 | $ S.div ! S.className "explorer-calculator__container" $ do 24 | S.a ! S.className "" 25 | ! S.href (toUrl Dashboard) 26 | #! P.onClick (Navigate $ toUrl Dashboard) 27 | $ S.text (translate (I18nL.common <<< I18nL.cBack) state.lang) 28 | placeholderView $ translate (I18nL.common <<< I18nL.cCalculator) state.lang 29 | -------------------------------------------------------------------------------- /frontend/src/Explorer/View/Common.Test.purs: -------------------------------------------------------------------------------- 1 | module Explorer.View.Common.Test where 2 | 3 | import Prelude 4 | import Control.Monad.Aff (Aff) 5 | import Control.Monad.State (StateT) 6 | import Data.Identity (Identity) 7 | import Explorer.View.Common (getMaxPaginationNumber) 8 | import Test.Spec (Group, describe, it) 9 | import Test.Spec.Assertions (shouldEqual) 10 | 11 | testCommonViews :: forall r. StateT (Array (Group (Aff r Unit))) Identity Unit 12 | testCommonViews = 13 | describe "Explorer.View.Common" do 14 | 15 | describe "getMaxPaginationNumber" do 16 | it "rounds not to upper number" 17 | let result = getMaxPaginationNumber 530 10 18 | in result `shouldEqual` 53 19 | it "rounds to upper number" 20 | let result = getMaxPaginationNumber 539 10 21 | in result `shouldEqual` 54 22 | -------------------------------------------------------------------------------- /frontend/src/Explorer/View/Dashboard/Dashboard.purs: -------------------------------------------------------------------------------- 1 | module Explorer.View.Dashboard.Dashboard (dashboardView) where 2 | 3 | import Prelude 4 | 5 | import Explorer.Types.Actions (Action) 6 | import Explorer.Types.State (State) 7 | import Explorer.View.Dashboard.Blocks (dashBoardBlocksView) 8 | import Explorer.View.Dashboard.Hero (heroView) 9 | import Explorer.View.Dashboard.Transactions (transactionsView) 10 | 11 | import Pux.DOM.HTML (HTML) as P 12 | import Text.Smolder.HTML (div) as S 13 | import Text.Smolder.HTML.Attributes (className) as S 14 | import Text.Smolder.Markup ((!)) 15 | 16 | -- TODO (ks): Currently network- offer- and api views are removed 17 | -- since they don't have any meaningful data 18 | dashboardView :: State -> P.HTML Action 19 | dashboardView state = 20 | S.div ! S.className "explorer-dashboard" $ do 21 | heroView state 22 | -- networkView state 23 | dashBoardBlocksView state 24 | transactionsView state 25 | -- offerView state 26 | -- apiView state 27 | -------------------------------------------------------------------------------- /frontend/src/Explorer/View/Dashboard/Hero.purs: -------------------------------------------------------------------------------- 1 | module Explorer.View.Dashboard.Hero 2 | ( heroView 3 | ) where 4 | 5 | import Prelude 6 | 7 | import Data.Lens ((^.)) 8 | 9 | import Explorer.I18n.Lang (translate) 10 | import Explorer.I18n.Lenses (common, hero, cTitle, hrSubtitle) as I18nL 11 | import Explorer.Lenses.State (testnet, lang) 12 | import Explorer.State (heroSearchContainerId) 13 | import Explorer.Types.Actions (Action) 14 | import Explorer.Types.State (State) 15 | import Explorer.View.Common (logoView) 16 | import Explorer.View.Search (searchInputView) 17 | 18 | import Pux.DOM.HTML (HTML) as P 19 | 20 | import Text.Smolder.HTML (div, h1, h2) as S 21 | import Text.Smolder.HTML.Attributes (className, id) as S 22 | import Text.Smolder.Markup (text) as S 23 | import Text.Smolder.Markup ((!)) 24 | 25 | heroView :: State -> P.HTML Action 26 | heroView state = 27 | let 28 | lang' = state ^. lang 29 | in 30 | S.div ! S.className "explorer-dashboard__hero" 31 | ! S.id "explorer-dashboard__hero-id" 32 | $ S.div ! S.className "hero-container" $ do 33 | logoView $ state ^. testnet 34 | S.h1 ! S.className "hero-headline" 35 | $ S.text (translate (I18nL.common <<< I18nL.cTitle) lang') 36 | S.h2 ! S.className "hero-subheadline" 37 | $ S.text $ (translate (I18nL.hero <<< I18nL.hrSubtitle) lang') 38 | searchInputView heroSearchContainerId state 39 | -------------------------------------------------------------------------------- /frontend/src/Explorer/View/Dashboard/Lenses.purs: -------------------------------------------------------------------------------- 1 | module Explorer.View.Dashboard.Lenses where 2 | 3 | import Prelude 4 | import Data.Lens (Lens') 5 | import Explorer.Lenses.State (dbViewBlocksExpanded, dashboard, dbViewSelectedApiCode, dbViewTxsExpanded, viewStates) 6 | import Explorer.Types.State (DashboardAPICode, DashboardViewState, State) 7 | 8 | -- lenses 9 | 10 | dashboardViewState :: Lens' State DashboardViewState 11 | dashboardViewState = viewStates <<< dashboard 12 | 13 | dashboardBlocksExpanded :: Lens' State Boolean 14 | dashboardBlocksExpanded = dashboardViewState <<< dbViewBlocksExpanded 15 | 16 | dashboardTransactionsExpanded :: Lens' State Boolean 17 | dashboardTransactionsExpanded = dashboardViewState <<< dbViewTxsExpanded 18 | 19 | dashboardSelectedApiCode :: Lens' State DashboardAPICode 20 | dashboardSelectedApiCode = dashboardViewState <<< dbViewSelectedApiCode 21 | -------------------------------------------------------------------------------- /frontend/src/Explorer/View/Dashboard/Network.purs: -------------------------------------------------------------------------------- 1 | module Explorer.View.Dashboard.Network (networkView) where 2 | 3 | import Prelude 4 | 5 | import Data.Foldable (for_) 6 | import Data.Lens ((^.)) 7 | 8 | import Explorer.I18n.Lang (Language, translate) 9 | import Explorer.I18n.Lenses (cADA, dbTotalAmountOfTransactions, cTransactions, dbTotalAmountOf, dbTotalSupply, dbPriceSince, dbPriceForOne, dbPriceAverage, cNetwork, common, dashboard, dbLastBlocks, dbLastBlocksDescription) as I18nL 10 | import Explorer.Lenses.State (lang) 11 | import Explorer.Types.Actions (Action) 12 | import Explorer.Types.State (State) 13 | import Explorer.Util.String (substitute) 14 | 15 | import Pux.DOM.HTML (HTML) as P 16 | 17 | import Text.Smolder.HTML (div, h3, h4, p) as S 18 | import Text.Smolder.HTML.Attributes (className) as S 19 | import Text.Smolder.Markup (text) as S 20 | import Text.Smolder.Markup ((!)) 21 | 22 | 23 | -- FIXME (jk): just for now, will use later `real` ADTs 24 | type NetworkItems = Array NetworkItem 25 | 26 | -- FIXME (jk): just for now, will use later `real` ADTs 27 | type NetworkItem = 28 | { headline :: String 29 | , subheadline :: String 30 | , description :: String 31 | } 32 | 33 | networkItems :: Language -> NetworkItems 34 | networkItems lang = 35 | let ada = translate (I18nL.common <<< I18nL.cADA) lang in 36 | [ { headline: translate (I18nL.dashboard <<< I18nL.dbLastBlocks) lang 37 | , subheadline: "123456" 38 | , description: flip substitute ["20.02.2017 17:51:00", "50"] 39 | $ translate (I18nL.dashboard <<< I18nL.dbLastBlocksDescription) lang 40 | } 41 | , { headline: translate (I18nL.dashboard <<< I18nL.dbPriceAverage) lang 42 | , subheadline: flip substitute ["1,000,000$", ada] 43 | $ translate (I18nL.dashboard <<< I18nL.dbPriceForOne) lang 44 | , description: flip substitute ["20% more"] 45 | $ translate (I18nL.dashboard <<< I18nL.dbPriceSince) lang 46 | } 47 | , { headline: translate (I18nL.dashboard <<< I18nL.dbTotalSupply) lang 48 | , subheadline: flip substitute ["9,876,543,210 "] $ translate (I18nL.common <<< I18nL.cADA) lang 49 | , description: flip substitute [ada] $ translate (I18nL.dashboard <<< I18nL.dbTotalAmountOf) lang 50 | } 51 | , { headline: translate (I18nL.common <<< I18nL.cTransactions) lang 52 | , subheadline: "82,491,247,592,742,929" 53 | , description: translate (I18nL.dashboard <<< I18nL.dbTotalAmountOfTransactions) lang 54 | } 55 | ] 56 | 57 | 58 | networkView :: State -> P.HTML Action 59 | networkView state = 60 | let lang' = state ^. lang in 61 | S.div ! S.className "explorer-dashboard__wrapper" 62 | $ S.div ! S.className "explorer-dashboard__container" $ do 63 | S.h3 ! S.className "headline" 64 | $ S.text (translate (I18nL.common <<< I18nL.cNetwork) lang') 65 | S.div ! S.className "explorer-dashboard__teaser" 66 | $ for_ (networkItems lang') (networkItem state) 67 | 68 | networkItem :: State -> NetworkItem -> P.HTML Action 69 | networkItem state item = 70 | S.div ! S.className "teaser-item" $ do 71 | S.h3 ! S.className "teaser-item__headline" 72 | $ S.text item.headline 73 | S.h4 ! S.className "teaser-item__subheadline" 74 | $ S.text item.subheadline 75 | S.p ! S.className "teaser-item__description" 76 | $ S.text item.description 77 | -------------------------------------------------------------------------------- /frontend/src/Explorer/View/Dashboard/Offer.purs: -------------------------------------------------------------------------------- 1 | module Explorer.View.Dashboard.Offer (offerView) where 2 | 3 | import Prelude hiding (div) 4 | 5 | import Data.Foldable (for_) 6 | import Data.Lens ((^.)) 7 | 8 | import Explorer.I18n.Lang (Language, translate) 9 | import Explorer.I18n.Lenses (dbApiDescription 10 | , dbAddressSearch, dbAddressSearchDescription, dbBlockchainOffer, dbBlockSearch 11 | , dbBlockSearchDescription, dbTransactionSearch, dbTransactionSearchDescription 12 | , cApi , common, dashboard) as I18nL 13 | import Explorer.Lenses.State (lang) 14 | import Explorer.Types.Actions (Action) 15 | import Explorer.Types.State (State) 16 | 17 | import Pux.DOM.HTML (HTML) as P 18 | 19 | import Text.Smolder.HTML (div, h3, p) as S 20 | import Text.Smolder.HTML.Attributes (className) as S 21 | import Text.Smolder.Markup (text) as S 22 | import Text.Smolder.Markup ((!)) 23 | 24 | -- FIXME (jk): just for now, will use later `real` ADTs 25 | type OfferItems = Array OfferItem 26 | 27 | -- FIXME (jk): just for now, will use later `real` ADTs 28 | type OfferItem = 29 | { headline :: String 30 | , description :: String 31 | } 32 | 33 | offerItems :: Language -> OfferItems 34 | offerItems lang = 35 | [ { headline: translate (I18nL.dashboard <<< I18nL.dbBlockSearch) lang 36 | , description: translate (I18nL.dashboard <<< I18nL.dbBlockSearchDescription) lang 37 | } 38 | , { headline: translate (I18nL.dashboard <<< I18nL.dbAddressSearch) lang 39 | , description: translate (I18nL.dashboard <<< I18nL.dbAddressSearchDescription) lang 40 | } 41 | , { headline: translate (I18nL.dashboard <<< I18nL.dbTransactionSearch) lang 42 | , description: translate (I18nL.dashboard <<< I18nL.dbTransactionSearchDescription) lang 43 | } 44 | , { headline: translate (I18nL.common <<< I18nL.cApi) lang 45 | , description: translate (I18nL.dashboard <<< I18nL.dbApiDescription) lang 46 | } 47 | ] 48 | 49 | offerView :: State -> P.HTML Action 50 | offerView state = 51 | let lang' = state ^. lang in 52 | S.div ! S.className "explorer-dashboard__wrapper" 53 | $ S.div ! S.className "explorer-dashboard__container" $ do 54 | S.h3 ! S.className "headline" 55 | $ S.text (translate (I18nL.dashboard <<< I18nL.dbBlockchainOffer) lang') 56 | S.div ! S.className "explorer-dashboard__teaser" 57 | $ for_ (offerItems lang') (offerItem state) 58 | 59 | offerItem :: State -> OfferItem -> P.HTML Action 60 | offerItem state item = 61 | S.div ! S.className "teaser-item" $ do 62 | S.h3 ! S.className "teaser-item__headline" 63 | $ S.text item.headline 64 | S.p ! S.className "teaser-item__description" 65 | $ S.text item.description 66 | -------------------------------------------------------------------------------- /frontend/src/Explorer/View/Dashboard/Shared.purs: -------------------------------------------------------------------------------- 1 | module Explorer.View.Dashboard.Shared (headerView) where 2 | 3 | import Prelude 4 | 5 | import Data.Maybe (Maybe(..)) 6 | 7 | import Pux.DOM.HTML (HTML) as P 8 | import Text.Smolder.HTML (div, h3) as S 9 | import Text.Smolder.HTML.Attributes (className) as S 10 | import Text.Smolder.Markup (text) as S 11 | import Text.Smolder.Markup ((!)) 12 | 13 | import Explorer.Types.Actions (Action) 14 | import Explorer.Types.State (State) 15 | import Explorer.View.Common (emptyView) 16 | import Explorer.View.Dashboard.Types (HeaderLink(..), HeaderOptions(..)) 17 | 18 | headerView :: State -> HeaderOptions -> P.HTML Action 19 | headerView state (HeaderOptions options) = 20 | S.div ! S.className "explorer-dashboard__header" $ do 21 | S.h3 ! S.className "headline" 22 | $ S.text options.headline 23 | -- S.div 24 | -- ! S.className "more__container" 25 | -- $ linkView options.link 26 | where 27 | linkView link = case link of 28 | Just (HeaderLink link') -> 29 | S.div ! S.className "more__link bg-arrow-right" 30 | $ S.text link'.label 31 | Nothing -> emptyView 32 | -------------------------------------------------------------------------------- /frontend/src/Explorer/View/Dashboard/Types.purs: -------------------------------------------------------------------------------- 1 | module Explorer.View.Dashboard.Types where 2 | 3 | import Data.Maybe (Maybe) 4 | import Explorer.Types.Actions (Action) 5 | 6 | 7 | newtype HeaderOptions = HeaderOptions 8 | { headline :: String 9 | , link :: Maybe HeaderLink 10 | } 11 | 12 | newtype HeaderLink = HeaderLink 13 | { label :: String 14 | , action :: Action 15 | } 16 | -------------------------------------------------------------------------------- /frontend/src/Explorer/View/Dashboard/api.css: -------------------------------------------------------------------------------- 1 | 2 | .explorer-dashboard { 3 | 4 | .api-content { 5 | lost-utility: clearfix; 6 | &__container { 7 | lost-column: 1/1; 8 | } 9 | } 10 | 11 | @media (--md) { 12 | .api-content { 13 | &__container { 14 | lost-column: 1/2 2; 15 | } 16 | } 17 | } 18 | 19 | .api-code { 20 | font-family: var(--fontFamily0-Regular); 21 | font-weight: normal; 22 | 23 | &__tabs { 24 | padding: 0 18px; 25 | height: 40px; 26 | box: horizontal; 27 | } 28 | 29 | @media (--md) { 30 | &__tabs { 31 | padding: 0 24px; 32 | } 33 | } 34 | 35 | &__tab { 36 | box-item: center; 37 | margin-right: 12px; 38 | font-size: 12px; 39 | button: standard-button; 40 | color: color(var(--color8) a(0.5)); 41 | border-bottom: none; 42 | &.selected { 43 | color: var(--color8); 44 | border-bottom: 1px solid var(--color8); 45 | } 46 | } 47 | } 48 | 49 | 50 | .api-snippet { 51 | 52 | background-color: var(--color6); 53 | font-family: var(--fontFamily0-Regular); 54 | font-weight: normal; 55 | padding: 18px; 56 | margin-bottom: 12px; 57 | 58 | &__headline { 59 | font-size: 12px; 60 | color: color(var(--color8) a(0.5)); 61 | position: relative; 62 | &::after { 63 | content: ''; 64 | position: absolute; 65 | left: 0; 66 | bottom: 0; 67 | height: 5px; 68 | width: 24px; 69 | border-bottom: 1px solid color(var(--color8) a(0.4)); 70 | 71 | } 72 | } 73 | 74 | &__code { 75 | font-size: 10px; 76 | color: var(--color8); 77 | padding-top: 6px; 78 | } 79 | } 80 | 81 | @media (--md) { 82 | .api-snippet { 83 | padding: 18px 24px; 84 | } 85 | 86 | } 87 | 88 | .api-about { 89 | font-family: var(--fontFamily0-Regular); 90 | font-weight: normal; 91 | padding: 0 18px; 92 | 93 | &__headline { 94 | color: var(--color8); 95 | font-size: 13px; 96 | } 97 | 98 | &__description { 99 | color: color(var(--color8) a(0.5)); 100 | font-size: 10px; 101 | } 102 | 103 | &__button { 104 | font-family: var(--fontFamily0-Regular); 105 | font-weight: normal; 106 | font-size: 12; 107 | 108 | margin-top: 24px; 109 | padding: 18px 24px; 110 | 111 | border-radius: 2px; 112 | button: outline-button; 113 | button-color: var(--color5) var(--color4) var(--color4); 114 | button-border: 1px var(--color5) transparent transparent; 115 | button-background: transparent var(--color5) var(--color5); 116 | } 117 | } 118 | 119 | @media (--md) { 120 | .api-about { 121 | padding: 40px 24px 0 24px; 122 | } 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /frontend/src/Explorer/View/Dashboard/dashboard.css: -------------------------------------------------------------------------------- 1 | 2 | .explorer-dashboard { 3 | 4 | &__wrapper { 5 | background-color: var(--color6); 6 | 7 | &:nth-child(even) { 8 | background-color: var(--color4); 9 | } 10 | } 11 | 12 | &__container { 13 | @extend .outer-container; 14 | padding: 24px 0; 15 | } 16 | @media (--md) { 17 | &__container { 18 | padding: 46px 0px 38px 0px; 19 | } 20 | } 21 | 22 | &__header { 23 | 24 | lost-utility: clearfix; 25 | 26 | .headline { 27 | lost-column: 1/1; 28 | /*lost-column: 1/2;*/ 29 | /* Change it to ^ again, if we will roll back a "more" link (`more__container`) */ 30 | } 31 | 32 | .more { 33 | &__container { 34 | lost-column: 1/2; 35 | text-align: right; 36 | padding-right: 18px; 37 | } 38 | 39 | &__link { 40 | button: standard-button; 41 | padding-right: 20px; 42 | background-repeat: no-repeat; 43 | background-position: right center; 44 | background-size: 13px 13px; 45 | 46 | font-family: var(--fontFamily0-Regular); 47 | color: var(--color5); 48 | font-size: 12px; 49 | font-weight: normal; 50 | line-height: 2; /* same line-height as the left hand headline */ 51 | } 52 | } 53 | 54 | @media (--md) { 55 | .more { 56 | padding-right: 24px; 57 | } 58 | 59 | } 60 | } 61 | 62 | &__teaser { 63 | padding: 0 18px; 64 | lost-utility: clearfix; 65 | } 66 | 67 | @media (--md) { 68 | &__teaser { 69 | padding: 0 24px; 70 | } 71 | } 72 | 73 | .teaser-item { 74 | lost-column: 1/2; 75 | 76 | padding-bottom: 24px; 77 | 78 | font-family: var(--fontFamily0-Regular); 79 | color: var(--color8); 80 | font-weight: normal; 81 | 82 | &__headline { 83 | position: relative; 84 | font-size: 13px; 85 | 86 | &::after { 87 | content: ''; 88 | position: absolute; 89 | left: 0; 90 | bottom: 0; 91 | height: 5px; 92 | width: 24px; 93 | border-bottom: 1px solid color(var(--color8) a(0.4)); 94 | } 95 | } 96 | 97 | 98 | &__subheadline { 99 | font-size: 12px; 100 | padding-top: 5px; 101 | line-height: normal; /* overridden */ 102 | } 103 | 104 | &__description { 105 | padding-top: 5px; 106 | font-size: 10px; 107 | color: var(--color13); 108 | } 109 | } 110 | 111 | @media (--sm) { 112 | .teaser-item { 113 | lost-column: 1/4; 114 | } 115 | } 116 | 117 | } 118 | -------------------------------------------------------------------------------- /frontend/src/Explorer/View/Dashboard/hero.css: -------------------------------------------------------------------------------- 1 | 2 | .explorer-dashboard { 3 | 4 | &__hero { 5 | color: var(--color0); 6 | background-color: transparent; 7 | text-align: center; 8 | } 9 | 10 | .hero { 11 | 12 | &-container { 13 | @extend .hide; 14 | } 15 | 16 | @media (--md) { 17 | &-container { 18 | @extend .show; 19 | @extend .outer-container; 20 | lost-utility: clearfix; 21 | 22 | padding: 18px 48px var(--height-header-top) 48px; 23 | 24 | .logo__container { 25 | width: 100%; 26 | padding-bottom: 18px; 27 | } 28 | 29 | .logo__wrapper { 30 | height: 100%; 31 | box: horizontal center; 32 | } 33 | 34 | .logo__img { 35 | width: 49px; 36 | height: 45px; 37 | } 38 | 39 | .explorer-search { 40 | &__container { 41 | lost-column: 7/12; 42 | lost-offset: 2.5/12; 43 | 44 | @extend .animated; 45 | @extend .fadeIn; 46 | 47 | animation-duration: .3s; 48 | /* We do need a little delay 49 | to hide previous state */ 50 | animation-delay: .1s; 51 | 52 | &.focused { 53 | transition: all 0.3s ease; 54 | lost-column: 10/12; 55 | lost-offset: 1/12; 56 | } 57 | } 58 | } 59 | } 60 | } 61 | 62 | 63 | &-headline { 64 | font-size: 26px; 65 | line-height: 1.25; 66 | color: var(--color3); 67 | font-family: var(--fontFamily0-Light); 68 | } 69 | 70 | &-subheadline { 71 | font-size: 13px; 72 | line-height: 1.4; 73 | font-family: var(--fontFamily0-Light); 74 | color: color(var(--color3) a(0.5)); 75 | margin:10px 0; 76 | padding-bottom: 15px; 77 | } 78 | 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /frontend/src/Explorer/View/Dashboard/transactions.css: -------------------------------------------------------------------------------- 1 | 2 | .explorer-dashboard { 3 | 4 | .transactions { 5 | 6 | &__container { 7 | padding-bottom: 24px; 8 | } 9 | 10 | &__waiting { 11 | padding: 10px 24px; 12 | 13 | font-size: 10px; 14 | font-family: var(--fontFamily0-Regular); 15 | color: var(--color13); 16 | } 17 | 18 | &__row { 19 | 20 | lost-utility: clearfix; 21 | 22 | font-size: 10px; 23 | font-family: var(--fontFamily0-Regular); 24 | color: var(--color8); 25 | padding: 10px 18px; 26 | 27 | &:nth-child(odd) { 28 | background-color: var(--color7); 29 | } 30 | 31 | } 32 | 33 | @media (--md) { 34 | &__row { 35 | padding: 10px 24px; 36 | } 37 | } 38 | 39 | &__column { 40 | 41 | &--hash-container { 42 | lost-column: 1/1 1; 43 | 44 | .hash { 45 | overflow: hidden; 46 | text-overflow: ellipsis; 47 | display: block; 48 | button: standard-button; 49 | button-color: var(--color8) var(--color5) var(--color5); 50 | } 51 | } 52 | 53 | &--currency { 54 | @extend .hide; 55 | 56 | text-align: right; 57 | 58 | .ada, 59 | .usd { 60 | padding-right: 10px; 61 | background-repeat: no-repeat; 62 | background-position: right center; 63 | } 64 | 65 | .ada { 66 | background-size: 8px 7px; 67 | } 68 | 69 | .usd { 70 | background-size: 6px 10px; 71 | } 72 | } 73 | 74 | &--date { 75 | @extend .hide; 76 | } 77 | 78 | } 79 | 80 | @media (--md) { 81 | &__column { 82 | 83 | &--hash-container { 84 | lost-column: 8/12; 85 | 86 | .hash { 87 | display: inline-block; /* needed to select text in browser */ 88 | } 89 | 90 | } 91 | 92 | &--date { 93 | @extend .show; 94 | lost-column: 2/12; 95 | } 96 | 97 | &--currency { 98 | @extend .show; 99 | lost-column: 2/12; 100 | } 101 | 102 | } 103 | } 104 | 105 | &__footer { 106 | text-align: center; 107 | } 108 | 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /frontend/src/Explorer/View/Header.purs: -------------------------------------------------------------------------------- 1 | module Explorer.View.Header (headerView) where 2 | 3 | import Prelude 4 | 5 | import Data.Lens ((^.)) 6 | import Data.Monoid (mempty) 7 | import Explorer.I18n.Lang (Language, translate) 8 | import Explorer.I18n.Lenses (common, cAddress, cBlock, cCalculator, cEpoch, cGenesis, cSlot, cTitle, cTransaction, notfound, nfTitle) as I18nL 9 | import Explorer.Lenses.State (gViewMobileMenuOpenend, gViewSelectedSearch, globalViewState, testnet, lang, route, viewStates) 10 | import Explorer.Routes (Route(..)) 11 | import Explorer.State (headerSearchContainerId, mobileMenuSearchContainerId) 12 | import Explorer.Types.Actions (Action(..)) 13 | import Explorer.Types.State (State) 14 | import Explorer.View.CSS (header, headerId) as CSS 15 | import Explorer.View.Common (clickableLogoView, langView) 16 | import Explorer.View.Search (searchInputView, searchItemViews) 17 | import Pux.DOM.Events (onClick) as P 18 | import Pux.DOM.HTML (HTML) as P 19 | import Text.Smolder.HTML (div, header) as S 20 | import Text.Smolder.HTML.Attributes (className, id) as S 21 | import Text.Smolder.Markup ((#!), (!)) 22 | import Text.Smolder.Markup (text) as S 23 | 24 | headerView :: State -> P.HTML Action 25 | headerView state = 26 | let lang' = state ^. lang 27 | selectedSearch = state ^. (viewStates <<< globalViewState <<< gViewSelectedSearch) 28 | mobileMenuOpenend = state ^. (viewStates <<< globalViewState <<< gViewMobileMenuOpenend) 29 | in 30 | S.header ! S.className CSS.header 31 | ! S.id CSS.headerId $ do 32 | S.div ! S.className "explorer-header__wrapper--vtop" 33 | $ S.div ! S.className "explorer-header__container" $ do 34 | clickableLogoView Dashboard $ state ^. testnet 35 | -- desktop views 36 | S.div ! S.className "middle-content__search" 37 | $ S.div ! S.className "middle-content__search--wrapper" 38 | $ searchInputView headerSearchContainerId state 39 | S.div ! S.className "right-content__currency" 40 | $ mempty 41 | -- mobile views 42 | S.div ! S.className "middle-content__title" 43 | $ if mobileMenuOpenend 44 | then searchItemViews lang' selectedSearch 45 | else S.text $ title (state ^. route) lang' 46 | S.div ! S.className "right-content__hamburger" 47 | $ S.div ! S.className (if mobileMenuOpenend 48 | then "cross__icon bg-icon-cross" 49 | else "hamburger__icon bg-icon-hamburger") 50 | #! P.onClick (const <<< GlobalToggleMobileMenu $ not mobileMenuOpenend) 51 | $ mempty 52 | S.div ! S.className "explorer-header__wrapper--vmiddle" $ do 53 | S.div ! S.className "vmiddle-content__search--wrapper" 54 | $ searchInputView mobileMenuSearchContainerId state 55 | S.div ! S.className "explorer-header__wrapper--vbottom" 56 | $ langView state 57 | 58 | title :: Route -> Language -> String 59 | title Dashboard lang = translate (I18nL.common <<< I18nL.cTitle) lang 60 | title (Tx id) lang = translate (I18nL.common <<< I18nL.cTransaction) lang 61 | title (Address address) lang = translate (I18nL.common <<< I18nL.cAddress) lang 62 | title (Epoch epoch) lang = translate (I18nL.common <<< I18nL.cEpoch) lang 63 | title (EpochSlot epoch slot) lang = 64 | epochTitle <> " / " <> slotTitle 65 | where 66 | epochTitle = translate (I18nL.common <<< I18nL.cEpoch) lang 67 | slotTitle = translate (I18nL.common <<< I18nL.cSlot) lang 68 | title Calculator lang = translate (I18nL.common <<< I18nL.cCalculator) lang 69 | title (Block hash) lang = translate (I18nL.common <<< I18nL.cBlock) lang 70 | title GenesisBlock lang = translate (I18nL.common <<< I18nL.cGenesis) lang 71 | title Playground _ = "Playground" 72 | title NotFound lang = translate (I18nL.notfound <<< I18nL.nfTitle) lang 73 | -------------------------------------------------------------------------------- /frontend/src/Explorer/View/Layout.purs: -------------------------------------------------------------------------------- 1 | module Explorer.View.Layout where 2 | 3 | import Prelude 4 | import Data.Lens ((^.)) 5 | 6 | import Pux.DOM.HTML (HTML) as P 7 | 8 | import Text.Smolder.HTML (div) as S 9 | import Text.Smolder.HTML.Attributes (className) as S 10 | import Text.Smolder.Markup ((!)) 11 | 12 | import Explorer.Lenses.State (gViewMobileMenuOpenend, globalViewState, route, viewStates) 13 | import Explorer.Routes (Route(..)) 14 | import Explorer.Types.Actions (Action) 15 | import Explorer.Types.State (State) 16 | import Explorer.View.Address (addressView) 17 | import Explorer.View.Block (blockView) 18 | import Explorer.View.Blocks (blocksView) 19 | import Explorer.View.CSS (route) as CSS 20 | import Explorer.View.Calculator (calculatorView) 21 | import Explorer.View.Dashboard.Dashboard (dashboardView) 22 | import Explorer.View.Footer (footerView) 23 | import Explorer.View.GenesisBlock (genesisBlockView) 24 | import Explorer.View.Header (headerView) 25 | import Explorer.View.NotFound (notFoundView) 26 | import Explorer.View.Playground (playgroundView) 27 | import Explorer.View.Transaction (transactionView) 28 | 29 | view :: State -> P.HTML Action 30 | view state = 31 | let mobileMenuClazz = if state ^. (viewStates <<< globalViewState <<< gViewMobileMenuOpenend) 32 | then " mobile__menu--opened" 33 | else "" 34 | routeClazz = CSS.route $ state ^. route 35 | in 36 | S.div ! S.className ("explorer-container" <> mobileMenuClazz <> " " <> routeClazz) 37 | $ S.div ! S.className "explorer-bg__container" 38 | $ S.div ! S.className "explorer-content__wrapper" $ do 39 | S.div ! S.className "explorer-content" $ do 40 | case state ^. route of 41 | Dashboard -> dashboardView state 42 | (Tx id) -> transactionView state 43 | (Address address) -> addressView state 44 | (Epoch epoch) -> blocksView state 45 | (EpochSlot epoch slot) -> blocksView state 46 | Calculator -> calculatorView state 47 | (Block hash) -> blockView state 48 | GenesisBlock -> genesisBlockView state 49 | Playground -> playgroundView state 50 | NotFound -> notFoundView state 51 | footerView state 52 | headerView state 53 | -------------------------------------------------------------------------------- /frontend/src/Explorer/View/NotFound.purs: -------------------------------------------------------------------------------- 1 | module Explorer.View.NotFound (notFoundView) where 2 | 3 | import Prelude 4 | 5 | import Data.Lens ((^.)) 6 | import Data.Monoid (mempty) 7 | 8 | import Explorer.Lenses.State (lang) 9 | import Explorer.Routes (Route(Dashboard), toUrl) 10 | import Explorer.Types.Actions (Action(..)) 11 | import Explorer.Types.State (State) 12 | 13 | import Pux.DOM.Events (onClick) as P 14 | 15 | import Text.Smolder.HTML (div, a) as S 16 | import Text.Smolder.HTML.Attributes (className, href) as S 17 | import Text.Smolder.Markup ((!), (#!)) 18 | 19 | import Pux.DOM.HTML (HTML) as P 20 | 21 | notFoundView :: State -> P.HTML Action 22 | notFoundView state = 23 | let lang' = state ^. lang in 24 | S.div ! S.className "explorer-404" 25 | $ S.div ! S.className "explorer-404__wrapper" 26 | $ S.div ! S.className "explorer-404__container" 27 | $ S.a ! S.href (toUrl Dashboard) 28 | #! P.onClick (Navigate (toUrl Dashboard)) 29 | ! S.className "bg-image-404" 30 | $ mempty 31 | -------------------------------------------------------------------------------- /frontend/src/Explorer/View/Playground.purs: -------------------------------------------------------------------------------- 1 | module Explorer.View.Playground where 2 | 3 | import Prelude 4 | 5 | import Data.Monoid (mempty) 6 | import Explorer.Types.Actions (Action) 7 | import Explorer.Types.State (State) 8 | import Pux.DOM.HTML (HTML) as P 9 | import Text.Smolder.HTML (div) as S 10 | import Text.Smolder.HTML.Attributes (className) as S 11 | import Text.Smolder.Markup ((!)) 12 | 13 | playgroundView :: State -> P.HTML Action 14 | playgroundView state = 15 | S.div ! S.className "explorer-calculator" 16 | $ S.div ! S.className "explorer-calculator__container" 17 | $ mempty 18 | -------------------------------------------------------------------------------- /frontend/src/Explorer/View/Types.purs: -------------------------------------------------------------------------------- 1 | module Exporer.View.Types where 2 | 3 | import Data.Maybe (Maybe) 4 | import Data.Time.NominalDiffTime (NominalDiffTime) 5 | import Data.Tuple (Tuple) 6 | import Pos.Explorer.Web.ClientTypes (CCoin, CAddress, CTxId) 7 | 8 | -- put all view related types here in this file to generate lenses from it 9 | 10 | newtype TxHeaderViewProps = TxHeaderViewProps 11 | { txhHash :: CTxId 12 | , txhTimeIssued :: Maybe NominalDiffTime 13 | , txhAmount :: CCoin 14 | } 15 | 16 | 17 | newtype TxBodyViewProps = TxBodyViewProps 18 | { txbInputs :: Array (Tuple CAddress CCoin) 19 | , txbOutputs :: Array (Tuple CAddress CCoin) 20 | , txbAmount :: CCoin 21 | } 22 | -------------------------------------------------------------------------------- /frontend/src/Explorer/View/address.css: -------------------------------------------------------------------------------- 1 | 2 | .explorer-address { 3 | 4 | &__wrapper { 5 | background-color: var(--color4); 6 | 7 | &:nth-child(even) { 8 | background-color: var(--color6); 9 | } 10 | } 11 | 12 | 13 | &__container { 14 | @extend .outer-container; 15 | padding: 24px 0px; 16 | 17 | } 18 | @media (--md) { 19 | &__container { 20 | padding: 56px 48px 48px 48px; 21 | } 22 | } 23 | 24 | .message { 25 | padding: 0 18px; 26 | font-size: 10px; 27 | font-family: var(--fontFamily0-Regular); 28 | color: var(--color13); 29 | } 30 | 31 | @media (--md) { 32 | .message { 33 | padding: 0 24px; 34 | } 35 | } 36 | 37 | .address-hash { 38 | background-color: var(--color6); 39 | padding: 5px 10px; 40 | font-family: var(--fontFamily0-Regular); 41 | color: color(var(--color8) a(0.6)); 42 | font-size: 12px; 43 | font-weight: normal; 44 | } 45 | 46 | .address-overview { 47 | lost-utility: clearfix; 48 | } 49 | 50 | .address-detail { 51 | 52 | font-family: var(--fontFamily0-Regular); 53 | color: var(--color8); 54 | font-size: 10px; 55 | font-weight: normal; 56 | 57 | &__row { 58 | lost-utility: clearfix; 59 | background-color: transparent; 60 | 61 | &:nth-child(even) { 62 | background-color: var(--color6); 63 | }; 64 | 65 | &__column { 66 | /* DON'T REMOVE THIS `__column` declaration 67 | It is needed after updating to latest post-css plugins 68 | Maybe a bug of `postcss-css-variables` ??? 69 | see https://github.com/MadLittleMods/postcss-css-variables/issues/50 70 | */ 71 | background-color: transparent; 72 | } 73 | 74 | } 75 | 76 | 77 | &__column { 78 | padding: 10px 18px; 79 | font-family: var(--fontFamily0-Regular); 80 | color: var(--color8); 81 | font-size: 10px; 82 | font-weight: normal; 83 | 84 | &.label { 85 | @neat-span-columns 4; 86 | overflow: hidden; 87 | text-overflow: ellipsis; 88 | } 89 | 90 | &.amount { 91 | @neat-span-columns 8; 92 | padding-left: 0; 93 | overflow: hidden; 94 | text-overflow: ellipsis; 95 | } 96 | 97 | .ada { 98 | padding-right: 10px; 99 | background-repeat: no-repeat; 100 | background-position: right center; 101 | background-size: 8px 7px; 102 | } 103 | } 104 | 105 | @media (--md) { 106 | &__column { 107 | padding: 10px 24px; 108 | } 109 | } 110 | } 111 | 112 | @media (--lg) { 113 | .address-detail { 114 | lost-column: 8/12; 115 | } 116 | } 117 | 118 | .address-qr { 119 | 120 | font-family: var(--fontFamily0-Regular); 121 | color: var(--color8); 122 | font-size: 10px; 123 | font-weight: normal; 124 | 125 | &__tab { 126 | @extend .hide; 127 | } 128 | 129 | @media (--md) { 130 | &__tab { 131 | @extend .show; 132 | padding: 10px 24px; 133 | } 134 | } 135 | 136 | &__wrapper { 137 | padding: 12px 18px; 138 | background-color: var(--color6); 139 | box: horizontal; 140 | } 141 | 142 | @media (--md) { 143 | &__wrapper { 144 | padding: 10px 24px; 145 | } 146 | } 147 | 148 | &__image { 149 | background-color: color(var(--color2) a(0.1)); 150 | width: 96px; 151 | height: 96px; 152 | box-item: top; 153 | } 154 | 155 | &__description { 156 | padding-left: 12px; 157 | box-item: flex-1; 158 | } 159 | 160 | } 161 | 162 | @media (--lg) { 163 | .address-qr { 164 | lost-column: 4/12; 165 | } 166 | } 167 | .address-failed { 168 | padding: 0 18px 30px 18px; 169 | font-size: 10px; 170 | font-family: var(--fontFamily0-Regular); 171 | color: var(--color1); 172 | } 173 | 174 | @media (--md) { 175 | .address-failed { 176 | padding: 0 24px 30px 24px; 177 | } 178 | } 179 | 180 | .transaction-header { 181 | background-color: var(--color7); /* overridden */ 182 | } 183 | 184 | &__tx-container { 185 | lost-utility: clearfix; 186 | } 187 | 188 | } 189 | -------------------------------------------------------------------------------- /frontend/src/Explorer/View/block.css: -------------------------------------------------------------------------------- 1 | 2 | .explorer-block { 3 | 4 | &__wrapper { 5 | background-color: var(--color4); 6 | 7 | &:nth-child(even) { 8 | background-color: var(--color6); 9 | } 10 | } 11 | 12 | 13 | &__container { 14 | @extend .outer-container; 15 | padding: 24px 0; 16 | } 17 | 18 | @media (--md) { 19 | &__container { 20 | padding: 56px 48px 48px 48px; 21 | } 22 | } 23 | 24 | .subheadline { 25 | font-family: var(--fontFamily0-Bold); 26 | color: var(--color8); 27 | font-size: 10px; 28 | font-weight: bold; 29 | padding: 10px 24px; 30 | } 31 | 32 | .blocks-wrapper { 33 | lost-utility: clearfix; 34 | } 35 | 36 | .summary-container { 37 | lost-column: 1/1; 38 | } 39 | 40 | @media (--lg) { 41 | .summary-container { 42 | lost-column: 1/2; 43 | } 44 | } 45 | 46 | .hashes-container { 47 | margin-top: 24px; 48 | lost-column: 1/1; 49 | } 50 | 51 | @media (--lg) { 52 | .hashes-container { 53 | margin-top: 0; 54 | lost-column: 1/2; 55 | } 56 | } 57 | 58 | .row { 59 | lost-utility: clearfix; 60 | background-color: var(--color6); 61 | padding: 0 24px; 62 | 63 | &:nth-child(even) { 64 | background-color: transparent; 65 | }; 66 | } 67 | 68 | .row__summary { 69 | 70 | } 71 | 72 | .row__hashes { 73 | .column__hash--link { 74 | button: standard-button; 75 | button-color: var(--color8) color(var(--color5) a(0.8)) var(--color5); 76 | } 77 | } 78 | 79 | .column { 80 | lost-column: 2.5/12; 81 | padding: 10px 0; 82 | font-family: var(--fontFamily0-Regular); 83 | color: var(--color8); 84 | font-size: 10px; 85 | font-weight: normal; 86 | overflow: hidden; 87 | 88 | &:last-child { 89 | lost-column: 9.5/12; 90 | } 91 | 92 | &__hash, 93 | &__hash--link { 94 | text-overflow: ellipsis; 95 | } 96 | 97 | .ada { 98 | padding-right: 10px; 99 | background-repeat: no-repeat; 100 | background-position: right center; 101 | background-size: 8px 7px; 102 | } 103 | } 104 | 105 | .summary-empty { 106 | 107 | &__container { 108 | padding: 10px 18px; 109 | 110 | font-size: 10px; 111 | font-family: var(--fontFamily0-Regular); 112 | color: var(--color13); 113 | 114 | } 115 | 116 | @media (--md) { 117 | &__container { 118 | padding: 10px 24px; 119 | } 120 | } 121 | } 122 | 123 | .transaction-header { 124 | background-color: var(--color7); /* overridden */ 125 | } 126 | 127 | 128 | &__tx-container { 129 | lost-utility: clearfix; 130 | } 131 | 132 | } 133 | -------------------------------------------------------------------------------- /frontend/src/Explorer/View/blocks.css: -------------------------------------------------------------------------------- 1 | 2 | /* blocks (slot) / epoch page */ 3 | 4 | .explorer-blocks { 5 | 6 | &__wrapper { 7 | background-color: var(--color4); 8 | } 9 | 10 | 11 | &__container { 12 | @extend .outer-container; 13 | padding: 46px 0px 38px 0px; 14 | 15 | } 16 | 17 | @media (--md) { 18 | &__container { 19 | max-width:var(--max-width-desktop); 20 | padding: 56px 48px 48px 48px; 21 | } 22 | } 23 | } 24 | 25 | 26 | /* shared */ 27 | 28 | .blocks-header { 29 | padding: 10px 24px; 30 | 31 | font-size: 10px; 32 | font-family: var(--fontFamily0-Bold); 33 | font-weight: bold; 34 | color: var(--color8); 35 | 36 | lost-utility: clearfix; 37 | } 38 | 39 | .blocks-column { 40 | 41 | &__epoch { 42 | lost-column: 1/2; 43 | /* TODO change it ^ to 3/12 if `since` (age) column will be back */ 44 | button: standard-button; 45 | button-color: var(--color8) var(--color5) var(--color5); 46 | } 47 | &__slot { 48 | lost-column: 1/2; 49 | /* TODO change it ^ to 3/12 if `since` (age) column will be back */ 50 | button: standard-button; 51 | button-color: var(--color8) var(--color5) var(--color5); 52 | } 53 | &__age { 54 | @extend .hide; 55 | /*lost-column: 6/12 3;*/ 56 | /* TODO set it ^ back if `since` (age) column will be back */ 57 | } 58 | &__txs { 59 | @extend .hide; 60 | } 61 | &__totalSend { 62 | @extend .hide; 63 | 64 | .ada { 65 | padding-right: 10px; 66 | background-repeat: no-repeat; 67 | background-position: right center; 68 | background-size: 8px 7px; 69 | } 70 | 71 | } 72 | &__lead { 73 | @extend .hide; 74 | } 75 | &__size { 76 | @extend .hide; 77 | white-space: nowrap; 78 | } 79 | } 80 | 81 | @media (--md) { 82 | .blocks-column { 83 | &__epoch { 84 | lost-column: 1/12; 85 | } 86 | &__slot { 87 | lost-column: 1/12; 88 | } 89 | &__age { 90 | @extend .hide; 91 | /*lost-column: 3/12;*/ 92 | /* TODO set it ^ back if `since` (age) column will be back */ 93 | } 94 | &__txs { 95 | @extend .show; 96 | lost-column: 2/12; 97 | } 98 | &__totalSend { 99 | @extend .show; 100 | lost-column: 3/12; 101 | /* TODO change it ^ to 2/12 if `since` column will be back */ 102 | } 103 | &__lead { 104 | @extend .show; 105 | lost-column: 3/12; 106 | /* TODO change it ^ to 2/12 if `since` column will be back */ 107 | } 108 | &__size { 109 | @extend .show; 110 | lost-column: 2/12; 111 | /* TODO change it ^ to 1/12 if `since` column will be back */ 112 | } 113 | } 114 | } 115 | 116 | 117 | .blocks-waiting { 118 | padding: 10px 24px; 119 | 120 | font-size: 10px; 121 | font-family: var(--fontFamily0-Regular); 122 | color: var(--color13); 123 | } 124 | 125 | .blocks-failed { 126 | padding: 0 24px 30px 24px; 127 | font-size: 10px; 128 | font-family: var(--fontFamily0-Regular); 129 | color: var(--color1); 130 | } 131 | 132 | .blocks-message { 133 | padding: 0 18px; 134 | font-size: 10px; 135 | font-family: var(--fontFamily0-Regular); 136 | color: var(--color13); 137 | } 138 | 139 | .blocks-body { 140 | padding-bottom: 24px; 141 | 142 | &__wrapper { 143 | position: relative; 144 | } 145 | 146 | &__cover { 147 | @extend .hide; 148 | position: absolute; 149 | top: 0; 150 | bottom: 0; 151 | left: 0; 152 | right: 0; 153 | background-color: color(var(--color3) a(0.5)); 154 | } 155 | 156 | &__cover-label { 157 | position: relative; 158 | top: 50%; 159 | transform: translateY(-50%); 160 | text-align: center; 161 | font-size: 10px; 162 | font-family: var(--fontFamily0-Regular); 163 | color: var(--color8); 164 | } 165 | 166 | &__row { 167 | lost-utility: clearfix; 168 | display: block; 169 | padding: 10px 24px; 170 | color: var(--color8); 171 | 172 | font-size: 10px; 173 | font-family: var(--fontFamily0-Regular); 174 | font-weight: normal; 175 | 176 | &:nth-child(odd) { 177 | background-color: var(--color7); 178 | } 179 | } 180 | } 181 | 182 | .blocks-footer { 183 | text-align: center; 184 | } 185 | -------------------------------------------------------------------------------- /frontend/src/Explorer/View/calculator.css: -------------------------------------------------------------------------------- 1 | 2 | .explorer-calculator { 3 | background-color: var(--color4); 4 | 5 | &__container { 6 | @neat-outer-container; 7 | } 8 | 9 | } 10 | -------------------------------------------------------------------------------- /frontend/src/Explorer/View/footer.css: -------------------------------------------------------------------------------- 1 | 2 | :root { 3 | --height-footer-bottom: 70px; 4 | 5 | --cardano-logo-width: 103.5px; 6 | --cardano-logo-height: 30px; 7 | 8 | 9 | } 10 | .explorer-footer { 11 | background-color: var(--color2); 12 | 13 | &__container { 14 | lost-utility: clearfix; 15 | } 16 | 17 | &__top { 18 | border-bottom: 1px solid color(var(--color3) a(0.1)); 19 | 20 | .nav__container { 21 | @extend .outer-container; 22 | padding: 24px 18px 0 18px; 23 | } 24 | 25 | @media (--md) { 26 | .nav__container { 27 | padding: 48px 72px 24px 72px; 28 | } 29 | } 30 | 31 | .nav-item { 32 | 33 | &__container { 34 | padding-bottom: 24px; 35 | } 36 | 37 | &__container { 38 | lost-column: 1/2; 39 | } 40 | 41 | @media (--md) { 42 | &__container { 43 | lost-column: 1/5; 44 | } 45 | } 46 | 47 | &__header { 48 | padding-bottom: 12px; 49 | color: var(--color0); 50 | font-family: var(--fontFamily0-Light); 51 | font-size: 12px; 52 | } 53 | 54 | &__item { 55 | font-family: var(--fontFamily0-Light); 56 | font-size: 11px; 57 | line-height: 20px; 58 | display: block; 59 | 60 | button: standard-button; 61 | button-color: color(var(--color3) a(0.5)) color(var(--color3) a(0.3)) color(var(--color3) a(0.3)); 62 | } 63 | } 64 | } 65 | 66 | &__bottom { 67 | @extend .outer-container; 68 | color: color(var(--color0) a(0.5)); 69 | padding: 0 18px; 70 | 71 | .content { 72 | height: var(--height-footer-bottom); 73 | 74 | .logo__container { 75 | box: horizontal; 76 | height: 100%; 77 | } 78 | 79 | .logo__cardano-name { 80 | display: block; 81 | width: 90.2px; 82 | height: 26px; 83 | box-item: center; 84 | } 85 | 86 | @media (--md) { 87 | .logo__cardano-name { 88 | width: 104px; 89 | height: 30px; 90 | } 91 | } 92 | 93 | .logo__iohk-name { 94 | width: 120.1px; 95 | height: 26px; 96 | margin-left: 24px; 97 | box-item: center; 98 | display: block; 99 | } 100 | 101 | @media (--md) { 102 | .logo__iohk-name { 103 | width: 138.5px; 104 | height: 30px; 105 | } 106 | } 107 | 108 | .split { 109 | display: inline-block; 110 | background: var(--color14); 111 | opacity: 0.2; 112 | width: 1px; 113 | height: 30px; 114 | margin: 0 24px; 115 | box-item: center; 116 | } 117 | 118 | .support { 119 | color: var(--color3); 120 | opacity: 0.7; 121 | font-family: var(--fontFamily0-Regular); 122 | font-size: 8px; 123 | box-sizing: border-box; 124 | text-transform: uppercase; 125 | box-item: center; 126 | display: block; 127 | } 128 | 129 | @media (--md) { 130 | .support { 131 | font-size: 9px; 132 | } 133 | } 134 | 135 | &__left { 136 | lost-column: 1/1; 137 | box: horizontal; 138 | } 139 | 140 | @media (--md) { 141 | &__left { 142 | lost-column: 3/4; 143 | } 144 | } 145 | 146 | &__right { 147 | @extend .hide; 148 | } 149 | 150 | @media (--md) { 151 | &__right { 152 | @extend .show; 153 | box: horizontal right; 154 | lost-column: 1/4; 155 | } 156 | } 157 | } 158 | 159 | .copy { 160 | font-family: var(--fontFamily0-Light); 161 | font-size: 10px; 162 | box-item: center; 163 | } 164 | 165 | .lang__select { 166 | padding-right: 10px; 167 | } 168 | } 169 | 170 | &__meta { 171 | text-align: center; 172 | 173 | .version { 174 | font-family: var(--fontFamily0-Light); 175 | font-size: 9px; 176 | color: color(var(--color3) a(0.3)) 177 | } 178 | 179 | .commit { 180 | font-family: var(--fontFamily0-Light); 181 | font-size: 9px; 182 | padding-left: 5px; 183 | 184 | button: standard-button; 185 | button-color: color(var(--color3) a(0.3)) color(var(--color3) a(0.3)) color(var(--color3) a(0.3)); 186 | } 187 | } 188 | 189 | @media (--md) { 190 | &__bottom { 191 | padding: 10px 72px 5px 72px; 192 | } 193 | } 194 | 195 | } 196 | -------------------------------------------------------------------------------- /frontend/src/Explorer/View/layout.css: -------------------------------------------------------------------------------- 1 | 2 | .explorer-bg__container { 3 | position: absolute; 4 | top: 0; 5 | left: 0; 6 | right: 0; 7 | bottom: 0; 8 | background-color: var(--color2); 9 | background-image: var(--bg-header-url); 10 | background-repeat: no-repeat; 11 | background-size: 100% var(--min-height-header-bg-image); 12 | } 13 | 14 | .explorer-content__wrapper { 15 | position: absolute; 16 | top: 0; 17 | left: 0; 18 | right: 0; 19 | bottom: 0; 20 | } 21 | 22 | .explorer-content { 23 | padding-top: var(--height-header-top-mobile); 24 | } 25 | 26 | @media (--md) { 27 | .explorer-content { 28 | padding-top: var(--height-header-top); 29 | } 30 | } 31 | 32 | @media (--md) { 33 | .explorer-route-dashboard { 34 | .explorer-content { 35 | padding-top: 0; 36 | } 37 | } 38 | } 39 | 40 | .label-count { 41 | color: var(--color1); 42 | } 43 | -------------------------------------------------------------------------------- /frontend/src/Explorer/View/notfound.css: -------------------------------------------------------------------------------- 1 | 2 | .explorer-404 { 3 | 4 | &__wrapper { 5 | background-color: var(--color4); 6 | } 7 | 8 | &__container { 9 | @neat-outer-container; 10 | padding: 56px 48px 48px 48px; 11 | box: vertical center; 12 | } 13 | 14 | .bg-image-404 { 15 | padding-top: 222px; 16 | padding-bottom: 222px; 17 | width: 267.5px; 18 | height: 152.5px; 19 | box-item: center; 20 | display: block; 21 | } 22 | 23 | @media (--xs) { 24 | .bg-image-404 { 25 | padding-top: 222px; 26 | padding-bottom: 222px; 27 | width: 133.75px; 28 | height: 76.25px; 29 | box-item: center; 30 | } 31 | } 32 | 33 | .description { 34 | padding: 0 24px 24px 24px; 35 | font-family: var(--fontFamily0-Regular); 36 | color: var(--color8); 37 | font-size: 10px; 38 | font-weight: normal; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /frontend/src/Explorer/View/search.css: -------------------------------------------------------------------------------- 1 | 2 | .explorer-search { 3 | 4 | &__container { 5 | border: 1px solid color(var(--color0) a(0.3)); 6 | border-radius: 2px; 7 | padding: 5px 5px 5px 0px; 8 | box: horizontal; 9 | 10 | &.focused { 11 | border: 1px solid color(var(--color0) a(0.5)); 12 | } 13 | } 14 | @media (--sm) { 15 | &__container { 16 | width:100%; 17 | margin: 0; 18 | } 19 | } 20 | 21 | &__wrapper { 22 | box-item: flex-1; 23 | box: horizontal; 24 | } 25 | 26 | &__label { 27 | color: color(var(--color3) a(0.5)); 28 | font-family: var(--fontFamily0-Light); 29 | box: center; 30 | padding-left: 15px; 31 | } 32 | 33 | &__input { 34 | border: none; 35 | font-size: 13px; 36 | font-family: var(--fontFamily0-Regular); 37 | padding: 0 5px; 38 | color: color(var(--color0) a(0.3)); 39 | 40 | &.focused { 41 | color: var(--color0); 42 | } 43 | 44 | background-color: transparent; 45 | 46 | &::placeholder { 47 | font-family: var(--fontFamily0-Light); 48 | color: color(var(--color0) a(0.3)); 49 | } 50 | 51 | &--address-tx { 52 | box-item: flex-1; 53 | } 54 | 55 | &--epoch { 56 | width: 55px; 57 | } 58 | 59 | @media (--lg) { 60 | &--epoch { 61 | width: 100px; 62 | } 63 | } 64 | 65 | &--slot { 66 | width: 55px; 67 | } 68 | 69 | @media (--lg) { 70 | &--slot { 71 | width: 100px; 72 | } 73 | } 74 | 75 | } 76 | 77 | 78 | @media (--lg) { 79 | &__input { 80 | padding: 0 15px; 81 | } 82 | } 83 | 84 | &__btn { 85 | box-item: right; 86 | button: standard-button; 87 | height: 30px; 88 | width: 40px; 89 | 90 | background-repeat: no-repeat; 91 | background-position: center center; 92 | background-size: 15px 15px; 93 | opacity: 0.5; 94 | 95 | &.focused { 96 | opacity: 1; 97 | } 98 | } 99 | 100 | &-nav { 101 | 102 | &__container { 103 | box: horizontal center; 104 | } 105 | 106 | &__item { 107 | box: center; 108 | padding-right: 5px; 109 | 110 | color: color(var(--color3) a(0.5)); 111 | font-size: 13px; 112 | font-family: var(--fontFamily0-Light); 113 | cursor: pointer; 114 | white-space: nowrap; 115 | 116 | &:hover, 117 | &:active, 118 | &.selected { 119 | color: color(var(--color0)); 120 | font-family: var(--fontFamily0-Regular); 121 | } 122 | 123 | } 124 | 125 | @media (--lg) { 126 | &__item { 127 | padding-right: 15px; 128 | } 129 | } 130 | } 131 | 132 | } 133 | -------------------------------------------------------------------------------- /frontend/src/Explorer/View/transaction.css: -------------------------------------------------------------------------------- 1 | 2 | .explorer-transaction { 3 | 4 | &__wrapper { 5 | background-color: var(--color4); 6 | 7 | &:nth-child(even) { 8 | background-color: var(--color6); 9 | } 10 | } 11 | 12 | &__container { 13 | @neat-outer-container; 14 | padding: 24px 0; 15 | } 16 | 17 | @media (--md) { 18 | &__container { 19 | padding: 56px 48px 48px 48px; 20 | } 21 | } 22 | 23 | &__message { 24 | padding: 0 18px; 25 | font-size: 10px; 26 | font-family: var(--fontFamily0-Regular); 27 | color: var(--color13); 28 | } 29 | 30 | @media (--md) { 31 | &__message { 32 | padding: 0 24px; 33 | } 34 | } 35 | 36 | .table-summary { 37 | 38 | font-family: var(--fontFamily0-Regular); 39 | font-size: 10px; 40 | color: var(--color8); 41 | width: 100%; 42 | 43 | tr { 44 | 45 | &:nth-child(odd) { 46 | background-color: transparent; 47 | } 48 | 49 | &:nth-child(even) { 50 | background-color: var(--color7); 51 | } 52 | } 53 | 54 | td { 55 | padding: 10px 10px 10px 0; 56 | 57 | &:first-child { 58 | padding-left: 18px; 59 | width: 164px; 60 | } 61 | 62 | @media (--md) { 63 | &:first-child { 64 | padding-left: 24px; 65 | } 66 | } 67 | 68 | .ada { 69 | padding-right: 10px; 70 | background-repeat: no-repeat; 71 | background-position: right center; 72 | background-size: 8px 7px; 73 | } 74 | 75 | .link { 76 | color: var(--color8); 77 | button: standard-button; 78 | button-color: var(--color8) color(var(--color5) a(0.8)) var(--color5); 79 | } 80 | } 81 | 82 | } 83 | 84 | .tx-failed { 85 | padding: 0 18px 30px 18px; 86 | font-size: 10px; 87 | font-family: var(--fontFamily0-Regular); 88 | color: var(--color1); 89 | } 90 | 91 | @media (--md) { 92 | .tx-failed { 93 | padding: 0 24px 30px 24px; 94 | } 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /frontend/src/Lib/BigNumber/BigNumber.Test.purs: -------------------------------------------------------------------------------- 1 | module BigNumber.Test where 2 | 3 | import Prelude 4 | import BigNumber (BIGNUMBER, BigNumberFormat(..), defaultFormat, dividedByInt, fromString, toFormat, toString, toString') 5 | import Control.Monad.Aff (Aff) 6 | import Control.Monad.Eff.Class (liftEff) 7 | import Control.Monad.State (StateT) 8 | import Data.Identity (Identity) 9 | import Data.Maybe (Maybe(..), fromMaybe) 10 | import Data.Newtype (unwrap) 11 | import Test.Spec (Group, describe, it) 12 | import Test.Spec.Assertions (shouldEqual) 13 | 14 | 15 | testBigNumber :: forall r. StateT (Array (Group (Aff (bigNumber :: BIGNUMBER | r) Unit))) Identity Unit 16 | testBigNumber = 17 | describe "BigNumber" do 18 | it "creates an instance of a BigNumber from a string" do 19 | let value = "12345678901" 20 | result = fromString value 21 | (fromMaybe "failed" $ show <$> result) `shouldEqual` value 22 | it "converts a BigNumber to a string by using base of 10 (default base) " do 23 | let value = "750000" 24 | result = fromMaybe "0" $ toString' <$> fromString value 25 | result `shouldEqual` value 26 | it "converts a BigNumber to a string by using base of 9 " do 27 | let value = "362.875" 28 | result = fromMaybe "0" $ toString <$> fromString value <*> Just 9 29 | result `shouldEqual` "442.77777777777777777778" 30 | it "divides a BigNumber by 20 " do 31 | let value = "355" 32 | mBigNumberDivided = dividedByInt <$> (fromString value) <*> Just 5 33 | result = fromMaybe "0" $ toString' <$> mBigNumberDivided 34 | result `shouldEqual` "71" 35 | it "formats a BigNumber by a given format " do 36 | let df = unwrap defaultFormat 37 | newFormat = BigNumberFormat $ df { decimalSeparator = "," 38 | , groupSeparator = "." 39 | } 40 | result <- liftEff $ case fromString "123456789.1" of 41 | Nothing -> pure "0" 42 | Just bn -> toFormat bn newFormat 1 43 | result `shouldEqual` "123.456.789,1" 44 | it "formats a BigNumber by given decimal places " do 45 | let value = "123456789.123456789" 46 | result <- liftEff $ case fromString "123456789.123456789" of 47 | Nothing -> pure "0" 48 | Just bn -> toFormat bn defaultFormat 1 49 | result `shouldEqual` "123,456,789.1" 50 | -------------------------------------------------------------------------------- /frontend/src/Lib/BigNumber/BigNumber.js: -------------------------------------------------------------------------------- 1 | const BigNumber = require('bignumber.js'); 2 | 3 | exports.bigNumberImpl = function (just, nothing, value) { 4 | try { 5 | const bigNumber = new BigNumber(value); 6 | return just(bigNumber); 7 | } catch (e) { 8 | return nothing; 9 | } 10 | } 11 | 12 | exports.dividedByImpl = function (bigNumber, by) { 13 | return bigNumber.dividedBy(by); 14 | } 15 | 16 | exports.toStringImpl = function (bigNumber, base) { 17 | return bigNumber.toString(base); 18 | } 19 | 20 | const format = function (format) { 21 | BigNumber.config({ 22 | FORMAT: format 23 | }); 24 | } 25 | 26 | exports.formatImpl = format; 27 | 28 | exports.toFormatImpl = function (bigNumber, formatObj, decimalPlaces) { 29 | // Note: Set format before adding decimal places. 30 | // This is needed to change formats while switching locales 31 | format(formatObj); 32 | return bigNumber.toFormat(decimalPlaces); 33 | } 34 | -------------------------------------------------------------------------------- /frontend/src/Lib/BigNumber/BigNumber.purs: -------------------------------------------------------------------------------- 1 | module BigNumber 2 | ( BIGNUMBER 3 | , BigNumber 4 | , BigNumberFormat(..) 5 | , defaultFormat 6 | , dividedByInt 7 | , fromString 8 | , format 9 | , toFormat 10 | , toString 11 | , toString' 12 | ) 13 | where 14 | 15 | import Prelude 16 | import Control.Monad.Eff (kind Effect, Eff) 17 | import Control.Monad.Eff.Uncurried (EffFn1, EffFn3, runEffFn1, runEffFn3) 18 | import Data.Function.Uncurried (Fn2, Fn3, runFn2, runFn3) 19 | import Data.Maybe (Maybe(..)) 20 | import Data.Newtype (class Newtype) 21 | 22 | -- | BigNumber effect 23 | foreign import data BIGNUMBER :: Effect 24 | -- | An reeeaaaaally big integer 25 | foreign import data BigNumber :: Type 26 | 27 | bigNumberBase :: Int 28 | bigNumberBase = 10 29 | 30 | instance sBigNumber :: Show BigNumber where 31 | show bn = toString bn bigNumberBase 32 | 33 | foreign import bigNumberImpl :: forall a. Fn3 (a -> Maybe a) (Maybe a) a (Maybe BigNumber) 34 | 35 | bigNumber :: forall a. a -> Maybe BigNumber 36 | bigNumber value = runFn3 bigNumberImpl Just Nothing value 37 | 38 | fromString :: String -> Maybe BigNumber 39 | fromString = bigNumber 40 | 41 | foreign import toStringImpl :: Fn2 BigNumber Int String 42 | 43 | toString :: BigNumber -> Int -> String 44 | toString = runFn2 toStringImpl 45 | 46 | toString' :: BigNumber -> String 47 | toString' bn = runFn2 toStringImpl bn bigNumberBase 48 | 49 | foreign import dividedByImpl :: forall a. Fn2 BigNumber a BigNumber 50 | 51 | dividedByInt :: BigNumber -> Int -> BigNumber 52 | dividedByInt = runFn2 dividedByImpl 53 | 54 | -- Format definition 55 | -- http://mikemcl.github.io/bignumber.js/#format 56 | newtype BigNumberFormat = BigNumberFormat 57 | { decimalSeparator :: String 58 | , groupSeparator :: String 59 | , groupSize :: Int 60 | , secondaryGroupSize :: Int 61 | , fractionGroupSeparator :: String 62 | , fractionGroupSize :: Int 63 | } 64 | 65 | derive instance ntBigNumberFormat :: Newtype BigNumberFormat _ 66 | 67 | defaultFormat :: BigNumberFormat 68 | defaultFormat = BigNumberFormat 69 | { decimalSeparator: "." 70 | , groupSeparator: "," 71 | , groupSize: 3 72 | , secondaryGroupSize: 0 73 | , fractionGroupSeparator: " " 74 | , fractionGroupSize: 0 75 | } 76 | 77 | foreign import formatImpl :: forall eff. EffFn1 (bigNumber :: BIGNUMBER | eff) BigNumberFormat Unit 78 | 79 | format :: forall eff. BigNumberFormat -> Eff (bigNumber :: BIGNUMBER | eff) Unit 80 | format = runEffFn1 formatImpl 81 | 82 | -- | `toFormat` function using `BigNumberFormat` and 83 | -- | a given value of decimal place 84 | foreign import toFormatImpl :: forall eff. EffFn3 (bigNumber :: BIGNUMBER | eff) BigNumber BigNumberFormat Int String 85 | 86 | toFormat :: forall eff. BigNumber -> BigNumberFormat -> Int -> Eff (bigNumber :: BIGNUMBER | eff) String 87 | toFormat = runEffFn3 toFormatImpl 88 | -------------------------------------------------------------------------------- /frontend/src/Lib/Waypoints/Waypoints.js: -------------------------------------------------------------------------------- 1 | require('@noframework.waypoints'); 2 | 3 | exports.waypointImpl = function (elementId, callback, offset) { 4 | return new Waypoint({ 5 | element: document.getElementById(elementId), 6 | handler: function(direction) { 7 | callback(direction)(); 8 | }, 9 | offset: offset 10 | }); 11 | } 12 | 13 | exports.destroy = function (waypoint) { 14 | return waypoint.destroy(); 15 | } 16 | -------------------------------------------------------------------------------- /frontend/src/Lib/Waypoints/Waypoints.purs: -------------------------------------------------------------------------------- 1 | module Waypoints where 2 | 3 | import Prelude 4 | import Control.Monad.Eff (kind Effect, Eff) 5 | import Control.Monad.Eff.Uncurried (EffFn3, runEffFn3) 6 | import DOM.Node.Types (ElementId) 7 | import Data.Function.Uncurried (Fn1) 8 | import Data.Generic (class Generic, gShow) 9 | import Data.Newtype (class Newtype) 10 | 11 | foreign import data WAYPOINT :: Effect 12 | foreign import data Waypoint :: Type 13 | 14 | type WaypointHandler eff = WaypointDirection -> Eff (waypoint :: WAYPOINT | eff) Unit 15 | 16 | type WaypointOffset = Int 17 | 18 | defaultWaypointOffset :: WaypointOffset 19 | defaultWaypointOffset = 0 20 | 21 | newtype WaypointDirection = WaypointDirection String 22 | 23 | derive instance gWaypointDirection :: Generic WaypointDirection 24 | derive instance ntWaypointDirection :: Newtype WaypointDirection _ 25 | derive instance eqWaypointDirection :: Eq WaypointDirection 26 | instance sWaypointDirection :: Show WaypointDirection where 27 | show = gShow 28 | 29 | up :: WaypointDirection 30 | up = WaypointDirection "up" 31 | 32 | down :: WaypointDirection 33 | down = WaypointDirection "down" 34 | 35 | foreign import waypointImpl :: forall eff. EffFn3 (waypoint :: WAYPOINT | eff) ElementId (WaypointHandler eff) WaypointOffset Waypoint 36 | 37 | -- | Initializes a `Waypoint` 38 | waypoint :: forall eff. ElementId -> (WaypointHandler eff) -> Eff (waypoint :: WAYPOINT | eff) Waypoint 39 | waypoint elemId handler = runEffFn3 waypointImpl elemId handler defaultWaypointOffset 40 | 41 | -- | Initializes a `Waypoint` with an offset 42 | waypoint' :: forall eff. ElementId -> (WaypointHandler eff) -> WaypointOffset -> Eff (waypoint :: WAYPOINT | eff) Waypoint 43 | waypoint' = runEffFn3 waypointImpl 44 | 45 | foreign import destroy :: forall eff. Fn1 Waypoint (Eff (waypoint :: WAYPOINT | eff) Unit) 46 | -------------------------------------------------------------------------------- /frontend/src/Main.Test.purs: -------------------------------------------------------------------------------- 1 | module Main.Test where 2 | 3 | import Prelude 4 | import BigNumber (BIGNUMBER) 5 | import BigNumber.Test (testBigNumber) 6 | import Control.Monad.Eff (Eff) 7 | import Data.Time.Test (testNominalDiffTime) 8 | import Explorer.Api.Socket.Test (testApiSocket) 9 | import Explorer.Routes.Test (testRoutes) 10 | import Explorer.Update.Test (testUpdate) 11 | import Explorer.Util.Config.Test (testConfigUtil) 12 | import Explorer.Util.Data.Test (testDataUtil) 13 | import Explorer.Util.String.Test (testStringUtil) 14 | import Explorer.Util.Time.Test (testPrettyDuration) 15 | import Explorer.View.CSS.Test (testCSS) 16 | import Explorer.View.Common.Test (testCommonViews) 17 | import Test.Spec.Reporter.Console (consoleReporter) 18 | import Test.Spec.Runner (RunnerEffects, run) 19 | 20 | main :: Eff (RunnerEffects (bigNumber :: BIGNUMBER)) Unit 21 | main = run [consoleReporter] do 22 | testApiSocket 23 | testBigNumber 24 | testCommonViews 25 | testConfigUtil 26 | testCSS 27 | testNominalDiffTime 28 | testPrettyDuration 29 | testDataUtil 30 | testStringUtil 31 | testRoutes 32 | testUpdate 33 | -------------------------------------------------------------------------------- /frontend/src/Test/MockFactory.purs: -------------------------------------------------------------------------------- 1 | -- | Helper functions to mock data. 2 | -- | 3 | -- | _Note_: All these functions are used for testing only. 4 | 5 | module Explorer.Test.MockFactory where 6 | 7 | import Prelude 8 | 9 | import Data.Array ((..)) 10 | import Data.Lens (set) 11 | import Data.Maybe (Maybe(..)) 12 | import Data.Time.NominalDiffTime (NominalDiffTime, mkTime) 13 | import Data.Tuple (Tuple(..)) 14 | import Explorer.Types.State (CTxBriefs) 15 | import Explorer.Util.Factory (mkCAddress, mkCHash, mkCTxId, mkCoin) 16 | import Pos.Explorer.Web.ClientTypes (CAddress, CAddressSummary(..), CAddressType(..), CBlockEntry(..), CCoin, CGenesisAddressInfo(..), CGenesisSummary(..), CHash, CTxBrief(..), CTxEntry(..), CTxId) 17 | import Pos.Explorer.Web.Lenses.ClientTypes (_CAddressSummary, _CBlockEntry, _CTxEntry, caTxList, cbeBlkHash, cbeEpoch, cbeSlot, cbeTimeIssued, cteId, cteTimeIssued) 18 | 19 | -- | Creates a `CTxEntry` with "empty" data 20 | mkEmptyCTxEntry :: CTxEntry 21 | mkEmptyCTxEntry = CTxEntry 22 | { cteId: mkCTxId "--" 23 | , cteTimeIssued: mkTime 0.0 24 | , cteAmount: mkCoin "0" 25 | } 26 | 27 | -- | Update hash of a transcation 28 | setIdOfTx :: CTxId -> CTxEntry -> CTxEntry 29 | setIdOfTx txId tx = 30 | set (_CTxEntry <<< cteId) txId tx 31 | 32 | -- | Update time of a transaction 33 | setTimeOfTx :: NominalDiffTime -> CTxEntry -> CTxEntry 34 | setTimeOfTx time tx = 35 | set (_CTxEntry <<< cteTimeIssued) time tx 36 | 37 | mkEmptyCAddressSummary :: CAddressSummary 38 | mkEmptyCAddressSummary = CAddressSummary 39 | { caAddress: mkCAddress "--" 40 | , caType: CUnknownAddress 41 | , caTxNum: 0 42 | , caBalance: mkCoin "0" 43 | , caTxList: [] 44 | , caIsRedeemed: Nothing 45 | } 46 | 47 | -- | Update txs of a `CAddressSummary` 48 | setTxOfAddressSummary :: CTxBriefs -> CAddressSummary -> CAddressSummary 49 | setTxOfAddressSummary txs addr = 50 | set (_CAddressSummary <<< caTxList) txs addr 51 | 52 | mkCBlockEntry :: CBlockEntry 53 | mkCBlockEntry = CBlockEntry 54 | { cbeEpoch: 0 55 | , cbeSlot: 0 56 | , cbeBlkHash: mkCHash "0" 57 | , cbeTimeIssued: Nothing 58 | , cbeTxNum: 0 59 | , cbeTotalSent: mkCoin "0" 60 | , cbeFees: mkCoin "0" 61 | , cbeSize: 0 62 | , cbeBlockLead: Nothing 63 | } 64 | 65 | 66 | -- | Update time of a slot 67 | setTimeOfBlock :: NominalDiffTime -> CBlockEntry -> CBlockEntry 68 | setTimeOfBlock time block = 69 | set (_CBlockEntry <<< cbeTimeIssued) (Just time) block 70 | 71 | -- | Update slot / epoch of a slot 72 | setEpochSlotOfBlock :: Int -> Int -> CBlockEntry -> CBlockEntry 73 | setEpochSlotOfBlock epoch slot block = 74 | set (_CBlockEntry <<< cbeEpoch) epoch $ 75 | set (_CBlockEntry <<< cbeSlot) slot block 76 | 77 | -- | Update hash of a slot 78 | setHashOfBlock :: CHash -> CBlockEntry -> CBlockEntry 79 | setHashOfBlock hash block = 80 | set (_CBlockEntry <<< cbeBlkHash) hash block 81 | 82 | mkCTxBriefs :: Array Int -> CTxBriefs 83 | mkCTxBriefs indexes = 84 | map mkCTxBrief indexes 85 | 86 | mkCTxBrief :: Int -> CTxBrief 87 | mkCTxBrief index = CTxBrief 88 | { ctbId: mkCTxId $ show index 89 | , ctbTimeIssued: mkTime 0.0 90 | , ctbInputs: mkCtbInOutputs [index] 91 | , ctbOutputs: mkCtbInOutputs ((index + 1)..(index + 2)) 92 | , ctbInputSum: mkCoin "0" 93 | , ctbOutputSum: mkCoin "0" 94 | } 95 | 96 | mkCtbInOutput :: Int -> Int -> Tuple CAddress CCoin 97 | mkCtbInOutput addr coin = 98 | Tuple (mkCAddress $ "address-" <> show addr) (mkCoin $ show coin) 99 | 100 | mkCtbInOutputs :: Array Int -> Array (Tuple CAddress CCoin) 101 | mkCtbInOutputs indexes = 102 | map (\index -> mkCtbInOutput index index) indexes 103 | 104 | mkCGenesisSummary :: CGenesisSummary 105 | mkCGenesisSummary = CGenesisSummary 106 | { cgsNumTotal : 2 107 | , cgsNumRedeemed : 1 108 | , cgsNumRemaining : 1 109 | , cgsAmountRedeemed : mkCoin "15000000" 110 | , cgsAmountRemaining : mkCoin "2225295000000" 111 | } 112 | 113 | mkCGenesisAddressInfo :: CGenesisAddressInfo 114 | mkCGenesisAddressInfo = 115 | -- Commenting out RSCoin addresses until they can actually be displayed. 116 | -- See comment in src/Pos/Explorer/Web/ClientTypes.hs for more information. 117 | CGenesisAddressInfo 118 | { cgaiCardanoAddress : mkCAddress "3meLwrCDE4C7RofEdkZbUuR75ep3EcTmZv9ebcdjfMtv5H" 119 | -- , cgaiRSCoinAddress : mkCAddress "JwvXUQ31cvrFpqqtx6fB-NOp0Q-eGQs74yXMGa-72Ak=" 120 | , cgaiGenesisAmount : mkCoin "15000000" 121 | , cgaiIsRedeemed : false 122 | } 123 | -------------------------------------------------------------------------------- /frontend/src/index.css: -------------------------------------------------------------------------------- 1 | /* cardano-sl explorer*/ 2 | @import "animate.css"; 3 | @import "global.css"; 4 | @import "./Explorer/View/common.css"; 5 | @import "./Explorer/View/search.css"; 6 | @import "./Explorer/View/layout.css"; 7 | @import "./Explorer/View/header.css"; 8 | @import "./Explorer/View/footer.css"; 9 | @import "./Explorer/View/Dashboard/dashboard.css"; 10 | @import "./Explorer/View/Dashboard/api.css"; 11 | @import "./Explorer/View/Dashboard/hero.css"; 12 | @import "./Explorer/View/Dashboard/transactions.css"; 13 | @import "./Explorer/View/address.css"; 14 | @import "./Explorer/View/calculator.css"; 15 | @import "./Explorer/View/transaction.css"; 16 | @import "./Explorer/View/block.css"; 17 | @import "./Explorer/View/blocks.css"; 18 | @import "./Explorer/View/genesis.css"; 19 | @import "./Explorer/View/notfound.css"; 20 | 21 | /* normalize */ 22 | @reset-global pc; 23 | @reset-global mobile; 24 | -------------------------------------------------------------------------------- /frontend/src/index.js: -------------------------------------------------------------------------------- 1 | // styles 2 | import './index.css'; 3 | // app 4 | import Main from './Main.purs'; 5 | import {initialState} from './Explorer/State.purs'; 6 | 7 | // HMR 8 | if(module.hot) { 9 | var main = Main.main(window.__puxLastState || initialState)() 10 | main.state.subscribe(function (state) { 11 | window.__puxLastState = state; 12 | }); 13 | module.hot.accept(); 14 | } else { 15 | Main.main(initialState)(); 16 | } 17 | -------------------------------------------------------------------------------- /frontend/src/index.tpl.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 |