From 1e713c61c9ec0044bcda215e7b6b0d21975aa4be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Leszczyk?= Date: Thu, 14 Apr 2022 18:29:07 +0200 Subject: [PATCH 01/17] fix(dashboard-v2): fix current usage graph on portals without Stripe configured --- .../src/components/CurrentUsage/CurrentUsage.js | 10 ++++++++-- .../src/components/CurrentUsage/GraphBar.js | 4 ++-- .../src/components/CurrentUsage/UsageGraph.js | 4 +++- .../src/contexts/plans/PlansProvider.js | 15 ++++++++++++--- packages/dashboard-v2/src/hooks/useActivePlan.js | 1 + 5 files changed, 26 insertions(+), 8 deletions(-) diff --git a/packages/dashboard-v2/src/components/CurrentUsage/CurrentUsage.js b/packages/dashboard-v2/src/components/CurrentUsage/CurrentUsage.js index 3947638d..f9dbbc36 100644 --- a/packages/dashboard-v2/src/components/CurrentUsage/CurrentUsage.js +++ b/packages/dashboard-v2/src/components/CurrentUsage/CurrentUsage.js @@ -1,6 +1,7 @@ import { useEffect, useMemo, useState } from "react"; import fileSize from "pretty-bytes"; import { Link } from "gatsby"; +import cn from "classnames"; import useSWR from "swr"; import { useUser } from "../../contexts/user"; @@ -62,7 +63,9 @@ const ErrorMessage = () => ( ); export default function CurrentUsage() { + const { activePlan, plans } = useActivePlan(); const { usage, error, loading } = useUsageData(); + const nextPlan = useMemo(() => plans.find(({ tier }) => tier > activePlan?.tier), [plans, activePlan]); const storageUsage = size(usage.storageUsed); const storageLimit = size(usage.storageLimit); const filesUsedLabel = useMemo(() => ({ value: usage.filesUsed, unit: "files" }), [usage.filesUsed]); @@ -89,7 +92,7 @@ export default function CurrentUsage() { {storageLimit.text} - +
@@ -97,7 +100,10 @@ export default function CurrentUsage() { UPGRADE {" "} diff --git a/packages/dashboard-v2/src/components/CurrentUsage/GraphBar.js b/packages/dashboard-v2/src/components/CurrentUsage/GraphBar.js index 1afab541..fd9a015e 100644 --- a/packages/dashboard-v2/src/components/CurrentUsage/GraphBar.js +++ b/packages/dashboard-v2/src/components/CurrentUsage/GraphBar.js @@ -21,11 +21,11 @@ const BarLabel = styled.span.attrs({ `} `; -export const GraphBar = ({ value, limit, label }) => { +export const GraphBar = ({ value, limit, label, className }) => { const percentage = typeof limit !== "number" || limit === 0 ? 0 : (value / limit) * 100; return ( -
+
diff --git a/packages/dashboard-v2/src/components/CurrentUsage/UsageGraph.js b/packages/dashboard-v2/src/components/CurrentUsage/UsageGraph.js index 3f6f23c2..de4e7e46 100644 --- a/packages/dashboard-v2/src/components/CurrentUsage/UsageGraph.js +++ b/packages/dashboard-v2/src/components/CurrentUsage/UsageGraph.js @@ -1,9 +1,11 @@ import styled from "styled-components"; +import usageGraphBg from "../../../static/images/usage-graph-bg.svg"; + export const UsageGraph = styled.div.attrs({ className: "w-full my-3 grid grid-flow-row grid-rows-2", })` height: 146px; - background: url(/images/usage-graph-bg.svg) no-repeat; + background: url(${usageGraphBg}) no-repeat; background-size: cover; `; diff --git a/packages/dashboard-v2/src/contexts/plans/PlansProvider.js b/packages/dashboard-v2/src/contexts/plans/PlansProvider.js index 135c9bcb..7c6579ad 100644 --- a/packages/dashboard-v2/src/contexts/plans/PlansProvider.js +++ b/packages/dashboard-v2/src/contexts/plans/PlansProvider.js @@ -19,7 +19,14 @@ const aggregatePlansAndLimits = (plans, limits, { includeFreePlan }) => { // Decorate each plan with its corresponding limits data, if available. if (limits?.length) { - return sortedPlans.map((plan) => ({ ...plan, limits: limits[plan.tier] || null })); + return limits.map((limitsDescriptor, index) => { + const asssociatedPlan = sortedPlans.find((plan) => plan.tier === index) || {}; + + return { + ...asssociatedPlan, + limits: limitsDescriptor || null, + }; + }); } // If we don't have the limits data yet, set just return the plans. @@ -40,10 +47,12 @@ export const PlansProvider = ({ children }) => { if (plansError || limitsError) { setLoading(false); setError(plansError || limitsError); - } else if (rawPlans) { + } else if (rawPlans || limits) { setLoading(false); setPlans( - aggregatePlansAndLimits(rawPlans, limits?.userLimits, { includeFreePlan: !settings.isSubscriptionRequired }) + aggregatePlansAndLimits(rawPlans || [], limits?.userLimits, { + includeFreePlan: !settings.isSubscriptionRequired, + }) ); } }, [rawPlans, limits, plansError, limitsError, settings.isSubscriptionRequired]); diff --git a/packages/dashboard-v2/src/hooks/useActivePlan.js b/packages/dashboard-v2/src/hooks/useActivePlan.js index de71a036..e843f510 100644 --- a/packages/dashboard-v2/src/hooks/useActivePlan.js +++ b/packages/dashboard-v2/src/hooks/useActivePlan.js @@ -10,6 +10,7 @@ export default function useActivePlan(user) { useEffect(() => { if (user) { + console.log("setActivePlan"); setActivePlan(plans.find((plan) => plan.tier === user.tier)); } }, [plans, user]); From e4fe4dd9015bc1e41db89f020c708f5411201d42 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Leszczyk?= Date: Thu, 14 Apr 2022 18:30:27 +0200 Subject: [PATCH 02/17] ops(dashboard-v2): publish new dashboard under /v2 prefix --- docker/nginx/conf.d/server/server.account | 4 ++++ packages/dashboard-v2/gatsby-config.js | 1 + packages/dashboard-v2/package.json | 4 ++-- .../src/components/AvatarUploader/AvatarUploader.js | 6 +++--- packages/dashboard-v2/src/layouts/AuthLayout.js | 7 +++++-- packages/dashboard-v2/src/layouts/DashboardLayout.js | 4 +++- .../dashboard-v2/src/pages/settings/developer-settings.js | 4 +++- packages/dashboard-v2/src/pages/settings/export.js | 4 +++- packages/dashboard-v2/src/pages/settings/notifications.js | 7 ++++--- 9 files changed, 28 insertions(+), 13 deletions(-) diff --git a/docker/nginx/conf.d/server/server.account b/docker/nginx/conf.d/server/server.account index 127ba4bf..9737003d 100644 --- a/docker/nginx/conf.d/server/server.account +++ b/docker/nginx/conf.d/server/server.account @@ -3,6 +3,10 @@ listen 443 ssl http2; include /etc/nginx/conf.d/include/ssl-settings; include /etc/nginx/conf.d/include/init-optional-variables; +location /v2 { + proxy_pass http://dashboard-v2:9000; +} + location / { proxy_pass http://dashboard:3000; } diff --git a/packages/dashboard-v2/gatsby-config.js b/packages/dashboard-v2/gatsby-config.js index 0e269557..d087fa65 100644 --- a/packages/dashboard-v2/gatsby-config.js +++ b/packages/dashboard-v2/gatsby-config.js @@ -11,6 +11,7 @@ module.exports = { title: `Account Dashboard`, siteUrl: `https://account.${GATSBY_PORTAL_DOMAIN}`, }, + pathPrefix: "/v2", trailingSlash: "never", plugins: [ "gatsby-plugin-image", diff --git a/packages/dashboard-v2/package.json b/packages/dashboard-v2/package.json index 694a6129..80070815 100644 --- a/packages/dashboard-v2/package.json +++ b/packages/dashboard-v2/package.json @@ -11,8 +11,8 @@ "develop": "gatsby develop", "develop:secure": "dotenv -e .env.development -- gatsby develop --https -p=443", "start": "gatsby develop", - "build": "gatsby build", - "serve": "gatsby serve", + "build": "gatsby build --prefix-paths", + "serve": "gatsby serve --prefix-paths", "clean": "gatsby clean", "lint": "eslint .", "prettier": "prettier .", diff --git a/packages/dashboard-v2/src/components/AvatarUploader/AvatarUploader.js b/packages/dashboard-v2/src/components/AvatarUploader/AvatarUploader.js index 2a3e400d..f97ca2d5 100644 --- a/packages/dashboard-v2/src/components/AvatarUploader/AvatarUploader.js +++ b/packages/dashboard-v2/src/components/AvatarUploader/AvatarUploader.js @@ -3,14 +3,14 @@ import { useEffect, useState } from "react"; import { useUser } from "../../contexts/user"; // import { SimpleUploadIcon } from "../Icons"; -const AVATAR_PLACEHOLDER = "/images/avatar-placeholder.svg"; +import avatarPlaceholder from "../../../static/images/avatar-placeholder.svg"; export const AvatarUploader = (props) => { const { user } = useUser(); - const [imageUrl, setImageUrl] = useState(AVATAR_PLACEHOLDER); + const [imageUrl, setImageUrl] = useState(avatarPlaceholder); useEffect(() => { - setImageUrl(user.avatarUrl ?? AVATAR_PLACEHOLDER); + setImageUrl(user.avatarUrl ?? avatarPlaceholder); }, [user]); return ( diff --git a/packages/dashboard-v2/src/layouts/AuthLayout.js b/packages/dashboard-v2/src/layouts/AuthLayout.js index 8141e606..323e4319 100644 --- a/packages/dashboard-v2/src/layouts/AuthLayout.js +++ b/packages/dashboard-v2/src/layouts/AuthLayout.js @@ -3,10 +3,13 @@ import styled from "styled-components"; import { UserProvider } from "../contexts/user"; +import skynetLogo from "../../static/images/logo-black-text.svg"; +import authBg from "../../static/images/auth-bg.svg"; + const Layout = styled.div.attrs({ className: "min-h-screen w-screen bg-black flex", })` - background-image: url(/images/auth-bg.svg); + background-image: url(${authBg}); background-repeat: no-repeat; background-position: center center; `; @@ -36,7 +39,7 @@ const AuthLayout =
- Skynet + Skynet
{children}
diff --git a/packages/dashboard-v2/src/layouts/DashboardLayout.js b/packages/dashboard-v2/src/layouts/DashboardLayout.js index 633057eb..8ac7c393 100644 --- a/packages/dashboard-v2/src/layouts/DashboardLayout.js +++ b/packages/dashboard-v2/src/layouts/DashboardLayout.js @@ -7,10 +7,12 @@ import { Footer } from "../components/Footer"; import { UserProvider, useUser } from "../contexts/user"; import { FullScreenLoadingIndicator } from "../components/LoadingIndicator"; +import dashboardBg from "../../static/images/dashboard-bg.svg"; + const Wrapper = styled.div.attrs({ className: "min-h-screen overflow-hidden", })` - background-image: url(/images/dashboard-bg.svg); + background-image: url(${dashboardBg}); background-position: center -280px; background-repeat: no-repeat; `; diff --git a/packages/dashboard-v2/src/pages/settings/developer-settings.js b/packages/dashboard-v2/src/pages/settings/developer-settings.js index cb58ab51..3f1917c1 100644 --- a/packages/dashboard-v2/src/pages/settings/developer-settings.js +++ b/packages/dashboard-v2/src/pages/settings/developer-settings.js @@ -10,6 +10,8 @@ import { AddSponsorKeyForm } from "../../components/forms/AddSponsorKeyForm"; import { Metadata } from "../../components/Metadata"; import HighlightedLink from "../../components/HighlightedLink"; +import apiKeysImg from "../../../static/images/api-keys.svg"; + const DeveloperSettingsPage = () => { const { data: allKeys = [], mutate: reloadKeys, error } = useSWR("user/apikeys"); const apiKeys = allKeys.filter(({ public: isPublic }) => isPublic === "false"); @@ -103,7 +105,7 @@ const DeveloperSettingsPage = () => {
- +
diff --git a/packages/dashboard-v2/src/pages/settings/export.js b/packages/dashboard-v2/src/pages/settings/export.js index 437f2b2f..aeb8aed3 100644 --- a/packages/dashboard-v2/src/pages/settings/export.js +++ b/packages/dashboard-v2/src/pages/settings/export.js @@ -6,6 +6,8 @@ import { Switch } from "../../components/Switch"; import { Button } from "../../components/Button"; import { Metadata } from "../../components/Metadata"; +import exportImg from "../../../static/images/import-export.svg"; + const useExportOptions = () => { const [pinnedFiles, setPinnedFiles] = useState(false); const [uploadHistory, setUploadHistory] = useState(false); @@ -65,7 +67,7 @@ const ExportPage = () => {
- +
diff --git a/packages/dashboard-v2/src/pages/settings/notifications.js b/packages/dashboard-v2/src/pages/settings/notifications.js index fc109d5c..5467f3a5 100644 --- a/packages/dashboard-v2/src/pages/settings/notifications.js +++ b/packages/dashboard-v2/src/pages/settings/notifications.js @@ -1,11 +1,12 @@ import * as React from "react"; -import { StaticImage } from "gatsby-plugin-image"; import UserSettingsLayout from "../../layouts/UserSettingsLayout"; import { Switch } from "../../components/Switch"; import { Metadata } from "../../components/Metadata"; +import inboxImg from "../../../static/images/inbox.svg"; + const NotificationsPage = () => { return ( <> @@ -37,8 +38,8 @@ const NotificationsPage = () => { -
- +
+
From babb6a48ad5dd2e6154908995a5583141c64eef1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Leszczyk?= Date: Thu, 14 Apr 2022 18:33:39 +0200 Subject: [PATCH 03/17] ops(dashboard-v2): fix SkynetClient on multi-server portals --- packages/dashboard-v2/src/services/skynetClient.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/dashboard-v2/src/services/skynetClient.js b/packages/dashboard-v2/src/services/skynetClient.js index a01e96f9..cb236ade 100644 --- a/packages/dashboard-v2/src/services/skynetClient.js +++ b/packages/dashboard-v2/src/services/skynetClient.js @@ -1,3 +1,6 @@ import { SkynetClient } from "skynet-js"; -export default new SkynetClient(`https://${process.env.GATSBY_PORTAL_DOMAIN}`); +const { NODE_ENV, GATSBY_PORTAL_DOMAIN } = process.env; + +// In production-like environment, let SkynetClient figure out the best portal +export default new SkynetClient(NODE_ENV === "development" ? `https://${GATSBY_PORTAL_DOMAIN}` : undefined); From 03c9fe01b238e113a3daac243cb81e914ef0c475 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Leszczyk?= Date: Thu, 14 Apr 2022 18:34:16 +0200 Subject: [PATCH 04/17] fix(dashboard-v2): fix skylinks validation for sponsor keys --- packages/dashboard-v2/src/components/forms/AddSponsorKeyForm.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/dashboard-v2/src/components/forms/AddSponsorKeyForm.js b/packages/dashboard-v2/src/components/forms/AddSponsorKeyForm.js index 0f8b8c62..236cdc9b 100644 --- a/packages/dashboard-v2/src/components/forms/AddSponsorKeyForm.js +++ b/packages/dashboard-v2/src/components/forms/AddSponsorKeyForm.js @@ -80,7 +80,7 @@ export const AddSponsorKeyForm = forwardRef(({ onSuccess }, ref) => { json: { name, public: "true", - skylinks: [...skylinks, nextSkylink].filter(Boolean).map(parseSkylink), + skylinks: [...skylinks, nextSkylink].filter(Boolean).map((skylink) => parseSkylink(skylink)), }, }) .json(); From 77d3145c0bc771ec54e39aa52842df9943ee4c37 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Leszczyk?= Date: Thu, 14 Apr 2022 18:37:30 +0200 Subject: [PATCH 05/17] ops(dashboard-v2): don't actually launch the new dashboard yet :) --- docker/nginx/conf.d/server/server.account | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/docker/nginx/conf.d/server/server.account b/docker/nginx/conf.d/server/server.account index 9737003d..9d444296 100644 --- a/docker/nginx/conf.d/server/server.account +++ b/docker/nginx/conf.d/server/server.account @@ -3,9 +3,10 @@ listen 443 ssl http2; include /etc/nginx/conf.d/include/ssl-settings; include /etc/nginx/conf.d/include/init-optional-variables; -location /v2 { - proxy_pass http://dashboard-v2:9000; -} +# Uncomment to launch new Dashboard under /v2 path +# location /v2 { +# proxy_pass http://dashboard-v2:9000; +# } location / { proxy_pass http://dashboard:3000; From d6fe2d2f1d875481af84b7b4408a1f43e6a1bc12 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Leszczyk?= Date: Thu, 14 Apr 2022 18:41:20 +0200 Subject: [PATCH 06/17] chore(dashboard-v2): cleanup console.log call --- packages/dashboard-v2/src/hooks/useActivePlan.js | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/dashboard-v2/src/hooks/useActivePlan.js b/packages/dashboard-v2/src/hooks/useActivePlan.js index e843f510..de71a036 100644 --- a/packages/dashboard-v2/src/hooks/useActivePlan.js +++ b/packages/dashboard-v2/src/hooks/useActivePlan.js @@ -10,7 +10,6 @@ export default function useActivePlan(user) { useEffect(() => { if (user) { - console.log("setActivePlan"); setActivePlan(plans.find((plan) => plan.tier === user.tier)); } }, [plans, user]); From 7248147ba72f127f6f37094dbad1404d53669c65 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Leszczyk?= Date: Fri, 15 Apr 2022 16:19:04 +0200 Subject: [PATCH 07/17] fix(dashboard-v2): always set portal domain for SkynetClient --- packages/dashboard-v2/src/services/skynetClient.js | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/packages/dashboard-v2/src/services/skynetClient.js b/packages/dashboard-v2/src/services/skynetClient.js index cb236ade..a01e96f9 100644 --- a/packages/dashboard-v2/src/services/skynetClient.js +++ b/packages/dashboard-v2/src/services/skynetClient.js @@ -1,6 +1,3 @@ import { SkynetClient } from "skynet-js"; -const { NODE_ENV, GATSBY_PORTAL_DOMAIN } = process.env; - -// In production-like environment, let SkynetClient figure out the best portal -export default new SkynetClient(NODE_ENV === "development" ? `https://${GATSBY_PORTAL_DOMAIN}` : undefined); +export default new SkynetClient(`https://${process.env.GATSBY_PORTAL_DOMAIN}`); From 916a420b720cb03a6a01391078bb81c038fe2e10 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Leszczyk?= Date: Fri, 15 Apr 2022 16:01:52 +0200 Subject: [PATCH 08/17] refactor(dashboard-v2): replace pretty-bytes with in-house solution --- packages/dashboard-v2/package.json | 1 - .../src/components/CurrentPlan/CurrentPlan.js | 4 ++-- .../components/CurrentUsage/CurrentUsage.js | 4 ++-- .../FileList/useFormattedFilesData.js | 4 ++-- .../src/components/Uploader/UploaderItem.js | 4 ++-- .../Uploader/buildUploadErrorMessage.js | 4 ++-- packages/dashboard-v2/src/lib/humanBytes.js | 21 +++++++++++++++++++ .../src/pages/auth/registration.js | 4 ++-- packages/dashboard-v2/src/pages/upgrade.js | 6 +++--- packages/dashboard-v2/yarn.lock | 5 ----- 10 files changed, 36 insertions(+), 21 deletions(-) create mode 100644 packages/dashboard-v2/src/lib/humanBytes.js diff --git a/packages/dashboard-v2/package.json b/packages/dashboard-v2/package.json index 80070815..2e17c56b 100644 --- a/packages/dashboard-v2/package.json +++ b/packages/dashboard-v2/package.json @@ -33,7 +33,6 @@ "nanoid": "^3.3.1", "path-browserify": "^1.0.1", "postcss": "^8.4.6", - "pretty-bytes": "^6.0.0", "react": "^17.0.1", "react-dom": "^17.0.1", "react-dropzone": "^12.0.4", diff --git a/packages/dashboard-v2/src/components/CurrentPlan/CurrentPlan.js b/packages/dashboard-v2/src/components/CurrentPlan/CurrentPlan.js index d6df8506..f9bc101a 100644 --- a/packages/dashboard-v2/src/components/CurrentPlan/CurrentPlan.js +++ b/packages/dashboard-v2/src/components/CurrentPlan/CurrentPlan.js @@ -1,9 +1,9 @@ import dayjs from "dayjs"; import relativeTime from "dayjs/plugin/relativeTime"; -import prettyBytes from "pretty-bytes"; import { useUser } from "../../contexts/user"; import useActivePlan from "../../hooks/useActivePlan"; +import humanBytes from "../../lib/humanBytes"; import { ContainerLoadingIndicator } from "../LoadingIndicator"; import LatestPayment from "./LatestPayment"; @@ -33,7 +33,7 @@ const CurrentPlan = () => {

{activePlan.name}

{activePlan.price === 0 && activePlan.limits && ( -

{prettyBytes(activePlan.limits.storageLimit, { binary: true })} without paying a dime! 🎉

+

{humanBytes(activePlan.limits.storageLimit)} without paying a dime! 🎉

)} {activePlan.price !== 0 && (user.subscriptionCancelAtPeriodEnd ? ( diff --git a/packages/dashboard-v2/src/components/CurrentUsage/CurrentUsage.js b/packages/dashboard-v2/src/components/CurrentUsage/CurrentUsage.js index f9dbbc36..c678585b 100644 --- a/packages/dashboard-v2/src/components/CurrentUsage/CurrentUsage.js +++ b/packages/dashboard-v2/src/components/CurrentUsage/CurrentUsage.js @@ -1,5 +1,4 @@ import { useEffect, useMemo, useState } from "react"; -import fileSize from "pretty-bytes"; import { Link } from "gatsby"; import cn from "classnames"; import useSWR from "swr"; @@ -10,6 +9,7 @@ import { ContainerLoadingIndicator } from "../LoadingIndicator"; import { GraphBar } from "./GraphBar"; import { UsageGraph } from "./UsageGraph"; +import humanBytes from "../../lib/humanBytes"; const useUsageData = () => { const { user } = useUser(); @@ -45,7 +45,7 @@ const useUsageData = () => { }; const size = (bytes) => { - const text = fileSize(bytes ?? 0, { maximumFractionDigits: 0, binary: true }); + const text = humanBytes(bytes ?? 0, { precision: 0 }); const [value, unit] = text.split(" "); return { 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/Uploader/UploaderItem.js b/packages/dashboard-v2/src/components/Uploader/UploaderItem.js index 46653877..7e19051b 100644 --- a/packages/dashboard-v2/src/components/Uploader/UploaderItem.js +++ b/packages/dashboard-v2/src/components/Uploader/UploaderItem.js @@ -1,6 +1,5 @@ import * as React from "react"; import cn from "classnames"; -import bytes from "pretty-bytes"; import { StatusCodes } from "http-status-codes"; import copy from "copy-text-to-clipboard"; import path from "path-browserify"; @@ -9,6 +8,7 @@ import { ProgressBar } from "./ProgressBar"; import UploaderItemIcon from "./UploaderItemIcon"; import buildUploadErrorMessage from "./buildUploadErrorMessage"; import skynetClient from "../../services/skynetClient"; +import humanBytes from "../../lib/humanBytes"; const getFilePath = (file) => 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/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/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/upgrade.js b/packages/dashboard-v2/src/pages/upgrade.js index 9f69487e..f3a531da 100644 --- a/packages/dashboard-v2/src/pages/upgrade.js +++ b/packages/dashboard-v2/src/pages/upgrade.js @@ -1,5 +1,4 @@ import * as React from "react"; -import bytes from "pretty-bytes"; import styled from "styled-components"; import { useUser } from "../contexts/user"; @@ -14,6 +13,7 @@ import { usePortalSettings } from "../contexts/portal-settings"; import { Alert } from "../components/Alert"; import HighlightedLink from "../components/HighlightedLink"; import { Metadata } from "../components/Metadata"; +import humanBytes from "../lib/humanBytes"; const PAID_PORTAL_BREAKPOINTS = [ { @@ -67,9 +67,9 @@ const Price = ({ price }) => (
); -const bandwidth = (value) => `${bytes(value, { bits: true })}/s`; +const bandwidth = (value) => `${humanBytes(value, { bits: true })}/s`; -const storage = (value) => bytes(value, { binary: true }); +const storage = (value) => humanBytes(value, { binary: true }); const localizedNumber = (value) => value.toLocaleString(); diff --git a/packages/dashboard-v2/yarn.lock b/packages/dashboard-v2/yarn.lock index 0ac39653..bf3123ca 100644 --- a/packages/dashboard-v2/yarn.lock +++ b/packages/dashboard-v2/yarn.lock @@ -12696,11 +12696,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" From 5a2a2b650815cfe378196172cb06c77eb388dcde Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Leszczyk?= Date: Fri, 15 Apr 2022 16:09:03 +0200 Subject: [PATCH 09/17] feat(dashboard-v2): add Stripe integration --- packages/dashboard-v2/gatsby-browser.js | 14 ++- packages/dashboard-v2/gatsby-ssr.js | 14 ++- packages/dashboard-v2/package.json | 2 + .../src/contexts/plans/PlansProvider.js | 16 ++-- packages/dashboard-v2/src/pages/upgrade.js | 95 +++++++++++++++++-- packages/dashboard-v2/yarn.lock | 12 +++ 6 files changed, 128 insertions(+), 25 deletions(-) diff --git a/packages/dashboard-v2/gatsby-browser.js b/packages/dashboard-v2/gatsby-browser.js index 030fcadd..927fd206 100644 --- a/packages/dashboard-v2/gatsby-browser.js +++ b/packages/dashboard-v2/gatsby-browser.js @@ -1,5 +1,7 @@ import * as React from "react"; import { SWRConfig } from "swr"; +import { Elements } from "@stripe/react-stripe-js"; +import { loadStripe } from "@stripe/stripe-js"; import "@fontsource/sora/300.css"; // light import "@fontsource/sora/400.css"; // normal import "@fontsource/sora/500.css"; // medium @@ -11,15 +13,19 @@ import swrConfig from "./src/lib/swrConfig"; import { MODAL_ROOT_ID } from "./src/components/Modal"; import { PortalSettingsProvider } from "./src/contexts/portal-settings"; +const stripePromise = loadStripe(process.env.GATSBY_STRIPE_PUBLISHABLE_KEY); + export function wrapPageElement({ element, props }) { const Layout = element.type.Layout ?? React.Fragment; return ( - - {element} -
- + + + {element} +
+ + ); diff --git a/packages/dashboard-v2/gatsby-ssr.js b/packages/dashboard-v2/gatsby-ssr.js index 030fcadd..927fd206 100644 --- a/packages/dashboard-v2/gatsby-ssr.js +++ b/packages/dashboard-v2/gatsby-ssr.js @@ -1,5 +1,7 @@ import * as React from "react"; import { SWRConfig } from "swr"; +import { Elements } from "@stripe/react-stripe-js"; +import { loadStripe } from "@stripe/stripe-js"; import "@fontsource/sora/300.css"; // light import "@fontsource/sora/400.css"; // normal import "@fontsource/sora/500.css"; // medium @@ -11,15 +13,19 @@ import swrConfig from "./src/lib/swrConfig"; import { MODAL_ROOT_ID } from "./src/components/Modal"; import { PortalSettingsProvider } from "./src/contexts/portal-settings"; +const stripePromise = loadStripe(process.env.GATSBY_STRIPE_PUBLISHABLE_KEY); + export function wrapPageElement({ element, props }) { const Layout = element.type.Layout ?? React.Fragment; return ( - - {element} -
- + + + {element} +
+ + ); diff --git a/packages/dashboard-v2/package.json b/packages/dashboard-v2/package.json index 2e17c56b..53edfa12 100644 --- a/packages/dashboard-v2/package.json +++ b/packages/dashboard-v2/package.json @@ -22,6 +22,8 @@ "dependencies": { "@fontsource/sora": "^4.5.3", "@fontsource/source-sans-pro": "^4.5.3", + "@stripe/react-stripe-js": "^1.7.1", + "@stripe/stripe-js": "^1.27.0", "classnames": "^2.3.1", "copy-text-to-clipboard": "^3.0.1", "dayjs": "^1.10.8", diff --git a/packages/dashboard-v2/src/contexts/plans/PlansProvider.js b/packages/dashboard-v2/src/contexts/plans/PlansProvider.js index 7c6579ad..906bd7f4 100644 --- a/packages/dashboard-v2/src/contexts/plans/PlansProvider.js +++ b/packages/dashboard-v2/src/contexts/plans/PlansProvider.js @@ -19,14 +19,16 @@ const aggregatePlansAndLimits = (plans, limits, { includeFreePlan }) => { // Decorate each plan with its corresponding limits data, if available. if (limits?.length) { - return limits.map((limitsDescriptor, index) => { - const asssociatedPlan = sortedPlans.find((plan) => plan.tier === index) || {}; + return limits + .map((limitsDescriptor, index) => { + const asssociatedPlan = sortedPlans.find((plan) => plan.tier === index) || {}; - return { - ...asssociatedPlan, - limits: limitsDescriptor || null, - }; - }); + return { + ...asssociatedPlan, + limits: limitsDescriptor || null, + }; + }) + .slice(includeFreePlan ? 1 : 2); } // If we don't have the limits data yet, set just return the plans. diff --git a/packages/dashboard-v2/src/pages/upgrade.js b/packages/dashboard-v2/src/pages/upgrade.js index f3a531da..61973ec0 100644 --- a/packages/dashboard-v2/src/pages/upgrade.js +++ b/packages/dashboard-v2/src/pages/upgrade.js @@ -1,5 +1,7 @@ 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"; @@ -13,7 +15,9 @@ 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 PAID_PORTAL_BREAKPOINTS = [ { @@ -77,6 +81,11 @@ const PlansSlider = () => { const { user, error: userError } = useUser(); const { plans, loading, activePlan, error: plansError } = useActivePlan(user); const { settings } = usePortalSettings(); + const [showPaymentError, setShowPaymentError] = React.useState(true); + 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 ( @@ -87,6 +96,22 @@ const PlansSlider = () => { ); } + const handleSubscribe = async (selectedPlan) => { + try { + const { sessionId } = await accountsService + .post("stripe/checkout", { + json: { + price: selectedPlan.stripe, + }, + }) + .json(); + await stripe.redirectToCheckout({ sessionId }); + } catch (error) { + console.log(error); + setShowPaymentError(true); + } + }; + return (
@@ -108,40 +133,90 @@ const PlansSlider = () => { { 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 ? ( + + ) : ( + + ))} + {isCurrent && } + {isLower && ( + + )}
{plan.limits && (
    - Pin up to {storage(plan.limits.storageLimit)} of censorship-resistant storage + Pin up to {storage(plan.limits.storageLimit)} on decentralized storage Support for up to {localizedNumber(plan.limits.maxNumberUploads)} files - {bandwidth(plan.limits.uploadBandwidth)} upload bandwidth - {bandwidth(plan.limits.downloadBandwidth)} download bandwidth + + {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" + 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

+
+ )}
); }; diff --git a/packages/dashboard-v2/yarn.lock b/packages/dashboard-v2/yarn.lock index bf3123ca..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" From 7e8d033bed3417f619fb578bcd01b8339409320c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Leszczyk?= Date: Fri, 15 Apr 2022 16:10:16 +0200 Subject: [PATCH 10/17] fix(dashboard-v2): fix Button styles on Safari when used as polymorphic component --- .../src/components/Button/Button.js | 28 +++++++++++++------ 1 file changed, 19 insertions(+), 9 deletions(-) diff --git a/packages/dashboard-v2/src/components/Button/Button.js b/packages/dashboard-v2/src/components/Button/Button.js index 328d52cd..2a49244e 100644 --- a/packages/dashboard-v2/src/components/Button/Button.js +++ b/packages/dashboard-v2/src/components/Button/Button.js @@ -5,15 +5,25 @@ import styled from "styled-components"; /** * Primary UI component for user interaction */ -export const Button = styled.button.attrs(({ disabled, $primary, type }) => ({ - type, - className: cn("px-6 py-2.5 rounded-full font-sans uppercase text-xs tracking-wide transition-[opacity_filter]", { - "bg-primary text-palette-600": $primary, - "bg-white border-2 border-black text-palette-600": !$primary, - "cursor-not-allowed opacity-60": disabled, - "hover:brightness-90": !disabled, - }), -}))``; +export const Button = styled.button.attrs(({ as: polymorphicAs, disabled, $primary, type }) => { + // We want to default to type=button in most cases, but sometimes we use this component + // as a polymorphic one (i.e. for links), and then we should avoid setting `type` property, + // as it breaks styling in Safari. + const typeAttr = polymorphicAs && polymorphicAs !== "button" ? undefined : type; + + return { + type: typeAttr, + className: cn( + "px-6 py-2.5 inline-block rounded-full font-sans uppercase text-xs tracking-wide transition-[opacity_filter]", + { + "bg-primary text-palette-600": $primary, + "bg-white border-2 border-black text-palette-600": !$primary, + "cursor-not-allowed opacity-60": disabled, + "hover:brightness-90": !disabled, + } + ), + }; +})``; Button.propTypes = { /** From b9d9059de40bae14f10cf37676aee879f9a7be79 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Leszczyk?= Date: Fri, 15 Apr 2022 16:10:45 +0200 Subject: [PATCH 11/17] feat(dashboard-v2): add platform-specific favicons --- .../src/components/Metadata/Metadata.js | 17 ++++++++++++++++- .../static/apple-touch-icon-144x144.png | Bin 0 -> 9842 bytes .../static/apple-touch-icon-152x152.png | Bin 0 -> 12071 bytes .../dashboard-v2/static/favicon-16x16.png | Bin 0 -> 453 bytes .../dashboard-v2/static/favicon-32x32.png | Bin 0 -> 1042 bytes packages/dashboard-v2/static/favicon.ico | Bin 2118 -> 5430 bytes .../dashboard-v2/static/mstile-144x144.png | Bin 0 -> 9842 bytes 7 files changed, 16 insertions(+), 1 deletion(-) create mode 100644 packages/dashboard-v2/static/apple-touch-icon-144x144.png create mode 100644 packages/dashboard-v2/static/apple-touch-icon-152x152.png create mode 100644 packages/dashboard-v2/static/favicon-16x16.png create mode 100644 packages/dashboard-v2/static/favicon-32x32.png create mode 100644 packages/dashboard-v2/static/mstile-144x144.png 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/static/apple-touch-icon-144x144.png b/packages/dashboard-v2/static/apple-touch-icon-144x144.png new file mode 100644 index 0000000000000000000000000000000000000000..ec40c3e02c28b36e5a95c103c7e8fb8af4fe853c GIT binary patch literal 9842 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 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..f65a5ddb0e8ac4376aee0cf8613dce0bfc9bcfac GIT binary patch literal 12071 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 literal 0 HcmV?d00001 diff --git a/packages/dashboard-v2/static/favicon-32x32.png b/packages/dashboard-v2/static/favicon-32x32.png new file mode 100644 index 0000000000000000000000000000000000000000..99ee3c1c4bbb8ef8e98fb9861dc8d9a59bebf791 GIT binary patch literal 1042 zcmV+t1nv8YP)3?uTv zjLJNfis?&GGLc9`gb(3MDrMM*bV^DnQWFx9J|qrdQlo?wnH5Qt3??Cih)56XT<6T0 zd-0A1y}00V?^%28z5Zu?{MWu*lK)vQ8!sWGm2fBSL$o#lD`>W?2oy3@Ja-9)KbngM+xXqTPu{r2J${ z&_mCdi)|QFY7XEwDL-2^>!|N5Wo^y*b}VSyIi!3K?!-W;nU06+W*v%bg(4eOK|Tq~ zD>g?(A)T#nZUe*ttgL9B91egz*rLdmE3&TA;UYBhwMxoQjoA61yd$~+rYf?@LjgpK zEW%h()%W9lK{Sg+?k`?~2i8?Iv#ZNPi?GP1;cc81mAqR-sYevk*XM~P($>{hT@+b2 z{z5l4OZnr?0Sp?yfTu+0R!jL8ifpWi&M47_zbmrwm?jGL2ns157MvfhT{ zutSl(A^7}Oyoq~oP?2p#x0J6eK|F?^@Ei_H`C=?B&)SAR71=gLwyXpOomGc*0R83n z5mES!YJ)H8L zxJ7hCCsyD`MfM_&NckMh6KVMXzX~!tCgl&}YQX{fu)gZBUIQKY4dYAAaa?nr_hUUP z4`~djlcLQw2-dr=bUvwE{{*jLyOf`QbmSdS+vnw3)iYR#rc6zHsq_K(u>=JI)o zsEw=2=YG77XREB*Y!+-1d27hpvhoKd<-cH#xS%wq;#Js)AMilw?0T#gQ5uJ5MBAR1 zR?~0`9<6BlFh|NyG_jo`$ZJ|{@5d_{X03!C2eVun1A zrwxzlm!;EU*uItk;?SAmLegE)p1~{F*z}rJWV6K$YIJR709Q1ds294D-GL9A90-R* zYL1s6CkhT2)5LZN{k9JvKui&=Hu*xPEo1|FTV3JWy_R>2*6bD4+>Y!MgRE)pM+RUJ z$S5&T=3}Oqf{mgZ5S9NK-j(vcc8(kwfZ>3;1S6iosSEuHxg^cxAL^kX^>>@u0ssI2 M07*qoM6N<$g0Xe$u>b%7 literal 0 HcmV?d00001 diff --git a/packages/dashboard-v2/static/favicon.ico b/packages/dashboard-v2/static/favicon.ico index 9229fbf780c52d9d25e2183c0effe239dd2728dd..5c4ae3031313d43c6a282374e98ce65102415205 100644 GIT binary patch literal 5430 zcmeI0eMpsO7>6I7s7R5+N}}T*NjcCB#L9NDK)Pku5@Egp3FoW2cQV#6NV5 z7-NhWF+xVj7$IZCkQORKG(yJMB3z6b5_-~z!zRAFey8`A$9L!0@q@8HI&g8{&vSo0 z-@fHsp38UTb#;27aJV)6Mw9)&a|5+|{ss6sd7o{~UULGy#1zkb*^MGCR8FF$)^c1T=@N$wA*R z$frZG$YK}>`(}_QpbdV7J5cJ?D50&|s*?|OpqRVR37W$gd=1B-1$KM!2Wa09ZSNrO z!C9z>i*OEJh7UkFl5pCK-$`5c5qKAruN9i022}4jq{ocZxVjXN)Oa<<_dszaV1C6` z!z8v{puIQ}#w&LvD900MG2cuQdmlW89lo8mYHJPWpw73MPJcV7cE9!c(z7eiC!oEq z1?}B1Xn$`)i67_d*bc)KM9=RceHwe#Sj&Tt*0dU4hc3`QteLO*ZdC?>b3wwnRGD*+ z#+`dG=-kvx&P`4`_uHG!E&Pog7M8GqeB_&sBUbNB92!CI(evN-uK;^r2)d)Cu;$&_ z{%Ywvtvh@YuE1T;ow4(*yEg$k%eC8CpM1q|9(0GT-m~N=zOQa|9OQTrW;ReaI)rM9>HQmks!aLlZ-h+a)!E?d zBex&EJK+!TYikbsKyP*h zXnfkIba$$ByIO;+=K!15%HQ`tX|JwVXzxNM!SZTu`nJ(r72gK>mNNTw z^po(fZ;lM}s`BC#|C#xq?}BeUyUuhUq|XJi874t-nz!bN_5R% zwU2|&x#oHlF2glY{W@6vn}}|Emv#PO{$hOlLF=h8*N6QT=-u!(*xK(w*O=Ac1X;h~ zu>Ay@OV+m-TP@s%DNtU`OY_?Y2i3MULoWgSQ|_5R{4}R0p!%z4+5+iQ4ZRaHpu3{F zIzt`O&2O`k9Ic?*;*6h%b`=c4X|QwKhHiJb)%V}1P9DwWCRqJ7e?#ck-mUH%y4G5E z%>3`5Z~7aXSi4iocPp3i<989PzT_}ueK)h^Bt~cEF<7pyY`%U!{^g*1ZE=#iKRKN@ z<-)fa=D~94{O3Fed^Pq(FJ5`>f!1^NZ-PJWux=dt=U{auA42-wiu$kDCPr^i68v1! z=ioBvEzo#Q!G|ykdN+K3)jqUdo%L+Q?gy=%pEFlo=RV`N+IlskHgXie8}KvaQd{p= zJ8b#w@W-NjPzybvJsyD@@I73Dv#{oV`|;29|KAD({x4M&1haWTaKR<`FNB7bqGZEz zY&4N-cz-&TC>l&I?=4HFdK1an-xnVJI9Rk)TL0S0D-Xs~K`_1q3qde_JqQL<31|pn Y%dwT%N*P+-Xo8&Nejx~!V}8ZpABV|x`Tzg` literal 2118 zcmV-M2)Xx(P)RT(~arVF-3X?a8o@+eOO1vEksNT^L4i?pS? zGjpFa_ntF5TNVgSR!EH+O{o}6G@z1bNRVez0u;Kt_nv!ax2u$A6CgkW0YZ2+L`XDR z1#DW{()IiQb7pobr6JWx{@i=coOAyFeE;`9t`LXuTbZsA!xQ~t-Jh2TFH3}{6DQEJ z6zHVn=d$E!>4ZfC^CXFAOg6;-{0rK#2(l(bi6EE4Cxy8zUvSIUSxJyd0Rx1evq8d3 zha9Xa(>gb=iy)OEOqCGRhoRgjg}D-58;WpffF}-SLzdsS%z?raVZb67@GTf{F2-MV z(;;3%66v9mpUF~Vb13?^4dbll7gOv$>{{hgcsXEBn}Asfe+!Jr!P=_3OLR`PC=u47m}Isz}7LsTJhYKlCX6 zBrpm{_G9fwFm8vh)jxegBJUdaX<*jugv4$kh1*sRUB<(^s6k0J8xl1@Q`Ifu62!Wz z+yV4rnwc|67!;s5J#&f^`FEE?D^M;J+P z;%n0lV?YH4c$so)2pvDO1^^yEi{W=B-2JRDm%Hq(_dtH+>c$*)kzLPGfJosn$QKta zdXmew2L_S72C-xyFa#|NKOLOcpAXIQu&HjCTdHZ^SoVznTV2RCE8l@Qznmh#O=J3h z78I6XfMX0$4v)2*l}sob3GuObZu4h&@jgg+93dg9d26nxo0q_V3qw69jAg11h#;Qj zDR}?8_2qtS+zU2;*M?vpB_R1gT_ocp*w^H!af_x%#v;V~H0>t?U?2=j)mQNTdY7PD zZXhCJQB%m#rURBP$K+3N@Hbh3=G!-{-;CwC{F!BN-%?!>mGOpHxy46*@Pyi|a1O0SL$oa(Oxy+yS{A3FTxaVkXbQxJ`r$#~X7z z9fPQnAa>SWcDz`7_lT%XxTM6vdWYOb$_E}95$<@4!#JM08fQ1lVbNqhSso#BHJ{WcsMGC+s#y>%;d%dC-$XAo6C?u463wTSt8Zevs@3$fuj%5eDahUm$bsR1c?k;$bnU{Q6G z%e5hDFGq%yXJFl#IBPZxFwkdI8<>z~c0H(G+CkpL$0@rrh6 z!b;0^Hrf0IDAoPZmRK~kif*_X;|+K=K;SP^z5r6+!1y}IjgWpAu)v^VWaa$>qR}3v zGas{V*m7Xe(PRkxXg2%`;~!Ar2(tJz&iRIrFF>whcV)?!F!w{qpRoSdE^(;fig+sT z)pDGaKyHFu>ZNk7Z#Xh)4S3=J0}u%!EL15d*q8U%n?De0HMc&=a~u3_7^$sr*=Z5=#{DWog{4r;P%E_h zCCAPZsfPA)*!UqQk7VpavYAR>lqbtMjYx2N<{jkw26<%#wZXJgG+ z*N|LpM^^j=o(nl5vj6G{7H?h8Y>jY>(6n6>)&N^XP*^gaKLT)S=|n$IDgsqZlJ*-%E4s_CX$-kA59MkIQa@fv`6?8@k zw^$U8So06Ne!n&3U&N(q*60gNWKkFOY>#U${)_>uw%0_Zh=dv&7HaewVf{yk?y^PX zX*3dZO~zAYCT&K^hMG}%GUjzB)AbYvw~dZ-5; z!<>{o530&fZU3Nf`+3q7K-7m>-d9&P+7gDpAywt9R6xgS?w{vDC|LpBIEbSh$nHyP0=?P zw;tMn89qqS+)N~k&F7e|6B6ir;v;d3??cJ&F#iP?H%Mm!b#eF8VSrZkti zGFk(^_;+E!A%^ce)A_=Ha+r79E$ViFRx$_Ca4go%YaylE-qQ4i87Y>Id~#4Me|QZS zy~FwqZM;@g@x?SS6;hRRY?lL)suBzkoldE#L;6}S+wRy2CSwue3)wHE_`m)L)*x*d wcr!_T1R3R2QHrnKyh^M|E8>b`?IF+j55uL_p0enzz5oCK07*qoM6N<$f=9md3IG5A diff --git a/packages/dashboard-v2/static/mstile-144x144.png b/packages/dashboard-v2/static/mstile-144x144.png new file mode 100644 index 0000000000000000000000000000000000000000..ec40c3e02c28b36e5a95c103c7e8fb8af4fe853c GIT binary patch literal 9842 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 literal 0 HcmV?d00001 From 525583af88dc68296449da005bed439478657e86 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Leszczyk?= Date: Fri, 15 Apr 2022 16:12:42 +0200 Subject: [PATCH 12/17] fix(dashboard-v2): allow passing additional classnames to Panels + ensure all slides are equal height --- packages/dashboard-v2/src/components/Panel/Panel.js | 9 +++++++-- packages/dashboard-v2/src/components/Slider/Slide.js | 2 +- packages/dashboard-v2/src/components/Slider/Slider.js | 2 +- 3 files changed, 9 insertions(+), 4 deletions(-) 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 }) => ( +
{title && {title}}
@@ -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/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 ( -
+
Date: Fri, 15 Apr 2022 16:53:49 +0200 Subject: [PATCH 13/17] refactor(dashboard-v2): move upgrade screen under /payments url --- .../components/CurrentPlan/SuggestedPlan.js | 2 +- .../components/CurrentUsage/CurrentUsage.js | 2 +- .../src/hooks/useUpgradeRedirect.js | 2 +- packages/dashboard-v2/src/pages/payments.js | 225 ++++++++++++++++- packages/dashboard-v2/src/pages/upgrade.js | 232 ------------------ 5 files changed, 226 insertions(+), 237 deletions(-) delete mode 100644 packages/dashboard-v2/src/pages/upgrade.js diff --git a/packages/dashboard-v2/src/components/CurrentPlan/SuggestedPlan.js b/packages/dashboard-v2/src/components/CurrentPlan/SuggestedPlan.js index 21aa9b48..ed59c382 100644 --- a/packages/dashboard-v2/src/components/CurrentPlan/SuggestedPlan.js +++ b/packages/dashboard-v2/src/components/CurrentPlan/SuggestedPlan.js @@ -14,7 +14,7 @@ const SuggestedPlan = ({ plans, activePlan }) => {

Discover {nextPlan.name}

{nextPlan.description}

-
diff --git a/packages/dashboard-v2/src/components/CurrentUsage/CurrentUsage.js b/packages/dashboard-v2/src/components/CurrentUsage/CurrentUsage.js index c678585b..cac40771 100644 --- a/packages/dashboard-v2/src/components/CurrentUsage/CurrentUsage.js +++ b/packages/dashboard-v2/src/components/CurrentUsage/CurrentUsage.js @@ -99,7 +99,7 @@ export default function CurrentUsage() { Files { - 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 ? ( + + ) : ( + + ))} + {isCurrent && } + {isLower && ( + + )} +
    + {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 61973ec0..00000000 --- a/packages/dashboard-v2/src/pages/upgrade.js +++ /dev/null @@ -1,232 +0,0 @@ -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 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(true); - 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) { - console.log(error); - setShowPaymentError(true); - } - }; - - 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 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 ? ( - - ) : ( - - ))} - {isCurrent && } - {isLower && ( - - )} -
    - {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 UpgradePage = () => ( - - - -); - -UpgradePage.Layout = DashboardLayout; - -export default UpgradePage; From 3ed20e670b759bf5c0ac49a79532bd38cbafd757 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Leszczyk?= Date: Fri, 15 Apr 2022 17:02:49 +0200 Subject: [PATCH 14/17] feat(dashboard-v2): add custom 404 page --- packages/dashboard-v2/src/pages/404.js | 61 +++++++------------------- 1 file changed, 17 insertions(+), 44 deletions(-) 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; From c325865faa6346d19558944f502ef7b0e1bf17b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Leszczyk?= Date: Fri, 15 Apr 2022 17:10:29 +0200 Subject: [PATCH 15/17] chore(dashboard-v2): hide notifications and import/export links --- packages/dashboard-v2/src/layouts/UserSettingsLayout.js | 2 ++ 1 file changed, 2 insertions(+) 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 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 16/17] 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], From 581c93d0cddef21359b322c6b277a35beb51423b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Leszczyk?= Date: Fri, 15 Apr 2022 23:16:15 +0200 Subject: [PATCH 17/17] Add stripe's publishable key for Gatsby --- docker-compose.accounts.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/docker-compose.accounts.yml b/docker-compose.accounts.yml index 3c2c46a9..468cc0b5 100644 --- a/docker-compose.accounts.yml +++ b/docker-compose.accounts.yml @@ -87,6 +87,7 @@ services: - .env environment: - GATSBY_PORTAL_DOMAIN=${PORTAL_DOMAIN} + - GATSBY_STRIPE_PUBLISHABLE_KEY=${STRIPE_PUBLISHABLE_KEY} volumes: - ./docker/data/dashboard-v2/.cache:/usr/app/.cache - ./docker/data/dashboard-v2/public:/usr/app/public