diff --git a/Dockerfile b/Dockerfile index b7627cb8..c8d5ecbf 100644 --- a/Dockerfile +++ b/Dockerfile @@ -9,7 +9,7 @@ RUN npm ci # This is where because may be the case that you would try # to build the app based on some `X_TAG` in my case (Git commit hash) # but the code hasn't changed. -FROM node:lts AS builder +FROM node:18 AS builder WORKDIR /opt/app COPY . . diff --git a/README.md b/README.md index d8347dde..c9856228 100644 --- a/README.md +++ b/README.md @@ -1,22 +1,25 @@ - # Fairdrive + At the intersection of innovation, interoperability, and decentralization, Fairdrive emerges for the cause of fair data. This initiative, driven by the community, is dedicated to promoting freedom. By facilitating decentralized storage, it allows developers to construct interoperable, decentralized, and open-source dApps. This, in turn, enables users to regain their privacy, assume ownership of their data, and control their digital identity. [Login here](https://app.fairdrive.fairdatasociety.org) ### What is Fairdrive -Fairdrive is a decentralized application (dApp) that facilitates distributed storage on the Swarm network. It features a "Drive" interface for managing pods, files and folders. Internally, Fairdrive uses Fair Data Protocol, which is built on top of Ethereum Swarm. +Fairdrive is a decentralized application (dApp) that facilitates distributed storage on the Swarm network. It features a "Drive" interface for managing pods, files and folders. Internally, Fairdrive uses Fair Data Protocol, which is built on top of Ethereum Swarm. ### Why Fairdrive + #### Pros + - **Data Encryption**: Fairdrive encrypts all data by default, providing an additional layer of security for your files. - **Data Ownership**: Unlike traditional cloud storage services, where your data is technically owned by the service provider, with Fairdrive, you are the sole owner of your data. - **Decentralized Storage**: Fairdrive stores data on the Swarm decentralized network. This means your data isn't stored in a single location, but is distributed across multiple nodes, enhancing data security and reliability. - **User Control**: Only you have access to your data. You control who can access your data and how it's used. #### Cons + - **Speed**: Due to its decentralized nature, Fairdrive can be slower than traditional cloud storage services. This is because data isn't stored in a single location, but is distributed across multiple nodes in the Swarm network. As a result, it can take longer to retrieve data. - **Learning Curve**: As a decentralized application (dApp), Fairdrive might have a steeper learning curve for users who are not familiar with blockchain technology and decentralized networks. - **Dependence on Swarm Network**: The performance and reliability of Fairdrive are dependent on the Swarm network. If there are issues with the network, it could affect the availability and performance of Fairdrive. @@ -24,15 +27,17 @@ Fairdrive is a decentralized application (dApp) that facilitates distributed sto ## Getting Help -If you need help using Fairdrive as user, check out [User Guide](docs/USER-GUIDE.md) and [FAQ](docs/FAQ.md). See [Getting Started](docs/GETTING-STARTED.md). -Technical overview of [Design](docs/DESIGN.md), [Functionality](docs/FUNCTIONALITY.md) and [Architecture](docs/ARCHITECTURE.md). +If you need help using Fairdrive as user, check out [User Guide](docs/USER-GUIDE.md) and [FAQ](docs/FAQ.md). See [Getting Started](docs/GETTING-STARTED.md). +Technical overview of [Design](docs/DESIGN.md), [Functionality](docs/FUNCTIONALITY.md) and [Architecture](docs/ARCHITECTURE.md). If you can't find the answer to your question, feel free to [contact us](docs/CONTACT.md). ## Development + See [**Development instructions**](docs/DEVELOPMENT.md) for information how to install and develop on local machines. ## Join Us in Building Fairdrive + Fairdrive is a community-driven initiative, and we welcome contributions from anyone who shares our vision for a decentralized, user-controlled digital world. Whether you're a developer, a designer, a writer, or just someone who's interested in what we're doing, there are many ways you can contribute to Fairdrive. If you're a developer, you can help us improve the Fairdrive application by fixing bugs, adding new features, or improving our documentation. Check out our [open issues](https://github.com/fairDataSociety/fairdrive-theapp/issues) to see what we're currently working on. @@ -47,8 +52,8 @@ We believe that everyone has something valuable to contribute, and we're committ Together, we can make Fairdrive the best it can be! - ## Development Stage Notice for Fairdrive + Please be aware that Fairdrive is currently in its development stage. This means that the application is still undergoing substantial updates, modifications, and improvements. As a result, certain functionalities may change, be added, or removed without prior notice. During this development stage, there's also a risk of data loss. While we strive to ensure the integrity and security of all data stored on Fairdrive, the decentralized nature of the application and the ongoing development work mean that we cannot guarantee complete data preservation. @@ -64,4 +69,4 @@ We strongly recommend that you keep backups of any critical data you store on Fa - **Testnet**: https://app.fairdrive.fairdatasociety.org - **Development**: https://app.fairdrive.dev.fairdatasociety.org -Current testnet deployment is on: Sepolia. \ No newline at end of file +Current testnet deployment is on: Sepolia. diff --git a/docs/USER-GUIDE.md b/docs/USER-GUIDE.md index b75c14ed..0a3163fa 100644 --- a/docs/USER-GUIDE.md +++ b/docs/USER-GUIDE.md @@ -19,38 +19,45 @@ Welcome to the Fairdrive User Guide! This guide will walk you through all the fe To use Fairdrive, you'll need to create an account using any of below options. Here's how: #### 1. Using Metamask + 1. Click Connect -2. Select Metamask -3. Type passphrase -4. Connect Metamask to site with your preffered account +2. Select Metamask +3. Type passphrase +4. Connect Metamask to site with your preferred account 5. Sign Message #### 2. Using FDP Portable account + 1. Go to the Fairdrive website. 2. Click on the "Register New Account" button. 3. Fill out the registration form with your details. 4. When you finish procedure your FDP account will be ready -4. Login into Fairdrive using your username/password +5. Login into Fairdrive using your username/password #### 3. Using Bloosom extension + 1. Make sure you are logged in inside Blossom extension 2. Click Connect 3. Select Blossom 4. Allow Fairdrive full access of your personal storage ## Logging In + **Metamask** account: + 1. Click Connect 2. Enter passphrase -3. Sign message +3. Sign message **FDP Portable account** to log in to Fairdrive: + 1. Go to the Fairdrive website. 2. Click on the "Log In" button. 3. Enter your username and password. 4. Click "Submit" to log in. ## Account Migration + You have the option to transition your Metamask-created Lite account to an FDP Portable account. This transition enables you to utilize a username/password combination to access your data from mobile devices or other devices where Metamask isn't installed. Your Lite account will be connected with your new Portable account. During this transition process, you'll be given a new mnemonic. It's crucial to note this down and keep it in a secure location, as it will serve as your mnemonic for your Portable account recovery. @@ -59,6 +66,7 @@ During this transition process, you'll be given a new mnemonic. It's crucial to ## Manage Your Profile You can view your profile information at any time: + 1. Click on Blockie in the top right corner of the screen. 2. Dropdown menu will display available options. @@ -99,15 +107,13 @@ To search your pods, files, and directories: To change the theme of Fairdrive: -1. Click on Blockie in the top right corner of the screen. +1. Click on Blockie in the top right corner of the screen. 2. Click Theme Toggle. ## Getting Help -## Getting Help - -If you need help using Fairdrive, check out our [User Guide](USER-GUIDE.md) and [FAQ](FAQ.md). -Start [here](GETTING-STARTED.md) or see [Design](DESIGN.md), [Functionality](FUNCTIONALITY.md) or [Architecture](ARCHITECTURE.md). +If you need help using Fairdrive, check out our [User Guide](USER-GUIDE.md) and [FAQ](FAQ.md). +Start [here](GETTING-STARTED.md) or see [Design](DESIGN.md), [Functionality](FUNCTIONALITY.md) or [Architecture](ARCHITECTURE.md). Developers can check [Development Instructions](DEVELOPMENT.md). If you can't find the answer to your question, feel free to [contact us](CONTACT.md). diff --git a/package-lock.json b/package-lock.json index 699ae0c4..73874493 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,7 +12,7 @@ "@emotion/react": "^11.4.1", "@emotion/styled": "^11.3.0", "@fairdatasociety/blossom": "^0.5.0", - "@fairdatasociety/fdp-storage": "^0.11.0", + "@fairdatasociety/fdp-storage": "^0.13.0", "@headlessui/react": "^1.7.14", "@metamask/sdk": "^0.5.6", "@types/react-blockies": "^1.4.1", @@ -3055,21 +3055,37 @@ "@fairdatasociety/fdp-storage": "^0.11.0" } }, + "node_modules/@fairdatasociety/blossom/node_modules/@fairdatasociety/fdp-storage": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@fairdatasociety/fdp-storage/-/fdp-storage-0.11.0.tgz", + "integrity": "sha512-Na56ZOmb2cBpm1Z4RaIn0ASJXlLrN/FBg3IfI9ExPDBVxd1jdeMg7P4YY2pQkHRp3fKOtHK/bB4v0nB+tHZIKQ==", + "dependencies": { + "@ethersphere/bee-js": "^6.2.0", + "@fairdatasociety/fdp-contracts-js": "^3.7.1", + "crypto-js": "^4.1.1", + "ethers": "^5.5.2", + "js-sha3": "^0.8.0" + }, + "engines": { + "node": ">=16.0.0", + "npm": ">=8.0.0" + } + }, "node_modules/@fairdatasociety/fdp-contracts-js": { - "version": "3.7.1", - "resolved": "https://registry.npmjs.org/@fairdatasociety/fdp-contracts-js/-/fdp-contracts-js-3.7.1.tgz", - "integrity": "sha512-G1TMIAJPIsfxEvubZh2qiiQb4XJx07jdmwUIvBNR2+PCdN2KnCNYQo1b4xkSFxk8BzY7hnXtricmIlNUmv3N0w==", + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/@fairdatasociety/fdp-contracts-js/-/fdp-contracts-js-3.8.0.tgz", + "integrity": "sha512-Lfr/kxCBZ4IYWwAkeRa2fSi9suYd5DxTG5xunfuwMm32FeB99fL7tI19sb5tjPyRj6bphJPaRWmFCdPJ+yfZsg==", "peerDependencies": { "ethers": ">=5.6.4" } }, "node_modules/@fairdatasociety/fdp-storage": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/@fairdatasociety/fdp-storage/-/fdp-storage-0.11.0.tgz", - "integrity": "sha512-Na56ZOmb2cBpm1Z4RaIn0ASJXlLrN/FBg3IfI9ExPDBVxd1jdeMg7P4YY2pQkHRp3fKOtHK/bB4v0nB+tHZIKQ==", + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/@fairdatasociety/fdp-storage/-/fdp-storage-0.13.0.tgz", + "integrity": "sha512-7AWAC/enq1SQCisMuD3wT5cKSOxe8KdC5f3nolWcruCUpdWcwe65YtjDeL88m/E/kcituQGAl2IgWGkqoysB/g==", "dependencies": { "@ethersphere/bee-js": "^6.2.0", - "@fairdatasociety/fdp-contracts-js": "^3.7.1", + "@fairdatasociety/fdp-contracts-js": "^3.8.0", "crypto-js": "^4.1.1", "ethers": "^5.5.2", "js-sha3": "^0.8.0" @@ -24969,21 +24985,35 @@ "integrity": "sha512-IoKkn2GEuD95yJ763AWuUA153oZEjKH5DOJgGTf48QFkiyuLlLc1OCGveh9tj5fE0LavK0M78x4ZqMKVlmT8BQ==", "requires": { "@fairdatasociety/fdp-storage": "^0.11.0" + }, + "dependencies": { + "@fairdatasociety/fdp-storage": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@fairdatasociety/fdp-storage/-/fdp-storage-0.11.0.tgz", + "integrity": "sha512-Na56ZOmb2cBpm1Z4RaIn0ASJXlLrN/FBg3IfI9ExPDBVxd1jdeMg7P4YY2pQkHRp3fKOtHK/bB4v0nB+tHZIKQ==", + "requires": { + "@ethersphere/bee-js": "^6.2.0", + "@fairdatasociety/fdp-contracts-js": "^3.7.1", + "crypto-js": "^4.1.1", + "ethers": "^5.5.2", + "js-sha3": "^0.8.0" + } + } } }, "@fairdatasociety/fdp-contracts-js": { - "version": "3.7.1", - "resolved": "https://registry.npmjs.org/@fairdatasociety/fdp-contracts-js/-/fdp-contracts-js-3.7.1.tgz", - "integrity": "sha512-G1TMIAJPIsfxEvubZh2qiiQb4XJx07jdmwUIvBNR2+PCdN2KnCNYQo1b4xkSFxk8BzY7hnXtricmIlNUmv3N0w==", + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/@fairdatasociety/fdp-contracts-js/-/fdp-contracts-js-3.8.0.tgz", + "integrity": "sha512-Lfr/kxCBZ4IYWwAkeRa2fSi9suYd5DxTG5xunfuwMm32FeB99fL7tI19sb5tjPyRj6bphJPaRWmFCdPJ+yfZsg==", "requires": {} }, "@fairdatasociety/fdp-storage": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/@fairdatasociety/fdp-storage/-/fdp-storage-0.11.0.tgz", - "integrity": "sha512-Na56ZOmb2cBpm1Z4RaIn0ASJXlLrN/FBg3IfI9ExPDBVxd1jdeMg7P4YY2pQkHRp3fKOtHK/bB4v0nB+tHZIKQ==", + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/@fairdatasociety/fdp-storage/-/fdp-storage-0.13.0.tgz", + "integrity": "sha512-7AWAC/enq1SQCisMuD3wT5cKSOxe8KdC5f3nolWcruCUpdWcwe65YtjDeL88m/E/kcituQGAl2IgWGkqoysB/g==", "requires": { "@ethersphere/bee-js": "^6.2.0", - "@fairdatasociety/fdp-contracts-js": "^3.7.1", + "@fairdatasociety/fdp-contracts-js": "^3.8.0", "crypto-js": "^4.1.1", "ethers": "^5.5.2", "js-sha3": "^0.8.0" diff --git a/package.json b/package.json index d3c19caa..f1cd9020 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,7 @@ "@emotion/react": "^11.4.1", "@emotion/styled": "^11.3.0", "@fairdatasociety/blossom": "^0.5.0", - "@fairdatasociety/fdp-storage": "^0.11.0", + "@fairdatasociety/fdp-storage": "^0.13.0", "@headlessui/react": "^1.7.14", "@metamask/sdk": "^0.5.6", "@types/react-blockies": "^1.4.1", diff --git a/src/@types/fdp-storage.d.ts b/src/@types/fdp-storage.d.ts new file mode 100644 index 00000000..98964aac --- /dev/null +++ b/src/@types/fdp-storage.d.ts @@ -0,0 +1,13 @@ +import type { + DirectoryItem as FdpDirectoryItem, + FileItem as FdpFileItem, +} from '@fairdatasociety/fdp-storage'; + +declare module '@fairdatasociety/fdp-storage' { + interface DirectoryItem extends FdpDirectoryItem { + path?: string; + } + interface FileItem extends FdpFileItem { + path?: string; + } +} diff --git a/src/api/directory.ts b/src/api/directory.ts index 7b09f900..e9980284 100644 --- a/src/api/directory.ts +++ b/src/api/directory.ts @@ -13,7 +13,10 @@ export async function createDirectory( directory = ''; } - await fdp.directory.create(podName, combine(directory, directoryName)); + await fdp.directory.create( + podName, + combine(...directory.split('/'), directoryName) + ); const time = getUnixTimestamp(); return { name: directoryName, diff --git a/src/api/files.ts b/src/api/files.ts index 8b87ae39..99ace5d3 100644 --- a/src/api/files.ts +++ b/src/api/files.ts @@ -1,4 +1,8 @@ -import { FdpStorage, FileItem } from '@fairdatasociety/fdp-storage'; +import { + FdpStorage, + FileItem, + UploadProgressInfo, +} from '@fairdatasociety/fdp-storage'; import { formatUrl } from '@utils/url'; interface DownloadFileData { @@ -78,7 +82,8 @@ export async function shareFile( export async function uploadFile( fdp: FdpStorage, - data: UploadFileData + data: UploadFileData, + progressCallback?: (info: UploadProgressInfo) => void ): Promise { const writePath = data.directory === 'root' ? '' : '/' + formatUrl(data.directory); @@ -87,7 +92,8 @@ export async function uploadFile( const fileMetadata = await fdp.file.uploadData( data.podName, `${writePath}/${data.file.name}`, - fileBytes + fileBytes, + progressCallback && { progressCallback } ); // todo remove this when fdp-storage implements this https://github.com/fairDataSociety/fdp-storage/issues/229 diff --git a/src/api/pod.ts b/src/api/pod.ts index 195fe655..3e0cb8a9 100644 --- a/src/api/pod.ts +++ b/src/api/pod.ts @@ -29,6 +29,10 @@ export function getFdpPathByDirectory(directory: string): string { return '/'; } + if (directory.startsWith('/')) { + return directory; + } + return '/' + directory; } diff --git a/src/components/Buttons/MainSideBarItem/MainSideBarItem.tsx b/src/components/Buttons/MainSideBarItem/MainSideBarItem.tsx index 1e3dac34..1de43878 100644 --- a/src/components/Buttons/MainSideBarItem/MainSideBarItem.tsx +++ b/src/components/Buttons/MainSideBarItem/MainSideBarItem.tsx @@ -18,13 +18,19 @@ interface MainSideBarItemProps { label: string; link: string; driveSideBarToggle: any; + onClick?: () => void; + className?: string; } +const DRIVE_PATH = '/drive'; + const MainSideBarItem: FC = ({ icons, label, link, driveSideBarToggle, + onClick, + className, }) => { const { theme } = useContext(ThemeContext); @@ -39,27 +45,28 @@ const MainSideBarItem: FC = ({ }, [link]); useEffect(() => { - if (router.pathname === '/drive') { + if (router.pathname === DRIVE_PATH) { driveSideBarToggle(); } // eslint-disable-next-line react-hooks/exhaustive-deps }, []); return ( -
{ - if (router.pathname === '/drive') { - setTimeout(() => driveSideBarToggle(), 100); - } - }} - > - - + +
{ + if (router.pathname === DRIVE_PATH && link === DRIVE_PATH) { + setTimeout(() => driveSideBarToggle(), 100); + } + onClick && onClick(); + }} + > + {theme === 'light' ? isActive ? icons.light.active @@ -69,7 +76,7 @@ const MainSideBarItem: FC = ({ : icons.dark.inactive} = ({ {label} - -
+
+ ); }; diff --git a/src/components/Cards/DriveCard/DriveCard.tsx b/src/components/Cards/DriveCard/DriveCard.tsx index a2c0e2ec..15ea63bd 100644 --- a/src/components/Cards/DriveCard/DriveCard.tsx +++ b/src/components/Cards/DriveCard/DriveCard.tsx @@ -1,4 +1,4 @@ -import { FC } from 'react'; +import { FC, MouseEvent, useRef } from 'react'; import prettyBytes from 'pretty-bytes'; import { Menu } from '@headlessui/react'; @@ -23,6 +23,10 @@ interface DriveCardProps extends UpdateDriveProps { handlePreviewClick?: () => void; } +const MOBILE_SCREEN_WIDTH = 640; +const MENU_LEFT_POS_CLASS = '-left-24'; +const MENU_RIGHT_POS_CLASS = 'right-0'; + const DriveCard: FC = ({ type, data, @@ -30,16 +34,48 @@ const DriveCard: FC = ({ updateDrive, }) => { const { intl } = useLocales(); + const menuRef = useRef(null); + + const onClickEvent = (event: MouseEvent) => { + const pageWidth = window.innerWidth; + + if (pageWidth > MOBILE_SCREEN_WIDTH) { + return; + } + + // farwardRef can't work because the menu is not visible at the moment when item is clicked. + // That's why the setTimeout is needed + setTimeout(() => { + const menuElement = menuRef.current?.firstChild as HTMLElement; + if (!menuElement) { + return; + } + menuElement.classList.remove(MENU_LEFT_POS_CLASS); + menuElement.classList.remove(MENU_RIGHT_POS_CLASS); + menuElement.classList.add( + event.pageX / pageWidth < 0.7 + ? MENU_LEFT_POS_CLASS + : MENU_RIGHT_POS_CLASS + ); + }, 20); + }; return ( - + -
+
= ({ />
-

- {shortenString(data.name, 24)} +

+ {shortenString(data.name, 14, 8)}

setMetamaskMigrationOpen(false)} /> + + setMobileNavigationOpen(false)} + /> ); } diff --git a/src/components/Dialogs/Drawer/Drawer.tsx b/src/components/Dialogs/Drawer/Drawer.tsx new file mode 100644 index 00000000..7e2d998f --- /dev/null +++ b/src/components/Dialogs/Drawer/Drawer.tsx @@ -0,0 +1,81 @@ +import { Button } from '@components/Buttons'; +import ThemeContext from '@context/ThemeContext'; +import { Dialog, Transition } from '@headlessui/react'; +import { Fragment, useContext } from 'react'; + +import CloseLight from '@media/UI/close-light.svg'; +import CloseDark from '@media/UI/close-light.svg'; + +type DrawerProps = { + children: React.ReactNode; + open: boolean; + onClose: () => void; + className?: string; +}; + +export default function Drawer({ + children, + open, + onClose, + className, +}: DrawerProps) { + const { theme } = useContext(ThemeContext); + + return ( + + +
+ + + + + +
+
+
+
+
+
+ ); +} diff --git a/src/components/Dialogs/MetamaskMigrationDialog/MetamaskCreateAccount.tsx b/src/components/Dialogs/MetamaskMigrationDialog/MetamaskCreateAccount.tsx index 665bf1d7..444301d1 100644 --- a/src/components/Dialogs/MetamaskMigrationDialog/MetamaskCreateAccount.tsx +++ b/src/components/Dialogs/MetamaskMigrationDialog/MetamaskCreateAccount.tsx @@ -14,6 +14,7 @@ import ThemeContext from '@context/ThemeContext'; import { RegistrationRequest } from '@fairdatasociety/fdp-storage/dist/account/types'; import { useLocales } from '@context/LocalesContext'; import { useMetamask } from '@context/MetamaskContext'; +import { errorToString } from '@utils/error'; interface MetamaskCreateAccountProps { username: string; @@ -70,7 +71,7 @@ export default function MetamaskCreateAccount({ } catch (error) { console.error(error); closeTimer(); - setBalanceError(String(error)); + setBalanceError(errorToString(error)); setCanProceed(true); } }; @@ -103,7 +104,7 @@ export default function MetamaskCreateAccount({ onConfirm(); } catch (error) { - setErrorMessage(String(error)); + setErrorMessage(errorToString(error)); } finally { setLoading(false); } @@ -130,15 +131,7 @@ export default function MetamaskCreateAccount({ }; const getFeePrice = async () => { - const { address } = fdpClientRef.current.account.wallet; - const { publicKey } = fdpClientRef.current.account; - - const price = await estimateRegistrationPrice( - username, - address, - publicKey, - network - ); + const price = await estimateRegistrationPrice(network); setMinBalance(price); }; @@ -190,7 +183,7 @@ export default function MetamaskCreateAccount({ <>
{intl.get('YOUR_ACCOUNT_IS')}
- + {address} diff --git a/src/components/DirectoryPath/DirectoryPath.tsx b/src/components/DirectoryPath/DirectoryPath.tsx index 295b3e39..a8908fae 100644 --- a/src/components/DirectoryPath/DirectoryPath.tsx +++ b/src/components/DirectoryPath/DirectoryPath.tsx @@ -9,6 +9,7 @@ interface DirectoryPathProps { directory: string; onDirectorySelect: (newDirectory: string) => void; onBackToDrive: () => void; + className?: string; } const MAX_FOLDERS = 3; @@ -21,6 +22,7 @@ const DirectoryPath = ({ directory, onDirectorySelect, onBackToDrive, + className, }: DirectoryPathProps) => { const { theme } = useContext(ThemeContext); const [folders, displayedFolders] = useMemo(() => { @@ -35,7 +37,7 @@ const DirectoryPath = ({ }; return ( -
+
{podName && (
diff --git a/src/components/Inputs/CustomCheckbox/CustomCheckbox.tsx b/src/components/Inputs/CustomCheckbox/CustomCheckbox.tsx index ee0ae07f..60797807 100644 --- a/src/components/Inputs/CustomCheckbox/CustomCheckbox.tsx +++ b/src/components/Inputs/CustomCheckbox/CustomCheckbox.tsx @@ -48,7 +48,10 @@ const CustomCheckbox: FC = ({ )}
-
diff --git a/src/components/Inputs/SearchBar/SearchBar.tsx b/src/components/Inputs/SearchBar/SearchBar.tsx index f9498380..af193210 100644 --- a/src/components/Inputs/SearchBar/SearchBar.tsx +++ b/src/components/Inputs/SearchBar/SearchBar.tsx @@ -10,42 +10,59 @@ import CloseDarkIcon from '@media/UI/close-light.svg'; import classes from './SearchBar.module.scss'; import { useLocales } from '@context/LocalesContext'; +import { useForm } from 'react-hook-form'; +import PodContext from '@context/PodContext'; interface SearchBarProps {} const SearchBar: FC = () => { const { theme } = useContext(ThemeContext); - const { search, updateSearch } = useContext(SearchContext); + const { updateSearch, searchDisabled } = useContext(SearchContext); + const { activePod, loading } = useContext(PodContext); const { intl } = useLocales(); + const { register, handleSubmit, reset } = useForm<{ search: '' }>(); + + const onSubmitInternal = ({ search }) => updateSearch(search); return ( -
- {theme === 'light' ? ( - - ) : ( - - )} +
+ + {theme === 'light' ? ( + + ) : ( + + )} + updateSearch(e.target.value)} + {...register('search', { required: true })} /> -
updateSearch('')}> +
{ + reset(); + updateSearch(''); + }} + > {theme === 'light' ? ( ) : ( )}
-
+
); }; diff --git a/src/components/Invite/Invite.tsx b/src/components/Invite/Invite.tsx index 56d35497..0e48a075 100644 --- a/src/components/Invite/Invite.tsx +++ b/src/components/Invite/Invite.tsx @@ -1,4 +1,4 @@ -import { FC, useEffect, useState } from 'react'; +import { FC, useContext, useEffect, useState } from 'react'; import { Button } from '@components/Buttons'; import { useForm } from 'react-hook-form'; import { @@ -20,6 +20,9 @@ import CustomCheckbox from '@components/Inputs/CustomCheckbox/CustomCheckbox'; import { useFdpStorage } from '@context/FdpStorageContext'; import copy from 'copy-to-clipboard'; import { useLocales } from '@context/LocalesContext'; +import ThemeContext from '@context/ThemeContext'; +import InfoLight from '@media/UI/info-light.svg'; +import InfoDark from '@media/UI/info-dark.svg'; export const STEP_CREATE = 'create'; export const STEP_FILL = 'fill'; @@ -42,6 +45,7 @@ const Invite: FC = () => { const [termsAccepted, setTermsAccepted] = useState(false); const { wallet } = useFdpStorage(); const { intl } = useLocales(); + const { theme } = useContext(ThemeContext); /** * When user click by Save name button @@ -120,6 +124,7 @@ const Invite: FC = () => {
{step === STEP_CREATE && ( <> +

{intl.get('INVITES_DESCRIPTION')}

= () => {
-
+
-
+
{intl.get('INVITES_ADDRESS_BOOK')} +
+ {theme === 'light' ? : } + + {intl.get('INVITES_TOOLTIP')} + +
= ({ setCurrentPage((oldPage) => Math.max(oldPage - 1, 0)); }; - const getActionClasses = (invite: Invite) => - `flex-item cursor-pointer text-color-accents-purple-black dark:brighten dark:text-color-shade-white-night ${ - hoverInviteId === invite.id ? '' : 'hidden' - }`; + const getActionClasses = () => + 'flex-item cursor-pointer text-color-accents-purple-black dark:brighten dark:text-color-shade-white-night'; const isPreviousButtonDisabled = currentPage === 0; const isNextButtonDisabled = @@ -274,14 +272,14 @@ const Invites: FC = ({ {invite.name || invite.id}

Edit the name setInviteAction(invite, InviteMode.Edit)} /> Delete the invite = ({ >
{showDriveSideBar ? : null} -
+
{loginType === 'metamask' && inviteKey && ( = ({ setLoading(true); try { await handleSubmitForm(data.password); + setMetamaskPassphraseExplanation(false); closeModal(); } catch (e) { setErrorMessage(intl.get('GENERIC_ERROR', { message: e.message })); @@ -77,6 +82,16 @@ const PasswordModal: FC = ({ loading={loading} />
+
+ +

+ {intl.get('PASSPHRASE_EXPLANATION_2')} +

+
+
diff --git a/src/components/Modals/PreviewFileModal/PreviewFileModal.tsx b/src/components/Modals/PreviewFileModal/PreviewFileModal.tsx index 828a967c..5d1c56e2 100644 --- a/src/components/Modals/PreviewFileModal/PreviewFileModal.tsx +++ b/src/components/Modals/PreviewFileModal/PreviewFileModal.tsx @@ -30,9 +30,11 @@ import ShareDarkIcon from '@media/UI/share-dark.svg'; import DeleteLightIcon from '@media/UI/delete-light.svg'; import DeleteDarkIcon from '@media/UI/delete-dark.svg'; import Spinner from '@components/Spinner/Spinner'; -import FilePreview from '@components/FilePreview/FilePreview'; +import FilePreview, { + isFilePreviewSupported, +} from '@components/FilePreview/FilePreview'; import { FileItem } from '@fairdatasociety/fdp-storage'; -import { extractFileExtension } from '@utils/filename'; +import { extractFileExtension, rootPathToRelative } from '@utils/filename'; import { useLocales } from '@context/LocalesContext'; interface PreviewModalProps { @@ -54,29 +56,30 @@ const PreviewFileModal: FC = ({ const { activePod, directoryName } = useContext(PodContext); const [loading, setLoading] = useState(false); - const [imageSource, setImageSource] = useState(''); + const [fileContent, setFileContent] = useState(null); const [errorMessage, setErrorMessage] = useState(''); const [showShareFileModal, setShowShareFileModal] = useState(false); const [showConfirmDeleteModal, setShowConfirmDeleteModal] = useState(false); const { intl } = useLocales(); + const directory = rootPathToRelative(previewFile.path || directoryName); + useEffect(() => { + if (!isFilePreviewSupported(previewFile?.name)) { + return; + } + setLoading(true); downloadFile(fdpClientRef.current, { filename: previewFile?.name, - directory: directoryName, + directory, podName: activePod, }) .then(async (response) => { const blob = await response.arrayBuffer(); const content = new Blob([blob]); - if (previewFile?.name.endsWith('.json')) { - const json = await content.text(); - return setImageSource(JSON.parse(json)); - } - - setImageSource(window.URL.createObjectURL(content)); + setFileContent(content); }) .catch((e) => { setErrorMessage(intl.get('FILE_PREVIEW_ERROR')); @@ -91,7 +94,7 @@ const PreviewFileModal: FC = ({ downloadFile(fdpClientRef.current, { filename: previewFile?.name, - directory: directoryName, + directory, podName: activePod, }) .then((response) => { @@ -118,7 +121,7 @@ const PreviewFileModal: FC = ({ deleteFile(fdpClientRef.current, { file_name: previewFile?.name, podName: activePod, - path: formatDirectory(directoryName), + path: formatDirectory(directory), }) .then(() => { trackEvent({ @@ -152,16 +155,16 @@ const PreviewFileModal: FC = ({ headerTitle={intl.get('PREVIEW_FILE')} className="w-full md:w-98" > - {imageSource ? ( + {fileContent ? ( setErrorMessage(intl.get('FILE_PREVIEW_ERROR'))} /> ) : null} - + {errorMessage ? (
@@ -169,7 +172,7 @@ const PreviewFileModal: FC = ({
) : null} -

+

{previewFile?.name}

@@ -253,7 +256,7 @@ const PreviewFileModal: FC = ({ closeModal={() => setShowShareFileModal(false)} fileName={previewFile?.name} podName={activePod} - path={formatDirectory(directoryName)} + path={formatDirectory(directory)} /> = ({ diff --git a/src/components/Modals/UploadFileModal/UploadFileModal.tsx b/src/components/Modals/UploadFileModal/UploadFileModal.tsx index 7b9ebc39..c077b9be 100644 --- a/src/components/Modals/UploadFileModal/UploadFileModal.tsx +++ b/src/components/Modals/UploadFileModal/UploadFileModal.tsx @@ -1,4 +1,4 @@ -import { FC, useContext, useState } from 'react'; +import { FC, useContext, useRef, useState } from 'react'; import { useDropzone } from 'react-dropzone'; import { useMatomo } from '@datapunt/matomo-tracker-react'; @@ -21,6 +21,8 @@ import { CreatorModalProps } from '@interfaces/handlers'; import { addItemToCache, ContentType } from '@utils/cache'; import { getFdpPathByDirectory } from '@api/pod'; import { useLocales } from '@context/LocalesContext'; +import { FileItem, UploadProgressInfo } from '@fairdatasociety/fdp-storage'; +import ProgressBar from '@components/ProgressBar/ProgressBar'; const UploadFileModal: FC = ({ showModal, @@ -33,33 +35,35 @@ const UploadFileModal: FC = ({ const [loading, setLoading] = useState(false); const [message, setMessage] = useState(null); - const [fileToUpload, setFileToUpload] = useState(null); + const [filesToUpload, setFilesToUpload] = useState(null); + const uploadedItemsRef = useRef([]); + const [failedUplods, setFailedUplods] = useState([]); + const [uploadPercentage, setUploadPercentage] = useState(0); const [errorMessage, setErrorMessage] = useState(''); const { fdpClientRef, getAccountAddress } = useFdpStorage(); const { getRootProps, getInputProps } = useDropzone({ - onDrop: (acceptedFiles: any) => { + onDrop: (acceptedFiles: File[]) => { + uploadedItemsRef.current = []; + setFailedUplods([]); + setErrorMessage(''); if (activePod) { - setFileToUpload(acceptedFiles[0]); + setFilesToUpload(acceptedFiles); } }, }); const { intl } = useLocales(); - const handleUpload = async () => { - if (!(fileToUpload && activePod)) { - return; - } - - setLoading(true); - try { + const onClose = async () => { + if (uploadedItemsRef.current.length > 0) { const userAddress = await getAccountAddress(); const directory = directoryName || 'root'; const fdpPath = getFdpPathByDirectory(directory); - const item = await uploadFile(fdpClientRef.current, { - file: fileToUpload, - directory: directoryName, - podName: activePod, - }); + + setMessage(intl.get('SUCCESSFULLY_UPLOADED')); + + uploadedItemsRef.current.forEach((item) => + addItemToCache(userAddress, activePod, fdpPath, item, ContentType.FILE) + ); trackEvent({ category: 'Upload', @@ -69,12 +73,85 @@ const UploadFileModal: FC = ({ href: window.location.href, }); - addItemToCache(userAddress, activePod, fdpPath, item, ContentType.FILE); updateDrive({ isUseCacheOnly: true, }); - closeModal(); - setMessage(intl.get('SUCCESSFULLY_UPLOADED')); + } + + closeModal(); + }; + + const calculateUploadPercentage = ( + completedCount: number, + totalCount: number, + currentPercentage: number + ): number => { + return ( + ((completedCount * 100 + currentPercentage) / (totalCount * 100)) * 100 + ); + }; + + const handleUpload = async () => { + setErrorMessage(''); + + if (loading || !(filesToUpload && activePod)) { + return; + } + + setLoading(true); + setUploadPercentage(0); + + try { + setFailedUplods([]); + + await filesToUpload.reduce(async (prevUpload, file) => { + try { + await prevUpload; + + if (uploadedItemsRef.current.some(({ name }) => name === file.name)) { + return; + } + + const item = await uploadFile( + fdpClientRef.current, + { + file, + directory: directoryName, + podName: activePod, + }, + (event: UploadProgressInfo) => { + const { uploadPercentage } = event.data || {}; + + if (uploadPercentage) { + setUploadPercentage( + calculateUploadPercentage( + uploadedItemsRef.current.length + failedUplods.length, + filesToUpload.length, + uploadPercentage + ) + ); + } + } + ); + + uploadedItemsRef.current.push(item); + } catch (error) { + setFailedUplods((failedUplods) => [...failedUplods, file]); + setUploadPercentage( + calculateUploadPercentage( + uploadedItemsRef.current.length + failedUplods.length, + filesToUpload.length, + 100 + ) + ); + } + }, Promise.resolve()); + + if (uploadedItemsRef.current.length === filesToUpload.length) { + onClose(); + } else { + throw new Error("Some files weren't uploaded successfully."); + } } catch (e) { setErrorMessage(`${e.message}`); } finally { @@ -82,10 +159,22 @@ const UploadFileModal: FC = ({ } }; + const getFileUploadStatus = ( + file: File + ): 'pending' | 'success' | 'failed' => { + if (uploadedItemsRef.current.some((item) => item.name === file.name)) { + return 'success'; + } + if (failedUplods.some((failedFile) => failedFile.name === file.name)) { + return 'failed'; + } + return 'pending'; + }; + return ( , dark: , @@ -113,11 +202,21 @@ const UploadFileModal: FC = ({

- {fileToUpload ? ( -

- {intl.get('READY_TO_UPLOAD')} {fileToUpload?.name} -

- ) : null} + {filesToUpload && + filesToUpload.map((file) => { + const status = getFileUploadStatus(file); + return ( +

+ {status === 'pending' && intl.get('READY_TO_UPLOAD')}{' '} + {file?.name} +

+ ); + })} {errorMessage ? (
@@ -125,13 +224,15 @@ const UploadFileModal: FC = ({
) : null} + {loading && } +
diff --git a/src/components/NavigationBars/AuthenticationNavbar/AuthenticationNavbar.tsx b/src/components/NavigationBars/AuthenticationNavbar/AuthenticationNavbar.tsx index 4672192b..ed42efa6 100644 --- a/src/components/NavigationBars/AuthenticationNavbar/AuthenticationNavbar.tsx +++ b/src/components/NavigationBars/AuthenticationNavbar/AuthenticationNavbar.tsx @@ -20,24 +20,6 @@ const AuthenticationNavbar: FC = () => {
- - - -
+ )} +
+ ); }; export default DriveActionHeaderMobile; diff --git a/src/components/NavigationBars/DriveSideBar/DriveSideBar.tsx b/src/components/NavigationBars/DriveSideBar/DriveSideBar.tsx index 6d81d753..dc921cd2 100644 --- a/src/components/NavigationBars/DriveSideBar/DriveSideBar.tsx +++ b/src/components/NavigationBars/DriveSideBar/DriveSideBar.tsx @@ -25,8 +25,14 @@ import { useLocales } from '@context/LocalesContext'; const DriveSideBar: FC = () => { const { theme } = useContext(ThemeContext); - const { pods, setPods, activePod, setActivePod, setDirectoryName } = - useContext(PodContext); + const { + loading: podLoading, + pods, + setPods, + activePod, + setActivePod, + setDirectoryName, + } = useContext(PodContext); const { fdpClientRef } = useFdpStorage(); const [loading, setLoading] = useState(false); @@ -55,6 +61,9 @@ const DriveSideBar: FC = () => { }; const handleOpenPod = (podName: string) => { + if (podLoading) { + return; + } setActivePod(podName); setDirectoryName('root'); }; diff --git a/src/components/NavigationBars/MainNavigationBar/MainNavigationBar.tsx b/src/components/NavigationBars/MainNavigationBar/MainNavigationBar.tsx index 9abcc575..0678c37c 100644 --- a/src/components/NavigationBars/MainNavigationBar/MainNavigationBar.tsx +++ b/src/components/NavigationBars/MainNavigationBar/MainNavigationBar.tsx @@ -4,28 +4,62 @@ import { useFdpStorage } from '@context/FdpStorageContext'; import Logo from '@components/Logo/Logo'; import { SearchBar } from '@components/Inputs'; -import { UserDropdownToggle } from '@components/Buttons'; +import { Button, UserDropdownToggle } from '@components/Buttons'; // import { ActivityDropdownToggle } from '@components/Buttons'; import UserDropdown from './UserDropdown/UserDropdown'; import UserContext from '@context/UserContext'; import LanguageDropdown from '@components/Dropdowns/LanguageDropdown/LanguageDropdown'; +import { useDialogs } from '@context/DialogsContext'; + +import NavigationMenuLight from '@media/UI/drive-view-list-light.svg'; +import NavigationMenuDark from '@media/UI/drive-view-list-dark.svg'; +import ThemeContext from '@context/ThemeContext'; +import PodContext from '@context/PodContext'; +import { Transition } from '@headlessui/react'; + // import ActivityDropdown from './ActivityDropdown/ActivityDropdown'; const MainNavigationBar: FC> = () => { + const { theme } = useContext(ThemeContext); const [showUserDropdown, setShowUserDropdown] = useState(false); const { wallet } = useFdpStorage(); const { metamaskMigrationNotification } = useContext(UserContext); + const { setMobileNavigationOpen } = useDialogs(); + const { activePod } = useContext(PodContext); return (