Merge branch 'master' into ivo/nginx_passes_apikey
This commit is contained in:
commit
15ac008772
|
@ -2,7 +2,8 @@ import dayjs from "dayjs";
|
||||||
import relativeTime from "dayjs/plugin/relativeTime";
|
import relativeTime from "dayjs/plugin/relativeTime";
|
||||||
|
|
||||||
import { useUser } from "../../contexts/user";
|
import { useUser } from "../../contexts/user";
|
||||||
import useSubscriptionPlans from "../../hooks/useSubscriptionPlans";
|
import useActivePlan from "../../hooks/useActivePlan";
|
||||||
|
import { ContainerLoadingIndicator } from "../LoadingIndicator";
|
||||||
|
|
||||||
import LatestPayment from "./LatestPayment";
|
import LatestPayment from "./LatestPayment";
|
||||||
import SuggestedPlan from "./SuggestedPlan";
|
import SuggestedPlan from "./SuggestedPlan";
|
||||||
|
@ -11,13 +12,10 @@ dayjs.extend(relativeTime);
|
||||||
|
|
||||||
const CurrentPlan = () => {
|
const CurrentPlan = () => {
|
||||||
const { user, error: userError } = useUser();
|
const { user, error: userError } = useUser();
|
||||||
const { activePlan, plans, error: plansError } = useSubscriptionPlans(user);
|
const { plans, activePlan, error: plansError } = useActivePlan(user);
|
||||||
|
|
||||||
if (!user || !activePlan) {
|
if (!user || !activePlan) {
|
||||||
return (
|
return <ContainerLoadingIndicator />;
|
||||||
// TODO: a nicer loading indicator
|
|
||||||
<div className="flex flex-col space-y-4 h-full justify-center items-center">Loading...</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (userError || plansError) {
|
if (userError || plansError) {
|
||||||
|
|
|
@ -1,24 +1,50 @@
|
||||||
import * as React from "react";
|
import { useEffect, useMemo, useState } from "react";
|
||||||
import fileSize from "pretty-bytes";
|
import fileSize from "pretty-bytes";
|
||||||
import { Link } from "gatsby";
|
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 { GraphBar } from "./GraphBar";
|
||||||
import { UsageGraph } from "./UsageGraph";
|
import { UsageGraph } from "./UsageGraph";
|
||||||
|
|
||||||
// TODO: get real data
|
const useUsageData = () => {
|
||||||
const useUsageData = () => ({
|
const { user } = useUser();
|
||||||
files: {
|
const { activePlan, error } = useActivePlan(user);
|
||||||
used: 19_521,
|
const { data: stats, error: statsError } = useSWR("user/stats");
|
||||||
limit: 20_000,
|
|
||||||
},
|
const [loading, setLoading] = useState(true);
|
||||||
storage: {
|
const [usage, setUsage] = useState({});
|
||||||
used: 23_000_000_000,
|
|
||||||
limit: 1_000_000_000_000,
|
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 size = (bytes) => {
|
||||||
const text = fileSize(bytes, { maximumFractionDigits: 1 });
|
const text = fileSize(bytes ?? 0, { maximumFractionDigits: 0 });
|
||||||
const [value, unit] = text.split(" ");
|
const [value, unit] = text.split(" ");
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
@ -28,12 +54,26 @@ const size = (bytes) => {
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function CurrentUsage() {
|
const ErrorMessage = () => (
|
||||||
const { files, storage } = useUsageData();
|
<div className="flex text-palette-300 flex-col space-y-4 h-full justify-center items-center">
|
||||||
|
<p>We were not able to fetch the current usage data.</p>
|
||||||
|
<p>We'll try again automatically.</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
const storageUsage = size(storage.used);
|
export default function CurrentUsage() {
|
||||||
const storageLimit = size(storage.limit);
|
const { usage, error, loading } = useUsageData();
|
||||||
const filesUsedLabel = React.useMemo(() => ({ value: files.used, unit: "files" }), [files.used]);
|
const storageUsage = size(usage.storageUsed);
|
||||||
|
const storageLimit = size(usage.storageLimit);
|
||||||
|
const filesUsedLabel = useMemo(() => ({ value: usage.filesUsed, unit: "files" }), [usage.filesUsed]);
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return <ContainerLoadingIndicator />;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return <ErrorMessage />;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
@ -41,7 +81,7 @@ export default function CurrentUsage() {
|
||||||
{storageUsage.text} of {storageLimit.text}
|
{storageUsage.text} of {storageLimit.text}
|
||||||
</h4>
|
</h4>
|
||||||
<p className="text-palette-400">
|
<p className="text-palette-400">
|
||||||
{files.used} of {files.limit} files
|
{usage.filesUsed} of {usage.filesLimit} files
|
||||||
</p>
|
</p>
|
||||||
<div className="relative mt-7 font-sans uppercase text-xs">
|
<div className="relative mt-7 font-sans uppercase text-xs">
|
||||||
<div className="flex place-content-between">
|
<div className="flex place-content-between">
|
||||||
|
@ -49,8 +89,8 @@ export default function CurrentUsage() {
|
||||||
<span>{storageLimit.text}</span>
|
<span>{storageLimit.text}</span>
|
||||||
</div>
|
</div>
|
||||||
<UsageGraph>
|
<UsageGraph>
|
||||||
<GraphBar value={storage.used} limit={storage.limit} label={storageUsage} />
|
<GraphBar value={usage.storageUsed} limit={usage.storageLimit} label={storageUsage} />
|
||||||
<GraphBar value={files.used} limit={files.limit} label={filesUsedLabel} />
|
<GraphBar value={usage.filesUsed} limit={usage.filesLimit} label={filesUsedLabel} />
|
||||||
</UsageGraph>
|
</UsageGraph>
|
||||||
<div className="flex place-content-between">
|
<div className="flex place-content-between">
|
||||||
<span>Files</span>
|
<span>Files</span>
|
||||||
|
@ -62,7 +102,7 @@ export default function CurrentUsage() {
|
||||||
UPGRADE
|
UPGRADE
|
||||||
</Link>{" "}
|
</Link>{" "}
|
||||||
{/* TODO: proper URL */}
|
{/* TODO: proper URL */}
|
||||||
<span>{files.limit}</span>
|
<span>{usage.filesLimit}</span>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -2,6 +2,7 @@ import * as React from "react";
|
||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
|
|
||||||
import { Table, TableBody, TableCell, TableRow } from "../Table";
|
import { Table, TableBody, TableCell, TableRow } from "../Table";
|
||||||
|
import { ContainerLoadingIndicator } from "../LoadingIndicator";
|
||||||
|
|
||||||
import useFormattedActivityData from "./useFormattedActivityData";
|
import useFormattedActivityData from "./useFormattedActivityData";
|
||||||
|
|
||||||
|
@ -12,10 +13,10 @@ export default function ActivityTable({ type }) {
|
||||||
if (!items.length) {
|
if (!items.length) {
|
||||||
return (
|
return (
|
||||||
<div className="flex w-full h-full justify-center items-center text-palette-400">
|
<div className="flex w-full h-full justify-center items-center text-palette-400">
|
||||||
{/* TODO: proper loading indicator / error message */}
|
{/* TODO: proper error message */}
|
||||||
{!data && !error && <p>Loading...</p>}
|
{!data && !error && <ContainerLoadingIndicator />}
|
||||||
{!data && error && <p>An error occurred while loading this data.</p>}
|
{!data && error && <p>An error occurred while loading this data.</p>}
|
||||||
{data && <p>No files found.</p>}
|
{data && !error && <p>No files found.</p>}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,7 +23,7 @@ export default function LatestActivity() {
|
||||||
<Tab id="uploads" title="Uploads" />
|
<Tab id="uploads" title="Uploads" />
|
||||||
<Tab id="downloads" title="Downloads" />
|
<Tab id="downloads" title="Downloads" />
|
||||||
<TabPanel tabId="uploads" className="pt-4">
|
<TabPanel tabId="uploads" className="pt-4">
|
||||||
<ActivityTable type="uplodads" />
|
<ActivityTable type="uploads" />
|
||||||
<ViewAllLink to="/files?tab=uploads" />
|
<ViewAllLink to="/files?tab=uploads" />
|
||||||
</TabPanel>
|
</TabPanel>
|
||||||
<TabPanel tabId="downloads" className="pt-4">
|
<TabPanel tabId="downloads" className="pt-4">
|
||||||
|
|
|
@ -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) => (
|
||||||
|
<Wrapper {...props}>
|
||||||
|
<CircledProgressIcon size="50%" className="max-w-[150px] max-h-[150px] animate-[spin_3s_linear_infinite]" />
|
||||||
|
</Wrapper>
|
||||||
|
);
|
|
@ -0,0 +1 @@
|
||||||
|
export * from "./ContainerLoadingIndicator";
|
|
@ -0,0 +1,7 @@
|
||||||
|
import { createContext } from "react";
|
||||||
|
|
||||||
|
export const PlansContext = createContext({
|
||||||
|
plans: [],
|
||||||
|
limits: [],
|
||||||
|
error: null,
|
||||||
|
});
|
|
@ -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 <PlansContext.Provider value={{ plans, error, loading }}>{children}</PlansContext.Provider>;
|
||||||
|
};
|
|
@ -0,0 +1,2 @@
|
||||||
|
export * from "./PlansProvider";
|
||||||
|
export * from "./usePlans";
|
|
@ -0,0 +1,5 @@
|
||||||
|
import { useContext } from "react";
|
||||||
|
|
||||||
|
import { PlansContext } from "./PlansContext";
|
||||||
|
|
||||||
|
export const usePlans = () => useContext(PlansContext);
|
|
@ -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,
|
||||||
|
};
|
||||||
|
}
|
|
@ -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,
|
|
||||||
};
|
|
||||||
}
|
|
|
@ -8,6 +8,7 @@ import { PageContainer } from "../components/PageContainer";
|
||||||
import { NavBar } from "../components/Navbar";
|
import { NavBar } from "../components/Navbar";
|
||||||
import { Footer } from "../components/Footer";
|
import { Footer } from "../components/Footer";
|
||||||
import { UserProvider, useUser } from "../contexts/user";
|
import { UserProvider, useUser } from "../contexts/user";
|
||||||
|
import { ContainerLoadingIndicator } from "../components/LoadingIndicator";
|
||||||
|
|
||||||
const Wrapper = styled.div.attrs({
|
const Wrapper = styled.div.attrs({
|
||||||
className: "min-h-screen overflow-hidden",
|
className: "min-h-screen overflow-hidden",
|
||||||
|
@ -25,7 +26,7 @@ const Layout = ({ children }) => {
|
||||||
<Wrapper>
|
<Wrapper>
|
||||||
{!user && (
|
{!user && (
|
||||||
<div className="fixed inset-0 flex justify-center items-center bg-palette-100/50">
|
<div className="fixed inset-0 flex justify-center items-center bg-palette-100/50">
|
||||||
<p>Loading...</p> {/* TODO: Do something nicer here */}
|
<ContainerLoadingIndicator className="!text-palette-200/50" />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{user && <>{children}</>}
|
{user && <>{children}</>}
|
||||||
|
|
|
@ -2,6 +2,7 @@ import * as React from "react";
|
||||||
import { useMedia } from "react-use";
|
import { useMedia } from "react-use";
|
||||||
|
|
||||||
import theme from "../lib/theme";
|
import theme from "../lib/theme";
|
||||||
|
import { PlansProvider } from "../contexts/plans/PlansProvider";
|
||||||
import { ArrowRightIcon } from "../components/Icons";
|
import { ArrowRightIcon } from "../components/Icons";
|
||||||
import { Panel } from "../components/Panel";
|
import { Panel } from "../components/Panel";
|
||||||
import { Tab, TabPanel, Tabs } from "../components/Tabs";
|
import { Tab, TabPanel, Tabs } from "../components/Tabs";
|
||||||
|
@ -16,7 +17,7 @@ const IndexPage = () => {
|
||||||
const showRecentActivity = useMedia(`(min-width: ${theme.screens.md})`);
|
const showRecentActivity = useMedia(`(min-width: ${theme.screens.md})`);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<PlansProvider>
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
<Slider
|
<Slider
|
||||||
slides={[
|
slides={[
|
||||||
|
@ -60,7 +61,7 @@ const IndexPage = () => {
|
||||||
<LatestActivity />
|
<LatestActivity />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</>
|
</PlansProvider>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
Reference in New Issue