├── .firebaserc
├── .github
├── FUNDING.yml
└── workflows
│ └── main.yml
├── .gitignore
├── LICENSE
├── README.md
├── firebase.json
├── package-lock.json
├── package.json
├── public
├── favicon.ico
├── index.html
├── manifest.json
└── robots.txt
├── src
├── Assets
│ ├── Logo.svg
│ └── changelog.md
├── CLI
│ ├── index.js
│ ├── package-lock.json
│ ├── package.json
│ └── utils.js
├── Components
│ ├── App.js
│ ├── BeautifyButton.js
│ ├── Changelog.js
│ ├── CopyCSV.js
│ ├── CopyJSON.js
│ ├── Csv.js
│ ├── DownloadCSV.js
│ ├── DownloadJSON.js
│ ├── Footer.js
│ ├── Header.js
│ ├── IndentSelector.js
│ ├── Json.js
│ ├── Menu.js
│ ├── MultipleDeletions.js
│ ├── MultipleMerge.js
│ ├── MultipleReplaces.js
│ ├── RandomButton.js
│ ├── Seo.js
│ ├── TextareaIndent.js
│ ├── TransformToCSV.js
│ ├── TransformToJSON.js
│ ├── UglifyButton.js
│ ├── UploadCSV.js
│ ├── UploadFiles.js
│ └── UploadJSON.js
├── Functions
│ ├── Firebase.js
│ ├── useUndoableState.js
│ └── useWindowDimensions.js
├── Styles
│ └── index.css
├── Tests
│ └── App.test.js
└── index.js
└── yarn.lock
/.firebaserc:
--------------------------------------------------------------------------------
1 | {
2 | "projects": {
3 | "default": "jsonmatic-pro"
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | # These are supported funding model platforms
2 |
3 | github: erikmartinjordan
4 |
--------------------------------------------------------------------------------
/.github/workflows/main.yml:
--------------------------------------------------------------------------------
1 | # This is a basic workflow to deploy functions to Firebase
2 |
3 | name: deployToFirebase
4 |
5 | # Controls when the action will run. Triggers the workflow on push or pull request
6 | # events but only for the master branch
7 | on:
8 | push:
9 | branches: [ master ]
10 |
11 | # A workflow run is made up of one or more jobs that can run sequentially or in parallel
12 | jobs:
13 | # This workflow contains a single job called "build"
14 | build:
15 | # The type of runner that the job will run on
16 | runs-on: macos-latest
17 |
18 | # Steps represent a sequence of tasks that will be executed as part of the job
19 | steps:
20 | # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
21 | - uses: actions/checkout@v2
22 | - uses: actions/setup-node@v2.1.2
23 | with:
24 | node-version: '12'
25 |
26 | # Install npm
27 | - name: Installing npm
28 | run: npm install
29 |
30 | # Build the project
31 | - name: Build project
32 | run: npm run build
33 | env:
34 | CI: ""
35 |
36 | # Running tests
37 | - name: Running tests
38 | run: npm run test
39 |
40 | # Firebase deploy
41 | - name: Firebase deploy
42 | run: npm install -g firebase-tools && firebase deploy --token ${{ secrets.FIREBASE_TOKEN }}
43 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # production
12 | /build
13 |
14 | # misc
15 | .DS_Store
16 | .env.local
17 | .env.development.local
18 | .env.test.local
19 | .env.production.local
20 |
21 | npm-debug.log*
22 | yarn-debug.log*
23 | yarn-error.log*
24 | .firebase/hosting.YnVpbGQ.cache
25 |
26 | # CLI node-modules
27 | src/CLI/node_modules
28 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 Erik Martín Jordán
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 | The easiest way to transform a CSV into a JSON and vice versa. You can try the [webapp](https://jsonmatic-pro.web.app/) without installing any package. The app runs in your browser; **data isn't uploaded to any server**.
10 |
11 |
12 |
13 |
14 |
15 | ## Features
16 |
17 | * Transform a CSV into a JSON (and vice versa)
18 | * Beautify/uglify the JSON
19 | * Configure indentation
20 | * Replace/delete a property in multiple JSON files at once
21 | * Merge JSON files
22 |
23 | ## Install
24 |
25 | You can also install and use the app through the CLI:
26 |
27 | ```
28 | npm i jsonmatic -g
29 | ```
30 |
31 | ## Use
32 |
33 | Transform a CSV into JSON and vice versa:
34 |
35 | ```
36 | jsonmatic transform
37 | ```
38 |
39 | Replace a property in multiple JSON files at once:
40 |
41 | ```
42 | jsonmatic replace [options]
43 | ```
44 |
45 | Merge multiple JSON files:
46 |
47 | ```
48 | jsonmatic merge
49 | ```
50 |
51 | ## Note
52 |
53 | The first column is reserved for unique keys. Use dot notation to create properties and subproperties.
54 |
55 | ## Author
56 |
57 | [Erik Martín Jordán](https://erikmartinjordan.com)
58 |
59 | ## License
60 |
61 | This project is open source and available under the MIT License.
62 |
--------------------------------------------------------------------------------
/firebase.json:
--------------------------------------------------------------------------------
1 | {
2 | "hosting": {
3 | "public": "build",
4 | "ignore": [
5 | "firebase.json",
6 | "**/.*",
7 | "**/node_modules/**"
8 | ],
9 | "rewrites": [
10 | {
11 | "source": "**",
12 | "destination": "/index.html"
13 | }
14 | ]
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "spreadsheet-json",
3 | "version": "0.2.4",
4 | "private": true,
5 | "type": "module",
6 | "dependencies": {
7 | "@primer/octicons-react": "^12.1.0",
8 | "@testing-library/jest-dom": "^5.11.4",
9 | "@testing-library/react": "^11.1.0",
10 | "@testing-library/user-event": "^12.1.10",
11 | "dreamjs": "^0.2.0",
12 | "file-saver": "^2.0.5",
13 | "firebase": "^8.3.1",
14 | "moment": "^2.29.1",
15 | "react": "^17.0.1",
16 | "react-data-grid": "^7.0.0-canary.36",
17 | "react-dom": "^17.0.1",
18 | "react-markdown": "^6.0.1",
19 | "react-router-dom": "^5.2.0",
20 | "react-scripts": "4.0.2",
21 | "web-vitals": "^1.0.1"
22 | },
23 | "scripts": {
24 | "start": "react-scripts start",
25 | "build": "react-scripts build",
26 | "test": "react-scripts test",
27 | "eject": "react-scripts eject"
28 | },
29 | "eslintConfig": {
30 | "extends": [
31 | "react-app",
32 | "react-app/jest"
33 | ]
34 | },
35 | "browserslist": {
36 | "production": [
37 | ">0.2%",
38 | "not dead",
39 | "not op_mini all"
40 | ],
41 | "development": [
42 | "last 1 chrome version",
43 | "last 1 firefox version",
44 | "last 1 safari version"
45 | ]
46 | },
47 | "devDependencies": {}
48 | }
49 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/erikmartinjordan/jsonmatic/7b28f776be943093762721f2daa15c91f41b9940/public/favicon.ico
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 | jsonmatic
11 |
12 |
13 | You need to enable JavaScript to run this app.
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "jsonmatic",
3 | "name": "Transform a CSV into a JSON",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | }
10 | ],
11 | "start_url": ".",
12 | "display": "standalone",
13 | "theme_color": "#000000",
14 | "background_color": "#ffffff"
15 | }
16 |
--------------------------------------------------------------------------------
/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 | Disallow:
4 |
--------------------------------------------------------------------------------
/src/Assets/Logo.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/Assets/changelog.md:
--------------------------------------------------------------------------------
1 | ## 18.06.21 - Replace with the CLI
2 |
3 | * You can replace a property in multiple JSON files using the CLI
4 |
5 | ## 17.06.21 – CLI
6 |
7 | 
8 |
9 | * CLI creation
10 | * Transform a CSV into a JSON and vice versa
11 | * Merge multiple JSONs
12 |
13 |
14 | ## 07.06.21 – Project restructuration
15 |
16 | 
17 |
18 | * Restructured project in Functions, Components, Assets, Styles
19 | * Changed icon of transform buttons depending on screen resolution
20 | * Fixed screen issues on mobile devices
21 |
22 | ## 04.06.21 – New copy/download options and bug fixing
23 |
24 | * Fixed splitting on reading a CSV
25 | * Updated textarea font to `monospace` as suggested on HN
26 | * Removed `fingerprintjs` and analytics component
27 | * Added CSV copy/download options
28 | * Replaced dot separator with `__separator__` on TransformToCSV component
29 | * Autofocus on table cell
30 |
31 | ## 03.06.21 – Upload CSV
32 |
33 | * Option to upload CSV instead of just copy/paste
34 | * Fixed textarea on mobile devices
35 |
36 | ## 01.06.21 – Randomize JSON
37 |
38 | * Create a dummy JSON
39 |
40 | ## 24.05.21 – Keyboard shortcut to upload files
41 |
42 | * Added keyboard control to upload files
43 | * Added dummy CSV animation when transforming JSON
44 | * Deleted '+' sign on keyboard shortcuts hint because it can create confusion
45 | * Added animation to keyboard hint on 'Upload' component
46 |
47 | ## 21.05.21 — Resize table
48 |
49 | 
50 |
51 | * Resizable table
52 | * Autoresize on double click
53 | * Disabled "Generate CSV" button while editing JSON
54 |
55 | ## 20.05.21 — Transform JSON into CSV
56 |
57 | * Transform JSON into CSV
58 |
59 | ## 12.05.21 — JSON edition
60 |
61 | 
62 |
63 | * Edit JSON textarea
64 | * Buttons are disabled while JSON edition
65 | * Fixed bug: listener of keyboard active when some table cell is selected
66 |
67 | ## 05.05.21 — Multiple JSON merge
68 |
69 | 
70 |
71 | * Merge multiple JSON files
72 | * Select merge order (priority)
73 | * Download merged file
74 |
75 | ## 02.05.21 — Multiple JSON deletion
76 |
77 | * Delete several properties of multiple JSON files separating them with commas: `prop1.subprop1`, `prop2.subprop2`, etc.
78 | * Delete a single property using rules (*equal to*, *greater than*, and *lesser than*)
79 | * Displaying number of deletions
80 |
81 | ## 30.04.21 — Rules
82 |
83 | * Replace fields if a property is greater/lesser than a certain value
84 | * Added tilt effect on hover buttons 💅
85 | * Added file name as title into JSON box
86 | * Displaying number of replaces
87 |
88 | ## 29.04.21 — Multiple JSON replacement
89 |
90 | 
91 |
92 | * Added a component to replace a property in multiple JSON files
93 | * Replace/download
94 |
95 | ## 27.04.21 — Changelog
96 |
97 | * Added changelog
98 |
99 | ## 17.03.21 — Launch 🚀
100 |
101 | Transform a CSV into a JSON automatically:
102 |
103 | * Copy a CSV and paste it
104 | * Add/delete columns & rows
105 | * Undo/redo options
106 | * Keyboard shortcuts
107 | * Download the JSON as file or copy it
108 | * Beautify/uglify the JSON
109 | * 2-space/4-space indentation
--------------------------------------------------------------------------------
/src/CLI/index.js:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 | import chalk from 'chalk';
3 | import path from 'path';
4 | import fs from 'fs';
5 | import { program } from 'commander';
6 | import * as utils from './utils.js';
7 |
8 | // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
9 | // Defining the program
10 | // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
11 | let { version } = JSON.parse(fs.readFileSync(new URL('./package.json', import.meta.url)));
12 |
13 | program
14 | .version(version)
15 |
16 | program
17 | .option('-h, --help')
18 | .action(options => {
19 |
20 | console.log(chalk.cyan(``));
21 | console.log(chalk.cyan(` ██╗███████╗ ██████╗ ███╗ ██╗███╗ ███╗ █████╗ ████████╗██╗ ██████╗`));
22 | console.log(chalk.cyan(` ██║██╔════╝██╔═══██╗████╗ ██║████╗ ████║██╔══██╗╚══██╔══╝██║██╔════╝`));
23 | console.log(chalk.cyan(` ██║███████╗██║ ██║██╔██╗ ██║██╔████╔██║███████║ ██║ ██║██║ `));
24 | console.log(chalk.cyan(`██ ██║╚════██║██║ ██║██║╚██╗██║██║╚██╔╝██║██╔══██║ ██║ ██║██║`));
25 | console.log(chalk.cyan(`╚█████╔╝███████║╚██████╔╝██║ ╚████║██║ ╚═╝ ██║██║ ██║ ██║ ██║╚██████╗`));
26 | console.log(chalk.cyan(` ╚════╝ ╚══════╝ ╚═════╝ ╚═╝ ╚═══╝╚═╝ ╚═╝╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═════╝`));
27 | console.log(chalk.cyan(`⚗️ https://github.com/erikmartinjordan/jsonmatic`));
28 | console.log(chalk.cyan(``));
29 | console.log(program.helpInformation());
30 |
31 | })
32 |
33 | program
34 | .command('transform')
35 | .arguments(' ')
36 | .description('✨ transform a CSV into a JSON or vice versa')
37 | .action((source, destination) => {
38 |
39 | console.log('Transforming...');
40 | if (source.endsWith('csv') && destination.endsWith('json')) generateJSON(source, destination);
41 | else if(source.endsWith('json') && destination.endsWith('csv')) generateCSV(source, destination);
42 | else console.log(chalk.red('❌ The files are not valid.'));
43 |
44 | });
45 |
46 | program
47 | .command('replace')
48 | .arguments(' ')
49 | .option('-g, --greater', 'is greater than')
50 | .option('-e, --equal', 'is equal to')
51 | .option('-l, --lesser', 'is less than')
52 | .description('↔️ replace a property in multiple JSON files')
53 | .action((property, currentValue, replaceValue, files, options) => {
54 |
55 | console.log('Replacing...');
56 | if (Object.keys(options).length !== 1) console.log(chalk.red('❌ You must select only one option. Type jsonmatic --help to get more info.'));
57 | else if(files.some(file => !file.endsWith('json'))) console.log(chalk.red('❌ The files are not valid.'));
58 | else generateReplace(property, currentValue, replaceValue, files, Object.keys(options)[0]);
59 |
60 |
61 | });
62 |
63 | program
64 | .command('merge')
65 | .arguments('')
66 | .description('➕ merge multiple JSON files into one unique file')
67 | .action(files => {
68 |
69 | console.log('Generating files...');
70 | if (files.every(file => file.endsWith('json'))) generateMerge(files);
71 | else console.log(chalk.red('❌ The files are not valid.'));
72 |
73 | });
74 |
75 | program
76 | .parse();
77 |
78 | // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
79 | // Defining the JSON generator
80 | // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
81 | function generateJSON(source, destination){
82 |
83 | try{
84 |
85 | let rawdata = fs.readFileSync(source);
86 | let csv = rawdata.toString().split(/\r\n|\n|\r/).map(line => line.split(','));
87 |
88 | utils.validateCSV(csv);
89 |
90 | let json = utils.transformToJSON(csv);
91 |
92 | fs.writeFileSync(destination, JSON.stringify(json, null, 2));
93 |
94 | console.log(chalk.green(`✅ ${source} was transformed into ${destination}`));
95 |
96 | }
97 | catch(e){
98 |
99 | console.log(chalk.red(`❌ ${e}`));
100 | process.exit(1);
101 |
102 | }
103 |
104 | }
105 |
106 |
107 | // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
108 | // Defining the CSV generator
109 | // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
110 | function generateCSV(source, destination){
111 |
112 | try{
113 |
114 | let rawdata = fs.readFileSync(source);
115 | let json = JSON.parse(rawdata);
116 |
117 | utils.validateJSON(json);
118 |
119 | let csv = utils.transformToCSV(json);
120 |
121 | fs.writeFileSync(destination, csv.map(row => row.join(',')).join('\n'));
122 |
123 | console.log(chalk.green(`✅ ${source} was transformed into ${destination}`));
124 |
125 | }
126 | catch(e){
127 |
128 | console.log(chalk.red(`❌ ${e}`));
129 | process.exit(1);
130 |
131 | }
132 |
133 | }
134 |
135 | // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
136 | // Defining the replace generator
137 | // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
138 | function generateReplace(property, currentValue, replaceValue, files, operation){
139 |
140 | try{
141 |
142 | let jsonfiles = files.map(file => ({
143 |
144 | name: file,
145 | json: JSON.parse(fs.readFileSync(file))
146 |
147 | }));
148 |
149 | let [replaced, numReplaces] = utils.replaceMultipleJSONs(property, currentValue, replaceValue, jsonfiles, operation);
150 |
151 | replaced.forEach(file => fs.writeFileSync(file.name, JSON.stringify(file.json, null, 2)));
152 |
153 | console.log(chalk.green(`✅ ${numReplaces} fields replaced.`));
154 |
155 | }
156 | catch(e){
157 |
158 | console.log(chalk.red(`❌ ${e}`));
159 | process.exit(1);
160 |
161 | }
162 |
163 | }
164 |
165 | // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
166 | // Defining the merge generator
167 | // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
168 | function generateMerge(files){
169 |
170 | try{
171 |
172 | let jsonfiles = files.map(file => ({
173 |
174 | name: file,
175 | json: JSON.parse(fs.readFileSync(file))
176 |
177 | }));
178 |
179 | let merge = utils.mergeMultipleJSONs(jsonfiles);
180 |
181 | fs.writeFileSync('merge.json', JSON.stringify(merge, null, 2));
182 |
183 | console.log(chalk.green(`✅ ${files} merged into merge.json`));
184 |
185 | }
186 | catch(e){
187 |
188 | console.log(chalk.red(`❌ ${e}`));
189 | process.exit(1);
190 |
191 | }
192 |
193 | }
--------------------------------------------------------------------------------
/src/CLI/package-lock.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "jsonmatic",
3 | "version": "0.0.9",
4 | "lockfileVersion": 1,
5 | "requires": true,
6 | "dependencies": {
7 | "ansi-styles": {
8 | "version": "4.3.0",
9 | "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
10 | "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
11 | "requires": {
12 | "color-convert": "^2.0.1"
13 | }
14 | },
15 | "chalk": {
16 | "version": "4.1.1",
17 | "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.1.tgz",
18 | "integrity": "sha512-diHzdDKxcU+bAsUboHLPEDQiw0qEe0qd7SYUn3HgcFlWgbDcfLGswOHYeGrHKzG9z6UYf01d9VFMfZxPM1xZSg==",
19 | "requires": {
20 | "ansi-styles": "^4.1.0",
21 | "supports-color": "^7.1.0"
22 | }
23 | },
24 | "color-convert": {
25 | "version": "2.0.1",
26 | "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
27 | "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
28 | "requires": {
29 | "color-name": "~1.1.4"
30 | }
31 | },
32 | "color-name": {
33 | "version": "1.1.4",
34 | "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
35 | "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
36 | },
37 | "commander": {
38 | "version": "7.2.0",
39 | "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz",
40 | "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw=="
41 | },
42 | "has-flag": {
43 | "version": "4.0.0",
44 | "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
45 | "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="
46 | },
47 | "supports-color": {
48 | "version": "7.2.0",
49 | "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
50 | "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
51 | "requires": {
52 | "has-flag": "^4.0.0"
53 | }
54 | }
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/src/CLI/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "jsonmatic",
3 | "version": "0.2.4",
4 | "description": "⚗️ Transform a CSV (spreadsheet) into a JSON",
5 | "main": "index.js",
6 | "type": "module",
7 | "scripts": {
8 | "test": "echo \"Error: no test specified\" && exit 1",
9 | "start": "node index.js",
10 | "prepublish": "cp ../../README.md README.md",
11 | "postpublish": "rm README.md"
12 | },
13 | "bin": {
14 | "jsonmatic": "index.js"
15 | },
16 | "keywords": [
17 | "JSON",
18 | "CSV",
19 | "transform",
20 | "spreadsheet"
21 | ],
22 | "author": "Erik Martín Jordán",
23 | "license": "MIT",
24 | "bugs": {
25 | "url": "https://github.com/erikmartinjordan/jsonmatic/issues"
26 | },
27 | "homepage": "https://github.com/erikmartinjordan/jsonmatic#readme",
28 | "dependencies": {
29 | "chalk": "^4.1.1",
30 | "commander": "^7.2.0"
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/src/CLI/utils.js:
--------------------------------------------------------------------------------
1 | // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
2 | // Validates a CSV
3 | // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
4 | const validateCSV = (csv) => {
5 |
6 | let empty = {};
7 |
8 | let firstCol = csv.map(e => e[0]);
9 | let firstRow = csv[0];
10 |
11 | let duplicateKeys = firstCol.some(e => empty[e] ? true : (empty[e] = true, false));
12 | let firstRowEmpty = firstRow.every(e => e === '');
13 |
14 | if(duplicateKeys) return {error: `JSON has duplicated keys`};
15 | if(firstRowEmpty) return {error: `First row shouldn't be empty`};
16 |
17 | return `ok`;
18 |
19 | }
20 |
21 | // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
22 | // Validates a JSON
23 | // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
24 | const validateJSON = (json) => {
25 |
26 | try{
27 |
28 | JSON.parse(json);
29 | return 'ok';
30 |
31 | }
32 | catch(e){
33 |
34 | return {error: 'Invalid JSON'};
35 |
36 | }
37 |
38 | }
39 |
40 | // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
41 | // Transforms a JSON into a CSV
42 | // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
43 | const transformToCSV = (json) => {
44 |
45 | const getDeepKeys = (obj) => {
46 |
47 | let keys = Object.keys(obj).map(key => {
48 |
49 | if(typeof obj[key] === 'object'){
50 |
51 | let subkeys = getDeepKeys(obj[key]);
52 |
53 | return subkeys.map(subkey => key + '__separator__' + subkey);
54 |
55 | }
56 | else{
57 |
58 | return key;
59 |
60 | }
61 |
62 | });
63 |
64 | return keys.flat(Infinity);
65 |
66 | }
67 |
68 | let keys = getDeepKeys(json);
69 |
70 | let firstRow = ['key', ...new Set(keys.map(key => key.split('__separator__').slice(1).join('__separator__')))];
71 | let firstCol = ['key', ...new Set(keys.map(key => key.split('__separator__').shift()))];
72 |
73 | let csv = new Array(firstCol.length).fill('').map(() => new Array(firstRow.length).fill(''));
74 |
75 | for(let i = 0; i < firstCol.length; i ++){
76 | for(let j = 0; j < firstRow.length; j ++){
77 |
78 | if(i === 0) csv[0][j] = firstRow[j].split('__separator__').join('.');
79 | if(j === 0) csv[i][0] = firstCol[i].split('__separator__').join('.');
80 | if(i && j) csv[i][j] = `${firstCol[i]}__separator__${firstRow[j]}`.split('__separator__').reduce((ref, prop) => ref = ref?.[prop], json);
81 |
82 | }
83 | }
84 |
85 | return csv;
86 |
87 | }
88 |
89 | // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
90 | // Transforms a CSV into a JSON
91 | // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
92 | const transformToJSON = (csv) => {
93 |
94 | let json = {};
95 |
96 | csv.forEach((row, i) => {
97 | csv[i].forEach((column, j) => {
98 |
99 | let key = csv[i][0];
100 | let value = csv[i][j];
101 | let properties = csv[0][j];
102 |
103 | if(i > 0 && j > 0 && key && value){
104 |
105 | json[key] = json[key] || {};
106 |
107 | let ref = json[key];
108 | let subproperties = properties.split('.');
109 | let last = subproperties.pop();
110 |
111 | subproperties.forEach(property => {
112 |
113 | ref[property] = ref[property] || {};
114 | ref = ref[property];
115 |
116 | });
117 |
118 | ref[last] = isNaN(value) ? value : parseFloat(value);
119 |
120 | }
121 |
122 | })
123 | });
124 |
125 | return json;
126 |
127 | }
128 |
129 | // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
130 | // Replace multiple JSONs props
131 | // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
132 | const replaceMultipleJSONs = (property, currentValue, replaceValue, jsonfiles, operation) => {
133 |
134 | let numReplaces = 0;
135 |
136 | let jsonFilesReplaced = jsonfiles.map(({name, json}) => {
137 |
138 | let clone = {...json};
139 |
140 | let props = property.split('.');
141 |
142 | let val = props.slice(0 ).reduce((ref, prop) => ref = ref?.[prop], clone);
143 | let ref = props.slice(0, -1).reduce((ref, prop) => ref = ref?.[prop], clone);
144 |
145 | let last = props.pop();
146 |
147 | let _currentValue = isNaN(currentValue) ? currentValue : parseFloat(currentValue);
148 | let _replaceValue = isNaN(replaceValue) ? replaceValue : parseFloat(replaceValue);
149 |
150 | let rule = {
151 |
152 | 'equal': () => val === _currentValue,
153 | 'greater': () => val > _currentValue,
154 | 'lesser': () => val < _currentValue
155 |
156 | }[operation]();
157 |
158 | if(rule){
159 |
160 | ref[last] = _replaceValue;
161 | numReplaces ++;
162 |
163 | }
164 |
165 | return {
166 | name: name,
167 | json: clone
168 | };
169 |
170 | });
171 |
172 | return [jsonFilesReplaced, numReplaces];
173 |
174 | }
175 |
176 |
177 | // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
178 | // Merge multiple JSONs into one
179 | // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
180 | const mergeMultipleJSONs = (jsonfiles) => {
181 |
182 | const union = (objA, objB) => {
183 |
184 | if(typeof objA === 'object'){
185 |
186 | let merged = {...objA};
187 |
188 | Object.keys(objB).forEach(key => {
189 |
190 | merged[key] = merged[key] ? union(merged[key], objB[key]) : objB[key];
191 |
192 | });
193 |
194 | return merged;
195 |
196 | }
197 |
198 | return objA;
199 |
200 | }
201 |
202 | let merged = jsonfiles.reduce((acc, {json}) => union(acc, json), null);
203 |
204 | return merged;
205 |
206 | }
207 |
208 | export { transformToCSV, transformToJSON, validateCSV, validateJSON, mergeMultipleJSONs, replaceMultipleJSONs }
--------------------------------------------------------------------------------
/src/Components/App.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import Header from './Header';
3 | import Json from './Json';
4 | import Csv from './Csv';
5 | import TransformToJSON from './TransformToJSON';
6 | import TransformToCSV from './TransformToCSV';
7 | import Menu from './Menu';
8 | import useUndoableState from '../Functions/useUndoableState';
9 |
10 | const App = () => {
11 |
12 | const [csv, setCsv, undo, redo] = useUndoableState([
13 |
14 | ['key', 'road', 'coord.lat', 'coord.lng', 'elem'],
15 | ['1', 'AP-7', 42.02, 2.82, '🦄'],
16 | ['2', 'C-32', 41.35, 2.09, '🦧'],
17 | ['3', 'B-20', 41.44, 2.18, '🐰'],
18 |
19 | ]);
20 |
21 | const [json, setJson] = useState(
22 |
23 | {
24 | "1": {
25 | "road": "AP-7",
26 | "coord": {
27 | "lat": 42.02,
28 | "lng": 2.82
29 | },
30 | "elem": "🦄"
31 | },
32 | "2": {
33 | "road": "C-32",
34 | "coord": {
35 | "lat": 41.35,
36 | "lng": 2.09
37 | },
38 | "elem": "🦧"
39 | },
40 | "3": {
41 | "road": "B-20",
42 | "coord": {
43 | "lat": 41.44,
44 | "lng": 2.18
45 | },
46 | "elem": "🐰"
47 | }
48 | }
49 |
50 | );
51 |
52 | const [select, setSelect] = useState(['', '', '', '']);
53 | const [edit, setEdit] = useState(false);
54 |
55 | return (
56 |
57 |
58 |
59 |
65 |
66 |
71 |
77 |
78 |
84 |
85 |
93 |
94 | );
95 |
96 | }
97 |
98 | export default App;
--------------------------------------------------------------------------------
/src/Components/BeautifyButton.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { SmileyIcon } from '@primer/octicons-react';
3 |
4 | const BeautifyButton = ({edit, indent, setIndent}) => {
5 |
6 | const beautify = () => {
7 |
8 | if(!indent)
9 | setIndent(2);
10 |
11 | }
12 |
13 | return(
14 | Beautify
15 | );
16 |
17 | }
18 |
19 | export default BeautifyButton;
--------------------------------------------------------------------------------
/src/Components/Changelog.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from 'react';
2 | import ReactMarkdown from 'react-markdown';
3 | import changelog from '../Assets/changelog.md';
4 |
5 | const Changelog = () => {
6 |
7 | const [markdown, setMarkdown] = useState(null);
8 |
9 | useEffect(() => {
10 |
11 | const fetchMarkdown = async () => {
12 |
13 | let res = await fetch(changelog);
14 | let text = await res.text();
15 |
16 | setMarkdown(text);
17 |
18 | }
19 |
20 | fetchMarkdown();
21 |
22 | }, []);
23 |
24 | return(
25 |
26 |
What's new?
27 |
31 | }}
32 | />
33 |
34 | );
35 |
36 | }
37 |
38 | export default Changelog;
--------------------------------------------------------------------------------
/src/Components/CopyCSV.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import { ClippyIcon, CheckCircleIcon } from '@primer/octicons-react';
3 |
4 | const CopyCSV = ({csv}) => {
5 |
6 | const [alert, setAlert] = useState(null);
7 |
8 | const copy = () => {
9 |
10 | let text = csv.join('\n').split(',').join('\t');
11 |
12 | navigator.clipboard.writeText(text);
13 |
14 | setAlert('copied');
15 |
16 | setTimeout(() => setAlert(null), 1500);
17 |
18 | }
19 |
20 | return(
21 | { alert !== 'copied' ? <> Copy CSV> : <> Copied>}
22 | );
23 |
24 | }
25 |
26 | export default CopyCSV;
--------------------------------------------------------------------------------
/src/Components/CopyJSON.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import { ClippyIcon, CheckCircleIcon } from '@primer/octicons-react';
3 |
4 | const CopyCSV = ({edit, json, indent}) => {
5 |
6 | const [alert, setAlert] = useState(null);
7 |
8 | const copy = () => {
9 |
10 | navigator.clipboard.writeText(JSON.stringify(json, null, parseInt(indent)));
11 |
12 | setAlert('copied');
13 |
14 | setTimeout(() => setAlert(null), 1500);
15 |
16 | }
17 |
18 | return(
19 | { alert !== 'copied' ? <> Copy JSON> : <> Copied>}
20 | );
21 |
22 | }
23 |
24 | export default CopyCSV;
--------------------------------------------------------------------------------
/src/Components/Csv.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from 'react';
2 | import UploadCSV from './UploadCSV';
3 | import CopyCSV from './CopyCSV';
4 | import DownloadCSV from './DownloadCSV';
5 |
6 | const Csv = ({csv, setCsv, select, setSelect}) => {
7 |
8 | const [drag, setDrag] = useState(false);
9 | const [resize, setResize] = useState(false);
10 |
11 | useEffect(() => {
12 |
13 | const onClick = async (e) => {
14 |
15 | let ref = document.getElementById('Table');
16 |
17 | if(ref && !ref.contains(e.target))
18 | setSelect(['', '', '', '']);
19 |
20 | }
21 |
22 | window.addEventListener('click', onClick);
23 |
24 | return () => window.removeEventListener('click', onClick);
25 |
26 | }, [select, setSelect]);
27 |
28 | const selectBox = (row, col) => {
29 |
30 | setSelect([row, col, row, col]);
31 |
32 | }
33 |
34 | const handleMouseDown = (e, row, col) => {
35 |
36 | let [iniRow, iniCol, endRow, endCol] = select;
37 |
38 | if(e.button === 0){
39 |
40 | setSelect([row, col, row, col]);
41 |
42 | setDrag(true);
43 |
44 | }
45 | if(e.button === 2 && iniRow === endRow && iniCol === endCol){
46 |
47 | setSelect([row, col, row, col]);
48 |
49 | }
50 |
51 | }
52 |
53 | const handleMouseUp = () => {
54 |
55 | setDrag(false);
56 |
57 | }
58 |
59 | const handleMultipleSel = (e, row, col) => {
60 |
61 | if(drag){
62 |
63 | e.preventDefault();
64 |
65 | let [iniRow, iniCol, endRow, endCol] = select;
66 |
67 | if(iniRow <= row && iniCol <= col)
68 | setSelect([iniRow, iniCol, row, col]);
69 |
70 | if(iniRow >= row && iniCol >= col)
71 | setSelect([row, col, endRow, endCol])
72 |
73 | }
74 |
75 | }
76 |
77 | const editValue = (e, row, col) => {
78 |
79 | const copyCsv = JSON.parse(JSON.stringify(csv));
80 |
81 | copyCsv[row][col] = e.target.value;
82 |
83 | setCsv(copyCsv);
84 |
85 | }
86 |
87 | const getClassName = (row, col) => {
88 |
89 | let [iniRow, iniCol, endRow, endCol] = select;
90 |
91 | if(select.every(el => el === ''))
92 | return '';
93 |
94 | if(row >= iniRow && row <= endRow && col >= iniCol && col <= endCol)
95 | return 'Selected';
96 |
97 | }
98 |
99 | const resizeColumn = (e, col) => {
100 |
101 | e.preventDefault();
102 |
103 | if(resize.col === col && e.clientX){
104 |
105 | let elem = document.getElementById(`input0${col}`);
106 |
107 | elem.style.width = `${Math.max(75, parseInt(resize.initialSize) + parseInt(e.clientX - resize.initialPos))}px`;
108 |
109 | }
110 |
111 | }
112 |
113 | const autoResizeColumn = (e, col) => {
114 |
115 | let longestRow = csv.reduce((acc, elem, row) => csv[acc][col].toString().length > csv[row][col].toString().length ? acc : row, 0);
116 |
117 | csv.forEach((_, row) => document.getElementById(`input${row}${col}`).style.width = csv[longestRow][col].toString().length + 0.5 + 'ch');
118 |
119 | }
120 |
121 | const handleMouseDownDragger = (e, col) => {
122 |
123 | e.stopPropagation();
124 |
125 | let elem = document.getElementById(`input0${col}`);
126 |
127 | setResize({
128 | col: col,
129 | initialPos: e.clientX,
130 | initialSize: elem.offsetWidth
131 | });
132 |
133 | }
134 |
135 | const handleMouseUpDragger = (e) => {
136 |
137 | setResize(false);
138 |
139 | }
140 |
141 | return(
142 |
143 |
144 |
145 |
146 | {csv.map((row, i) =>
147 | {csv[i].map((column, j) =>
148 | selectBox(i, j)}
151 | onMouseDown = {(e) => handleMouseDown(e, i, j)}
152 | onMouseUp = {(e) => handleMouseUp(e, i, j)}
153 | onMouseMove = {(e) => handleMultipleSel(e, i, j)}
154 | className = {getClassName(i, j)}>
155 | editValue(e, i, j)}>
159 |
160 | handleMouseDownDragger(e, j)}
165 | onMouseUp = {(e) => handleMouseUpDragger(e)}
166 | onDrag = {(e) => resizeColumn(e, j)}
167 | onDoubleClick = {(e) => autoResizeColumn(e, j)}>
168 |
169 | )}
170 |
171 | )}
172 |
173 |
174 |
175 |
176 |
179 |
182 |
185 |
186 |
187 | );
188 |
189 | }
190 |
191 | export default Csv;
--------------------------------------------------------------------------------
/src/Components/DownloadCSV.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import { saveAs } from 'file-saver';
3 | import { DownloadIcon, CheckCircleIcon } from '@primer/octicons-react';
4 |
5 | const DownloadCSV = ({csv}) => {
6 |
7 | const [alert, setAlert] = useState(null);
8 |
9 | const download = () => {
10 |
11 | var fileName = 'result.csv';
12 |
13 | var fileToSave = new Blob([csv.map(row => row.join(',')).join('\n')], {
14 | type: 'application/csv',
15 | name: fileName
16 | });
17 |
18 | saveAs(fileToSave, fileName);
19 |
20 | setAlert('downloaded');
21 |
22 | setTimeout(() => setAlert(null), 1500);
23 |
24 | }
25 |
26 | return(
27 | { alert !== 'downloaded' ? <> Download CSV> : <> Downloading>}
28 | );
29 |
30 | }
31 |
32 | export default DownloadCSV;
--------------------------------------------------------------------------------
/src/Components/DownloadJSON.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import { saveAs } from 'file-saver';
3 | import { DownloadIcon, CheckCircleIcon } from '@primer/octicons-react';
4 |
5 | const DownloadJSON = ({json, edit, indent}) => {
6 |
7 | const [alert, setAlert] = useState(null);
8 |
9 | const download = () => {
10 |
11 | var fileName = 'result.json';
12 |
13 | var fileToSave = new Blob([JSON.stringify(json, null, parseInt(indent))], {
14 | type: 'application/json',
15 | name: fileName
16 | });
17 |
18 | saveAs(fileToSave, fileName);
19 |
20 | setAlert('downloaded');
21 |
22 | setTimeout(() => setAlert(null), 1500);
23 |
24 | }
25 |
26 | return(
27 | { alert !== 'downloaded' ? <> Download JSON> : <> Downloading>}
28 | );
29 |
30 | }
31 |
32 | export default DownloadJSON;
--------------------------------------------------------------------------------
/src/Components/Footer.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Link } from 'react-router-dom';
3 | import { ReactComponent as Logo } from '../Assets/Logo.svg';
4 |
5 | const Footer = () => {
6 |
7 | return(
8 |
9 |
10 |
Home
11 |
What's new?
12 |
Replace
13 |
Merge
14 |
Delete
15 |
GitHub
16 |
17 | );
18 |
19 | }
20 |
21 | export default Footer;
--------------------------------------------------------------------------------
/src/Components/Header.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { ReactComponent as Logo } from '../Assets/Logo.svg';
3 |
4 | const Header = () => {
5 |
6 | return(
7 |
8 |
9 |
jsonmatic – transform a CSV into a JSON
10 |
First column is reserved for unique object keys . You can use dot notation in the header cells to create subproperties.
11 |
12 | );
13 |
14 | }
15 |
16 | export default Header;
--------------------------------------------------------------------------------
/src/Components/IndentSelector.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { TriangleDownIcon } from '@primer/octicons-react';
3 |
4 | const IndentSelector = ({edit, indent, setIndent}) => {
5 |
6 | return(
7 |
8 | setIndent(parseInt(e.target.value))}>
9 | No indentation
10 | 2 spaces indent
11 | 4 spaces indent
12 |
13 |
14 |
15 | );
16 |
17 | }
18 |
19 | export default IndentSelector;
--------------------------------------------------------------------------------
/src/Components/Json.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import IndentSelector from './IndentSelector';
3 | import BeautifyButton from './BeautifyButton';
4 | import UglifyButton from './UglifyButton';
5 | import RandomButton from './RandomButton';
6 | import TextareaIndent from './TextareaIndent';
7 | import UploadJSON from './UploadJSON';
8 | import CopyJSON from './CopyJSON';
9 | import DownloadJSON from './DownloadJSON';
10 |
11 | const Json = ({edit, json, setEdit, setJson}) => {
12 |
13 | const [indent, setIndent] = useState(2);
14 |
15 | return(
16 |
17 |
18 |
22 |
27 |
32 |
37 |
38 |
45 |
46 |
51 |
55 |
60 |
61 |
62 | );
63 |
64 | }
65 |
66 | export default Json;
--------------------------------------------------------------------------------
/src/Components/Menu.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from 'react';
2 | import { CopyIcon, ClippyIcon, EllipsisIcon, ArrowLeftIcon, ArrowRightIcon } from '@primer/octicons-react';
3 |
4 | const Menu = ({select, setSelect, csv, setCsv, undo, redo}) => {
5 |
6 | const [menu, setMenu] = useState(false);
7 | const [pos, setPos] = useState({x: 0, y: 0});
8 | const [OS, setOS] = useState(null);
9 |
10 | useEffect(() => {
11 |
12 | let macOS = ['iPhone', 'iPad', 'Mac', 'iPod'];
13 |
14 | let currentOS = navigator.platform;
15 |
16 | let isMac = macOS.some(device => currentOS.includes(device));
17 |
18 | if(isMac)
19 | setOS('Mac');
20 |
21 | }, []);
22 |
23 | const merge = (arr1, arr2, x, y) => {
24 |
25 | let arr1_rows = arr1.length;
26 | let arr2_rows = arr2.length;
27 |
28 | let arr1_cols = arr1[0].length;
29 | let arr2_cols = arr2[0].length;
30 |
31 | let merg_rows = Math.max(arr1_rows, arr2_rows, x + arr2_rows);
32 | let merg_cols = Math.max(arr1_cols, arr2_cols, y + arr2_cols);
33 |
34 | let merged = Array(merg_rows).fill('').map(e => Array(merg_cols).fill(''));
35 |
36 | for(let i = 0; i < arr1_rows; i ++){
37 |
38 | merged[i].splice(0, arr1[i].length, ...arr1[i]);
39 |
40 | }
41 |
42 | for(let i = x, j = 0; i < x + arr2_rows; i ++, j ++){
43 |
44 | merged[i].splice(y, arr2[j].length, ...arr2[j]);
45 |
46 | }
47 |
48 | setCsv(merged);
49 |
50 | }
51 |
52 | const copy = () => {
53 |
54 | let text = '';
55 |
56 | let [iniRow, iniCol, endRow, endCol] = select;
57 |
58 | for(let i = iniRow; i <= endRow; i ++){
59 | for(let j = iniCol; j <= endCol; j ++){
60 |
61 | text += csv[i][j];
62 |
63 | if(j !== endCol)
64 | text += '\t';
65 |
66 | }
67 |
68 | if(i !== endRow)
69 | text += '\n';
70 |
71 | }
72 |
73 | navigator.clipboard.writeText(text);
74 |
75 | }
76 |
77 | const paste = async () => {
78 |
79 | let clipboard = await navigator.clipboard.readText();
80 |
81 | clipboard = clipboard.split('\n').map(row => row.split('\t'));
82 |
83 | merge(csv, clipboard, select[0], select[1]);
84 |
85 | }
86 |
87 | const addRow = () => {
88 |
89 | let temp = JSON.parse(JSON.stringify(csv));
90 |
91 | let row = Array(temp[0].length).fill('');
92 |
93 | temp.splice(select[0] + 1, 0, row);
94 |
95 | setCsv(temp);
96 | setMenu(false);
97 |
98 | }
99 |
100 | const addCol = () => {
101 |
102 | let temp = JSON.parse(JSON.stringify(csv));
103 |
104 | temp.forEach(row => row.splice(select[1] + 1, 0, ''));
105 |
106 | setCsv(temp);
107 | setMenu(false);
108 |
109 | }
110 |
111 | const deleteRows = () => {
112 |
113 | let temp = JSON.parse(JSON.stringify(csv));
114 |
115 | let [iniRow, iniCol, endRow, endCol] = select; // eslint-disable-line no-unused-vars
116 |
117 | temp.splice(iniRow, endRow - iniRow + 1);
118 |
119 | setCsv(temp);
120 | setMenu(false);
121 |
122 | }
123 |
124 | const deleteCols = () => {
125 |
126 | let temp = JSON.parse(JSON.stringify(csv));
127 |
128 | let [iniRow, iniCol, endRow, endCol] = select; // eslint-disable-line no-unused-vars
129 |
130 | temp.forEach(row => row.splice(iniCol, endCol - iniCol + 1));
131 |
132 | setCsv(temp);
133 | setMenu(false);
134 |
135 | }
136 |
137 | const deleteValues = () => {
138 |
139 | let temp = JSON.parse(JSON.stringify(csv));
140 |
141 | let [iniRow, iniCol, endRow, endCol] = select;
142 |
143 | for(let i = iniRow; i <= endRow; i ++)
144 | for(let j = iniCol; j <= endCol; j ++)
145 | temp[i][j] = '';
146 |
147 | setCsv(temp);
148 |
149 | }
150 |
151 | const moveSelection = (direction) => {
152 |
153 | let temp = [...select];
154 |
155 | let [iniRow, iniCol, endRow, endCol] = temp;
156 |
157 | let cols = csv[0].length;
158 | let rows = csv.length;
159 |
160 | if(temp.every(el => el === '') || iniRow !== endRow || iniCol !== endCol){
161 |
162 | setSelect([0, 0, 0, 0]);
163 | return;
164 | }
165 |
166 | switch(direction){
167 |
168 | case 'ArrowRight': temp[1] = temp[3] = Math.min(cols - 1, temp[1] + 1); break;
169 | case 'ArrowLeft': temp[1] = temp[3] = Math.max(0, temp[1] - 1); break;
170 | case 'ArrowDown': temp[0] = temp[2] = Math.min(rows - 1, temp[2] + 1); break;
171 | case 'ArrowUp': temp[0] = temp[2] = Math.max(0, temp[2] - 1); break;
172 | default: break;
173 |
174 | }
175 |
176 | setSelect(temp);
177 |
178 | }
179 |
180 | const selectAll = () => {
181 |
182 | let cols = csv[0].length;
183 | let rows = csv.length;
184 |
185 | setSelect([0, 0, rows - 1, cols - 1]);
186 |
187 | }
188 |
189 | const focus = () => {
190 |
191 | let [iniRow, iniCol] = select;
192 |
193 | let ref = document.getElementById(`input${iniRow}${iniCol}`);
194 |
195 | if(ref)
196 | ref.focus();
197 |
198 | }
199 |
200 | useEffect(() => {
201 |
202 | const onDown = async (e) => {
203 |
204 | if((e.ctrlKey && e.key === 'v') || (e.metaKey && e.key === 'v')){
205 |
206 | e.preventDefault();
207 |
208 | paste();
209 |
210 | }
211 |
212 | if((e.ctrlKey && e.key === 'a') || (e.metaKey && e.key === 'a')){
213 |
214 | e.preventDefault();
215 |
216 | selectAll();
217 |
218 | }
219 |
220 | if(e.key === 'ArrowRight' || e.key === 'ArrowLeft' || e.key === 'ArrowUp' || e.key === 'ArrowDown'){
221 |
222 | e.preventDefault();
223 |
224 | moveSelection(e.key);
225 |
226 | }
227 |
228 | if((e.ctrlKey && e.key === 'c') || (e.metaKey && e.key === 'c')){
229 |
230 | e.preventDefault();
231 |
232 | copy();
233 |
234 | }
235 |
236 | if((e.ctrlKey && e.key === 'ArrowRight') || (e.metaKey && e.key === 'ArrowRight')){
237 |
238 | e.preventDefault();
239 |
240 | addCol();
241 |
242 | }
243 |
244 | if((e.ctrlKey && e.key === 'ArrowDown') || (e.metaKey && e.key === 'ArrowDown')){
245 |
246 | e.preventDefault();
247 |
248 | addRow();
249 |
250 | }
251 |
252 | if((e.ctrlKey && e.key === 'ArrowLeft') || (e.metaKey && e.key === 'ArrowLeft')){
253 |
254 | e.preventDefault();
255 |
256 | deleteCols();
257 |
258 | }
259 |
260 | if((e.ctrlKey && e.key === 'ArrowUp') || (e.metaKey && e.key === 'ArrowUp')){
261 |
262 | e.preventDefault();
263 |
264 | deleteRows();
265 |
266 | }
267 |
268 | if(e.key === 'Delete'){
269 |
270 | deleteValues();
271 |
272 | }
273 |
274 | if(e.key.length === 1){
275 |
276 | focus();
277 |
278 | }
279 |
280 | }
281 |
282 | const onLeftClick = (e) => {
283 |
284 | let menu = document.getElementById('Menu');
285 |
286 | if(!menu || !menu.contains(e.target)){
287 |
288 | setMenu(false);
289 |
290 | }
291 |
292 | }
293 |
294 | const onRightClick = (e) => {
295 |
296 | let table = document.getElementById('Table');
297 |
298 | if(table.contains(e.target)){
299 |
300 | e.preventDefault();
301 |
302 | setPos({x: e.pageX, y: e.pageY});
303 | setMenu(true);
304 |
305 | window.addEventListener('click', onLeftClick);
306 |
307 | }
308 |
309 | }
310 |
311 | if(select.some(el => el !== '')){
312 |
313 | window.addEventListener('keydown', onDown);
314 | window.addEventListener('contextmenu', onRightClick);
315 | window.addEventListener('click', onLeftClick);
316 |
317 | }
318 |
319 | return () => {
320 |
321 | window.removeEventListener('keydown', onDown);
322 | window.removeEventListener('contextmenu', onRightClick);
323 | window.removeEventListener('click', onLeftClick);
324 |
325 | }
326 |
327 | }, [csv, select]); // eslint-disable-line react-hooks/exhaustive-deps
328 |
329 | return(
330 |
331 | { menu
332 | ?
333 |
334 | Copy {OS === 'Mac' ? '⌘' : 'ctrl'}C
335 | Paste {OS === 'Mac' ? '⌘' : 'ctrl'}V
336 |
337 | Undo {OS === 'Mac' ? '⌘' : 'ctrl'}Z
338 | Redo {OS === 'Mac' ? '⌘' : 'ctrl'}⇧Z
339 |
340 | Add row {OS === 'Mac' ? '⌘' : 'ctrl'}↓
341 | Add column{OS === 'Mac' ? '⌘' : 'ctrl'}→
342 |
343 | Delete row(s) {OS === 'Mac' ? '⌘' : 'ctrl'}↑
344 | Delete column(s) {OS === 'Mac' ? '⌘' : 'ctrl'}←
345 |
346 |
347 | : null
348 | }
349 |
350 | );
351 |
352 | }
353 |
354 | export default Menu;
--------------------------------------------------------------------------------
/src/Components/MultipleDeletions.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import { saveAs } from 'file-saver';
3 | import UploadFiles from './UploadFiles';
4 | import { DownloadIcon, TriangleDownIcon } from '@primer/octicons-react';
5 |
6 | const MultipleReplaces = () => {
7 |
8 | const [jsonfiles, setJsonfiles] = useState([]);
9 | const [numDeletions, setNumdeletions] = useState(null);
10 | const [operation, setOperation] = useState('equal');
11 | const [path, setPath] = useState('');
12 | const [currentValue, setCurrentValue] = useState('');
13 |
14 | const deletion = () => {
15 |
16 | let numDeletions = 0;
17 |
18 | let jsonFilesReplaced = jsonfiles.map(({name, json}) => {
19 |
20 | let clone = {...json};
21 |
22 | path.split(',').forEach(path => {
23 |
24 | let props = path.trim().split('.');
25 |
26 | let val = props.slice(0 ).reduce((ref, prop) => ref = ref?.[prop], clone);
27 | let ref = props.slice(0, -1).reduce((ref, prop) => ref = ref?.[prop], clone);
28 |
29 | let last = props.pop();
30 |
31 | let _currentValue = isNaN(currentValue) ? currentValue : parseFloat(currentValue);
32 |
33 | let rule = {
34 |
35 | 'equal': () => val === _currentValue,
36 | 'greater': () => val > _currentValue,
37 | 'lesser': () => val < _currentValue
38 |
39 | }[operation]();
40 |
41 | if(rule || _currentValue === '*'){
42 |
43 | try{
44 |
45 | delete ref[last];
46 | numDeletions ++;
47 |
48 | }catch(e){
49 |
50 | // Ignore deletion because property doesn't exist
51 |
52 | }
53 |
54 | }
55 |
56 | });
57 |
58 | return {
59 | name: name,
60 | json: clone
61 | };
62 |
63 | });
64 |
65 |
66 | setJsonfiles(jsonFilesReplaced);
67 | setNumdeletions(numDeletions);
68 |
69 | }
70 |
71 | const download = () => {
72 |
73 | jsonfiles.forEach(({name, json}) => {
74 |
75 | let fileName = name;
76 |
77 | let fileToSave = new Blob([JSON.stringify(json, null, 2)], {
78 | type: 'application/json',
79 | name: fileName
80 | });
81 |
82 | saveAs(fileToSave, fileName);
83 |
84 | });
85 |
86 | }
87 |
88 | return(
89 |
90 |
Delete multiple properties in JSON files at once
91 |
Select your files (they won't be uploaded to any server), they remain in your browser.
92 |
96 | { jsonfiles.length > 0
97 | ?
98 | Rules
99 |
100 |
If
101 |
setPath(e.target.value)} value = {path}/>
102 |
103 | setOperation(e.target.value)}>
104 | is equal to
105 | is greater than
106 | is lesser than
107 |
108 |
109 |
110 |
setCurrentValue(e.target.value)} value = {currentValue}/>
111 |
112 |
113 |
Hint: Separate multiple properties with commas and set value to * for brute force deletion
114 |
115 |
116 | Delete
117 | Download all files
118 |
119 |
120 |
{numDeletions !== null ? `${numDeletions} fields deleted` : null}
121 |
122 |
123 | : null
124 | }
125 |
126 | );
127 |
128 | }
129 |
130 | export default MultipleReplaces;
--------------------------------------------------------------------------------
/src/Components/MultipleMerge.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import { saveAs } from 'file-saver';
3 | import UploadFiles from './UploadFiles';
4 | import { DownloadIcon, GrabberIcon, GitMergeIcon } from '@primer/octicons-react';
5 | import { mergeMultipleJSONs } from '../CLI/utils';
6 |
7 | const MultipleReplaces = () => {
8 |
9 | const [jsonfiles, setJsonfiles] = useState([]);
10 | const [dragged, setDragged] = useState(null);
11 | const [merged, setMerged] = useState(null);
12 |
13 | const merge = () => {
14 |
15 | let merged = mergeMultipleJSONs(jsonfiles);
16 |
17 | setMerged(merged);
18 |
19 | }
20 |
21 | const download = () => {
22 |
23 | let fileName = `merged.json`;
24 |
25 | let fileToSave = new Blob([JSON.stringify(merged, null, 2)], {
26 | type: 'application/json',
27 | name: fileName
28 | });
29 |
30 | saveAs(fileToSave, fileName);
31 |
32 | }
33 |
34 | const handleDragStart = (e) => {
35 |
36 | setDragged(e.target.id);
37 |
38 | }
39 |
40 | const handleDragOver = (e) => {
41 |
42 | let newPosition = parseInt(e.target.id);
43 |
44 | if(newPosition >= 0){
45 |
46 | let copy = [...jsonfiles];
47 |
48 | ((from, to, elem) => {
49 |
50 | copy.splice(from, 1);
51 | copy.splice(to, 0, elem);
52 |
53 | })(dragged, newPosition, jsonfiles[dragged]);
54 |
55 | setJsonfiles(copy);
56 | setDragged(newPosition);
57 |
58 | }
59 |
60 | }
61 |
62 | const handleDragEnd = () => {
63 |
64 | setDragged(null);
65 |
66 | }
67 |
68 | return(
69 |
70 |
Merge multiple JSON files
71 |
Select your files (they won't be uploaded to any server), they remain in your browser.
72 |
76 | { jsonfiles.length > 0
77 | ?
78 | Select merge order
79 | Higher priority files are on the left. Files with higher priority overwrite them with less priority:
80 |
81 | {jsonfiles.map(({name}, key) =>
82 |
83 | {name}
84 |
85 | )}
86 |
Merge all files
87 |
88 | { merged
89 | ?
90 |
91 |
merged.json
92 | {JSON.stringify(merged, null, 2)}
93 |
94 |
Download
95 |
96 | : null
97 | }
98 |
99 | : null
100 | }
101 |
102 | );
103 |
104 | }
105 |
106 | export default MultipleReplaces;
--------------------------------------------------------------------------------
/src/Components/MultipleReplaces.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import { saveAs } from 'file-saver';
3 | import UploadFiles from './UploadFiles';
4 | import { DownloadIcon, TriangleDownIcon } from '@primer/octicons-react';
5 | import { replaceMultipleJSONs } from '../CLI/utils';
6 |
7 | const MultipleReplaces = () => {
8 |
9 | const [jsonfiles, setJsonfiles] = useState([]);
10 | const [numReplaces, setNumReplaces] = useState(null);
11 | const [operation, setOperation] = useState('equal');
12 | const [path, setPath] = useState('');
13 | const [replaceValue, setReplaceValue] = useState('');
14 | const [currentValue, setCurrentValue] = useState('');
15 |
16 | const replace = () => {
17 |
18 | let res = replaceMultipleJSONs(path, currentValue, replaceValue, jsonfiles, operation);
19 |
20 | setJsonfiles(res[0]);
21 | setNumReplaces(res[1]);
22 |
23 | }
24 |
25 | const download = () => {
26 |
27 | jsonfiles.forEach(({name, json}) => {
28 |
29 | let fileName = name;
30 |
31 | let fileToSave = new Blob([JSON.stringify(json, null, 2)], {
32 | type: 'application/json',
33 | name: fileName
34 | });
35 |
36 | saveAs(fileToSave, fileName);
37 |
38 | });
39 |
40 | }
41 |
42 | return(
43 |
44 |
Replace multiple properties in JSON files at once
45 |
Select your files (they won't be uploaded to any server), they remain in your browser.
46 |
50 | { jsonfiles.length > 0
51 | ?
52 | Rules
53 |
68 |
69 | Replace
70 | Download all files
71 |
72 |
73 |
{numReplaces !== null ? `${numReplaces} fields replaced` : null}
74 |
75 |
76 | : null
77 | }
78 |
79 | );
80 |
81 | }
82 |
83 | export default MultipleReplaces;
--------------------------------------------------------------------------------
/src/Components/RandomButton.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import dream from 'dreamjs';
3 | import { FlameIcon } from '@primer/octicons-react';
4 |
5 | const RandomButton = ({edit, setJson}) => {
6 |
7 | const randomize = () => {
8 |
9 | let people = ~~(Math.random() * 100);
10 |
11 | dream
12 | .schema({
13 | name: 'name',
14 | age: 'age',
15 | address: 'address',
16 | contact: {
17 | phone: 'phone',
18 | servicePhone: /^(800[1-9]{6})$/
19 | }
20 | })
21 | .generateRnd(people)
22 | .output((err, result) => {
23 |
24 | let json = {};
25 |
26 | result.forEach((obj, i) => json[i] = obj)
27 |
28 | setJson(json);
29 |
30 | });
31 |
32 | }
33 |
34 | return(
35 | Randomize
36 | );
37 |
38 | }
39 |
40 | export default RandomButton;
--------------------------------------------------------------------------------
/src/Components/Seo.js:
--------------------------------------------------------------------------------
1 | import { useEffect } from 'react';
2 | import { withRouter } from 'react-router-dom';
3 |
4 | const Seo = () => {
5 |
6 | let sitemap = {
7 |
8 | '/': {
9 | 'title': `Jsonmatic`,
10 | 'description': `Transform a CSV (spreadsheet) into a JSON.`
11 | },
12 | '/changelog': {
13 | 'title': `What's new — Jsonmatic`,
14 | 'description': `Changelog and relevant changes of the website.`
15 | },
16 | '/replace': {
17 | 'title': `Replace — Jsonmatic`,
18 | 'description': `Replace a property from multiple JSON files at once, and download the new file.`
19 | },
20 | '/merge': {
21 | 'title': `Merge — Jsonmatic`,
22 | 'description': `Merge multiple JSON files, and download the new merged file.`
23 | },
24 | '/delete': {
25 | 'title': `Delete — Jsonmatic`,
26 | 'description': `Replace a property from multiple JSON files at once, and download the new file.`
27 | }
28 |
29 | }
30 |
31 | useEffect(() => {
32 |
33 | let url = window.location.pathname;
34 |
35 | document.title = sitemap[url].title;
36 | document.querySelector(`meta[name = 'description']`).content = sitemap[url].description;
37 |
38 | });
39 |
40 | return null;
41 |
42 | }
43 |
44 | export default withRouter(Seo);
--------------------------------------------------------------------------------
/src/Components/TextareaIndent.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from 'react';
2 | import { validateJSON } from '../CLI/utils';
3 |
4 | const TextareaIndent = ({edit, setEdit, indent, json, setJson}) => {
5 |
6 | const [alert, setAlert] = useState(null);
7 | const [text, setText] = useState({value: '', caret: -1, target: null});
8 |
9 | useEffect(() => {
10 |
11 | setText({value: JSON.stringify(json, null, parseInt(indent))});
12 |
13 | }, [json, indent]);
14 |
15 | useEffect(() => {
16 |
17 | if(text.caret >= 0){
18 |
19 | text.target.setSelectionRange(text.caret + indent, text.caret + indent);
20 |
21 | }
22 |
23 | }, [indent, text]);
24 |
25 | const iniEdition = () => {
26 |
27 | setEdit(true);
28 | setAlert(null);
29 |
30 | }
31 |
32 | const endEdition = () => {
33 |
34 | let validation = validateJSON(text.value);
35 |
36 | if(validation === 'ok'){
37 |
38 | setJson(JSON.parse(text.value));
39 | setEdit(false);
40 |
41 | }
42 | else{
43 |
44 | setAlert(validation.error);
45 |
46 | }
47 |
48 | }
49 |
50 | const handleTab = (e) => {
51 |
52 | let content = e.target.value;
53 | let caret = e.target.selectionStart;
54 |
55 | if(e.key === 'Tab'){
56 |
57 | e.preventDefault();
58 |
59 | let newText = content.substring(0, caret) + ' '.repeat(indent) + content.substring(caret);
60 |
61 | setText({value: newText, caret: caret, target: e.target});
62 |
63 | }
64 |
65 | }
66 |
67 | const handleText = (e) => setText({value: e.target.value, caret: -1, target: e.target});
68 |
69 | return(
70 |
80 | );
81 |
82 | }
83 |
84 | export default TextareaIndent;
--------------------------------------------------------------------------------
/src/Components/TransformToCSV.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { ArrowLeftIcon, ArrowUpIcon } from '@primer/octicons-react';
3 | import useWindowDimensions from '../Functions/useWindowDimensions';
4 | import { transformToCSV } from '../CLI/utils';
5 |
6 | const TransformToCSV = ({json, edit, setCsv}) => {
7 |
8 | const [width] = useWindowDimensions();
9 |
10 | const transform = async () => {
11 |
12 | setCsv([
13 |
14 | ['...', '...', '...', '...', '...'],
15 | ['...', '...', '...', '...', '...'],
16 | ['...', '...', '...', '...', '...'],
17 | ['...', '...', '...', '...', '...'],
18 |
19 | ]);
20 |
21 | await new Promise(resolve => setTimeout(resolve, 1000));
22 |
23 | setCsv(transformToCSV(json));
24 |
25 | }
26 |
27 | return(
28 |
29 | {width > 768 ? : }Generate CSV
30 |
31 | );
32 |
33 | }
34 |
35 | export default TransformToCSV;
--------------------------------------------------------------------------------
/src/Components/TransformToJSON.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { ArrowRightIcon, ArrowDownIcon } from '@primer/octicons-react';
3 | import useWindowDimensions from '../Functions/useWindowDimensions';
4 | import { transformToJSON, validateCSV } from '../CLI/utils';
5 |
6 | const TransformToJSON = ({csv, setJson}) => {
7 |
8 | const [width] = useWindowDimensions();
9 |
10 | const transform = async () => {
11 |
12 | setJson({'Generating JSON': 'Wait a few seconds...'});
13 |
14 | await new Promise(resolve => setTimeout(resolve, 1000));
15 |
16 | let validation = validateCSV(csv);
17 |
18 | if(validation === 'ok'){
19 |
20 | setJson(transformToJSON(csv));
21 |
22 | }
23 | else{
24 |
25 | setJson({'Error': validation.error});
26 |
27 | }
28 |
29 |
30 | }
31 |
32 | return(
33 |
34 | {width > 768 ? : }Generate JSON
35 |
36 | );
37 |
38 | }
39 |
40 | export default TransformToJSON;
--------------------------------------------------------------------------------
/src/Components/UglifyButton.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { FileZipIcon } from '@primer/octicons-react';
3 |
4 | const UglifyButton = ({edit, setIndent}) => {
5 |
6 | const uglify = () => {
7 |
8 | setIndent(0);
9 |
10 | }
11 |
12 | return(
13 | Uglify
14 | );
15 |
16 | }
17 |
18 | export default UglifyButton;
--------------------------------------------------------------------------------
/src/Components/UploadCSV.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { UploadIcon } from '@primer/octicons-react';
3 |
4 | const UploadCSV = ({setCsv}) => {
5 |
6 | const upload = (e) => {
7 |
8 | var reader = new FileReader();
9 |
10 | reader.onload = () => {
11 |
12 | let raw = reader.result;
13 |
14 | let csv = raw.split(/\r\n|\n|\r/).map(line => line.split(','));
15 |
16 | setCsv(csv);
17 |
18 | };
19 |
20 | reader.readAsText(e.target.files[0]);
21 |
22 | }
23 |
24 | const simulateClick = () => document.getElementById('uploadCSV').click();
25 |
26 | return(
27 |
28 | Upload CSV
29 |
30 |
31 | );
32 |
33 | }
34 |
35 | export default UploadCSV;
--------------------------------------------------------------------------------
/src/Components/UploadFiles.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from 'react';
2 | import { UploadIcon, PlusCircleIcon } from '@primer/octicons-react';
3 |
4 | const UploadFiles = ({jsonfiles, setJsonfiles}) => {
5 |
6 | const [OS, setOS] = useState(null);
7 |
8 | useEffect(() => {
9 |
10 | let macOS = ['iPhone', 'iPad', 'Mac', 'iPod'];
11 |
12 | let currentOS = navigator.platform;
13 |
14 | let isMac = macOS.some(device => currentOS.includes(device));
15 |
16 | if(isMac)
17 | setOS('Mac');
18 |
19 | const onDown = async (e) => {
20 |
21 | if((e.ctrlKey && e.key === 'u') || (e.metaKey && e.key === 'u')){
22 |
23 | e.preventDefault();
24 |
25 | document.getElementById('upload').click();
26 |
27 | }
28 |
29 | }
30 |
31 | window.addEventListener('keydown', onDown);
32 |
33 |
34 | return () => {
35 |
36 | window.removeEventListener('keydown', onDown);
37 |
38 | }
39 |
40 | }, []);
41 |
42 | const upload = async (e) => {
43 |
44 | let files = Array.from(e.target.files).map(file => {
45 |
46 | let reader = new FileReader();
47 |
48 | return new Promise(resolve => {
49 |
50 | reader.onload = () => resolve({
51 |
52 | name: file.name,
53 | json: JSON.parse(reader.result)
54 |
55 | });
56 |
57 | reader.readAsText(file);
58 |
59 | });
60 |
61 | });
62 |
63 | let uploaded = await Promise.all(files);
64 |
65 | setJsonfiles([...jsonfiles, ...uploaded]);
66 |
67 | }
68 |
69 | return(
70 |
71 | { jsonfiles.length === 0
72 | ?
73 |
74 |
75 | Click here to upload files
76 |
77 |
78 |
Or press {OS === 'Mac' ? '⌘' : 'ctrl'}U
79 |
80 |
81 | :
82 | Files
83 |
84 | {jsonfiles.map(({name, json}, key) =>
85 |
86 |
{name}
87 | {JSON.stringify(json, null, 2)}
88 |
)
89 | }
90 |
91 |
Add more files
92 |
93 |
94 |
95 |
96 | }
97 |
98 | );
99 |
100 | }
101 |
102 | export default UploadFiles;
--------------------------------------------------------------------------------
/src/Components/UploadJSON.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { UploadIcon } from '@primer/octicons-react';
3 |
4 | const UploadJSON = ({edit, setJson}) => {
5 |
6 | const upload = (e) => {
7 |
8 | var reader = new FileReader();
9 |
10 | reader.onload = () => {
11 |
12 | let raw = reader.result;
13 |
14 | try{
15 |
16 | let json = JSON.parse(raw);
17 |
18 | setJson(json);
19 |
20 | }
21 | catch(e){
22 |
23 | }
24 |
25 | };
26 |
27 | reader.readAsText(e.target.files[0]);
28 |
29 | }
30 |
31 | const simulateClick = () => document.getElementById('uploadJSON').click();
32 |
33 | return(
34 |
35 | Upload JSON
36 |
37 |
38 | );
39 |
40 | }
41 |
42 | export default UploadJSON;
--------------------------------------------------------------------------------
/src/Functions/Firebase.js:
--------------------------------------------------------------------------------
1 | import firebase from 'firebase/app';
2 | import 'firebase/firestore';
3 |
4 | ///////////////////////////////////////////////
5 | //Modify this line to set the environment
6 | ///////////////////////////////////////////////
7 | ///////////////////////////////////////////////
8 | ///////////////////////////////////////////////
9 | export let environment = 'PRO';
10 | ///////////////////////////////////////////////
11 | ///////////////////////////////////////////////
12 | ///////////////////////////////////////////////
13 | var configPRE = {
14 | apiKey: "AIzaSyB1eLRPMC5_ZdPQ6Hxz6oVSOxI2ebCyakM",
15 | authDomain: "jsonmatic-pre.firebaseapp.com",
16 | projectId: "jsonmatic-pre",
17 | storageBucket: "jsonmatic-pre.appspot.com",
18 | messagingSenderId: "399731256775",
19 | appId: "1:399731256775:web:1530bd3dcb14a5776f3de2"
20 | };
21 |
22 | var configPRO = {
23 | apiKey: "AIzaSyAxDzeibKdWIUmO8rV9KMpRFTt07EpU_yM",
24 | authDomain: "jsonmatic-pro.firebaseapp.com",
25 | projectId: "jsonmatic-pro",
26 | storageBucket: "jsonmatic-pro.appspot.com",
27 | messagingSenderId: "724237445395",
28 | appId: "1:724237445395:web:6b75fa3f918ad4833bd2e6"
29 | };
30 |
31 | firebase.initializeApp(environment === 'PRE' ? configPRE : configPRO);
32 |
33 | export default firebase;
--------------------------------------------------------------------------------
/src/Functions/useUndoableState.js:
--------------------------------------------------------------------------------
1 | import { useState, useEffect } from 'react';
2 |
3 | const useUndoableState = (csv) => {
4 |
5 | const [buffer, setBuffer] = useState([csv]);
6 | const [index, setIndex] = useState(0);
7 | const maxBufferSize = 10;
8 |
9 | const undo = () => {
10 |
11 | let pos = index - 1;
12 |
13 | if(pos < 0)
14 | pos = 0;
15 |
16 | setIndex(pos);
17 |
18 | }
19 |
20 | const redo = () => {
21 |
22 | let pos = index + 1;
23 |
24 | if(pos > buffer.length - 1)
25 | pos = buffer.length - 1;
26 |
27 | setIndex(pos);
28 |
29 | }
30 |
31 | const setCsv = (csv) => {
32 |
33 | let copyBuffer = JSON.parse(JSON.stringify(buffer));
34 | let copyCsv = JSON.parse(JSON.stringify(csv));
35 | let pos = index + 1;
36 |
37 | if(pos > maxBufferSize - 1)
38 | pos = maxBufferSize - 1;
39 |
40 | copyBuffer.splice(pos, copyBuffer.length - pos, copyCsv);
41 |
42 | setBuffer(copyBuffer);
43 | setIndex(pos);
44 |
45 | }
46 |
47 | useEffect(() => {
48 |
49 | const onDown = (e) => {
50 |
51 | if((e.ctrlKey && e.key === 'z') || (e.metaKey && e.key === 'z')){
52 |
53 | e.preventDefault();
54 |
55 | undo();
56 |
57 | }
58 |
59 | if((e.ctrlKey && e.shiftKey && e.key === 'z') || (e.metaKey && e.shiftKey && e.key === 'z')){
60 |
61 | e.preventDefault();
62 |
63 | redo();
64 |
65 | }
66 |
67 | }
68 |
69 | window.addEventListener('keydown', onDown);
70 |
71 | return () => window.removeEventListener('keydown', onDown);
72 |
73 |
74 | }, [buffer, index]); // eslint-disable-line react-hooks/exhaustive-deps
75 |
76 | return [buffer[index], setCsv, undo, redo];
77 |
78 | }
79 |
80 | export default useUndoableState;
--------------------------------------------------------------------------------
/src/Functions/useWindowDimensions.js:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from 'react';
2 |
3 | const useWindowDimensions = () => {
4 |
5 | const [height, setHeight] = useState(null);
6 | const [width, setWidth] = useState(null);
7 |
8 | useEffect(() => {
9 |
10 | const handleResolution = () => {
11 |
12 | setHeight(window.screen.height);
13 | setWidth(window.screen.width);
14 | }
15 |
16 | handleResolution();
17 |
18 | window.addEventListener('resize', handleResolution);
19 |
20 | return () => window.removeEventListener('resize', handleResolution);
21 |
22 | }, []);
23 |
24 | return [width, height];
25 |
26 | }
27 |
28 | export default useWindowDimensions;
--------------------------------------------------------------------------------
/src/Styles/index.css:
--------------------------------------------------------------------------------
1 | *{
2 | font-family: -apple-system,BlinkMacSystemFont,Segoe UI,Helvetica,Arial,sans-serif,Apple Color Emoji,Segoe UI Emoji;
3 | -webkit-font-smoothing: antialiased;
4 | -moz-osx-font-smoothing: grayscale;
5 | box-sizing: border-box;
6 | }
7 | #root{
8 | min-height: 100vh;
9 | display: flex;
10 | flex-direction: column;
11 | }
12 | body{
13 | font-size: 17px;
14 | margin: 0;
15 | outline: none;
16 | }
17 | input, textarea{
18 | font-size: 17px;
19 | }
20 | code {
21 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', monospace;
22 | background: #f5f2f0;
23 | border-radius: 5px;
24 | color: #dd4a68;
25 | font-size: 14px;
26 | padding: 2px;
27 | }
28 | h1, h2{
29 | font-weight: 800;
30 | }
31 | button{
32 | align-items: center;
33 | background: #0071e3;
34 | border: none;
35 | border-radius: 6px;
36 | color: white;
37 | cursor: pointer;
38 | display: flex;
39 | font-size: 14px;
40 | font-weight: bold;
41 | height: max-content;
42 | margin: 10px;
43 | padding: 10px;
44 | white-space: nowrap;
45 | width: max-content;
46 | }
47 | button:hover{
48 | background: #0065cc;
49 | transition: 0.2s ease-in-out;
50 | }
51 | button:disabled{
52 | cursor: not-allowed;
53 | opacity: 0.5;
54 | }
55 | button:hover svg{
56 | animation: tilt 0.5s ease-in-out;
57 | }
58 | svg.octicon{
59 | margin-right: 10px;
60 | }
61 | select {
62 | background: transparent;
63 | border: none;
64 | font-size: 14px;
65 | padding: 10px;
66 | width: 150px;
67 | z-index: 2;
68 | -moz-appearance: none;
69 | -webkit-appearance: none;
70 | }
71 | select::-ms-expand {
72 | display: none;
73 | }
74 | @keyframes tilt{
75 | 50% {transform: rotate(20deg);}
76 | 100% {transform: rotate(0deg);}
77 | }
78 | @keyframes swirl_in{
79 | 0% {transform: rotate(0deg);}
80 | 100% {transform: rotate(180deg);}
81 | }
82 | @keyframes swirl_out{
83 | 0% {transform: rotate(180deg);}
84 | 100% {transform: rotate(0);}
85 | }
86 | @keyframes appear{
87 | 0% {transform: translateY( 3px); opacity: 0;}
88 | 50% {transform: translateY(-1px); opacity: 0.5;}
89 | 100% {transform: translateY( 0px); opacity: 1;}
90 | }
91 | /*
92 | *
93 | * App
94 | *
95 | *
96 | */
97 | .App{
98 | align-items: center;
99 | display: flex;
100 | flex-direction: column;
101 | min-height: 100vh;
102 | }
103 | .App .Subheader{
104 | font-size: 14px;
105 | padding: 10px;
106 | }
107 | .App .Subheader button{
108 | background: white;
109 | border: 2px solid #9CA3AF;
110 | color: black;
111 | display: inline-block;
112 | font-size: 14px;
113 | margin: 0;
114 | }
115 | .App .Subheader button:hover{
116 | background: #9CA3AF;
117 | transition: 0.2s ease-in-out;
118 | }
119 | .App h1{
120 | font-size: 30px;
121 | font-weight: 800;
122 | margin-bottom: 15px;
123 | text-align: center;
124 | }
125 | .App p,
126 | .App b,
127 | .App a{
128 | color: #9CA3AF;
129 | line-height: 1.5;
130 | margin-bottom: 20px;
131 | max-width: 500px;
132 | text-align: center;
133 | }
134 | .App .Header{
135 | align-items: center;
136 | background: black;
137 | color: white;
138 | display: flex;
139 | flex-direction: column;
140 | justify-content: center;
141 | min-height: 200px;
142 | padding: 20px;
143 | width: 100%;
144 | }
145 | .App .Header svg{
146 | height: 75px;
147 | width: 75px;
148 | }
149 | .App .Content{
150 | align-items: center;
151 | background: white;
152 | border-radius: 12px;
153 | display: flex;
154 | justify-content: space-between;
155 | padding: 20px;
156 | }
157 | .App .Buttons{
158 | display: flex;
159 | flex-direction: column;
160 | }
161 | .App .Buttons button{
162 | width: 170px;
163 | }
164 | .App .Table{
165 | border: 1px solid #e1e4e8;
166 | border-radius: 12px;
167 | max-height: 500px;
168 | max-width: 800px;
169 | overflow-x: auto;
170 | overflow-y: auto;
171 | }
172 | .App .Table table{
173 | border: 1px solid #e1e4e8;
174 | border-collapse: collapse;
175 | border-style: hidden;
176 | width: 100%;
177 | }
178 | .App .Table table tr:first-child{
179 | background: #f1f8ff;
180 | }
181 | .App .Table table tr:first-child td{
182 | border: none;
183 | }
184 | .App .Table table tr{
185 | border: 1px solid #e1e4e8;
186 | }
187 | .App .Table table td{
188 | border: 1px solid #e1e4e8;
189 | min-width: 50px;
190 | padding: 20px;
191 | position: relative;
192 | }
193 | .App .Table table td.Selected{
194 | background: #f1f8ff;
195 | border: 2px solid #0071e3!important;
196 | }
197 | .App .Table table td input{
198 | border: none;
199 | background: transparent;
200 | outline: none;
201 | overflow: auto;
202 | resize: both;
203 | width: 75px;
204 | }
205 | .App .Table table td .Dragger{
206 | cursor: col-resize;
207 | height: 100%;
208 | position: absolute;
209 | right: -5px;
210 | top: 0;
211 | width: 10px;
212 | z-index:999999;
213 | }
214 | .App .Table table td:last-of-type .Dragger{
215 | right: 0px;
216 | }
217 | .App .Footer{
218 | margin-top: auto;
219 | }
220 | .App .Hint{
221 | color: gray;
222 | font-size: small;
223 | margin-left: auto;
224 | }
225 | .App .UploadCSV input[type='file']{
226 | display: none;
227 | }
228 | /*
229 | *
230 | * Result
231 | *
232 | */
233 | .App .Result{
234 | align-items: center;
235 | display: flex;
236 | flex-direction: column;
237 | }
238 | .App .Result .TextareaIndent{
239 | border: 1px solid #e1e4e8;
240 | border-radius: 12px;
241 | height: 590px;
242 | outline: none;
243 | position: relative;
244 | width: 590px;
245 | }
246 | .App .Result .TextareaIndent textarea{
247 | border: none;
248 | border-radius: 12px;
249 | font-family: SFMono-Regular, Consolas, Liberation Mono, Menlo, monospace;
250 | font-size: 14px;
251 | height: 100%;
252 | outline: none;
253 | padding: 20px;
254 | resize: none;
255 | width: 100%;
256 | }
257 | .App .Result .TextareaIndent .Alert{
258 | color: red;
259 | font-size: 12px;
260 | position: absolute;
261 | right: 20px;
262 | top: 20px;
263 | }
264 | .App .Options{
265 | align-self: flex-end;
266 | }
267 | .App .Actions,
268 | .App .Options{
269 | align-items: center;
270 | display: flex;
271 | }
272 | .App .Actions button:first-child,
273 | .App .Options button{
274 | background: #fafbfc;
275 | border: 1px solid #e1e4e8;
276 | color: black;
277 | }
278 | /*
279 | *
280 | * Indent Selector
281 | *
282 | */
283 | .Selector{
284 | align-items: center;
285 | background: #fafbfc;
286 | border: 1px solid #e1e4e8;
287 | border-radius: 6px;
288 | display: flex;
289 | justify-content: space-between;
290 | margin: 10px;
291 | position: relative;
292 | }
293 | .Selector svg{
294 | position: absolute;
295 | right: 5px;
296 | }
297 | .Selector select{
298 | font-weight: bold;
299 | }
300 | /*
301 | *
302 | * Menu
303 | *
304 | *
305 | */
306 | .Menu{
307 | background: white;
308 | border-radius: 10px;
309 | box-shadow: rgb(15 15 15/10%) 0px 0px 0px 1px,rgb(15 15 15/20%) 0px 5px 10px,rgb(15 15 15/40%) 0px 15px 40px;
310 | overflow: hidden;
311 | min-width: 220px;
312 | }
313 | .Menu ul{
314 | padding: 0;
315 | }
316 | .Menu ul li{
317 | align-items: center;
318 | cursor: pointer;
319 | display: flex;
320 | list-style-type: none;
321 | padding: 10px 20px;
322 | }
323 | .Menu ul li:hover{
324 | background: rgba(1, 1, 1, 0.1);
325 | }
326 | .Menu hr{
327 | border :0;
328 | border-top: 1px solid #e1e4e8;
329 | margin-bottom: 5px;
330 | margin-top: 5px;
331 | }
332 | /*
333 | *
334 | * Changelog
335 | *
336 | *
337 | */
338 | .Changelog{
339 | margin: auto;
340 | margin-bottom: 50px;
341 | max-width: 650px;
342 | width: 100%;
343 | }
344 | .Changelog h2{
345 | margin-top: 50px;
346 | }
347 | .Changelog img{
348 | border-radius: 12px;
349 | max-width: 100%;
350 | }
351 | /*
352 | *
353 | * Footer
354 | *
355 | *
356 | */
357 | .Footer{
358 | align-items: center;
359 | background: black;
360 | display: flex;
361 | margin-top: auto;
362 | padding: 50px;
363 | }
364 | .Footer a{
365 | color: white;
366 | font-size: 14px;
367 | margin: 5px;
368 | text-decoration: none;
369 | white-space: nowrap;
370 | }
371 | .Footer svg{
372 | width: 30px;
373 | }
374 | /*
375 | *
376 | * Multiple replaces
377 | *
378 | *
379 | */
380 | .Multiple{
381 | margin: auto;
382 | max-width: 1200px;
383 | width: 100%;
384 | }
385 | .Multiple .Grid{
386 | display: flex;
387 | flex-wrap: wrap;
388 | }
389 | .Multiple .Grid .Block{
390 | border: 1px solid #e1e4e8;
391 | border-radius: 6px;
392 | height: 200px;
393 | margin: 10px;
394 | overflow-y: auto;
395 | padding: 20px;
396 | position: relative;
397 | white-space: break-spaces;
398 | width: 200px;
399 | }
400 | .Multiple .Grid .Block .Name{
401 | background: #e1e4e8;
402 | font-size: 10px;
403 | right: 0;
404 | padding: 5px;
405 | position: absolute;
406 | top: 0;
407 | }
408 | .Multiple .Upload{
409 | align-items: center;
410 | border: 3px dashed #e1e4e8;
411 | border-radius: 12px;
412 | display: flex;
413 | flex-direction: column;
414 | font-weight: 500;
415 | justify-content: center;
416 | max-width: 500px;
417 | padding: 20px;
418 | position: relative;
419 | text-align: center;
420 | }
421 | .Multiple .Upload .Hint{
422 | animation: 0.5s appear ease-in-out;
423 | font-size: small;
424 | margin-top: 10px;
425 | }
426 | .Multiple .Grid .Upload{
427 | height: 200px;
428 | margin: 10px;
429 | width: 200px;
430 | }
431 | .Multiple .Upload:hover svg{
432 | animation: tilt 0.5s ease-in-out;
433 | }
434 | .Multiple .Grid .Upload svg{
435 | animation: 0.3s swirl_out ease-in-out;
436 | }
437 | .Multiple .Grid .Upload:hover svg{
438 | animation: 0.3s swirl_in ease-in-out;
439 | }
440 | .Multiple input[type='file']{
441 | cursor: pointer;
442 | font-size: 0;
443 | height: 100%;
444 | left: 0;
445 | opacity: 0;
446 | position: absolute;
447 | top: 0;
448 | width: 100%;
449 | }
450 | .Multiple .Replace{
451 | align-items: center;
452 | display: flex;
453 | }
454 | .Multiple .Replace input{
455 | background: #e1e4e8;
456 | border: none;
457 | border-radius: 6px;
458 | margin: 10px;
459 | padding: 10px;
460 | }
461 | .Multiple .Actions{
462 | display: flex;
463 | margin-bottom: 10px;
464 | margin-top: 10px;
465 | }
466 | .Multiple .Actions button{
467 | margin: 0;
468 | }
469 | .Multiple .Actions button:nth-of-type(1){
470 | background: #fafbfc;
471 | border: 1px solid #e1e4e8;
472 | color: black;
473 | margin-right: 5px;
474 | }
475 | .Multiple .Alerts{
476 | font-size: 14px;
477 | }
478 | .Multiple .DragAndDrop{
479 | align-items: center;
480 | display: flex;
481 | }
482 | .Multiple .DragAndDrop .Element{
483 | align-items: center;
484 | border: 1px solid #e1e4e8;
485 | border-radius: 6px;
486 | display: flex;
487 | justify-content: space-between;
488 | margin-right: 5px;
489 | padding: 10px;
490 | width: max-content;
491 | }
492 | .Multiple .DragAndDrop svg.octicon-grabber{
493 | cursor: grab;
494 | margin: 0;
495 | }
496 | /*
497 | *
498 | * Mobile devices
499 | *
500 | *
501 | */
502 | @media(max-width: 768px){
503 | .App .Subheader{
504 | display: flex;
505 | flex-direction: column;
506 | align-items: center;
507 | }
508 | .App .Result,
509 | .App .Result textarea,
510 | .App .Table{
511 | width: 100%;
512 | }
513 | .App .Options{
514 | overflow-x: auto;
515 | width: 100%;
516 | }
517 | .Content{
518 | flex-direction: column;
519 | max-width: 100%;
520 | }
521 | .App .Result .TextareaIndent{
522 | width: 100%;
523 | }
524 | .App .Actions,
525 | .App .Csv{
526 | overflow-x: auto;
527 | width: 100%;
528 | }
529 | .App .Buttons{
530 | flex-direction: row;
531 | margin-bottom: 50px;
532 | margin-top: 50px;
533 | padding: 20px;
534 | }
535 | .Footer{
536 | flex-direction: column;
537 | padding: 20px;
538 | }
539 | .Changelog{
540 | padding: 20px;
541 | }
542 | .Multiple{
543 | padding: 20px;
544 | }
545 | }
--------------------------------------------------------------------------------
/src/Tests/App.test.js:
--------------------------------------------------------------------------------
1 | import { render, screen, fireEvent, waitFor } from '@testing-library/react';
2 | import { configure } from '@testing-library/dom';
3 | import App from '../Components/App';
4 | import firebase, { environment } from '../Functions/Firebase';
5 | import { transformToJSON, validateCSV, validateJSON } from '../CLI/utils';
6 | import '@testing-library/jest-dom';
7 |
8 | configure({ asyncUtilTimeout: 5000 });
9 |
10 | beforeEach(() => {
11 |
12 | jest.spyOn(firebase, 'firestore').mockImplementation(() => ({
13 |
14 | collection: jest.fn().mockReturnThis(),
15 | doc: jest.fn().mockReturnThis(),
16 | set: jest.fn().mockReturnThis()
17 |
18 | }));
19 |
20 | });
21 |
22 | test('Paste CSV displays table correctly', async () => {
23 |
24 | let csv = [
25 |
26 | ['key', 'road', 'coord.lat', 'coord.lng', 'elem'],
27 | ['1', 'C-58', 42.02, 2.82, '🦄'],
28 | ['2', 'C-32', 41.35, 2.09, '🦧'],
29 | ['3', 'B-20', 41.44, 2.18, '🐰']
30 |
31 | ].map(e => e.join(`\t`)).join(`\n`);
32 |
33 | Object.assign(navigator, {
34 | clipboard: {
35 | readText: () => csv
36 | }
37 | });
38 |
39 | await render( );
40 |
41 | fireEvent.click(screen.getByDisplayValue('key'));
42 |
43 | await waitFor(() => expect(document.getElementById('00')).toHaveClass('Selected'));
44 |
45 | document.dispatchEvent(
46 | new KeyboardEvent("keydown", {
47 | key: "v",
48 | ctrlKey: true,
49 | bubbles: true,
50 | metaKey: true
51 | })
52 | );
53 |
54 | await waitFor(() => expect(screen.getByDisplayValue('C-58')).toBeInTheDocument());
55 |
56 |
57 | });
58 |
59 | test('Duplicate key displays an error', async () => {
60 |
61 | let csv = [
62 |
63 | ['key', 'road', 'coord.lat', 'coord.lng', 'elem'],
64 | ['1', 'C-58', 42.02, 2.82, '🦄'],
65 | ['1', 'C-32', 41.35, 2.09, '🦧'],
66 | ['3', 'B-20', 41.44, 2.18, '🐰']
67 |
68 | ].map(e => e.join(`\t`)).join(`\n`);
69 |
70 | Object.assign(navigator, {
71 | clipboard: {
72 | readText: () => csv
73 | }
74 | });
75 |
76 | await render( );
77 |
78 | fireEvent.click(screen.getByDisplayValue('key'));
79 |
80 | await waitFor(() => expect(document.getElementById('00')).toHaveClass('Selected'));
81 |
82 | document.dispatchEvent(
83 | new KeyboardEvent("keydown", {
84 | key: "v",
85 | ctrlKey: true,
86 | bubbles: true,
87 | metaKey: true
88 | })
89 | );
90 |
91 | await waitFor(() => expect(screen.getByDisplayValue('C-58')).toBeInTheDocument());
92 |
93 | fireEvent.click(screen.getByText('Generate JSON'));
94 |
95 | await waitFor(() => expect(screen.getByText('JSON has duplicated keys', {exact: false})).toBeInTheDocument());
96 |
97 | });
98 |
99 | test('Transforms a CSV into a JSON correctly', async () => {
100 |
101 | let csv = [
102 |
103 | ['key', 'road', 'coord.lat', 'coord.lng', 'elem'],
104 | ['1', 'AP-7', 42.02, 2.82, '🦄'],
105 | ['2', 'C-32', 41.35, 2.09, '🦧'],
106 | ['3', 'B-20', 41.44, 2.18, '🐰'],
107 | ['4', 'AP-7', 41.42, 2.10, '🦊']
108 |
109 | ].map(e => e.join(`\t`)).join(`\n`);
110 |
111 | let json = {
112 |
113 | "1": {
114 | "road": "AP-7",
115 | "coord": {
116 | "lat": 42.02,
117 | "lng": 2.82
118 | },
119 | "elem": "🦄"
120 | },
121 | "2": {
122 | "road": "C-32",
123 | "coord": {
124 | "lat": 41.35,
125 | "lng": 2.09
126 | },
127 | "elem": "🦧"
128 | },
129 | "3": {
130 | "road": "B-20",
131 | "coord": {
132 | "lat": 41.44,
133 | "lng": 2.18
134 | },
135 | "elem": "🐰"
136 | }
137 | ,
138 | "4": {
139 | "road": "AP-7",
140 | "coord": {
141 | "lat": 41.42,
142 | "lng": 2.10
143 | },
144 | "elem": "🦊"
145 | }
146 |
147 | };
148 |
149 | Object.assign(navigator, {
150 | clipboard: {
151 | readText: () => csv
152 | }
153 | });
154 |
155 | await render( );
156 |
157 | fireEvent.click(screen.getByDisplayValue('key'));
158 |
159 | await waitFor(() => expect(document.getElementById('00')).toHaveClass('Selected'));
160 |
161 | document.dispatchEvent(
162 | new KeyboardEvent("keydown", {
163 | key: "v",
164 | ctrlKey: true,
165 | bubbles: true,
166 | metaKey: true
167 | })
168 | );
169 |
170 | await waitFor(() => expect(screen.getByDisplayValue('AP-7')).toBeInTheDocument());
171 |
172 | fireEvent.click(screen.getByText('Generate JSON'));
173 |
174 | await waitFor(() => expect(screen.getByText('Wait a few seconds...', {exact: false})).toBeInTheDocument());
175 |
176 | fireEvent.click(screen.getByText('Uglify'));
177 |
178 | await waitFor(() => expect(screen.getByText(JSON.stringify(json), {exact: false})).toBeInTheDocument());
179 |
180 | });
181 |
182 | test('Copies JSON into the clipboard', async () => {
183 |
184 | let csv = [
185 |
186 | ['key', 'road', 'coord.lat', 'coord.lng', 'elem'],
187 | ['1', 'AP-7', 42.02, 2.82, '🦄'],
188 | ['2', 'C-32', 41.35, 2.09, '🦧'],
189 | ['3', 'B-20', 41.44, 2.18, '🐰'],
190 | ['4', 'AP-7', 41.42, 2.10, '🦊']
191 |
192 | ].map(e => e.join(`\t`)).join(`\n`);
193 |
194 | let json = {
195 |
196 | "1": {
197 | "road": "AP-7",
198 | "coord": {
199 | "lat": 42.02,
200 | "lng": 2.82
201 | },
202 | "elem": "🦄"
203 | },
204 | "2": {
205 | "road": "C-32",
206 | "coord": {
207 | "lat": 41.35,
208 | "lng": 2.09
209 | },
210 | "elem": "🦧"
211 | },
212 | "3": {
213 | "road": "B-20",
214 | "coord": {
215 | "lat": 41.44,
216 | "lng": 2.18
217 | },
218 | "elem": "🐰"
219 | }
220 | ,
221 | "4": {
222 | "road": "AP-7",
223 | "coord": {
224 | "lat": 41.42,
225 | "lng": 2.10
226 | },
227 | "elem": "🦊"
228 | }
229 |
230 | };
231 |
232 | Object.assign(navigator, {
233 | clipboard: {
234 | readText: () => csv,
235 | writeText: () => json
236 | }
237 | });
238 |
239 | await render( );
240 |
241 | fireEvent.click(screen.getByDisplayValue('key'));
242 |
243 | await waitFor(() => expect(document.getElementById('00')).toHaveClass('Selected'));
244 |
245 | document.dispatchEvent(
246 | new KeyboardEvent("keydown", {
247 | key: "v",
248 | ctrlKey: true,
249 | bubbles: true,
250 | metaKey: true
251 | })
252 | );
253 |
254 | await waitFor(() => expect(screen.getByDisplayValue('AP-7')).toBeInTheDocument());
255 |
256 | fireEvent.click(screen.getByText('Generate JSON'));
257 |
258 | await waitFor(() => expect(screen.getByText('Wait a few seconds...', {exact: false})).toBeInTheDocument());
259 |
260 | fireEvent.click(screen.getByText('Uglify'));
261 |
262 | await waitFor(() => expect(screen.getByText(JSON.stringify(json), {exact: false})).toBeInTheDocument());
263 |
264 | fireEvent.click(screen.getByText('Copy JSON'));
265 |
266 | await waitFor(() => expect(screen.getByText('Copied', {exact: false})).toBeInTheDocument());
267 |
268 | });
269 |
270 | test('Copies CSV into the clipboad', async () => {
271 |
272 | let csv = [
273 |
274 | ['key', 'road', 'coord.lat', 'coord.lng', 'elem'],
275 | ['1', 'AP-7', 42.02, 2.82, '🦄'],
276 | ['2', 'C-32', 41.35, 2.09, '🦧'],
277 | ['3', 'B-20', 41.44, 2.18, '🐰'],
278 | ['4', 'AP-7', 41.42, 2.10, '🦊']
279 |
280 | ].map(e => e.join(`\t`)).join(`\n`);
281 |
282 | await render( );
283 |
284 | fireEvent.click(screen.getByDisplayValue('key'));
285 |
286 | await waitFor(() => expect(document.getElementById('00')).toHaveClass('Selected'));
287 |
288 | document.dispatchEvent(
289 | new KeyboardEvent("keydown", {
290 | key: "v",
291 | ctrlKey: true,
292 | bubbles: true,
293 | metaKey: true
294 | })
295 | );
296 |
297 | await waitFor(() => expect(screen.getByDisplayValue('AP-7')).toBeInTheDocument());
298 |
299 | fireEvent.click(screen.getByText('Copy CSV'));
300 |
301 | await waitFor(() => expect(screen.getByText('Copied', {exact: false})).toBeInTheDocument());
302 |
303 | })
304 |
305 | test('Downloads the JSON', async () => {
306 |
307 | window.URL.createObjectURL = jest.fn();
308 |
309 | await render( );
310 |
311 | fireEvent.click(screen.getByText('Download JSON'));
312 |
313 | await waitFor(() => expect(screen.getByText('Downloading', {exact: false})).toBeInTheDocument());
314 |
315 | });
316 |
317 | test('Downloads the CSV', async () => {
318 |
319 | window.URL.createObjectURL = jest.fn();
320 |
321 | await render( );
322 |
323 | fireEvent.click(screen.getByText('Download CSV'));
324 |
325 | await waitFor(() => expect(screen.getByText('Downloading', {exact: false})).toBeInTheDocument());
326 |
327 | });
328 |
329 | test('First row should not be empty', async () => {
330 |
331 | let csv = [
332 |
333 | ['', '', '', '', ''],
334 | ['test.1', 'AP-7', 42.02, 2.82, '🦄'],
335 | ['test.2', 'C-32', 41.35, 2.09, '🦧'],
336 | ['test.3', 'B-20', 41.44, 2.18, '🐰'],
337 |
338 | ];
339 |
340 | let res = validateCSV(csv);
341 |
342 | expect(res).toStrictEqual({error: `First row shouldn't be empty`});
343 |
344 | });
345 |
346 | test('#1 CSV -> JSON', async () => {
347 |
348 | let csv = [
349 |
350 | ['key', 'road', 'coord.lat', 'coord.lng', 'elem'],
351 | ['test.1', 'AP-7', 42.02, 2.82, '🦄'],
352 | ['test.2', 'C-32', 41.35, 2.09, '🦧'],
353 | ['test.3', 'B-20', 41.44, 2.18, '🐰'],
354 |
355 | ];
356 |
357 | let res = transformToJSON(csv);
358 |
359 | expect(res).toStrictEqual({
360 | "test.1": {
361 | "road": "AP-7",
362 | "coord": {
363 | "lat": 42.02,
364 | "lng": 2.82
365 | },
366 | "elem": "🦄"
367 | },
368 | "test.2": {
369 | "road": "C-32",
370 | "coord": {
371 | "lat": 41.35,
372 | "lng": 2.09
373 | },
374 | "elem": "🦧"
375 | },
376 | "test.3": {
377 | "road": "B-20",
378 | "coord": {
379 | "lat": 41.44,
380 | "lng": 2.18
381 | },
382 | "elem": "🐰"
383 | }
384 | });
385 |
386 | });
387 |
388 | test('App is in PRO', async () => {
389 |
390 | expect(environment).toBe('PRO');
391 |
392 | });
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import { Switch, Route } from 'react-router-dom';
4 | import { BrowserRouter } from 'react-router-dom';
5 | import App from './Components/App';
6 | import Changelog from './Components/Changelog';
7 | import MultipleReplaces from './Components/MultipleReplaces';
8 | import MultipleDeletions from './Components/MultipleDeletions';
9 | import MultipleMerge from './Components/MultipleMerge';
10 | import Footer from './Components/Footer';
11 | import Seo from './Components/Seo';
12 | import './Styles/index.css';
13 |
14 | ReactDOM.render(
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 | , document.getElementById('root'));
--------------------------------------------------------------------------------