- {/* 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:
- setFilter("type", value)}>
-
-
-
-
-
-
- Sort:
- setFilter("type", value)}>
-
-
-
-
-
-
-
-
- {/* 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) => (
+
+ {label}
+
+ ))}
+
+ );
+};
+
+const OptionsMenu = ({ skylink, onUpdated }) => {
+ const { inProgress, options } = useSkylinkOptions({ skylink, onUpdated });
+
+ return (
+
+
+ {options.map(({ label, callback }, index) => (
+
+ {label}
+
+ ))}
+
+ {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 (
+
+
+
+ Showing {start} to {end} of {count} results
+
+
+ {showPaginationButtons && (
+
+ setOffset(offset - pageSize)} className="!border-0">
+ Previous page
+
+ = count}
+ onClick={() => setOffset(offset + pageSize)}
+ className="!border-0"
+ >
+ Next page
+
+
+ )}
+
+ );
+};
diff --git a/packages/dashboard-v2/src/components/FileList/useFormattedFilesData.js b/packages/dashboard-v2/src/components/FileList/useFormattedFilesData.js
index 87bf1af6..10639458 100644
--- a/packages/dashboard-v2/src/components/FileList/useFormattedFilesData.js
+++ b/packages/dashboard-v2/src/components/FileList/useFormattedFilesData.js
@@ -1,7 +1,7 @@
import { useMemo } from "react";
-import prettyBytes from "pretty-bytes";
import dayjs from "dayjs";
import { DATE_FORMAT } from "../../lib/config";
+import humanBytes from "../../lib/humanBytes";
const parseFileName = (fileName) => {
const lastDotIndex = Math.max(0, fileName.lastIndexOf(".")) || Infinity;
@@ -16,7 +16,7 @@ const formatItem = ({ size, name: rawFileName, uploadedOn, downloadedOn, ...rest
return {
...rest,
date,
- size: prettyBytes(size),
+ size: humanBytes(size, { precision: 2 }),
type,
name,
};
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/Metadata/Metadata.js b/packages/dashboard-v2/src/components/Metadata/Metadata.js
index 5bb98330..0e1dd2b6 100644
--- a/packages/dashboard-v2/src/components/Metadata/Metadata.js
+++ b/packages/dashboard-v2/src/components/Metadata/Metadata.js
@@ -1,6 +1,13 @@
import { Helmet } from "react-helmet";
import { graphql, useStaticQuery } from "gatsby";
+import favicon from "../../../static/favicon.ico";
+import favicon16 from "../../../static/favicon-16x16.png";
+import favicon32 from "../../../static/favicon-32x32.png";
+import appleIcon144 from "../../../static/apple-touch-icon-144x144.png";
+import appleIcon152 from "../../../static/apple-touch-icon-152x152.png";
+import msTileIcon from "../../../static/mstile-144x144.png";
+
export const Metadata = ({ children }) => {
const { site } = useStaticQuery(
graphql`
@@ -18,7 +25,15 @@ export const Metadata = ({ children }) => {
return (
-
+
+
+
+
+
+
+
+
+
{children}
diff --git a/packages/dashboard-v2/src/components/Panel/Panel.js b/packages/dashboard-v2/src/components/Panel/Panel.js
index 27551ecd..fe50cc89 100644
--- a/packages/dashboard-v2/src/components/Panel/Panel.js
+++ b/packages/dashboard-v2/src/components/Panel/Panel.js
@@ -14,8 +14,8 @@ const PanelTitle = styled.h6.attrs({
*
* These additional props will be rendered onto the panel's body element.
*/
-export const Panel = ({ title, ...props }) => (
-
+export const Panel = ({ title, wrapperClassName, ...props }) => (
+
@@ -26,8 +26,13 @@ Panel.propTypes = {
* Label of the panel
*/
title: PropTypes.oneOfType([PropTypes.string, PropTypes.node]),
+ /**
+ * CSS class to be applied to the external container
+ */
+ wrapperClassName: PropTypes.string,
};
Panel.defaultProps = {
title: "",
+ wrapperClassName: "",
};
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 }) => (
-
- {label}
-
- ))}
+
+ {options.map(({ label, callback }) => (
+
+ {label}
+
+ ))}
+ {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/components/Slider/Slide.js b/packages/dashboard-v2/src/components/Slider/Slide.js
index 42d354ff..eafbc563 100644
--- a/packages/dashboard-v2/src/components/Slider/Slide.js
+++ b/packages/dashboard-v2/src/components/Slider/Slide.js
@@ -2,7 +2,7 @@ import styled from "styled-components";
import PropTypes from "prop-types";
const Slide = styled.div.attrs(({ isVisible }) => ({
- className: `slider-slide transition-opacity ${isVisible ? "" : "opacity-50 cursor-pointer select-none"}`,
+ className: `slider-slide transition-opacity h-full ${isVisible ? "" : "opacity-50 cursor-pointer select-none"}`,
}))``;
Slide.propTypes = {
diff --git a/packages/dashboard-v2/src/components/Slider/Slider.js b/packages/dashboard-v2/src/components/Slider/Slider.js
index 33c3bc2a..5b902c58 100644
--- a/packages/dashboard-v2/src/components/Slider/Slider.js
+++ b/packages/dashboard-v2/src/components/Slider/Slider.js
@@ -62,7 +62,7 @@ const Slider = ({ slides, breakpoints, scrollerClassName, className }) => {
const isVisible = index >= activeIndex && index < activeIndex + visibleSlides;
return (
-
+
file.webkitRelativePath || file.path || file.name;
@@ -88,7 +88,7 @@ export default function UploaderItem({ onUploadStateChange, upload }) {
{upload.status === "uploading" && (
- Uploading {bytes(upload.file.size * upload.progress)} of {bytes(upload.file.size)}
+ Uploading {humanBytes(upload.file.size * upload.progress)} of {humanBytes(upload.file.size)}
)}
{upload.status === "enqueued" &&
Upload in queue, please wait }
diff --git a/packages/dashboard-v2/src/components/Uploader/buildUploadErrorMessage.js b/packages/dashboard-v2/src/components/Uploader/buildUploadErrorMessage.js
index c41cd717..11cfed4b 100644
--- a/packages/dashboard-v2/src/components/Uploader/buildUploadErrorMessage.js
+++ b/packages/dashboard-v2/src/components/Uploader/buildUploadErrorMessage.js
@@ -1,5 +1,5 @@
import { getReasonPhrase } from "http-status-codes";
-import bytes from "pretty-bytes";
+import humanBytes from "../../lib/humanBytes";
export default function buildUploadErrorMessage(error) {
// The request was made and the server responded with a status code that falls out of the range of 2xx
@@ -29,7 +29,7 @@ export default function buildUploadErrorMessage(error) {
const matchTusMaxFileSizeError = error.message.match(/upload exceeds maximum size: \d+ > (?
\d+)/);
if (matchTusMaxFileSizeError) {
- return `File exceeds size limit of ${bytes(parseInt(matchTusMaxFileSizeError.groups.limit, 10))}`;
+ return `File exceeds size limit of ${humanBytes(matchTusMaxFileSizeError.groups.limit, { precision: 0 })}`;
}
// TODO: We should add a note "our team has been notified" and have some kind of notification with this error.
diff --git a/packages/dashboard-v2/src/hooks/useUpgradeRedirect.js b/packages/dashboard-v2/src/hooks/useUpgradeRedirect.js
index 54efd956..037db65f 100644
--- a/packages/dashboard-v2/src/hooks/useUpgradeRedirect.js
+++ b/packages/dashboard-v2/src/hooks/useUpgradeRedirect.js
@@ -17,7 +17,7 @@ export default function useUpgradeRedirect() {
if (isDataLoaded) {
if (settings.isSubscriptionRequired && !hasPaidSubscription) {
- navigate("/upgrade");
+ navigate("/payments");
} else {
setVerifyingSubscription(false);
}
diff --git a/packages/dashboard-v2/src/layouts/UserSettingsLayout.js b/packages/dashboard-v2/src/layouts/UserSettingsLayout.js
index f008ee40..9bda3f96 100644
--- a/packages/dashboard-v2/src/layouts/UserSettingsLayout.js
+++ b/packages/dashboard-v2/src/layouts/UserSettingsLayout.js
@@ -10,12 +10,14 @@ const Sidebar = () => (
Account
+ {/*
Notifications
Export
+ */}
Developer settings
diff --git a/packages/dashboard-v2/src/lib/humanBytes.js b/packages/dashboard-v2/src/lib/humanBytes.js
new file mode 100644
index 00000000..ac1fbfa2
--- /dev/null
+++ b/packages/dashboard-v2/src/lib/humanBytes.js
@@ -0,0 +1,21 @@
+const UNITS = ["B", "kB", "MB", "GB", "TB", "PB", "EB"];
+const BASE = 1024;
+const DEFAULT_OPTIONS = { precision: 1 };
+
+export default function humanBytes(bytes, { precision } = DEFAULT_OPTIONS) {
+ if (!Number.isFinite(bytes) || bytes < 0) {
+ throw new TypeError(`Expected a finite, positive number. Received: ${typeof bytes}: ${bytes}`);
+ }
+
+ let value = bytes;
+ let unitIndex = 0;
+
+ while (value >= BASE) {
+ value /= BASE;
+ unitIndex += 1;
+ }
+
+ const localizedValue = value.toLocaleString(undefined, { maximumFractionDigits: precision });
+
+ return `${localizedValue} ${UNITS[unitIndex]}`;
+}
diff --git a/packages/dashboard-v2/src/pages/404.js b/packages/dashboard-v2/src/pages/404.js
index fd99104f..36c9165a 100644
--- a/packages/dashboard-v2/src/pages/404.js
+++ b/packages/dashboard-v2/src/pages/404.js
@@ -1,54 +1,27 @@
import * as React from "react";
-import { Link } from "gatsby";
-// styles
-const pageStyles = {
- color: "#232129",
- padding: "96px",
- fontFamily: "-apple-system, Roboto, sans-serif, serif",
-};
-const headingStyles = {
- marginTop: 0,
- marginBottom: 64,
- maxWidth: 320,
-};
+import DashboardLayout from "../layouts/DashboardLayout";
-const paragraphStyles = {
- marginBottom: 48,
-};
-const codeStyles = {
- color: "#8A6534",
- padding: 4,
- backgroundColor: "#FFF4DB",
- fontSize: "1.25rem",
- borderRadius: 4,
-};
+import { Metadata } from "../components/Metadata";
+import HighlightedLink from "../components/HighlightedLink";
-// markup
const NotFoundPage = () => {
return (
-
- Not found
- Page not found
-
- Sorry{" "}
-
- 😔
- {" "}
- we couldn’t find what you were looking for.
-
- {process.env.NODE_ENV === "development" ? (
- <>
-
- Try creating a page in src/pages/
.
-
- >
- ) : null}
-
- Go home.
-
-
+
+
+ Not found
+
+
+ Oops! 😔
+ Whatever you're looking for is not here.
+
+ Would you like to go back to homepage ?
+
+
+
);
};
+NotFoundPage.Layout = DashboardLayout;
+
export default NotFoundPage;
diff --git a/packages/dashboard-v2/src/pages/auth/registration.js b/packages/dashboard-v2/src/pages/auth/registration.js
index 5764ad6a..899abe50 100644
--- a/packages/dashboard-v2/src/pages/auth/registration.js
+++ b/packages/dashboard-v2/src/pages/auth/registration.js
@@ -1,5 +1,4 @@
import { useCallback, useState } from "react";
-import bytes from "pretty-bytes";
import AuthLayout from "../../layouts/AuthLayout";
@@ -10,12 +9,13 @@ import { usePortalSettings } from "../../contexts/portal-settings";
import { PlansProvider, usePlans } from "../../contexts/plans";
import { Metadata } from "../../components/Metadata";
import { useUser } from "../../contexts/user";
+import humanBytes from "../../lib/humanBytes";
const FreePortalHeader = () => {
const { plans } = usePlans();
const freePlan = plans.find(({ price }) => price === 0);
- const freeStorage = freePlan?.limits ? bytes(freePlan.limits?.storageLimit, { binary: true }) : null;
+ const freeStorage = freePlan?.limits ? humanBytes(freePlan.limits?.storageLimit, { binary: true }) : null;
return (
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/src/pages/payments.js b/packages/dashboard-v2/src/pages/payments.js
index c720216c..adf2303b 100644
--- a/packages/dashboard-v2/src/pages/payments.js
+++ b/packages/dashboard-v2/src/pages/payments.js
@@ -1,11 +1,232 @@
import * as React from "react";
+import styled from "styled-components";
+import { useStripe } from "@stripe/react-stripe-js";
+import cn from "classnames";
+import { useUser } from "../contexts/user";
+import { PlansProvider } from "../contexts/plans/PlansProvider";
+import useActivePlan from "../hooks/useActivePlan";
import DashboardLayout from "../layouts/DashboardLayout";
+import { Panel } from "../components/Panel";
+import Slider from "../components/Slider/Slider";
+import { CheckmarkIcon } from "../components/Icons";
+import { Button } from "../components/Button";
+import { usePortalSettings } from "../contexts/portal-settings";
+import { Alert } from "../components/Alert";
+import HighlightedLink from "../components/HighlightedLink";
+import { Metadata } from "../components/Metadata";
+import accountsService from "../services/accountsService";
+import humanBytes from "../lib/humanBytes";
+import { Modal } from "../components/Modal";
-const PaymentsPage = () => {
- return <>PAYMENTS HISTORY>;
+const PAID_PORTAL_BREAKPOINTS = [
+ {
+ name: "lg",
+ scrollable: true,
+ visibleSlides: 3,
+ },
+ {
+ name: "sm",
+ scrollable: true,
+ visibleSlides: 2,
+ },
+ {
+ scrollable: true,
+ visibleSlides: 1,
+ },
+];
+
+const FREE_PORTAL_BREAKPOINTS = [
+ {
+ name: "xl",
+ scrollable: true,
+ visibleSlides: 4,
+ },
+ ...PAID_PORTAL_BREAKPOINTS,
+];
+
+const PlanSummaryItem = ({ children }) => (
+
+
+ {children}
+
+);
+
+const Description = styled.div`
+ display: -webkit-box;
+ -webkit-line-clamp: 4;
+ -webkit-box-orient: vertical;
+ overflow: hidden;
+ flex-shrink: 0;
+ height: 6rem;
+`;
+
+const Price = ({ price }) => (
+
+
+ $
+ {price}
+
+
per month
+
+);
+
+const bandwidth = (value) => `${humanBytes(value, { bits: true })}/s`;
+
+const storage = (value) => humanBytes(value, { binary: true });
+
+const localizedNumber = (value) => value.toLocaleString();
+
+const PlansSlider = () => {
+ const { user, error: userError } = useUser();
+ const { plans, loading, activePlan, error: plansError } = useActivePlan(user);
+ const { settings } = usePortalSettings();
+ const [showPaymentError, setShowPaymentError] = React.useState(false);
+ const stripe = useStripe();
+ // This will be the base plan that we compare upload/download speeds against.
+ // On will either be the user's active plan or lowest of available tiers.
+ const basePlan = activePlan || plans[0];
+
+ if (userError || plansError) {
+ return (
+
+
Oooops!
+
Something went wrong, please try again later.
+
+ );
+ }
+
+ const handleSubscribe = async (selectedPlan) => {
+ try {
+ const { sessionId } = await accountsService
+ .post("stripe/checkout", {
+ json: {
+ price: selectedPlan.stripe,
+ },
+ })
+ .json();
+ await stripe.redirectToCheckout({ sessionId });
+ } catch (error) {
+ setShowPaymentError(true);
+ console.error(error);
+ }
+ };
+
+ return (
+
+
+ Payments
+
+ {settings.isSubscriptionRequired && !activePlan && (
+
+ This Skynet portal requires a paid subscription.
+
+ If you're not ready for that yet, you can use your account on{" "}
+
+ SkynetFree.net
+ {" "}
+ to store up to 100GB for free.
+
+
+ )}
+ {!loading && (
+
{
+ const isHigherThanCurrent = plan.tier > activePlan?.tier;
+ const isCurrentPlanPaid = activePlan?.tier > 1;
+ const isCurrent = plan.tier === activePlan?.tier;
+ const isLower = plan.tier < activePlan?.tier;
+ const speed = plan.limits.uploadBandwidth;
+ const currentSpeed = basePlan?.limits?.uploadBandwidth;
+ const speedChange = speed > currentSpeed ? speed / currentSpeed : currentSpeed / speed;
+ const hasActivePlan = Boolean(activePlan);
+
+ return (
+
+ {isCurrent && (
+
+
+ Current plan
+
+
+ )}
+ {plan.name}
+ {plan.description}
+
+
+
+ {(!hasActivePlan || isHigherThanCurrent) &&
+ (isCurrentPlanPaid ? (
+
+ Upgrade
+
+ ) : (
+ handleSubscribe(plan)}>
+ Upgrade
+
+ ))}
+ {isCurrent && Current }
+ {isLower && (
+
+ Choose
+
+ )}
+
+ {plan.limits && (
+
+
+ Pin up to {storage(plan.limits.storageLimit)} on decentralized storage
+
+
+ Support for up to {localizedNumber(plan.limits.maxNumberUploads)} files
+
+
+ {speed === currentSpeed
+ ? `${bandwidth(plan.limits.uploadBandwidth)} upload and ${bandwidth(
+ plan.limits.downloadBandwidth
+ )} download`
+ : `${speedChange}X ${
+ speed > currentSpeed ? "faster" : "slower"
+ } upload and download speeds (${bandwidth(plan.limits.uploadBandwidth)} and ${bandwidth(
+ plan.limits.downloadBandwidth
+ )})`}
+
+
+ {plan.limits.maxUploadSize === plan.limits.storageLimit
+ ? "No limit to file upload size"
+ : `Upload files up to ${storage(plan.limits.maxUploadSize)}`}
+
+
+ )}
+
+ );
+ })}
+ breakpoints={settings.isSubscriptionRequired ? PAID_PORTAL_BREAKPOINTS : FREE_PORTAL_BREAKPOINTS}
+ className="px-8 sm:px-4 md:px-0 lg:px-0 mt-10"
+ />
+ )}
+ {showPaymentError && (
+ setShowPaymentError(false)}>
+ Oops! 😔
+ There was an error contacting our payments provider
+ Please try again later
+
+ )}
+
+ );
};
+const PaymentsPage = () => (
+
+
+
+);
+
PaymentsPage.Layout = DashboardLayout;
export default PaymentsPage;
diff --git a/packages/dashboard-v2/src/pages/upgrade.js b/packages/dashboard-v2/src/pages/upgrade.js
deleted file mode 100644
index 9f69487e..00000000
--- a/packages/dashboard-v2/src/pages/upgrade.js
+++ /dev/null
@@ -1,157 +0,0 @@
-import * as React from "react";
-import bytes from "pretty-bytes";
-import styled from "styled-components";
-
-import { useUser } from "../contexts/user";
-import { PlansProvider } from "../contexts/plans/PlansProvider";
-import useActivePlan from "../hooks/useActivePlan";
-import DashboardLayout from "../layouts/DashboardLayout";
-import { Panel } from "../components/Panel";
-import Slider from "../components/Slider/Slider";
-import { CheckmarkIcon } from "../components/Icons";
-import { Button } from "../components/Button";
-import { usePortalSettings } from "../contexts/portal-settings";
-import { Alert } from "../components/Alert";
-import HighlightedLink from "../components/HighlightedLink";
-import { Metadata } from "../components/Metadata";
-
-const PAID_PORTAL_BREAKPOINTS = [
- {
- name: "lg",
- scrollable: true,
- visibleSlides: 3,
- },
- {
- name: "sm",
- scrollable: true,
- visibleSlides: 2,
- },
- {
- scrollable: true,
- visibleSlides: 1,
- },
-];
-
-const FREE_PORTAL_BREAKPOINTS = [
- {
- name: "xl",
- scrollable: true,
- visibleSlides: 4,
- },
- ...PAID_PORTAL_BREAKPOINTS,
-];
-
-const PlanSummaryItem = ({ children }) => (
-
-
- {children}
-
-);
-
-const Description = styled.div`
- display: -webkit-box;
- -webkit-line-clamp: 4;
- -webkit-box-orient: vertical;
- overflow: hidden;
- flex-shrink: 0;
- height: 6rem;
-`;
-
-const Price = ({ price }) => (
-
-
- $
- {price}
-
-
per month
-
-);
-
-const bandwidth = (value) => `${bytes(value, { bits: true })}/s`;
-
-const storage = (value) => bytes(value, { binary: true });
-
-const localizedNumber = (value) => value.toLocaleString();
-
-const PlansSlider = () => {
- const { user, error: userError } = useUser();
- const { plans, loading, activePlan, error: plansError } = useActivePlan(user);
- const { settings } = usePortalSettings();
-
- if (userError || plansError) {
- return (
-
-
Oooops!
-
Something went wrong, please try again later.
-
- );
- }
-
- return (
-
-
- Upgrade
-
- {settings.isSubscriptionRequired && !activePlan && (
-
- This Skynet portal requires a paid subscription.
-
- If you're not ready for that yet, you can use your account on{" "}
-
- SkynetFree.net
- {" "}
- to store up to 100GB for free.
-
-
- )}
- {!loading && (
-
{
- const isHigherThanCurrent = plan.tier > activePlan?.tier;
- const isCurrent = plan.tier === activePlan?.tier;
-
- return (
-
- {plan.name}
- {plan.description}
-
-
-
-
- {isHigherThanCurrent && "Upgrade"}
- {isCurrent && "Current"}
- {!isHigherThanCurrent && !isCurrent && "Choose"}
-
-
- {plan.limits && (
-
-
- Pin up to {storage(plan.limits.storageLimit)} of censorship-resistant storage
-
-
- Support for up to {localizedNumber(plan.limits.maxNumberUploads)} files
-
- {bandwidth(plan.limits.uploadBandwidth)} upload bandwidth
- {bandwidth(plan.limits.downloadBandwidth)} download bandwidth
-
- )}
-
- );
- })}
- breakpoints={settings.isSubscriptionRequired ? PAID_PORTAL_BREAKPOINTS : FREE_PORTAL_BREAKPOINTS}
- className="px-8 sm:px-4 md:px-0 lg:px-0"
- />
- )}
-
- );
-};
-
-const UpgradePage = () => (
-
-
-
-);
-
-UpgradePage.Layout = DashboardLayout;
-
-export default UpgradePage;
diff --git a/packages/dashboard-v2/static/apple-touch-icon-144x144.png b/packages/dashboard-v2/static/apple-touch-icon-144x144.png
new file mode 100644
index 00000000..ec40c3e0
Binary files /dev/null and b/packages/dashboard-v2/static/apple-touch-icon-144x144.png differ
diff --git a/packages/dashboard-v2/static/apple-touch-icon-152x152.png b/packages/dashboard-v2/static/apple-touch-icon-152x152.png
new file mode 100644
index 00000000..f65a5ddb
Binary files /dev/null and b/packages/dashboard-v2/static/apple-touch-icon-152x152.png differ
diff --git a/packages/dashboard-v2/static/favicon-16x16.png b/packages/dashboard-v2/static/favicon-16x16.png
new file mode 100644
index 00000000..10935441
Binary files /dev/null and b/packages/dashboard-v2/static/favicon-16x16.png differ
diff --git a/packages/dashboard-v2/static/favicon-32x32.png b/packages/dashboard-v2/static/favicon-32x32.png
new file mode 100644
index 00000000..99ee3c1c
Binary files /dev/null and b/packages/dashboard-v2/static/favicon-32x32.png differ
diff --git a/packages/dashboard-v2/static/favicon.ico b/packages/dashboard-v2/static/favicon.ico
index 9229fbf7..5c4ae303 100644
Binary files a/packages/dashboard-v2/static/favicon.ico and b/packages/dashboard-v2/static/favicon.ico differ
diff --git a/packages/dashboard-v2/static/mstile-144x144.png b/packages/dashboard-v2/static/mstile-144x144.png
new file mode 100644
index 00000000..ec40c3e0
Binary files /dev/null and b/packages/dashboard-v2/static/mstile-144x144.png differ
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],
diff --git a/packages/dashboard-v2/yarn.lock b/packages/dashboard-v2/yarn.lock
index 0ac39653..ae43c2db 100644
--- a/packages/dashboard-v2/yarn.lock
+++ b/packages/dashboard-v2/yarn.lock
@@ -3107,6 +3107,18 @@
resolve-from "^5.0.0"
store2 "^2.12.0"
+"@stripe/react-stripe-js@^1.7.1":
+ version "1.7.1"
+ resolved "https://registry.yarnpkg.com/@stripe/react-stripe-js/-/react-stripe-js-1.7.1.tgz#6e1db8f4a0eaf2193b153173d4aa7c38b681310d"
+ integrity sha512-GiUPoMo0xVvmpRD6JR9JAhAZ0W3ZpnYZNi0KE+91+tzrSFVpChKZbeSsJ5InlZhHFk9NckJCt1wOYBTqNsvt3A==
+ dependencies:
+ prop-types "^15.7.2"
+
+"@stripe/stripe-js@^1.27.0":
+ version "1.27.0"
+ resolved "https://registry.yarnpkg.com/@stripe/stripe-js/-/stripe-js-1.27.0.tgz#ab0c82fa89fd40260de4414f69868b769e810550"
+ integrity sha512-SEiybUBu+tlsFKuzdFFydxxjkbrdzHo0tz/naYC5Dt9or/Ux2gcKJBPYQ4RmqQCNHFxgyNj6UYsclywwhe2inQ==
+
"@szmarczak/http-timer@^1.1.2":
version "1.1.2"
resolved "https://registry.yarnpkg.com/@szmarczak/http-timer/-/http-timer-1.1.2.tgz#b1665e2c461a2cd92f4c1bbf50d5454de0d4b421"
@@ -12696,11 +12708,6 @@ pretty-bytes@^5.4.1:
resolved "https://registry.yarnpkg.com/pretty-bytes/-/pretty-bytes-5.6.0.tgz#356256f643804773c82f64723fe78c92c62beaeb"
integrity sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg==
-pretty-bytes@^6.0.0:
- version "6.0.0"
- resolved "https://registry.yarnpkg.com/pretty-bytes/-/pretty-bytes-6.0.0.tgz#928be2ad1f51a2e336add8ba764739f9776a8140"
- integrity sha512-6UqkYefdogmzqAZWzJ7laYeJnaXDy2/J+ZqiiMtS7t7OfpXWTlaeGMwX8U6EFvPV/YWWEKRkS8hKS4k60WHTOg==
-
pretty-error@^2.1.1, pretty-error@^2.1.2:
version "2.1.2"
resolved "https://registry.yarnpkg.com/pretty-error/-/pretty-error-2.1.2.tgz#be89f82d81b1c86ec8fdfbc385045882727f93b6"