diff --git a/package-lock.json b/package-lock.json
index deebac5..6d180fc 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -10,13 +10,16 @@
"license": "BSD-3-Clause",
"dependencies": {
"@ethersphere/bee-js": "3.0.0",
+ "@ethersphere/manifest-js": "^1.0.0",
"@material-ui/core": "4.12.3",
"@material-ui/icons": "4.11.2",
"@material-ui/lab": "4.0.0-alpha.57",
"axios": "0.24.0",
"bignumber.js": "9.0.1",
+ "file-saver": "^2.0.5",
"formik": "2.2.9",
"formik-material-ui": "3.0.1",
+ "jszip": "^3.7.1",
"material-ui-dropzone": "3.5.0",
"notistack": "1.0.10",
"opener": "1.5.2",
@@ -26,6 +29,7 @@
"react-dom": "17.0.2",
"react-feather": "2.0.9",
"react-identicons": "1.2.5",
+ "react-router": "5.2.0",
"react-router-dom": "5.2.0",
"react-syntax-highlighter": "15.4.4",
"semver": "7.3.5",
@@ -38,6 +42,7 @@
"@commitlint/config-conventional": "14.1.0",
"@testing-library/jest-dom": "5.15.0",
"@testing-library/react": "12.1.2",
+ "@types/file-saver": "^2.0.4",
"@types/jest": "27.0.2",
"@types/qrcode.react": "1.0.2",
"@types/react": "17.0.34",
@@ -2174,6 +2179,14 @@
"node": ">= 6"
}
},
+ "node_modules/@ethersphere/manifest-js": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/@ethersphere/manifest-js/-/manifest-js-1.0.0.tgz",
+ "integrity": "sha512-ioZH4nxicrCktkA8lvr/82nuqaZt+Itv4TmblvLc0Irz7E8io2cdAHMeVBC636tIvU7pWyP6wYdFXl4wvnFdBQ==",
+ "dependencies": {
+ "mantaray-js": "^1.0.3"
+ }
+ },
"node_modules/@gar/promisify": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@gar/promisify/-/promisify-1.1.2.tgz",
@@ -3779,6 +3792,12 @@
"integrity": "sha512-C6N5s2ZFtuZRj54k2/zyRhNDjJwwcViAM3Nbm8zjBpbqAdZ00mr0CFxvSKeO8Y/e03WVFLpQMdHYVfUd6SB+Hw==",
"dev": true
},
+ "node_modules/@types/file-saver": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/@types/file-saver/-/file-saver-2.0.4.tgz",
+ "integrity": "sha512-sPZYQEIF/SOnLAvaz9lTuydniP+afBMtElRTdYkeV1QtEgvtJ7qolCPjly6O32QI8CbEmP5O/fztMXEDWfEcrg==",
+ "dev": true
+ },
"node_modules/@types/glob": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.2.0.tgz",
@@ -7128,8 +7147,7 @@
"node_modules/core-util-is": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz",
- "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==",
- "dev": true
+ "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ=="
},
"node_modules/cosmiconfig": {
"version": "7.0.1",
@@ -8534,6 +8552,11 @@
"url": "https://github.com/cheeriojs/dom-serializer?sponsor=1"
}
},
+ "node_modules/dom-walk": {
+ "version": "0.1.2",
+ "resolved": "https://registry.npmjs.org/dom-walk/-/dom-walk-0.1.2.tgz",
+ "integrity": "sha512-6QvTW9mrGeIegrFXdtQi9pk7O/nSK6lSdXW2eqUspN5LWD7UTji2Fqw5V2YLjBpHEoU9Xl/eUWNpDeZvoyOv2w=="
+ },
"node_modules/domain-browser": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/domain-browser/-/domain-browser-1.2.0.tgz",
@@ -10380,6 +10403,11 @@
"url": "https://opencollective.com/webpack"
}
},
+ "node_modules/file-saver": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/file-saver/-/file-saver-2.0.5.tgz",
+ "integrity": "sha512-P9bmyZ3h/PRG+Nzga+rbdI4OEpNDzAVyy74uVO9ATgzLK6VtAsYybF/+TOCvrc0MO793d6+42lLyZTw7/ArVzA=="
+ },
"node_modules/file-selector": {
"version": "0.1.19",
"resolved": "https://registry.npmjs.org/file-selector/-/file-selector-0.1.19.tgz",
@@ -11063,6 +11091,17 @@
"node": ">=8.0.0"
}
},
+ "node_modules/get-random-values": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/get-random-values/-/get-random-values-1.2.2.tgz",
+ "integrity": "sha512-lMyPjQyl0cNNdDf2oR+IQ/fM3itDvpoHy45Ymo2r0L1EjazeSl13SfbKZs7KtZ/3MDCeueiaJiuOEfKqRTsSgA==",
+ "dependencies": {
+ "global": "^4.4.0"
+ },
+ "engines": {
+ "node": "10 || 12 || >=14"
+ }
+ },
"node_modules/get-stream": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz",
@@ -11135,6 +11174,15 @@
"node": ">= 6"
}
},
+ "node_modules/global": {
+ "version": "4.4.0",
+ "resolved": "https://registry.npmjs.org/global/-/global-4.4.0.tgz",
+ "integrity": "sha512-wv/LAoHdRE3BeTGz53FAamhGlPLhlssK45usmGFThIi4XqnBmjKQ16u+RNbP7WvigRZDxUsM0J3gcQ5yicaL0w==",
+ "dependencies": {
+ "min-document": "^2.19.0",
+ "process": "^0.11.10"
+ }
+ },
"node_modules/global-modules": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/global-modules/-/global-modules-2.0.0.tgz",
@@ -11996,6 +12044,11 @@
"node": ">= 4"
}
},
+ "node_modules/immediate": {
+ "version": "3.0.6",
+ "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz",
+ "integrity": "sha1-nbHb0Pr43m++D13V5Wu2BigN5ps="
+ },
"node_modules/immer": {
"version": "8.0.1",
"resolved": "https://registry.npmjs.org/immer/-/immer-8.0.1.tgz",
@@ -15285,6 +15338,17 @@
"node": ">=4.0"
}
},
+ "node_modules/jszip": {
+ "version": "3.7.1",
+ "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.7.1.tgz",
+ "integrity": "sha512-ghL0tz1XG9ZEmRMcEN2vt7xabrDdqHHeykgARpmZ0BiIctWxM47Vt63ZO2dnp4QYt/xJVLLy5Zv1l/xRdh2byg==",
+ "dependencies": {
+ "lie": "~3.3.0",
+ "pako": "~1.0.2",
+ "readable-stream": "~2.3.6",
+ "set-immediate-shim": "~1.0.1"
+ }
+ },
"node_modules/killable": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/killable/-/killable-1.0.1.tgz",
@@ -15400,6 +15464,14 @@
"node": ">= 0.8.0"
}
},
+ "node_modules/lie": {
+ "version": "3.3.0",
+ "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz",
+ "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==",
+ "dependencies": {
+ "immediate": "~3.0.5"
+ }
+ },
"node_modules/lines-and-columns": {
"version": "1.1.6",
"resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.1.6.tgz",
@@ -15613,6 +15685,15 @@
"tmpl": "1.0.5"
}
},
+ "node_modules/mantaray-js": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/mantaray-js/-/mantaray-js-1.0.3.tgz",
+ "integrity": "sha512-ZMQCbrwuFOArtdYKvNd/oS/AnqhvCbldm+UCWQ+HH3Osna1SYBPZcnZpFhSwoNheamNBkhuAoOl9gI8saVRZqg==",
+ "dependencies": {
+ "get-random-values": "^1.2.2",
+ "js-sha3": "^0.8.0"
+ }
+ },
"node_modules/map-cache": {
"version": "0.2.2",
"resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz",
@@ -15795,6 +15876,14 @@
"node": ">=6"
}
},
+ "node_modules/min-document": {
+ "version": "2.19.0",
+ "resolved": "https://registry.npmjs.org/min-document/-/min-document-2.19.0.tgz",
+ "integrity": "sha1-e9KC4/WELtKVu3SM3Z8f+iyCRoU=",
+ "dependencies": {
+ "dom-walk": "^0.1.0"
+ }
+ },
"node_modules/min-indent": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz",
@@ -16851,8 +16940,7 @@
"node_modules/pako": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz",
- "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==",
- "dev": true
+ "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw=="
},
"node_modules/parallel-transform": {
"version": "1.2.0",
@@ -20722,7 +20810,6 @@
"version": "0.11.10",
"resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz",
"integrity": "sha1-czIwDoQBYb2j5podHZGn1LwW8YI=",
- "dev": true,
"engines": {
"node": ">= 0.6.0"
}
@@ -20730,8 +20817,7 @@
"node_modules/process-nextick-args": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
- "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==",
- "dev": true
+ "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag=="
},
"node_modules/progress": {
"version": "2.0.3",
@@ -21755,7 +21841,6 @@
"version": "2.3.7",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz",
"integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==",
- "dev": true,
"dependencies": {
"core-util-is": "~1.0.0",
"inherits": "~2.0.3",
@@ -21769,14 +21854,12 @@
"node_modules/readable-stream/node_modules/isarray": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
- "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=",
- "dev": true
+ "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE="
},
"node_modules/readable-stream/node_modules/string_decoder": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
- "dev": true,
"dependencies": {
"safe-buffer": "~5.1.0"
}
@@ -23227,6 +23310,14 @@
"integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=",
"dev": true
},
+ "node_modules/set-immediate-shim": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/set-immediate-shim/-/set-immediate-shim-1.0.1.tgz",
+ "integrity": "sha1-SysbJ+uAip+NzEgaWOXlb1mfP2E=",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
"node_modules/set-value": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.1.tgz",
@@ -29222,6 +29313,14 @@
}
}
},
+ "@ethersphere/manifest-js": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/@ethersphere/manifest-js/-/manifest-js-1.0.0.tgz",
+ "integrity": "sha512-ioZH4nxicrCktkA8lvr/82nuqaZt+Itv4TmblvLc0Irz7E8io2cdAHMeVBC636tIvU7pWyP6wYdFXl4wvnFdBQ==",
+ "requires": {
+ "mantaray-js": "^1.0.3"
+ }
+ },
"@gar/promisify": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@gar/promisify/-/promisify-1.1.2.tgz",
@@ -30397,6 +30496,12 @@
"integrity": "sha512-C6N5s2ZFtuZRj54k2/zyRhNDjJwwcViAM3Nbm8zjBpbqAdZ00mr0CFxvSKeO8Y/e03WVFLpQMdHYVfUd6SB+Hw==",
"dev": true
},
+ "@types/file-saver": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/@types/file-saver/-/file-saver-2.0.4.tgz",
+ "integrity": "sha512-sPZYQEIF/SOnLAvaz9lTuydniP+afBMtElRTdYkeV1QtEgvtJ7qolCPjly6O32QI8CbEmP5O/fztMXEDWfEcrg==",
+ "dev": true
+ },
"@types/glob": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.2.0.tgz",
@@ -33188,8 +33293,7 @@
"core-util-is": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz",
- "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==",
- "dev": true
+ "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ=="
},
"cosmiconfig": {
"version": "7.0.1",
@@ -34307,6 +34411,11 @@
"entities": "^2.0.0"
}
},
+ "dom-walk": {
+ "version": "0.1.2",
+ "resolved": "https://registry.npmjs.org/dom-walk/-/dom-walk-0.1.2.tgz",
+ "integrity": "sha512-6QvTW9mrGeIegrFXdtQi9pk7O/nSK6lSdXW2eqUspN5LWD7UTji2Fqw5V2YLjBpHEoU9Xl/eUWNpDeZvoyOv2w=="
+ },
"domain-browser": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/domain-browser/-/domain-browser-1.2.0.tgz",
@@ -35715,6 +35824,11 @@
}
}
},
+ "file-saver": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/file-saver/-/file-saver-2.0.5.tgz",
+ "integrity": "sha512-P9bmyZ3h/PRG+Nzga+rbdI4OEpNDzAVyy74uVO9ATgzLK6VtAsYybF/+TOCvrc0MO793d6+42lLyZTw7/ArVzA=="
+ },
"file-selector": {
"version": "0.1.19",
"resolved": "https://registry.npmjs.org/file-selector/-/file-selector-0.1.19.tgz",
@@ -36249,6 +36363,14 @@
"integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==",
"dev": true
},
+ "get-random-values": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/get-random-values/-/get-random-values-1.2.2.tgz",
+ "integrity": "sha512-lMyPjQyl0cNNdDf2oR+IQ/fM3itDvpoHy45Ymo2r0L1EjazeSl13SfbKZs7KtZ/3MDCeueiaJiuOEfKqRTsSgA==",
+ "requires": {
+ "global": "^4.4.0"
+ }
+ },
"get-stream": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz",
@@ -36297,6 +36419,15 @@
"is-glob": "^4.0.1"
}
},
+ "global": {
+ "version": "4.4.0",
+ "resolved": "https://registry.npmjs.org/global/-/global-4.4.0.tgz",
+ "integrity": "sha512-wv/LAoHdRE3BeTGz53FAamhGlPLhlssK45usmGFThIi4XqnBmjKQ16u+RNbP7WvigRZDxUsM0J3gcQ5yicaL0w==",
+ "requires": {
+ "min-document": "^2.19.0",
+ "process": "^0.11.10"
+ }
+ },
"global-modules": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/global-modules/-/global-modules-2.0.0.tgz",
@@ -36979,6 +37110,11 @@
"integrity": "sha512-2zeMQpbKz5dhZ9IwL0gbxSW5w0NK/MSAMtNuhgIHEPmaU3vPdKPL0UdvUCXs5SS4JAwsBxysK5sFMW8ocFiVjQ==",
"dev": true
},
+ "immediate": {
+ "version": "3.0.6",
+ "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz",
+ "integrity": "sha1-nbHb0Pr43m++D13V5Wu2BigN5ps="
+ },
"immer": {
"version": "8.0.1",
"resolved": "https://registry.npmjs.org/immer/-/immer-8.0.1.tgz",
@@ -39561,6 +39697,17 @@
"object.assign": "^4.1.2"
}
},
+ "jszip": {
+ "version": "3.7.1",
+ "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.7.1.tgz",
+ "integrity": "sha512-ghL0tz1XG9ZEmRMcEN2vt7xabrDdqHHeykgARpmZ0BiIctWxM47Vt63ZO2dnp4QYt/xJVLLy5Zv1l/xRdh2byg==",
+ "requires": {
+ "lie": "~3.3.0",
+ "pako": "~1.0.2",
+ "readable-stream": "~2.3.6",
+ "set-immediate-shim": "~1.0.1"
+ }
+ },
"killable": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/killable/-/killable-1.0.1.tgz",
@@ -39640,6 +39787,14 @@
"type-check": "~0.4.0"
}
},
+ "lie": {
+ "version": "3.3.0",
+ "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz",
+ "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==",
+ "requires": {
+ "immediate": "~3.0.5"
+ }
+ },
"lines-and-columns": {
"version": "1.1.6",
"resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.1.6.tgz",
@@ -39822,6 +39977,15 @@
"tmpl": "1.0.5"
}
},
+ "mantaray-js": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/mantaray-js/-/mantaray-js-1.0.3.tgz",
+ "integrity": "sha512-ZMQCbrwuFOArtdYKvNd/oS/AnqhvCbldm+UCWQ+HH3Osna1SYBPZcnZpFhSwoNheamNBkhuAoOl9gI8saVRZqg==",
+ "requires": {
+ "get-random-values": "^1.2.2",
+ "js-sha3": "^0.8.0"
+ }
+ },
"map-cache": {
"version": "0.2.2",
"resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz",
@@ -39957,6 +40121,14 @@
"integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==",
"dev": true
},
+ "min-document": {
+ "version": "2.19.0",
+ "resolved": "https://registry.npmjs.org/min-document/-/min-document-2.19.0.tgz",
+ "integrity": "sha1-e9KC4/WELtKVu3SM3Z8f+iyCRoU=",
+ "requires": {
+ "dom-walk": "^0.1.0"
+ }
+ },
"min-indent": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz",
@@ -40776,8 +40948,7 @@
"pako": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz",
- "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==",
- "dev": true
+ "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw=="
},
"parallel-transform": {
"version": "1.2.0",
@@ -43771,14 +43942,12 @@
"process": {
"version": "0.11.10",
"resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz",
- "integrity": "sha1-czIwDoQBYb2j5podHZGn1LwW8YI=",
- "dev": true
+ "integrity": "sha1-czIwDoQBYb2j5podHZGn1LwW8YI="
},
"process-nextick-args": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
- "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==",
- "dev": true
+ "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag=="
},
"progress": {
"version": "2.0.3",
@@ -44593,7 +44762,6 @@
"version": "2.3.7",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz",
"integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==",
- "dev": true,
"requires": {
"core-util-is": "~1.0.0",
"inherits": "~2.0.3",
@@ -44607,14 +44775,12 @@
"isarray": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
- "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=",
- "dev": true
+ "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE="
},
"string_decoder": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
- "dev": true,
"requires": {
"safe-buffer": "~5.1.0"
}
@@ -45784,6 +45950,11 @@
"integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=",
"dev": true
},
+ "set-immediate-shim": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/set-immediate-shim/-/set-immediate-shim-1.0.1.tgz",
+ "integrity": "sha1-SysbJ+uAip+NzEgaWOXlb1mfP2E="
+ },
"set-value": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.1.tgz",
diff --git a/package.json b/package.json
index c6234cf..59ca640 100644
--- a/package.json
+++ b/package.json
@@ -25,13 +25,16 @@
},
"dependencies": {
"@ethersphere/bee-js": "3.0.0",
+ "@ethersphere/manifest-js": "^1.0.0",
"@material-ui/core": "4.12.3",
"@material-ui/icons": "4.11.2",
"@material-ui/lab": "4.0.0-alpha.57",
"axios": "0.24.0",
"bignumber.js": "9.0.1",
+ "file-saver": "^2.0.5",
"formik": "2.2.9",
"formik-material-ui": "3.0.1",
+ "jszip": "^3.7.1",
"material-ui-dropzone": "3.5.0",
"notistack": "1.0.10",
"opener": "1.5.2",
@@ -41,6 +44,7 @@
"react-dom": "17.0.2",
"react-feather": "2.0.9",
"react-identicons": "1.2.5",
+ "react-router": "5.2.0",
"react-router-dom": "5.2.0",
"react-syntax-highlighter": "15.4.4",
"semver": "7.3.5",
@@ -50,6 +54,7 @@
"@commitlint/config-conventional": "14.1.0",
"@testing-library/jest-dom": "5.15.0",
"@testing-library/react": "12.1.2",
+ "@types/file-saver": "^2.0.4",
"@types/jest": "27.0.2",
"@types/qrcode.react": "1.0.2",
"@types/react": "17.0.34",
diff --git a/src/App.tsx b/src/App.tsx
index bb988d6..00d9a14 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -1,18 +1,17 @@
+import CssBaseline from '@material-ui/core/CssBaseline'
+import { ThemeProvider } from '@material-ui/core/styles'
+import { SnackbarProvider } from 'notistack'
import { ReactElement } from 'react'
import { BrowserRouter as Router } from 'react-router-dom'
import './App.css'
-
-import { ThemeProvider } from '@material-ui/core/styles'
-import CssBaseline from '@material-ui/core/CssBaseline'
-import { SnackbarProvider } from 'notistack'
-
-import BaseRouter from './routes'
import Dashboard from './layout/Dashboard'
-import { theme } from './theme'
-import { Provider as StampsProvider } from './providers/Stamps'
-import { Provider as PlatformProvider } from './providers/Platform'
import { Provider as BeeProvider } from './providers/Bee'
+import { Provider as FileProvider } from './providers/File'
+import { Provider as PlatformProvider } from './providers/Platform'
import { Provider as SettingsProvider } from './providers/Settings'
+import { Provider as StampsProvider } from './providers/Stamps'
+import BaseRouter from './routes'
+import { theme } from './theme'
const App = (): ReactElement => (
@@ -20,18 +19,20 @@ const App = (): ReactElement => (
-
-
-
- <>
-
-
-
-
- >
-
-
-
+
+
+
+
+ <>
+
+
+
+
+ >
+
+
+
+
diff --git a/src/components/ExpandableListItemLink.tsx b/src/components/ExpandableListItemLink.tsx
index 706472f..5c46679 100644
--- a/src/components/ExpandableListItemLink.tsx
+++ b/src/components/ExpandableListItemLink.tsx
@@ -1,8 +1,9 @@
import { Grid, IconButton, ListItem, Tooltip, Typography } from '@material-ui/core'
import { createStyles, makeStyles, Theme } from '@material-ui/core/styles'
-import { OpenInNewSharp } from '@material-ui/icons'
+import { ArrowForward, OpenInNewSharp } from '@material-ui/icons'
import { ReactElement, useState } from 'react'
import CopyToClipboard from 'react-copy-to-clipboard'
+import { useHistory } from 'react-router'
const useStyles = makeStyles((theme: Theme) =>
createStyles({
@@ -46,15 +47,35 @@ const useStyles = makeStyles((theme: Theme) =>
interface Props {
label: string
value: string
+ link?: string
+ navigationType?: 'NEW_WINDOW' | 'HISTORY_PUSH'
+ allowClipboard?: boolean
}
-export default function ExpandableListItemLink({ label, value }: Props): ReactElement | null {
+export default function ExpandableListItemLink({
+ label,
+ value,
+ link,
+ navigationType = 'NEW_WINDOW',
+ allowClipboard = true,
+}: Props): ReactElement | null {
const classes = useStyles()
const [copied, setCopied] = useState(false)
+ const history = useHistory()
const tooltipClickHandler = () => setCopied(true)
const tooltipCloseHandler = () => setCopied(false)
+ const displayValue = value.length > 22 ? value.slice(0, 19) + '...' : value
+
+ function onNavigation() {
+ if (navigationType === 'NEW_WINDOW') {
+ window.open(link || value)
+ } else {
+ history.push(link || value)
+ }
+ }
+
return (
@@ -62,15 +83,19 @@ export default function ExpandableListItemLink({ label, value }: Props): ReactEl
{label && {label}}
-
-
-
- {value.slice(0, 19)}...
-
-
-
+ {allowClipboard && (
+
+
+
+ {displayValue}
+
+
+
+ )}
+ {!allowClipboard &&
{displayValue}}
- window.open(value)} strokeWidth={1} />
+ {navigationType === 'NEW_WINDOW' && }
+ {navigationType === 'HISTORY_PUSH' && }
diff --git a/src/components/History.tsx b/src/components/History.tsx
new file mode 100644
index 0000000..e1236c2
--- /dev/null
+++ b/src/components/History.tsx
@@ -0,0 +1,37 @@
+import { ReactElement, useEffect, useState } from 'react'
+import { getPrettyDateString } from '../utils/date'
+import { getHistorySafe, HistoryItem, HISTORY_KEYS } from '../utils/local-storage'
+import ExpandableList from './ExpandableList'
+import ExpandableListItemLink from './ExpandableListItemLink'
+
+interface Props {
+ title: string
+ localStorageKey: HISTORY_KEYS
+}
+
+export function History({ title, localStorageKey }: Props): ReactElement | null {
+ const [items, setItems] = useState([])
+
+ useEffect(() => {
+ setItems(getHistorySafe(localStorageKey))
+ }, [localStorageKey])
+
+ if (!items.length) {
+ return null
+ }
+
+ return (
+
+ {items.map((x, i) => (
+
+ ))}
+
+ )
+}
diff --git a/src/components/HistoryHeader.tsx b/src/components/HistoryHeader.tsx
new file mode 100644
index 0000000..0f32c49
--- /dev/null
+++ b/src/components/HistoryHeader.tsx
@@ -0,0 +1,41 @@
+import { Box, createStyles, Grid, makeStyles, Typography } from '@material-ui/core'
+import { ArrowBack } from '@material-ui/icons'
+import { ReactElement } from 'react'
+import { useHistory } from 'react-router-dom'
+
+interface Props {
+ children: string
+}
+
+const useStyles = makeStyles(() =>
+ createStyles({
+ pressable: {
+ cursor: 'pointer',
+ },
+ icon: {
+ color: '#242424',
+ },
+ }),
+)
+
+export function HistoryHeader({ children }: Props): ReactElement {
+ const classes = useStyles()
+ const history = useHistory()
+
+ function goBack() {
+ history.goBack()
+ }
+
+ return (
+
+
+
+
+
+ {children}
+
+
+ )
+}
diff --git a/src/components/Loading.tsx b/src/components/Loading.tsx
new file mode 100644
index 0000000..e940da0
--- /dev/null
+++ b/src/components/Loading.tsx
@@ -0,0 +1,10 @@
+import { CircularProgress, Grid } from '@material-ui/core'
+import { ReactElement } from 'react'
+
+export function Loading(): ReactElement {
+ return (
+
+
+
+ )
+}
diff --git a/src/components/SideBar.tsx b/src/components/SideBar.tsx
index fc390d4..ff88822 100644
--- a/src/components/SideBar.tsx
+++ b/src/components/SideBar.tsx
@@ -1,16 +1,14 @@
-import type { ReactElement } from 'react'
-import { Link } from 'react-router-dom'
-
-import { createStyles, Theme, makeStyles } from '@material-ui/core/styles'
+import { Divider, Drawer, Grid, Link as MUILink, List } from '@material-ui/core'
+import { createStyles, makeStyles, Theme } from '@material-ui/core/styles'
import { OpenInNewSharp } from '@material-ui/icons'
-import { Divider, List, Drawer, Grid, Link as MUILink } from '@material-ui/core'
-import { Home, FileText, DollarSign, Settings, Layers, BookOpen } from 'react-feather'
+import type { ReactElement } from 'react'
+import { BookOpen, DollarSign, FileText, Home, Layers, Settings } from 'react-feather'
+import { Link } from 'react-router-dom'
+import Logo from '../assets/logo.svg'
import { ROUTES } from '../routes'
import SideBarItem from './SideBarItem'
import SideBarStatus from './SideBarStatus'
-import Logo from '../assets/logo.svg'
-
const navBarItems = [
{
label: 'Info',
@@ -19,7 +17,7 @@ const navBarItems = [
},
{
label: 'Files',
- path: ROUTES.FILES,
+ path: ROUTES.UPLOAD,
icon: FileText,
},
{
diff --git a/src/pages/files/AssetPreview.tsx b/src/pages/files/AssetPreview.tsx
index ea8fa9d..173304a 100644
--- a/src/pages/files/AssetPreview.tsx
+++ b/src/pages/files/AssetPreview.tsx
@@ -3,15 +3,18 @@ import { Web } from '@material-ui/icons'
import { ReactElement, useEffect, useState } from 'react'
import { File, Folder } from 'react-feather'
import { FitImage } from '../../components/FitImage'
-import { detectIndexHtml, getHumanReadableFileSize } from '../../utils/file'
+import { detectIndexHtml, getAssetNameFromFiles, getHumanReadableFileSize } from '../../utils/file'
import { SwarmFile } from '../../utils/SwarmFile'
import { AssetIcon } from './AssetIcon'
interface Props {
+ assetName?: string
files: SwarmFile[]
}
-export function AssetPreview({ files }: Props): ReactElement {
+// TODO: add optional prop for indexDocument when it is already known (e.g. downloading a manifest)
+
+export function AssetPreview({ assetName, files }: Props): ReactElement {
const [previewComponent, setPreviewComponent] = useState(undefined)
const [previewUri, setPreviewUri] = useState(undefined)
@@ -39,11 +42,13 @@ export function AssetPreview({ files }: Props): ReactElement {
}, [files])
const getPrimaryText = () => {
+ const name = getAssetNameFromFiles(files)
+
if (files.length === 1) {
- return 'Filename: ' + files[0].name
+ return 'Filename: ' + (assetName || name)
}
- return 'Folder name: ' + files[0].path.split('/')[0]
+ return 'Folder name: ' + (assetName || name)
}
const getKind = () => {
@@ -66,6 +71,8 @@ export function AssetPreview({ files }: Props): ReactElement {
return getHumanReadableFileSize(bytes)
}
+ const size = getSize()
+
return (
@@ -78,7 +85,7 @@ export function AssetPreview({ files }: Props): ReactElement {
{getPrimaryText()}
Kind: {getKind()}
- Size: {getSize()}
+ {size !== '0 bytes' && Size: {size}}
diff --git a/src/pages/files/AssetSummary.tsx b/src/pages/files/AssetSummary.tsx
new file mode 100644
index 0000000..c626fff
--- /dev/null
+++ b/src/pages/files/AssetSummary.tsx
@@ -0,0 +1,24 @@
+import { Box, Typography } from '@material-ui/core'
+import { ReactElement } from 'react'
+import ExpandableListItemKey from '../../components/ExpandableListItemKey'
+import ExpandableListItemLink from '../../components/ExpandableListItemLink'
+
+interface Props {
+ hash: string
+}
+
+export function AssetSummary({ hash }: Props): ReactElement {
+ return (
+ <>
+
+
+
+
+
+ The Swarm Gateway is graciously provided by the Swarm Foundation. This service is under development and provided
+ for testing purposes only. Learn more at{' '}
+ https://gateway.ethswarm.org/.
+
+ >
+ )
+}
diff --git a/src/pages/files/Download.tsx b/src/pages/files/Download.tsx
index 7bcd232..a59831c 100644
--- a/src/pages/files/Download.tsx
+++ b/src/pages/files/Download.tsx
@@ -1,41 +1,47 @@
import { Utils } from '@ethersphere/bee-js'
-import { Box } from '@material-ui/core'
+import { ManifestJs } from '@ethersphere/manifest-js'
import { useSnackbar } from 'notistack'
import { ReactElement, useContext, useState } from 'react'
+import { useHistory } from 'react-router-dom'
import ExpandableListItemInput from '../../components/ExpandableListItemInput'
+import { History } from '../../components/History'
import { Context as SettingsContext } from '../../providers/Settings'
+import { ROUTES } from '../../routes'
import { extractSwarmHash } from '../../utils'
-import { convertBeeFileToBrowserFile } from '../../utils/file'
-import { SwarmFile } from '../../utils/SwarmFile'
-import { AssetPreview } from './AssetPreview'
-import { DownloadActionBar } from './DownloadActionBar'
+import { determineHistoryName, HISTORY_KEYS, putHistory } from '../../utils/local-storage'
+import { FileNavigation } from './FileNavigation'
-export default function Files(): ReactElement {
- const { apiUrl, beeApi } = useContext(SettingsContext)
-
- const [reference, setReference] = useState('')
+export function Download(): ReactElement {
+ const [loading, setLoading] = useState(false)
+ const { beeApi } = useContext(SettingsContext)
const [referenceError, setReferenceError] = useState(undefined)
- const [downloadedFile, setDownloadedFile] = useState | null>(null)
const { enqueueSnackbar } = useSnackbar()
+ const history = useHistory()
const validateChange = (value: string) => {
- if (Utils.isHexString(value, 64) || Utils.isHexString(value, 128)) setReferenceError(undefined)
- else setReferenceError('Incorrect format of swarm hash. Expected 64 or 128 hexstring characters.')
- }
-
- function onDownload() {
- window.open(`${apiUrl}/bzz/${reference}/`, '_blank')
+ if (Utils.isHexString(value, 64) || Utils.isHexString(value, 128) || !value.trim().length) {
+ setReferenceError(undefined)
+ } else {
+ setReferenceError('Incorrect format of swarm hash. Expected 64 or 128 hexstring characters.')
+ }
}
async function onSwarmIdentifier(identifier: string) {
if (!beeApi) {
return
}
- setReference(identifier)
+
try {
- const response = await beeApi.downloadFile(identifier)
- setDownloadedFile(convertBeeFileToBrowserFile(response))
+ const manifestJs = new ManifestJs(beeApi)
+ const isManifest = await manifestJs.isManifest(identifier)
+
+ if (!isManifest) {
+ throw Error('The specified hash does not contain valid content.')
+ }
+ const indexDocument = await manifestJs.getIndexDocumentPath(identifier)
+ putHistory(HISTORY_KEYS.DOWNLOAD_HISTORY, identifier, determineHistoryName(identifier, indexDocument))
+ history.push(ROUTES.HASH.replace(':hash', identifier))
} catch (error: unknown) {
let message = typeof error === 'object' && error !== null && Reflect.get(error, 'message')
@@ -47,20 +53,11 @@ export default function Files(): ReactElement {
message = 'The specified hash was not found.'
}
enqueueSnackbar(Error: {message || 'Unknown'}, { variant: 'error' })
+ } finally {
+ setLoading(false)
}
}
- if (downloadedFile) {
- return (
- <>
-
-
-
- setDownloadedFile(null)} onDownload={onDownload} />
- >
- )
- }
-
function recognizeSwarmHash(value: string) {
if (value.length < 64) {
return value
@@ -76,16 +73,20 @@ export default function Files(): ReactElement {
}
return (
- onSwarmIdentifier(value)}
- onChange={validateChange}
- helperText={referenceError}
- confirmLabel={'Search'}
- confirmLabelDisabled={Boolean(referenceError)}
- placeholder="e.g. 31fb0362b1a42536134c86bc58b97ac0244e5c6630beec3e27c2d1cecb38c605"
- expandedOnly
- mapperFn={value => recognizeSwarmHash(value)}
- />
+ <>
+
+ onSwarmIdentifier(value)}
+ onChange={validateChange}
+ helperText={referenceError}
+ confirmLabel={'Search'}
+ confirmLabelDisabled={Boolean(referenceError) || loading}
+ placeholder="e.g. 31fb0362b1a42536134c86bc58b97ac0244e5c6630beec3e27c2d1cecb38c605"
+ expandedOnly
+ mapperFn={value => recognizeSwarmHash(value)}
+ />
+
+ >
)
}
diff --git a/src/pages/files/DownloadActionBar.tsx b/src/pages/files/DownloadActionBar.tsx
index 71252c1..9dc0e59 100644
--- a/src/pages/files/DownloadActionBar.tsx
+++ b/src/pages/files/DownloadActionBar.tsx
@@ -1,23 +1,31 @@
import { Button } from '@material-ui/core'
import { Clear } from '@material-ui/icons'
import { ReactElement } from 'react'
-import { Download } from 'react-feather'
+import { Download, Link } from 'react-feather'
import ExpandableListItemActions from '../../components/ExpandableListItemActions'
import { SwarmButton } from '../../components/SwarmButton'
interface Props {
+ onOpen: () => void
onDownload: () => void
onCancel: () => void
+ hasIndexDocument: boolean
+ loading: boolean
}
-export function DownloadActionBar({ onDownload, onCancel }: Props): ReactElement {
+export function DownloadActionBar({ onOpen, onDownload, onCancel, hasIndexDocument, loading }: Props): ReactElement {
return (
-
- Download This File
+ {hasIndexDocument && (
+
+ View Website
+
+ )}
+
+ Download
- }>
- Cancel
+ } disabled={loading}>
+ Close
)
diff --git a/src/pages/files/FileNavigation.tsx b/src/pages/files/FileNavigation.tsx
new file mode 100644
index 0000000..e7fe755
--- /dev/null
+++ b/src/pages/files/FileNavigation.tsx
@@ -0,0 +1,41 @@
+import { createStyles, makeStyles, Tab, Tabs, Theme } from '@material-ui/core'
+import { ReactElement } from 'react'
+import { useHistory } from 'react-router-dom'
+import { ROUTES } from '../../routes'
+
+interface Props {
+ active: 'UPLOAD' | 'DOWNLOAD'
+}
+
+const useStyles = makeStyles((theme: Theme) =>
+ createStyles({
+ root: {
+ flexGrow: 1,
+ marginBottom: theme.spacing(4),
+ },
+ leftTab: {
+ marginRight: theme.spacing(0.5),
+ },
+ rightTab: {
+ marginLeft: theme.spacing(0.5),
+ },
+ }),
+)
+
+export function FileNavigation({ active }: Props): ReactElement {
+ const classes = useStyles()
+ const history = useHistory()
+
+ function onChange(event: React.ChangeEvent>, newValue: number) {
+ history.push(newValue === 1 ? ROUTES.DOWNLOAD : ROUTES.UPLOAD)
+ }
+
+ return (
+
+
+
+
+
+
+ )
+}
diff --git a/src/pages/files/PostUploadSummary.tsx b/src/pages/files/PostUploadSummary.tsx
deleted file mode 100644
index 09fbddd..0000000
--- a/src/pages/files/PostUploadSummary.tsx
+++ /dev/null
@@ -1,38 +0,0 @@
-import { Box, Typography } from '@material-ui/core'
-import { ReactElement } from 'react'
-import { CornerUpLeft } from 'react-feather'
-import ExpandableListItemActions from '../../components/ExpandableListItemActions'
-import ExpandableListItemKey from '../../components/ExpandableListItemKey'
-import ExpandableListItemLink from '../../components/ExpandableListItemLink'
-import { SwarmButton } from '../../components/SwarmButton'
-
-interface Props {
- uploadReference: string
- onUploadNewClick: () => void
-}
-
-export function PostUploadSummary({ uploadReference, onUploadNewClick }: Props): ReactElement {
- return (
- <>
-
-
-
-
-
-
-
- Back to Upload
-
-
-
-
- The Swarm Gateway is graciously provided by the Swarm Foundation. This service is under development and provided
- for testing purposes only. Learn more at{' '}
- https://gateway.ethswarm.org/.
-
- >
- )
-}
diff --git a/src/pages/files/Share.tsx b/src/pages/files/Share.tsx
new file mode 100644
index 0000000..7d97687
--- /dev/null
+++ b/src/pages/files/Share.tsx
@@ -0,0 +1,121 @@
+import { ManifestJs } from '@ethersphere/manifest-js'
+import { Box } from '@material-ui/core'
+import { saveAs } from 'file-saver'
+import JSZip from 'jszip'
+import { ReactElement, useContext, useEffect, useState } from 'react'
+import { RouteComponentProps, useHistory } from 'react-router-dom'
+import { Loading } from '../../components/Loading'
+import { Context as SettingsContext } from '../../providers/Settings'
+import { ROUTES } from '../../routes'
+import { convertBeeFileToBrowserFile, convertManifestToFiles } from '../../utils/file'
+import { shortenHash } from '../../utils/hash'
+import { determineHistoryName, HISTORY_KEYS, putHistory } from '../../utils/local-storage'
+import { SwarmFile } from '../../utils/SwarmFile'
+import { AssetPreview } from './AssetPreview'
+import { AssetSummary } from './AssetSummary'
+import { DownloadActionBar } from './DownloadActionBar'
+
+interface MatchParams {
+ hash: string
+}
+
+export function Share(props: RouteComponentProps): ReactElement {
+ const { apiUrl, beeApi } = useContext(SettingsContext)
+ const reference = props.match.params.hash
+ const history = useHistory()
+
+ const [loading, setLoading] = useState(true)
+ const [downloading, setDownloading] = useState(false)
+ const [files, setFiles] = useState([])
+ const [swarmEntries, setSwarmEntries] = useState>({})
+ const [indexDocument, setIndexDocument] = useState(null)
+
+ async function prepare() {
+ if (!beeApi) {
+ return
+ }
+
+ const manifestJs = new ManifestJs(beeApi)
+ const isManifest = await manifestJs.isManifest(reference)
+
+ if (!isManifest) {
+ throw Error('The specified hash does not contain valid content.')
+ }
+ const entries = await manifestJs.getHashes(reference)
+ setSwarmEntries(entries)
+ const indexDocument = await manifestJs.getIndexDocumentPath(reference)
+ setIndexDocument(indexDocument)
+
+ if (Object.keys(entries).length === 1) {
+ const response = await beeApi.downloadFile(reference)
+ setFiles([new SwarmFile(convertBeeFileToBrowserFile(response) as File)])
+ } else {
+ setFiles(convertManifestToFiles(entries))
+ }
+ }
+
+ function onOpen() {
+ window.open(`${apiUrl}/bzz/${reference}/`, '_blank')
+ }
+
+ function onClose() {
+ // POP means there is no history - nowhere to go back yet
+ if (history.action === 'POP') {
+ history.push(ROUTES.UPLOAD)
+ } else {
+ history.goBack()
+ }
+ }
+
+ useEffect(() => {
+ setLoading(true)
+ prepare().then(() => {
+ setLoading(false)
+ })
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [reference])
+
+ async function onDownload() {
+ if (!beeApi) {
+ return
+ }
+ putHistory(HISTORY_KEYS.DOWNLOAD_HISTORY, reference, determineHistoryName(reference, indexDocument))
+ setDownloading(true)
+
+ if (Object.keys(swarmEntries).length === 1) {
+ window.open(`${apiUrl}/bzz/${reference}/`, '_blank')
+ } else {
+ const zip = new JSZip()
+ for (const [path, hash] of Object.entries(swarmEntries)) {
+ zip.file(path, await beeApi.downloadData(hash))
+ }
+ const content = await zip.generateAsync({ type: 'blob' })
+ saveAs(content, reference + '.zip')
+ }
+ setDownloading(false)
+ }
+
+ const assetName = shortenHash(reference)
+
+ if (loading) {
+ return
+ }
+
+ return (
+ <>
+
+
+
+
+
+
+ 1)}
+ loading={downloading}
+ />
+ >
+ )
+}
diff --git a/src/pages/files/Upload.tsx b/src/pages/files/Upload.tsx
index fd4f57b..773157b 100644
--- a/src/pages/files/Upload.tsx
+++ b/src/pages/files/Upload.tsx
@@ -1,32 +1,20 @@
-import { createStyles, makeStyles, Theme } from '@material-ui/core/styles'
import { useSnackbar } from 'notistack'
import { ReactElement, useContext, useEffect, useState } from 'react'
+import { useHistory } from 'react-router-dom'
+import { HistoryHeader } from '../../components/HistoryHeader'
+import { Context as FileContext } from '../../providers/File'
import { Context as SettingsContext } from '../../providers/Settings'
import { Context, EnrichedPostageBatch } from '../../providers/Stamps'
-import { detectIndexHtml } from '../../utils/file'
-import { SwarmFile } from '../../utils/SwarmFile'
+import { ROUTES } from '../../routes'
+import { detectIndexHtml, getAssetNameFromFiles } from '../../utils/file'
+import { HISTORY_KEYS, putHistory } from '../../utils/local-storage'
import { CreatePostageStampModal } from '../stamps/CreatePostageStampModal'
import { SelectPostageStampModal } from '../stamps/SelectPostageStampModal'
import { AssetPreview } from './AssetPreview'
-import { PostUploadSummary } from './PostUploadSummary'
import { StampPreview } from './StampPreview'
import { UploadActionBar } from './UploadActionBar'
-import { UploadArea } from './UploadArea'
-const useStyles = makeStyles((theme: Theme) =>
- createStyles({
- content: { marginTop: theme.spacing(2) },
- loadingProgress: { textAlign: 'center', padding: '50px' },
- }),
-)
-
-const MAX_FILE_SIZE = 1_000_000_000 // 1 gigabyte
-
-export default function Files(): ReactElement {
- const classes = useStyles()
- const [dropzoneKey, setDropzoneKey] = useState(0)
- const [files, setFiles] = useState([])
- const [uploadReference, setUploadReference] = useState('')
+export function Upload(): ReactElement {
const [isBuyingStamp, setBuyingStamp] = useState(false)
const [isSelectingStamp, setSelectingStamp] = useState(false)
const [stamp, setStamp] = useState(null)
@@ -34,7 +22,14 @@ export default function Files(): ReactElement {
const { stamps, refresh } = useContext(Context)
const { beeApi } = useContext(SettingsContext)
+ const { files, setFiles } = useContext(FileContext)
const { enqueueSnackbar } = useSnackbar()
+ const history = useHistory()
+
+ if (!files.length) {
+ setFiles([])
+ history.replace(ROUTES.UPLOAD)
+ }
useEffect(() => {
refresh()
@@ -51,9 +46,14 @@ export default function Files(): ReactElement {
beeApi
.uploadFiles(stamp.batchID, files as unknown as File[], { indexDocument })
- .then(hash => setUploadReference(hash.reference))
- .catch(e => enqueueSnackbar(`Error uploading: ${e.message}`, { variant: 'error' }))
- .finally(() => setUploading(false))
+ .then(hash => {
+ putHistory(HISTORY_KEYS.UPLOAD_HISTORY, hash.reference, getAssetNameFromFiles(files))
+ history.replace(ROUTES.HASH.replace(':hash', hash.reference))
+ })
+ .catch(e => {
+ enqueueSnackbar(`Error uploading: ${e.message}`, { variant: 'error' })
+ setUploading(false)
+ })
}
const reset = () => {
@@ -62,23 +62,12 @@ export default function Files(): ReactElement {
setUploading(false)
}
- const uploadNew = () => {
- setTimeout(() => {
- reset()
- setDropzoneKey(dropzoneKey + 1)
- setUploadReference('')
- }, 0)
- }
-
return (
<>
- {files.length ? (
-
- ) : (
-
- )}
- {stamp !== null && !uploadReference ? : null}
- {files.length && !uploadReference ? (
+ Upload
+ {files.length && }
+ {stamp !== null ? : null}
+ {files.length && (
0}
hasSelectedStamp={stamp !== null}
@@ -89,12 +78,7 @@ export default function Files(): ReactElement {
onClearStamp={() => setStamp(null)}
isUploading={isUploading}
/>
- ) : null}
-
- {uploadReference && (
-
uploadNew()} uploadReference={uploadReference} />
- )}
-
+ )}
{isBuyingStamp ? setBuyingStamp(false)} /> : null}
{stamps && isSelectingStamp ? (
void
maximumSizeInBytes: number
}
@@ -42,15 +44,17 @@ const useStyles = makeStyles((theme: Theme) =>
}),
)
-export function UploadArea({ setFiles, maximumSizeInBytes }: Props): ReactElement {
+export function UploadArea({ maximumSizeInBytes }: Props): ReactElement {
+ const { setFiles } = useContext(Context)
const classes = useStyles()
-
+ const history = useHistory()
const { enqueueSnackbar } = useSnackbar()
+ const [strictWebsiteMode, setStrictWebsiteMode] = useState(false)
+ const [version, setVersion] = useState(0)
const getDropzoneInputDomElement = () => document.querySelector('.MuiDropzoneArea-root input') as HTMLInputElement
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
- const onUploadFolderClick = () => {
+ const onUploadCollectionClick = () => {
const element = getDropzoneInputDomElement()
if (element) {
@@ -61,6 +65,16 @@ export function UploadArea({ setFiles, maximumSizeInBytes }: Props): ReactElemen
}
}
+ const onUploadWebsiteClick = () => {
+ onUploadCollectionClick()
+ setStrictWebsiteMode(true)
+ }
+
+ const onUploadFolderClick = () => {
+ onUploadCollectionClick()
+ setStrictWebsiteMode(false)
+ }
+
const onUploadFileClick = () => {
const element = getDropzoneInputDomElement()
@@ -72,9 +86,9 @@ export function UploadArea({ setFiles, maximumSizeInBytes }: Props): ReactElemen
}
}
- const resetComponentOnAddingInvalidContent = (files: SwarmFile[]) => {
- setFiles(files)
+ const resetComponentOnAddingInvalidContent = () => {
setTimeout(() => {
+ setVersion(x => x + 1)
setFiles([])
}, 0)
}
@@ -84,16 +98,20 @@ export function UploadArea({ setFiles, maximumSizeInBytes }: Props): ReactElemen
const swarmFiles = files.map(x => new SwarmFile(x))
const indexDocument = files.length === 1 ? files[0].name : detectIndexHtml(swarmFiles) || undefined
- if (files.length && !indexDocument) {
+ if (files.length && strictWebsiteMode && !indexDocument) {
enqueueSnackbar('To upload a website, there must be an index.html or index.htm in the root of the folder.', {
variant: 'error',
})
- resetComponentOnAddingInvalidContent(swarmFiles)
+ resetComponentOnAddingInvalidContent()
return
}
setFiles(swarmFiles)
+
+ if (files.length) {
+ history.push(ROUTES.UPLOAD_IN_PROGRESS)
+ }
}
}
@@ -101,9 +119,10 @@ export function UploadArea({ setFiles, maximumSizeInBytes }: Props): ReactElemen
<>
@@ -111,9 +130,18 @@ export function UploadArea({ setFiles, maximumSizeInBytes }: Props): ReactElemen
Add File
+
+ Add Folder
+
+
+ Add Website
+
- You can click the button above or simply drag and drop to add a file.
+
+ You can click the buttons above or simply drag and drop to add a file or folder. To upload a website to Swarm,
+ make sure that your folder contains an “index.html” file.
+
>
)
}
diff --git a/src/pages/files/UploadLander.tsx b/src/pages/files/UploadLander.tsx
new file mode 100644
index 0000000..73ff529
--- /dev/null
+++ b/src/pages/files/UploadLander.tsx
@@ -0,0 +1,17 @@
+import { ReactElement } from 'react'
+import { History } from '../../components/History'
+import { HISTORY_KEYS } from '../../utils/local-storage'
+import { FileNavigation } from './FileNavigation'
+import { UploadArea } from './UploadArea'
+
+const MAX_FILE_SIZE = 1_000_000_000 // 1 gigabyte
+
+export function UploadLander(): ReactElement {
+ return (
+ <>
+
+
+
+ >
+ )
+}
diff --git a/src/pages/files/index.tsx b/src/pages/files/index.tsx
deleted file mode 100644
index 4564bbd..0000000
--- a/src/pages/files/index.tsx
+++ /dev/null
@@ -1,28 +0,0 @@
-import { ReactElement, useContext } from 'react'
-
-import Download from './Download'
-import Upload from './Upload'
-import TabsContainer from '../../components/TabsContainer'
-import TroubleshootConnectionCard from '../../components/TroubleshootConnectionCard'
-import { Context as BeeContext } from '../../providers/Bee'
-
-export default function Files(): ReactElement {
- const { status } = useContext(BeeContext)
-
- if (!status.all) return
-
- return (
- ,
- },
- {
- label: 'upload',
- component: ,
- },
- ]}
- />
- )
-}
diff --git a/src/pages/stamps/SelectPostageStampModal.tsx b/src/pages/stamps/SelectPostageStampModal.tsx
index 55190b2..b137981 100644
--- a/src/pages/stamps/SelectPostageStampModal.tsx
+++ b/src/pages/stamps/SelectPostageStampModal.tsx
@@ -1,4 +1,4 @@
-import { Box, createStyles, FormControl, makeStyles, MenuItem, Select, Theme, Typography } from '@material-ui/core'
+import { createStyles, FormControl, makeStyles, MenuItem, Select, Theme } from '@material-ui/core'
import Button from '@material-ui/core/Button'
import Dialog from '@material-ui/core/Dialog'
import DialogContent from '@material-ui/core/DialogContent'
@@ -88,30 +88,15 @@ export function SelectPostageStampModal({ stamps, onSelect, onClose }: Props): R
-
-
-
- }>
- Select
-
- }>
- Cancel
-
-
-
-
-
- Please refer to the{' '}
-
- official Bee documentation
- {' '}
- to understand these values.
-
+
+ }>
+ Select
+
+ }>
+ Cancel
+
+
)
diff --git a/src/providers/File.tsx b/src/providers/File.tsx
new file mode 100644
index 0000000..94ba47f
--- /dev/null
+++ b/src/providers/File.tsx
@@ -0,0 +1,25 @@
+import { createContext, ReactChild, ReactElement, useState } from 'react'
+import { SwarmFile } from '../utils/SwarmFile'
+
+interface ContextInterface {
+ files: SwarmFile[]
+ setFiles: (files: SwarmFile[]) => void
+}
+
+const initialValues: ContextInterface = {
+ files: [],
+ setFiles: () => {}, // eslint-disable-line
+}
+
+export const Context = createContext(initialValues)
+export const Consumer = Context.Consumer
+
+interface Props {
+ children: ReactChild
+}
+
+export function Provider({ children }: Props): ReactElement {
+ const [files, setFiles] = useState(initialValues.files)
+
+ return {children}
+}
diff --git a/src/routes.tsx b/src/routes.tsx
index f189c51..97f7ca3 100644
--- a/src/routes.tsx
+++ b/src/routes.tsx
@@ -1,18 +1,22 @@
import type { ReactElement } from 'react'
-import { Switch } from 'react-router-dom'
-
-import { Route } from 'react-router-dom'
-
-import Info from './pages/info'
-import Status from './pages/status'
-import Files from './pages/files'
+import { Route, Switch } from 'react-router-dom'
import Accounting from './pages/accounting'
+import { Download } from './pages/files/Download'
+import { Share } from './pages/files/Share'
+import { Upload } from './pages/files/Upload'
+import { UploadLander } from './pages/files/UploadLander'
+import Info from './pages/info'
import Settings from './pages/settings'
import Stamps from './pages/stamps'
+import Status from './pages/status'
export enum ROUTES {
INFO = '/',
FILES = '/files',
+ UPLOAD = '/files/upload',
+ UPLOAD_IN_PROGRESS = '/files/upload/workflow',
+ DOWNLOAD = '/files/download',
+ HASH = '/files/hash/:hash',
ACCOUNTING = '/accounting',
SETTINGS = '/settings',
STAMPS = '/stamps',
@@ -21,7 +25,10 @@ export enum ROUTES {
const BaseRouter = (): ReactElement => (
-
+
+
+
+
diff --git a/src/utils/date.ts b/src/utils/date.ts
new file mode 100644
index 0000000..8335664
--- /dev/null
+++ b/src/utils/date.ts
@@ -0,0 +1,5 @@
+export function getPrettyDateString(date: Date): string {
+ const string = date.toString()
+
+ return string.split('GMT')[0].trim()
+}
diff --git a/src/utils/file.ts b/src/utils/file.ts
index c5c9d91..2d37f61 100644
--- a/src/utils/file.ts
+++ b/src/utils/file.ts
@@ -49,3 +49,25 @@ export function convertBeeFileToBrowserFile(file: FileData): Partia
arrayBuffer: () => new Promise(resolve => resolve(file.data)),
}
}
+
+export function convertManifestToFiles(files: Record): SwarmFile[] {
+ return Object.entries(files).map(
+ x =>
+ ({
+ name: x[0],
+ path: x[0],
+ type: 'n/a',
+ size: 0,
+ webkitRelativePath: x[0],
+ arrayBuffer: () => new Promise(resolve => resolve(new ArrayBuffer(0))),
+ } as SwarmFile),
+ )
+}
+
+export function getAssetNameFromFiles(files: SwarmFile[]): string {
+ if (files.length === 1) {
+ return files[0].name
+ }
+
+ return files[0].path.split('/')[0]
+}
diff --git a/src/utils/hash.ts b/src/utils/hash.ts
new file mode 100644
index 0000000..207cc9f
--- /dev/null
+++ b/src/utils/hash.ts
@@ -0,0 +1,3 @@
+export function shortenHash(hash: string, sliceLength = 8): string {
+ return `${hash.slice(0, sliceLength)}[…]${hash.slice(-sliceLength)}`
+}
diff --git a/src/utils/local-storage.ts b/src/utils/local-storage.ts
new file mode 100644
index 0000000..0c5dded
--- /dev/null
+++ b/src/utils/local-storage.ts
@@ -0,0 +1,70 @@
+import { shortenHash } from './hash'
+
+export enum HISTORY_KEYS {
+ UPLOAD_HISTORY = 'UPLOAD_HISTORY',
+ DOWNLOAD_HISTORY = 'DOWNLOAD_HISTORY',
+}
+
+export interface HistoryItem {
+ createdAt: number
+ name: string
+ hash: string
+}
+
+export function putHistory(key: string, hash: string, name: string): void {
+ const history = getHistorySafe(key)
+
+ const existingIndex = history.findIndex(x => x.hash === hash)
+
+ if (existingIndex !== -1) {
+ history.splice(existingIndex, 1)
+ }
+
+ history.unshift({
+ createdAt: Date.now(),
+ hash,
+ name,
+ })
+
+ if (history.length > 10) {
+ history.length = 10
+ }
+ localStorage.setItem(key, JSON.stringify(history))
+}
+
+export function getHistorySafe(key: string): HistoryItem[] {
+ const items = localStorage.getItem(key)
+
+ if (!items) {
+ return []
+ }
+ try {
+ const parsed = JSON.parse(items)
+
+ if (!Array.isArray(parsed) || !parsed.every(isHistoryItem)) {
+ return []
+ }
+
+ return parsed
+ } catch {
+ return []
+ }
+}
+
+function isHistoryItem(x: unknown): x is HistoryItem {
+ if (typeof x !== 'object' || x === null) {
+ return false
+ }
+
+ return 'createdAt' in x && 'hash' in x
+}
+
+export function determineHistoryName(hash: string, indexDocument?: string | null): string {
+ if (indexDocument === 'index.html') {
+ return `Website ${shortenHash(hash, 4)}`
+ } else if (indexDocument) {
+ return indexDocument
+ }
+
+ return `Folder ${shortenHash(hash, 4)}`
+}