From 3ef1ad9574c9193f83d8a1447fddb79266c1a4f4 Mon Sep 17 00:00:00 2001 From: Cafe137 <77121044+Cafe137@users.noreply.github.com> Date: Tue, 7 Dec 2021 16:06:21 +0100 Subject: [PATCH] feat: add website and folder upload and download (#260) * feat: add website and folder upload and download * feat: download-share-upload navigation * fix: check for files length in hasIndexDocument * fix: change router dependency * refactor: switch to @ethersphere/manfest-js * fix: hide previews on dropzone, fix spinner align, hide 0 size display * feat: add upload and download history * refactor: change drag and drop text * feat: make history ux better * refactor: improve code based on review * build: add missing react-router dependency * ci: remove beeload * revert(ci): remove beeload This reverts commit 4ce6cb0045a2d9aea3047ab395d214d8d368c532. --- package-lock.json | 217 +++++++++++++++++-- package.json | 5 + src/App.tsx | 43 ++-- src/components/ExpandableListItemLink.tsx | 45 +++- src/components/History.tsx | 37 ++++ src/components/HistoryHeader.tsx | 41 ++++ src/components/Loading.tsx | 10 + src/components/SideBar.tsx | 16 +- src/pages/files/AssetPreview.tsx | 17 +- src/pages/files/AssetSummary.tsx | 24 ++ src/pages/files/Download.tsx | 83 +++---- src/pages/files/DownloadActionBar.tsx | 20 +- src/pages/files/FileNavigation.tsx | 41 ++++ src/pages/files/PostUploadSummary.tsx | 38 ---- src/pages/files/Share.tsx | 121 +++++++++++ src/pages/files/Upload.tsx | 70 +++--- src/pages/files/UploadArea.tsx | 54 +++-- src/pages/files/UploadLander.tsx | 17 ++ src/pages/files/index.tsx | 28 --- src/pages/stamps/SelectPostageStampModal.tsx | 33 +-- src/providers/File.tsx | 25 +++ src/routes.tsx | 23 +- src/utils/date.ts | 5 + src/utils/file.ts | 22 ++ src/utils/hash.ts | 3 + src/utils/local-storage.ts | 70 ++++++ 26 files changed, 839 insertions(+), 269 deletions(-) create mode 100644 src/components/History.tsx create mode 100644 src/components/HistoryHeader.tsx create mode 100644 src/components/Loading.tsx create mode 100644 src/pages/files/AssetSummary.tsx create mode 100644 src/pages/files/FileNavigation.tsx delete mode 100644 src/pages/files/PostUploadSummary.tsx create mode 100644 src/pages/files/Share.tsx create mode 100644 src/pages/files/UploadLander.tsx delete mode 100644 src/pages/files/index.tsx create mode 100644 src/providers/File.tsx create mode 100644 src/utils/date.ts create mode 100644 src/utils/hash.ts create mode 100644 src/utils/local-storage.ts 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 - ) 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 - - - - - - - - - - Please refer to the{' '} - - official Bee documentation - {' '} - to understand these values. - + + + + ) 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)}` +}