diff --git a/packages/dashboard-v2/src/components/CurrentPlan/CurrentPlan.js b/packages/dashboard-v2/src/components/CurrentPlan/CurrentPlan.js index c5cdee36..f8a5cf9e 100644 --- a/packages/dashboard-v2/src/components/CurrentPlan/CurrentPlan.js +++ b/packages/dashboard-v2/src/components/CurrentPlan/CurrentPlan.js @@ -2,7 +2,8 @@ import dayjs from "dayjs"; import relativeTime from "dayjs/plugin/relativeTime"; import { useUser } from "../../contexts/user"; -import useSubscriptionPlans from "../../hooks/useSubscriptionPlans"; +import useActivePlan from "../../hooks/useActivePlan"; +import { ContainerLoadingIndicator } from "../LoadingIndicator"; import LatestPayment from "./LatestPayment"; import SuggestedPlan from "./SuggestedPlan"; @@ -11,13 +12,10 @@ dayjs.extend(relativeTime); const CurrentPlan = () => { const { user, error: userError } = useUser(); - const { activePlan, plans, error: plansError } = useSubscriptionPlans(user); + const { plans, activePlan, error: plansError } = useActivePlan(user); if (!user || !activePlan) { - return ( - // TODO: a nicer loading indicator -
Loading...
- ); + return ; } if (userError || plansError) { diff --git a/packages/dashboard-v2/src/components/CurrentUsage/CurrentUsage.js b/packages/dashboard-v2/src/components/CurrentUsage/CurrentUsage.js index b467e1ea..44be79ed 100644 --- a/packages/dashboard-v2/src/components/CurrentUsage/CurrentUsage.js +++ b/packages/dashboard-v2/src/components/CurrentUsage/CurrentUsage.js @@ -1,24 +1,50 @@ -import * as React from "react"; +import { useEffect, useMemo, useState } from "react"; import fileSize from "pretty-bytes"; import { Link } from "gatsby"; +import useSWR from "swr"; + +import { useUser } from "../../contexts/user"; +import useActivePlan from "../../hooks/useActivePlan"; +import { ContainerLoadingIndicator } from "../LoadingIndicator"; import { GraphBar } from "./GraphBar"; import { UsageGraph } from "./UsageGraph"; -// TODO: get real data -const useUsageData = () => ({ - files: { - used: 19_521, - limit: 20_000, - }, - storage: { - used: 23_000_000_000, - limit: 1_000_000_000_000, - }, -}); +const useUsageData = () => { + const { user } = useUser(); + const { activePlan, error } = useActivePlan(user); + const { data: stats, error: statsError } = useSWR("user/stats"); + + const [loading, setLoading] = useState(true); + const [usage, setUsage] = useState({}); + + const hasError = error || statsError; + const hasData = activePlan && stats; + + useEffect(() => { + if (hasData || hasError) { + setLoading(false); + } + + if (hasData && !hasError) { + setUsage({ + filesUsed: stats?.numUploads, + filesLimit: activePlan?.limits?.maxNumberUploads, + storageUsed: stats?.totalUploadsSize, + storageLimit: activePlan?.limits?.storageLimit, + }); + } + }, [hasData, hasError, stats, activePlan]); + + return { + error: error || statsError, + loading, + usage, + }; +}; const size = (bytes) => { - const text = fileSize(bytes, { maximumFractionDigits: 1 }); + const text = fileSize(bytes ?? 0, { maximumFractionDigits: 0 }); const [value, unit] = text.split(" "); return { @@ -28,12 +54,26 @@ const size = (bytes) => { }; }; -export default function CurrentUsage() { - const { files, storage } = useUsageData(); +const ErrorMessage = () => ( +
+

We were not able to fetch the current usage data.

+

We'll try again automatically.

+
+); - const storageUsage = size(storage.used); - const storageLimit = size(storage.limit); - const filesUsedLabel = React.useMemo(() => ({ value: files.used, unit: "files" }), [files.used]); +export default function CurrentUsage() { + const { usage, error, loading } = useUsageData(); + const storageUsage = size(usage.storageUsed); + const storageLimit = size(usage.storageLimit); + const filesUsedLabel = useMemo(() => ({ value: usage.filesUsed, unit: "files" }), [usage.filesUsed]); + + if (loading) { + return ; + } + + if (error) { + return ; + } return ( <> @@ -41,7 +81,7 @@ export default function CurrentUsage() { {storageUsage.text} of {storageLimit.text}

- {files.used} of {files.limit} files + {usage.filesUsed} of {usage.filesLimit} files

@@ -49,8 +89,8 @@ export default function CurrentUsage() { {storageLimit.text}
- - + +
Files @@ -62,7 +102,7 @@ export default function CurrentUsage() { UPGRADE {" "} {/* TODO: proper URL */} - {files.limit} + {usage.filesLimit}
diff --git a/packages/dashboard-v2/src/components/LatestActivity/ActivityTable.js b/packages/dashboard-v2/src/components/LatestActivity/ActivityTable.js index 647f9bf8..345a2daa 100644 --- a/packages/dashboard-v2/src/components/LatestActivity/ActivityTable.js +++ b/packages/dashboard-v2/src/components/LatestActivity/ActivityTable.js @@ -2,6 +2,7 @@ import * as React from "react"; import useSWR from "swr"; import { Table, TableBody, TableCell, TableRow } from "../Table"; +import { ContainerLoadingIndicator } from "../LoadingIndicator"; import useFormattedActivityData from "./useFormattedActivityData"; @@ -12,10 +13,10 @@ export default function ActivityTable({ type }) { if (!items.length) { return (
- {/* TODO: proper loading indicator / error message */} - {!data && !error &&

Loading...

} + {/* TODO: proper error message */} + {!data && !error && } {!data && error &&

An error occurred while loading this data.

} - {data &&

No files found.

} + {data && !error &&

No files found.

}
); } diff --git a/packages/dashboard-v2/src/components/LatestActivity/LatestActivity.js b/packages/dashboard-v2/src/components/LatestActivity/LatestActivity.js index 9c53554a..87825661 100644 --- a/packages/dashboard-v2/src/components/LatestActivity/LatestActivity.js +++ b/packages/dashboard-v2/src/components/LatestActivity/LatestActivity.js @@ -23,7 +23,7 @@ export default function LatestActivity() { - + diff --git a/packages/dashboard-v2/src/components/LoadingIndicator/ContainerLoadingIndicator.js b/packages/dashboard-v2/src/components/LoadingIndicator/ContainerLoadingIndicator.js new file mode 100644 index 00000000..de86a849 --- /dev/null +++ b/packages/dashboard-v2/src/components/LoadingIndicator/ContainerLoadingIndicator.js @@ -0,0 +1,18 @@ +import styled from "styled-components"; +import { CircledProgressIcon } from "../Icons"; + +/** + * This loading indicator is designed to be replace entire blocks (i.e. components) + * while they are fetching required data. + * + * It will take 50% of the parent's height, but won't get bigger than 150x150 px. + */ +const Wrapper = styled.div.attrs({ + className: "flex w-full h-full justify-center items-center p-8 text-palette-100", +})``; + +export const ContainerLoadingIndicator = (props) => ( + + + +); diff --git a/packages/dashboard-v2/src/components/LoadingIndicator/index.js b/packages/dashboard-v2/src/components/LoadingIndicator/index.js new file mode 100644 index 00000000..df7c2a88 --- /dev/null +++ b/packages/dashboard-v2/src/components/LoadingIndicator/index.js @@ -0,0 +1 @@ +export * from "./ContainerLoadingIndicator"; diff --git a/packages/dashboard-v2/src/contexts/plans/PlansContext.js b/packages/dashboard-v2/src/contexts/plans/PlansContext.js new file mode 100644 index 00000000..ff35b45e --- /dev/null +++ b/packages/dashboard-v2/src/contexts/plans/PlansContext.js @@ -0,0 +1,7 @@ +import { createContext } from "react"; + +export const PlansContext = createContext({ + plans: [], + limits: [], + error: null, +}); diff --git a/packages/dashboard-v2/src/contexts/plans/PlansProvider.js b/packages/dashboard-v2/src/contexts/plans/PlansProvider.js new file mode 100644 index 00000000..c481e296 --- /dev/null +++ b/packages/dashboard-v2/src/contexts/plans/PlansProvider.js @@ -0,0 +1,47 @@ +import { useEffect, useState } from "react"; +import useSWR from "swr"; + +import freePlan from "../../lib/tiers"; + +import { PlansContext } from "./PlansContext"; + +/** + * NOTE: this function heavily relies on the fact that each Plan's `tier` + * property corresponds to the plan's index in UserLimits array in + * skynet-accounts code. + * + * @see https://github.com/SkynetLabs/skynet-accounts/blob/7337e740b71b77e6d08016db801e293b8ad81abc/database/user.go#L53-L101 + */ +const aggregatePlansAndLimits = (plans, limits) => { + const sortedPlans = [freePlan, ...plans].sort((planA, planB) => planA.tier - planB.tier); + + // Decorate each plan with its corresponding limits data, if available. + if (limits?.length) { + return sortedPlans.map((plan) => ({ ...plan, limits: limits[plan.tier] || null })); + } + + // If we don't have the limits data yet, set just return the plans. + + return sortedPlans; +}; + +export const PlansProvider = ({ children }) => { + const { data: rawPlans, error: plansError } = useSWR("stripe/prices"); + const { data: limits, error: limitsError } = useSWR("limits"); + + const [plans, setPlans] = useState([freePlan]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + if (plansError || limitsError) { + setLoading(false); + setError(plansError || limitsError); + } else if (rawPlans) { + setLoading(false); + setPlans(aggregatePlansAndLimits(rawPlans, limits?.userLimits)); + } + }, [rawPlans, limits, plansError, limitsError]); + + return {children}; +}; diff --git a/packages/dashboard-v2/src/contexts/plans/index.js b/packages/dashboard-v2/src/contexts/plans/index.js new file mode 100644 index 00000000..84dd790f --- /dev/null +++ b/packages/dashboard-v2/src/contexts/plans/index.js @@ -0,0 +1,2 @@ +export * from "./PlansProvider"; +export * from "./usePlans"; diff --git a/packages/dashboard-v2/src/contexts/plans/usePlans.js b/packages/dashboard-v2/src/contexts/plans/usePlans.js new file mode 100644 index 00000000..f36e8595 --- /dev/null +++ b/packages/dashboard-v2/src/contexts/plans/usePlans.js @@ -0,0 +1,5 @@ +import { useContext } from "react"; + +import { PlansContext } from "./PlansContext"; + +export const usePlans = () => useContext(PlansContext); diff --git a/packages/dashboard-v2/src/hooks/useActivePlan.js b/packages/dashboard-v2/src/hooks/useActivePlan.js new file mode 100644 index 00000000..53e28b63 --- /dev/null +++ b/packages/dashboard-v2/src/hooks/useActivePlan.js @@ -0,0 +1,22 @@ +import { useEffect, useState } from "react"; + +import freeTier from "../lib/tiers"; +import { usePlans } from "../contexts/plans"; + +export default function useActivePlan(user) { + const { plans, error } = usePlans(); + + const [activePlan, setActivePlan] = useState(freeTier); + + useEffect(() => { + if (user) { + setActivePlan(plans.find((plan) => plan.tier === user.tier)); + } + }, [plans, user]); + + return { + error, + plans, + activePlan, + }; +} diff --git a/packages/dashboard-v2/src/hooks/useSubscriptionPlans.js b/packages/dashboard-v2/src/hooks/useSubscriptionPlans.js deleted file mode 100644 index 26658df8..00000000 --- a/packages/dashboard-v2/src/hooks/useSubscriptionPlans.js +++ /dev/null @@ -1,28 +0,0 @@ -import { useEffect, useState } from "react"; -import useSWR from "swr"; -import freeTier from "../lib/tiers"; - -export default function useSubscriptionPlans(user) { - const { data: paidPlans, error, mutate } = useSWR("stripe/prices"); - const [plans, setPlans] = useState([freeTier]); - const [activePlan, setActivePlan] = useState(freeTier); - - useEffect(() => { - if (paidPlans) { - setPlans((plans) => [...plans, ...paidPlans].sort((planA, planB) => planA.tier - planB.tier)); - } - }, [paidPlans]); - - useEffect(() => { - if (user) { - setActivePlan(plans.find((plan) => plan.tier === user.tier)); - } - }, [plans, user]); - - return { - error, - mutate, - plans, - activePlan, - }; -} diff --git a/packages/dashboard-v2/src/layouts/DashboardLayout.js b/packages/dashboard-v2/src/layouts/DashboardLayout.js index 07f4eabf..b369ece3 100644 --- a/packages/dashboard-v2/src/layouts/DashboardLayout.js +++ b/packages/dashboard-v2/src/layouts/DashboardLayout.js @@ -8,6 +8,7 @@ import { PageContainer } from "../components/PageContainer"; import { NavBar } from "../components/Navbar"; import { Footer } from "../components/Footer"; import { UserProvider, useUser } from "../contexts/user"; +import { ContainerLoadingIndicator } from "../components/LoadingIndicator"; const Wrapper = styled.div.attrs({ className: "min-h-screen overflow-hidden", @@ -25,7 +26,7 @@ const Layout = ({ children }) => { {!user && (
-

Loading...

{/* TODO: Do something nicer here */} +
)} {user && <>{children}} diff --git a/packages/dashboard-v2/src/pages/index.js b/packages/dashboard-v2/src/pages/index.js index 695e6ac3..4db97e04 100644 --- a/packages/dashboard-v2/src/pages/index.js +++ b/packages/dashboard-v2/src/pages/index.js @@ -2,6 +2,7 @@ import * as React from "react"; import { useMedia } from "react-use"; import theme from "../lib/theme"; +import { PlansProvider } from "../contexts/plans/PlansProvider"; import { ArrowRightIcon } from "../components/Icons"; import { Panel } from "../components/Panel"; import { Tab, TabPanel, Tabs } from "../components/Tabs"; @@ -16,7 +17,7 @@ const IndexPage = () => { const showRecentActivity = useMedia(`(min-width: ${theme.screens.md})`); return ( - <> +
{
)} - +
); };