From ea11c3de487d3ccba3131038541558171bf4628b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Leszczyk?= Date: Fri, 15 Apr 2022 20:45:22 +0200 Subject: [PATCH] feat(dashboard-v2): add sharing features, pagination and mobile view for /files page --- .../src/components/FileList/FileList.js | 71 +++----- .../src/components/FileList/FileTable.js | 170 +++++++----------- .../src/components/FileList/MobileFileList.js | 84 +++++++++ .../src/components/FileList/Pagination.js | 32 ++++ .../components/FileList/useSkylinkOptions.js | 35 ++++ .../components/FileList/useSkylinkSharing.js | 40 +++++ .../src/components/PopoverMenu/PopoverMenu.js | 29 ++- packages/dashboard-v2/src/pages/files.js | 2 +- packages/dashboard-v2/tailwind.config.js | 1 + 9 files changed, 310 insertions(+), 154 deletions(-) create mode 100644 packages/dashboard-v2/src/components/FileList/MobileFileList.js create mode 100644 packages/dashboard-v2/src/components/FileList/Pagination.js create mode 100644 packages/dashboard-v2/src/components/FileList/useSkylinkOptions.js create mode 100644 packages/dashboard-v2/src/components/FileList/useSkylinkSharing.js diff --git a/packages/dashboard-v2/src/components/FileList/FileList.js b/packages/dashboard-v2/src/components/FileList/FileList.js index 6342b970..ad6087ed 100644 --- a/packages/dashboard-v2/src/components/FileList/FileList.js +++ b/packages/dashboard-v2/src/components/FileList/FileList.js @@ -1,29 +1,40 @@ -import * as React from "react"; +import { useState } from "react"; import useSWR from "swr"; import { useMedia } from "react-use"; import theme from "../../lib/theme"; import { ContainerLoadingIndicator } from "../LoadingIndicator"; -import { Select, SelectOption } from "../Select"; -import { Switch } from "../Switch"; -import { TextInputIcon } from "../TextInputIcon/TextInputIcon"; -import { SearchIcon } from "../Icons"; import FileTable from "./FileTable"; import useFormattedFilesData from "./useFormattedFilesData"; +import { MobileFileList } from "./MobileFileList"; +import { Pagination } from "./Pagination"; + +const PAGE_SIZE = 10; const FileList = ({ type }) => { const isMediumScreenOrLarger = useMedia(`(min-width: ${theme.screens.md})`); - const { data, error } = useSWR(`user/${type}?pageSize=10`); + const [offset, setOffset] = useState(0); + const baseUrl = `user/${type}?pageSize=${PAGE_SIZE}`; + const { + data, + error, + mutate: refreshList, + } = useSWR(`${baseUrl}&offset=${offset}`, { + revalidateOnMount: true, + }); const items = useFormattedFilesData(data?.items || []); + const count = data?.count || 0; - const setFilter = (name, value) => console.log("filter", name, "set to", value); + // Next page preloading + const hasMoreRecords = data ? data.offset + data.pageSize < data.count : false; + const nextPageOffset = hasMoreRecords ? data.offset + data.pageSize : offset; + useSWR(`${baseUrl}&offset=${nextPageOffset}`); if (!items.length) { return (
- {/* TODO: proper error message */} {!data && !error && } {!data && error &&

An error occurred while loading this data.

} {data &&

No {type} found.

} @@ -32,42 +43,14 @@ const FileList = ({ type }) => { } return ( -
-
- } - onChange={console.log.bind(console)} - /> -
- setFilter("showSmallFiles", value)} className="mr-8"> - - Show small files - - -
- File type: - -
-
- Sort: - -
-
-
- {/* TODO: mobile view (it's not tabular) */} - {isMediumScreenOrLarger ? : "Mobile view"} -
+ <> + {isMediumScreenOrLarger ? ( + + ) : ( + + )} + + ); }; diff --git a/packages/dashboard-v2/src/components/FileList/FileTable.js b/packages/dashboard-v2/src/components/FileList/FileTable.js index c2f133d6..88477648 100644 --- a/packages/dashboard-v2/src/components/FileList/FileTable.js +++ b/packages/dashboard-v2/src/components/FileList/FileTable.js @@ -2,110 +2,78 @@ import { CogIcon, ShareIcon } from "../Icons"; import { PopoverMenu } from "../PopoverMenu/PopoverMenu"; import { Table, TableBody, TableCell, TableHead, TableHeadCell, TableRow } from "../Table"; import { CopyButton } from "../CopyButton"; +import { useSkylinkOptions } from "./useSkylinkOptions"; +import { useSkylinkSharing } from "./useSkylinkSharing"; -const buildShareMenu = (item) => { - return [ - { - label: "Facebook", - callback: () => { - console.info("share to Facebook", item); - }, - }, - { - label: "Twitter", - callback: () => { - console.info("share to Twitter", item); - }, - }, - { - label: "Discord", - callback: () => { - console.info("share to Discord", item); - }, - }, - ]; -}; +const SkylinkOptionsMenu = ({ skylink, onUpdated }) => { + const { inProgress, options } = useSkylinkOptions({ skylink, onUpdated }); -const buildOptionsMenu = (item) => { - return [ - { - label: "Preview", - callback: () => { - console.info("preview", item); - }, - }, - { - label: "Download", - callback: () => { - console.info("download", item); - }, - }, - { - label: "Unpin", - callback: () => { - console.info("unpin", item); - }, - }, - { - label: "Report", - callback: () => { - console.info("report", item); - }, - }, - ]; -}; - -export default function FileTable({ items }) { return ( - - - - Name - Type - - Size - - Uploaded - Skylink - Activity - - - - {items.map((item) => { - const { id, name, type, size, date, skylink } = item; + + + + ); +}; - return ( - - {name} - {type} - - {size} - - {date} - -
- - {skylink} -
-
- -
- - - - - - -
-
-
- ); - })} -
-
+const SkylinkSharingMenu = ({ skylink }) => { + const { options } = useSkylinkSharing(skylink); + + return ( + + + + ); +}; + +export default function FileTable({ items, onUpdated }) { + return ( +
+ + + + Name + Type + + Size + + Uploaded + Skylink + Activity + + + + {items.map((item) => { + const { id, name, type, size, date, skylink } = item; + + return ( + + {name} + {type} + + {size} + + {date} + +
+ + {skylink} +
+
+ +
+ + +
+
+
+ ); + })} +
+
+
); } diff --git a/packages/dashboard-v2/src/components/FileList/MobileFileList.js b/packages/dashboard-v2/src/components/FileList/MobileFileList.js new file mode 100644 index 00000000..bd11aa10 --- /dev/null +++ b/packages/dashboard-v2/src/components/FileList/MobileFileList.js @@ -0,0 +1,84 @@ +import { useState } from "react"; +import cn from "classnames"; + +import { ChevronDownIcon } from "../Icons"; +import { useSkylinkSharing } from "./useSkylinkSharing"; +import { ContainerLoadingIndicator } from "../LoadingIndicator"; +import { useSkylinkOptions } from "./useSkylinkOptions"; + +const SharingMenu = ({ skylink }) => { + const { options } = useSkylinkSharing(skylink); + + return ( +
+ {options.map(({ label, callback }, index) => ( + + ))} +
+ ); +}; + +const OptionsMenu = ({ skylink, onUpdated }) => { + const { inProgress, options } = useSkylinkOptions({ skylink, onUpdated }); + + return ( +
+
+ {options.map(({ label, callback }, index) => ( + + ))} +
+ {inProgress && ( + + )} +
+ ); +}; + +const ListItem = ({ item, onUpdated }) => { + const { name, type, size, date, skylink } = item; + const [open, setOpen] = useState(false); + + const toggle = () => setOpen((open) => !open); + + return ( +
+
+
+
{name}
+
+ {type} + {size} + {date} +
+
+ +
+
+ + +
+
+ ); +}; + +export const MobileFileList = ({ items, onUpdated }) => { + return ( +
+ {items.map((item) => ( + + ))} +
+ ); +}; diff --git a/packages/dashboard-v2/src/components/FileList/Pagination.js b/packages/dashboard-v2/src/components/FileList/Pagination.js new file mode 100644 index 00000000..248c03a3 --- /dev/null +++ b/packages/dashboard-v2/src/components/FileList/Pagination.js @@ -0,0 +1,32 @@ +import { Button } from "../Button"; + +export const Pagination = ({ count, offset, setOffset, pageSize }) => { + const start = count ? offset + 1 : 0; + const end = offset + pageSize > count ? count : offset + pageSize; + + const showPaginationButtons = offset > 0 || count > end; + + return ( + + ); +}; diff --git a/packages/dashboard-v2/src/components/FileList/useSkylinkOptions.js b/packages/dashboard-v2/src/components/FileList/useSkylinkOptions.js new file mode 100644 index 00000000..ad116cd4 --- /dev/null +++ b/packages/dashboard-v2/src/components/FileList/useSkylinkOptions.js @@ -0,0 +1,35 @@ +import { useMemo, useState } from "react"; + +import accountsService from "../../services/accountsService"; +import skynetClient from "../../services/skynetClient"; + +export const useSkylinkOptions = ({ skylink, onUpdated }) => { + const [inProgress, setInProgress] = useState(false); + + const options = useMemo( + () => [ + { + label: "Preview", + callback: async () => window.open(await skynetClient.getSkylinkUrl(skylink)), + }, + { + label: "Download", + callback: () => skynetClient.downloadFile(skylink), + }, + { + label: "Unpin", + callback: async () => { + setInProgress(true); + await accountsService.delete(`user/uploads/${skylink}`); + await onUpdated(); // No need to setInProgress(false), since at this point this hook should already be unmounted + }, + }, + ], + [skylink, onUpdated] + ); + + return { + inProgress, + options, + }; +}; diff --git a/packages/dashboard-v2/src/components/FileList/useSkylinkSharing.js b/packages/dashboard-v2/src/components/FileList/useSkylinkSharing.js new file mode 100644 index 00000000..0b09302b --- /dev/null +++ b/packages/dashboard-v2/src/components/FileList/useSkylinkSharing.js @@ -0,0 +1,40 @@ +import { useEffect, useMemo, useState } from "react"; +import copy from "copy-text-to-clipboard"; + +import skynetClient from "../../services/skynetClient"; + +const COPY_LINK_LABEL = "Copy link"; + +export const useSkylinkSharing = (skylink) => { + const [copied, setCopied] = useState(false); + const [copyLabel, setCopyLabel] = useState(COPY_LINK_LABEL); + + useEffect(() => { + if (copied) { + setCopyLabel("Copied!"); + + const timeout = setTimeout(() => setCopied(false), 1500); + + return () => clearTimeout(timeout); + } else { + setCopyLabel(COPY_LINK_LABEL); + } + }, [copied]); + + const options = useMemo( + () => [ + { + label: copyLabel, + callback: async () => { + setCopied(true); + copy(await skynetClient.getSkylinkUrl(skylink)); + }, + }, + ], + [skylink, copyLabel] + ); + + return { + options, + }; +}; diff --git a/packages/dashboard-v2/src/components/PopoverMenu/PopoverMenu.js b/packages/dashboard-v2/src/components/PopoverMenu/PopoverMenu.js index 1826cd92..dd5a8597 100644 --- a/packages/dashboard-v2/src/components/PopoverMenu/PopoverMenu.js +++ b/packages/dashboard-v2/src/components/PopoverMenu/PopoverMenu.js @@ -2,6 +2,9 @@ import { Children, cloneElement, useRef, useState } from "react"; import PropTypes from "prop-types"; import { useClickAway } from "react-use"; import styled, { css, keyframes } from "styled-components"; +import cn from "classnames"; + +import { ContainerLoadingIndicator } from "../LoadingIndicator"; const dropDown = keyframes` 0% { @@ -41,15 +44,15 @@ const Option = styled.li.attrs({ hover:before:content-['']`, })``; -export const PopoverMenu = ({ options, children, openClassName, ...props }) => { +export const PopoverMenu = ({ options, children, openClassName, inProgress, ...props }) => { const containerRef = useRef(); const [open, setOpen] = useState(false); useClickAway(containerRef, () => setOpen(false)); - const handleChoice = (callback) => () => { + const handleChoice = (callback) => async () => { + await callback(); setOpen(false); - callback(); }; return ( @@ -62,11 +65,16 @@ export const PopoverMenu = ({ options, children, openClassName, ...props }) => { )} {open && ( - {options.map(({ label, callback }) => ( - - ))} +
+ {options.map(({ label, callback }) => ( + + ))} + {inProgress && ( + + )} +
)} @@ -87,4 +95,9 @@ PopoverMenu.propTypes = { callback: PropTypes.func.isRequired, }) ).isRequired, + + /** + * If true, a loading icon will be displayed to signal an async action is taking place. + */ + inProgress: PropTypes.bool, }; diff --git a/packages/dashboard-v2/src/pages/files.js b/packages/dashboard-v2/src/pages/files.js index be856d4a..b927c09f 100644 --- a/packages/dashboard-v2/src/pages/files.js +++ b/packages/dashboard-v2/src/pages/files.js @@ -12,7 +12,7 @@ const FilesPage = () => { Files - + diff --git a/packages/dashboard-v2/tailwind.config.js b/packages/dashboard-v2/tailwind.config.js index 636cd40e..141d689e 100644 --- a/packages/dashboard-v2/tailwind.config.js +++ b/packages/dashboard-v2/tailwind.config.js @@ -29,6 +29,7 @@ module.exports = { textColor: (theme) => ({ ...theme("colors"), ...colors }), placeholderColor: (theme) => ({ ...theme("colors"), ...colors }), outlineColor: (theme) => ({ ...theme("colors"), ...colors }), + divideColor: (theme) => ({ ...theme("colors"), ...colors }), extend: { fontFamily: { sans: ["Sora", ...defaultTheme.fontFamily.sans],