├── .eslintrc ├── .gitignore ├── README.md ├── components ├── Copy.js ├── DataView.js ├── Error.js ├── Filter.js ├── Header.js ├── Input.js ├── JSON │ ├── JSONNode.js │ └── JSONView.js ├── JsonBrowse.js ├── JsonInput.js ├── Layout.js └── Spinner.js ├── package.json ├── pages ├── _document.js ├── index.js └── info.js ├── static ├── css │ └── fonts.css ├── favicon.png ├── fonts │ ├── SourceCodePro-Bold.woff │ ├── SourceCodePro-BoldItalic.woff │ ├── SourceCodePro-Italic.woff │ ├── SourceCodePro-Medium.woff │ ├── SourceCodePro-MediumIt.woff │ ├── SourceCodePro-Regular.woff │ ├── SourceCodePro-Semibold.woff │ └── SourceCodePro-SemiboldItalic.woff └── images │ ├── console.png │ └── jsonbrowse.png ├── utils ├── ascii.js ├── filterJson.js ├── json.js └── parseUri.js └── yarn.lock /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "airbnb", 3 | "rules": { 4 | "import/no-extraneous-dependencies": 0, 5 | "no-unused-expressions": 0, 6 | "arrow-parens": [1, "as-needed"], 7 | "template-curly-spacing": 0, 8 | "react/jsx-filename-extension": [1, { "extensions": [".js"] }], 9 | "react/react-in-jsx-scope": 0, 10 | "react/jsx-curly-spacing": [2, "always", {"spacing": { 11 | "objectLiterals": "never" 12 | }}], 13 | "react/prop-types": 0, // temp 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .next 3 | npm-debug.log 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # JSON Browse 2 | 3 | screenshot 2016-12-06 00 20 30 4 | 5 | Browse, filter and manipulate your JSON inside the browser. 6 | 7 | - Fetch local and external JSON or paste local code 8 | - Filter JSON like you would filter JS objects in the browser (e.g. `data.values[1].message`) 9 | - Copy filtered output to your clipboard as a javascript object or JSON string 10 | - Manipulate filtered output in your browser's javascript console 11 | 12 | Live version hosted at [jsonbrowse.com](https://jsonbrowse.com). 13 | 14 | ## Installation 15 | 16 | To run development server: 17 | 18 | ``` 19 | git clone https://github.com/jorilallo/jsonbrowse.git 20 | cd jsonbrowse 21 | 22 | npm install 23 | npm run dev 24 | ``` 25 | 26 | To build and start production server: 27 | 28 | ``` 29 | npm run build 30 | npm run start 31 | ``` 32 | 33 | JSON Browse is build using [`next.js`](https://github.com/zeit/next.js/), [`styled-components`](https://github.com/styled-components/styled-components) and 34 | easily deployable using [`now`](https://zeit.co/now/). 35 | 36 | ## Dependencies 37 | 38 | JSON Browse's all external requests are proxied with [jsonbrowse-proxy](https://github.com/jorilallo/jsonbrowse-proxy) 39 | which only adds CORS headers (no logging is performed). Local requests are made directly so calling `localhost` 40 | is possible. 41 | 42 | ## License 43 | 44 | MIT 45 | -------------------------------------------------------------------------------- /components/Copy.js: -------------------------------------------------------------------------------- 1 | import { Component } from 'react'; 2 | import s from 'styled-components'; 3 | import { Flex } from 'reflexbox'; 4 | import CopyToClipboard from 'react-copy-to-clipboard'; 5 | import { objectifyJson } from '../utils/json'; 6 | 7 | const prettifyJson = (jsonString) => { 8 | const obj = JSON.parse(jsonString); 9 | return JSON.stringify(obj, null, 2); 10 | } 11 | 12 | export default class extends Component { 13 | state = { 14 | copiedJs: false, 15 | copiedJson: false, 16 | } 17 | 18 | onJsCopy = () => this.onCopy('copiedJs') 19 | onJsonCopy = () => this.onCopy('copiedJson') 20 | 21 | onCopy = (type) => { 22 | this.setState({ [type]: true }); 23 | setTimeout(() => this.setState({ [type]: false }), 2000); 24 | } 25 | 26 | render() { 27 | const showShortcut = navigator.platform === 'MacIntel'; 28 | 29 | return ( 30 | 31 | 35 | { this.state.copiedJson ? '✔ Copied' : 'Copy as JSON' } 36 | 37 | 41 | { this.state.copiedJs ? '✔ Copied' : 'Copy as Javascript' } 42 | 43 | 44 | Open Javascript console for more options { showShortcut && "(⌥⌘J)" } 45 | 46 | 47 | ); 48 | } 49 | } 50 | 51 | const Container = s(Flex)` 52 | position: absolute; 53 | bottom: 0; 54 | right: 0; 55 | width: 200px; 56 | padding: 20px; 57 | z-index: 9999999999; 58 | 59 | background-color: rgb(54, 165, 253); 60 | color: #FFFFFF; 61 | 62 | @media only screen and (max-width: 425px) { 63 | display: none !important; 64 | } 65 | `; 66 | 67 | const Console = s.div` 68 | font-size: 14px; 69 | margin-top: 10px; 70 | padding-top: 10px; 71 | border-top: 1px dotted rgb(141, 204, 255); 72 | `; 73 | 74 | const Copy = s(CopyToClipboard)` 75 | margin-bottom: 10px; 76 | border-bottom: 1px solid rgb(54, 165, 253); 77 | 78 | font-size: 14px; 79 | cursor: pointer; 80 | 81 | &:hover { 82 | border-bottom: 1px dotted #FFFFFF; 83 | } 84 | 85 | &:last-child { 86 | margin-bottom: 0; 87 | } 88 | `; 89 | -------------------------------------------------------------------------------- /components/DataView.js: -------------------------------------------------------------------------------- 1 | import { Flex } from 'reflexbox'; 2 | import JSONView from './JSON/JSONView'; 3 | 4 | export default (props) => { 5 | // Re-indent 6 | const data = JSON.parse(props.json); 7 | const json = JSON.stringify(data, undefined, 2); 8 | 9 | // Expose for console access 10 | global.json = json; 11 | global.data = data; 12 | 13 | return ( 14 | 15 | 16 | 17 | ); 18 | }; 19 | -------------------------------------------------------------------------------- /components/Error.js: -------------------------------------------------------------------------------- 1 | import s from 'styled-components'; 2 | 3 | export default ({ 4 | children, 5 | onClick, 6 | }) => ( 7 | 8 | { children } 9 | 10 | 11 | ); 12 | 13 | const Error = s.div` 14 | display: flex; 15 | justify-content: space-between; 16 | padding: 10px 20px; 17 | 18 | background-color: #f63939; 19 | color: #FFFFFF; 20 | `; 21 | 22 | const Close = s.span` 23 | cursor: pointer; 24 | `; 25 | -------------------------------------------------------------------------------- /components/Filter.js: -------------------------------------------------------------------------------- 1 | import { Component, PropTypes } from 'react'; 2 | import s from 'styled-components'; 3 | 4 | import Input from './Input'; 5 | 6 | const FILTER_REGEX = /^(\.?[\w-]*(\[\d+\])?)(\.[\w-]*(\[\d+\])?)*$/ 7 | 8 | export default class extends Component { 9 | static propTypes = { 10 | filter: PropTypes.string, 11 | onChange: PropTypes.func.isRequired, 12 | } 13 | 14 | onChange = (event) => { 15 | const value = event.target.value; 16 | if (value.match(FILTER_REGEX)) { 17 | this.props.onChange(value); 18 | } 19 | } 20 | 21 | render() { 22 | return ( 23 | 28 | ); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /components/Header.js: -------------------------------------------------------------------------------- 1 | import Link from 'next/link'; 2 | import { Flex } from 'reflexbox'; 3 | import s from 'styled-components'; 4 | 5 | const linkProps = { 6 | style: { color: '#ffffff' }, 7 | }; 8 | 9 | const SAMPLE_URL = 'https://api.github.com/repos/jorilallo/jsonbrowse'; 10 | 11 | export default props => ( 12 | 13 | 14 | json.browse() 15 | 16 | 17 | 18 | 19 | Demo 20 | 21 | 22 | 23 | 24 | 25 | Info 26 | 27 | 28 | 29 | 30 | 31 | GitHub 32 | 33 | 34 | 35 | 36 | ); 37 | 38 | const breakpoint = '425px'; 39 | 40 | const Container = s(Flex)` 41 | padding: 12px 20px; 42 | flex-shrink: 0; 43 | height: 52px; 44 | 45 | background-color: rgb(35, 9, 198); 46 | color: #FFFFFF; 47 | 48 | @media only screen and (max-width: ${breakpoint}) { 49 | flex-direction: column; 50 | height: 80px; 51 | } 52 | `; 53 | 54 | const Title = s.span` 55 | margin: 0; 56 | font-size: 22px; 57 | font-weight: 500; 58 | color: #ffffff; 59 | text-decoration: none; 60 | `; 61 | 62 | const Actions = s(Flex)` 63 | margin-top: 3px; 64 | 65 | @media only screen and (max-width: ${breakpoint}) { 66 | margin-top: 12px; 67 | } 68 | `; 69 | 70 | const Separator = s.div` 71 | margin: 0 7px; 72 | 73 | &::after { 74 | content: " · "; 75 | color: #3c73bd; 76 | } 77 | `; 78 | -------------------------------------------------------------------------------- /components/Input.js: -------------------------------------------------------------------------------- 1 | import s from 'styled-components'; 2 | import { Flex } from 'reflexbox'; 3 | 4 | import Spinner from './Spinner'; 5 | 6 | export default props => ( 7 | 8 | 9 | { props.loading && () } 10 | 11 | 18 | 19 | ); 20 | 21 | const placeholderColor = 'rgb(159, 212, 255)'; 22 | 23 | const Input = s.input` 24 | margin: 0; 25 | padding: 8px 20px 8px 0; 26 | width: 100%; 27 | height: 36px; 28 | 29 | background-color: transparent; 30 | border: none; 31 | outline: none; 32 | border-radius: 0; 33 | 34 | color: #FFFFFF; 35 | font-size: 16px; 36 | 37 | &::-webkit-input-placeholder { 38 | color: ${ placeholderColor }; 39 | } 40 | 41 | &:-moz-placeholder { /* Firefox 18- */ 42 | color: ${ placeholderColor }; 43 | } 44 | 45 | &::-moz-placeholder { /* Firefox 19+ */ 46 | color: ${ placeholderColor }; 47 | } 48 | 49 | &:-ms-input-placeholder { 50 | color: ${ placeholderColor }; 51 | } 52 | `; 53 | 54 | const Wrapper = s(Flex)` 55 | flex-shrink: 0; 56 | background-color: rgb(54, 165, 253); 57 | `; 58 | 59 | const Loading = s(Flex)` 60 | width: 20px; 61 | `; 62 | -------------------------------------------------------------------------------- /components/JSON/JSONNode.js: -------------------------------------------------------------------------------- 1 | const objectType = object => ( 2 | Object.prototype.toString.call(object).slice(8, -1) 3 | ); 4 | 5 | const JSONNode = ({ data, indent, colors }) => { 6 | const updatedColors = { 7 | base: '#9599a7', 8 | string: '#050505', 9 | number: '#00bcd4', 10 | boolean: '#9c27b0', 11 | ...colors, 12 | }; 13 | 14 | const props = { 15 | indent: indent + 1, 16 | colors: updatedColors, 17 | }; 18 | 19 | switch (objectType(data)) { 20 | case 'Object': { 21 | const keys = Object.keys(data); 22 | return ( 23 | 24 | {'{'} 25 | { keys.map((key, index) => ( 26 |
27 | 32 | { index !== keys.length - 1 && ',' } 33 |
34 | )) } 35 | {'}'} 36 |
37 | ); 38 | } 39 | case 'Array': { 40 | return ( 41 | 42 | {'['} 43 | { data.map((value, index) => ( 44 |
45 | 49 | { index !== data.length - 1 && ',' } 50 |
51 | )) } 52 | {']'} 53 |
54 | ); 55 | } 56 | case 'String': 57 | return ("{ data }"); 58 | case 'Number': 59 | return ({ data }); 60 | case 'Boolean': 61 | return ({ data.toString() }); 62 | case 'Null': 63 | return (null); 64 | default: 65 | return null; 66 | } 67 | }; 68 | 69 | JSONNode.defaultProps = { 70 | indent: 0, 71 | }; 72 | 73 | const BaseNode = ({ children, colors }) => 74 | { children }; 75 | const StringNode = ({ children, colors }) => 76 | { children }; 77 | const BooleanNode = ({ children, colors }) => 78 | { children }; 79 | const NumberNode = ({ children, colors }) => 80 | { children }; 81 | 82 | const Indent = ({ m }) => ( 83 | // eslint-disable-line 84 | ); 85 | 86 | const KeyValue = ({ objectKey, value, indent, colors }) => ( 87 | 88 | 89 | "{ objectKey }":  90 | 91 | 92 | ); 93 | 94 | const ArrayValue = ({ value, indent, colors }) => ( 95 | 96 | 97 | 98 | 99 | ); 100 | 101 | const Value = ({ children }) => ( 102 | 103 | 104 | { children } 105 | 106 | ); 107 | 108 | export default JSONNode; 109 | -------------------------------------------------------------------------------- /components/JSON/JSONView.js: -------------------------------------------------------------------------------- 1 | import { Flex } from 'reflexbox'; 2 | import JSONNode from './JSONNode'; 3 | 4 | const LINE_COUNT_REGEX = /\r\n|\r|\n/; 5 | 6 | export default (props) => { 7 | const { 8 | data, 9 | } = props; 10 | 11 | // Calculate line count 12 | const jsonString = JSON.stringify(data, null, 2); 13 | const lineCount = jsonString.split(LINE_COUNT_REGEX).length; 14 | 15 | return ( 16 | 17 | 18 | 19 | 20 | ); 21 | }; 22 | 23 | const LineNumbers = ({ count }) => ( 24 |
25 | { Array.from(Array(count).keys()).map(lineNumber => ( 26 |
30 | { lineNumber + 1 } 31 |
32 | )) } 33 |
34 | ); 35 | -------------------------------------------------------------------------------- /components/JsonBrowse.js: -------------------------------------------------------------------------------- 1 | import { Component, PropTypes } from 'react'; 2 | import { Flex } from 'reflexbox'; 3 | import s from 'styled-components'; 4 | 5 | import filterJson from '../utils/filterJson'; 6 | 7 | import DataView from './DataView'; 8 | import Filter from './Filter'; 9 | import Copy from './Copy'; 10 | 11 | export default class extends Component { 12 | state = { 13 | filter: null, 14 | json: this.props.json, 15 | } 16 | 17 | setFilter = (filter) => { 18 | // Only set filter is it matches, otherwise keep previous output 19 | const filteredJson = filterJson(this.props.json, filter) 20 | 21 | if (filteredJson) { 22 | this.setState({ 23 | json: filteredJson, 24 | }); 25 | } 26 | }; 27 | 28 | render() { 29 | return ( 30 | 31 | 32 | 33 | 34 | 35 | ); 36 | } 37 | } 38 | 39 | const Container = s(Flex)` 40 | position: relative; 41 | flex-shrink: 0; 42 | `; 43 | 44 | const ValidNotice = s.div` 45 | padding: 8px 20px; 46 | background-color: rgb(54, 165, 253); 47 | color: #FFFFFF; 48 | `; 49 | -------------------------------------------------------------------------------- /components/JsonInput.js: -------------------------------------------------------------------------------- 1 | import { Component, PropTypes } from 'react'; 2 | import { Flex } from 'reflexbox'; 3 | import s from 'styled-components'; 4 | 5 | import Input from './Input'; 6 | 7 | export default class extends Component { 8 | static propTypes = { 9 | onUrlSubmit: PropTypes.func.isRequired, 10 | onTextareaChange: PropTypes.func.isRequired, 11 | onInputChange: PropTypes.func.isRequired, 12 | } 13 | 14 | state = { 15 | error: false, 16 | } 17 | 18 | onUrlSubmit = (event) => { 19 | event.preventDefault(); 20 | this.props.onUrlSubmit(); 21 | } 22 | 23 | render() { 24 | return ( 25 | 26 |
27 | 34 |
35 |