Merge pull request #1823 from SkynetLabs/dashboard-v2-current-tier
Dashboard v2 - active plan info
This commit is contained in:
commit
ccc9391d99
|
@ -23,6 +23,7 @@
|
||||||
"@fontsource/source-sans-pro": "^4.5.3",
|
"@fontsource/source-sans-pro": "^4.5.3",
|
||||||
"classnames": "^2.3.1",
|
"classnames": "^2.3.1",
|
||||||
"copy-text-to-clipboard": "^3.0.1",
|
"copy-text-to-clipboard": "^3.0.1",
|
||||||
|
"dayjs": "^1.10.8",
|
||||||
"gatsby": "^4.6.2",
|
"gatsby": "^4.6.2",
|
||||||
"gatsby-plugin-postcss": "^5.7.0",
|
"gatsby-plugin-postcss": "^5.7.0",
|
||||||
"http-status-codes": "^2.2.0",
|
"http-status-codes": "^2.2.0",
|
||||||
|
|
|
@ -6,7 +6,7 @@ import styled from "styled-components";
|
||||||
*/
|
*/
|
||||||
export const Button = styled.button.attrs(({ $primary }) => ({
|
export const Button = styled.button.attrs(({ $primary }) => ({
|
||||||
type: "button",
|
type: "button",
|
||||||
className: `px-6 py-3 rounded-full font-sans uppercase text-xs tracking-wide text-palette-600
|
className: `px-6 py-3 rounded-full font-sans uppercase text-xs tracking-wide text-palette-600 transition-[filter] hover:brightness-90
|
||||||
${$primary ? "bg-primary" : "bg-white border-2 border-black"}`,
|
${$primary ? "bg-primary" : "bg-white border-2 border-black"}`,
|
||||||
}))``;
|
}))``;
|
||||||
Button.propTypes = {
|
Button.propTypes = {
|
||||||
|
|
|
@ -0,0 +1,50 @@
|
||||||
|
import dayjs from "dayjs";
|
||||||
|
import relativeTime from "dayjs/plugin/relativeTime";
|
||||||
|
|
||||||
|
import { useUser } from "../../contexts/user";
|
||||||
|
import useSubscriptionPlans from "../../hooks/useSubscriptionPlans";
|
||||||
|
|
||||||
|
import LatestPayment from "./LatestPayment";
|
||||||
|
import SuggestedPlan from "./SuggestedPlan";
|
||||||
|
|
||||||
|
dayjs.extend(relativeTime);
|
||||||
|
|
||||||
|
const CurrentPlan = () => {
|
||||||
|
const { user, error: userError } = useUser();
|
||||||
|
const { activePlan, plans, error: plansError } = useSubscriptionPlans(user);
|
||||||
|
|
||||||
|
if (!user || !activePlan) {
|
||||||
|
return (
|
||||||
|
// TODO: a nicer loading indicator
|
||||||
|
<div className="flex flex-col space-y-4 h-full justify-center items-center">Loading...</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (userError || plansError) {
|
||||||
|
return (
|
||||||
|
<div className="flex text-palette-300 flex-col space-y-4 h-full justify-center items-center">
|
||||||
|
<p>An error occurred while loading this data.</p>
|
||||||
|
<p>We'll retry automatically.</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<h4>{activePlan.name}</h4>
|
||||||
|
<div className="text-palette-400">
|
||||||
|
{activePlan.price === 0 && <p>100GB without paying a dime! 🎉</p>}
|
||||||
|
{activePlan.price !== 0 &&
|
||||||
|
(user.subscriptionCancelAtPeriodEnd ? (
|
||||||
|
<p>Your subscription expires {dayjs(user.subscribedUntil).fromNow()}</p>
|
||||||
|
) : (
|
||||||
|
<p className="first-letter:uppercase">{dayjs(user.subscribedUntil).fromNow(true)} until the next payment</p>
|
||||||
|
))}
|
||||||
|
<LatestPayment user={user} />
|
||||||
|
<SuggestedPlan plans={plans} activePlan={activePlan} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CurrentPlan;
|
|
@ -0,0 +1,18 @@
|
||||||
|
import dayjs from "dayjs";
|
||||||
|
|
||||||
|
// TODO: this is not an accurate information, we need this data from the backend
|
||||||
|
const LatestPayment = ({ user }) => (
|
||||||
|
<div className="flex mt-6 justify-between items-center bg-palette-100/50 py-4 px-6 border-l-2 border-primary">
|
||||||
|
<div className="flex flex-col lg:flex-row">
|
||||||
|
<span>Latest payment</span>
|
||||||
|
<span className="lg:before:content-['-'] lg:before:px-2 text-xs lg:text-base">
|
||||||
|
{dayjs(user.subscribedUntil).subtract(1, "month").format("MM/DD/YYYY")}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="rounded py-1 px-2 bg-primary/10 font-sans text-primary uppercase text-xs">Success</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default LatestPayment;
|
|
@ -0,0 +1,24 @@
|
||||||
|
import { Link } from "gatsby";
|
||||||
|
import { useMemo } from "react";
|
||||||
|
|
||||||
|
import { Button } from "../Button";
|
||||||
|
|
||||||
|
const SuggestedPlan = ({ plans, activePlan }) => {
|
||||||
|
const nextPlan = useMemo(() => plans.find(({ tier }) => tier > activePlan.tier), [plans, activePlan]);
|
||||||
|
|
||||||
|
if (!nextPlan) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mt-7">
|
||||||
|
<p className="font-sans font-semibold text-xs uppercase text-primary">Discover {nextPlan.name}</p>
|
||||||
|
<p className="pt-1 text-xs sm:text-base">{nextPlan.description}</p>
|
||||||
|
<Button $primary as={Link} to={`/upgrade?selectedPlan=${nextPlan.id}`} className="mt-6">
|
||||||
|
Upgrade
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SuggestedPlan;
|
|
@ -0,0 +1,3 @@
|
||||||
|
import CurrentPlan from "./CurrentPlan";
|
||||||
|
|
||||||
|
export default CurrentPlan;
|
|
@ -0,0 +1,6 @@
|
||||||
|
import { createContext } from "react";
|
||||||
|
|
||||||
|
export const UserContext = createContext({
|
||||||
|
user: null,
|
||||||
|
error: null,
|
||||||
|
});
|
|
@ -0,0 +1,17 @@
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import useSWR from "swr";
|
||||||
|
|
||||||
|
import { UserContext } from "./UserContext";
|
||||||
|
|
||||||
|
export const UserProvider = ({ children }) => {
|
||||||
|
const { data: user, error, mutate } = useSWR("user");
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (user || error) {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}, [user, error]);
|
||||||
|
|
||||||
|
return <UserContext.Provider value={{ user, error, loading, mutate }}>{children}</UserContext.Provider>;
|
||||||
|
};
|
|
@ -0,0 +1,2 @@
|
||||||
|
export * from "./UserProvider";
|
||||||
|
export * from "./useUser";
|
|
@ -0,0 +1,5 @@
|
||||||
|
import { useContext } from "react";
|
||||||
|
|
||||||
|
import { UserContext } from "./UserContext";
|
||||||
|
|
||||||
|
export const useUser = () => useContext(UserContext);
|
|
@ -0,0 +1,28 @@
|
||||||
|
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,
|
||||||
|
};
|
||||||
|
}
|
|
@ -7,8 +7,9 @@ import { authenticatedOnly } from "../lib/swrConfig";
|
||||||
import { PageContainer } from "../components/PageContainer";
|
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";
|
||||||
|
|
||||||
const Layout = styled.div.attrs({
|
const Wrapper = styled.div.attrs({
|
||||||
className: "min-h-screen overflow-hidden",
|
className: "min-h-screen overflow-hidden",
|
||||||
})`
|
})`
|
||||||
background-image: url(/images/dashboard-bg.svg);
|
background-image: url(/images/dashboard-bg.svg);
|
||||||
|
@ -16,17 +17,35 @@ const Layout = styled.div.attrs({
|
||||||
background-repeat: no-repeat;
|
background-repeat: no-repeat;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
const Layout = ({ children }) => {
|
||||||
|
const { user } = useUser();
|
||||||
|
|
||||||
|
// Prevent from flashing the dashboard screen to unauthenticated users.
|
||||||
|
return (
|
||||||
|
<Wrapper>
|
||||||
|
{!user && (
|
||||||
|
<div className="fixed inset-0 flex justify-center items-center bg-palette-100/50">
|
||||||
|
<p>Loading...</p> {/* TODO: Do something nicer here */}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{user && <>{children}</>}
|
||||||
|
</Wrapper>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const DashboardLayout = ({ children }) => {
|
const DashboardLayout = ({ children }) => {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<SWRConfig value={authenticatedOnly}>
|
<SWRConfig value={authenticatedOnly}>
|
||||||
<Layout>
|
<UserProvider>
|
||||||
<NavBar />
|
<Layout>
|
||||||
<PageContainer>
|
<NavBar />
|
||||||
<main className="mt-14">{children}</main>
|
<PageContainer>
|
||||||
</PageContainer>
|
<main className="mt-14">{children}</main>
|
||||||
<Footer />
|
</PageContainer>
|
||||||
</Layout>
|
<Footer />
|
||||||
|
</Layout>
|
||||||
|
</UserProvider>
|
||||||
</SWRConfig>
|
</SWRConfig>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
|
@ -8,6 +8,7 @@ const redirectUnauthenticated = (key) =>
|
||||||
fetch(`${baseUrl}/${key}`).then((response) => {
|
fetch(`${baseUrl}/${key}`).then((response) => {
|
||||||
if (response.status === StatusCodes.UNAUTHORIZED) {
|
if (response.status === StatusCodes.UNAUTHORIZED) {
|
||||||
navigate(`/auth/login?return_to=${encodeURIComponent(window.location.href)}`);
|
navigate(`/auth/login?return_to=${encodeURIComponent(window.location.href)}`);
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return response.json();
|
return response.json();
|
||||||
|
|
|
@ -0,0 +1,9 @@
|
||||||
|
const freeTier = {
|
||||||
|
id: "starter",
|
||||||
|
tier: 1,
|
||||||
|
name: "Free",
|
||||||
|
price: 0,
|
||||||
|
description: "100 GB - Casual user with a few files you want to access from around the world? Try the Free tier",
|
||||||
|
};
|
||||||
|
|
||||||
|
export default freeTier;
|
|
@ -10,6 +10,7 @@ import DashboardLayout from "../layouts/DashboardLayout";
|
||||||
import Slider from "../components/Slider/Slider";
|
import Slider from "../components/Slider/Slider";
|
||||||
import CurrentUsage from "../components/CurrentUsage";
|
import CurrentUsage from "../components/CurrentUsage";
|
||||||
import Uploader from "../components/Uploader/Uploader";
|
import Uploader from "../components/Uploader/Uploader";
|
||||||
|
import CurrentPlan from "../components/CurrentPlan";
|
||||||
|
|
||||||
const IndexPage = () => {
|
const IndexPage = () => {
|
||||||
const showRecentActivity = useMedia(`(min-width: ${theme.screens.md})`);
|
const showRecentActivity = useMedia(`(min-width: ${theme.screens.md})`);
|
||||||
|
@ -49,11 +50,7 @@ const IndexPage = () => {
|
||||||
}
|
}
|
||||||
className="h-[330px]"
|
className="h-[330px]"
|
||||||
>
|
>
|
||||||
<ul>
|
<CurrentPlan />
|
||||||
<li>Current</li>
|
|
||||||
<li>Plan</li>
|
|
||||||
<li>Info</li>
|
|
||||||
</ul>
|
|
||||||
</Panel>,
|
</Panel>,
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -6296,6 +6296,11 @@ date-fns@^2.25.0:
|
||||||
resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.28.0.tgz#9570d656f5fc13143e50c975a3b6bbeb46cd08b2"
|
resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.28.0.tgz#9570d656f5fc13143e50c975a3b6bbeb46cd08b2"
|
||||||
integrity sha512-8d35hViGYx/QH0icHYCeLmsLmMUheMmTyV9Fcm6gvNwdw31yXXH+O85sOBJ+OLnLQMKZowvpKb6FgMIQjcpvQw==
|
integrity sha512-8d35hViGYx/QH0icHYCeLmsLmMUheMmTyV9Fcm6gvNwdw31yXXH+O85sOBJ+OLnLQMKZowvpKb6FgMIQjcpvQw==
|
||||||
|
|
||||||
|
dayjs@^1.10.8:
|
||||||
|
version "1.10.8"
|
||||||
|
resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.10.8.tgz#267df4bc6276fcb33c04a6735287e3f429abec41"
|
||||||
|
integrity sha512-wbNwDfBHHur9UOzNUjeKUOJ0fCb0a52Wx0xInmQ7Y8FstyajiV1NmK1e00cxsr9YrE9r7yAChE0VvpuY5Rnlow==
|
||||||
|
|
||||||
debug@2, debug@2.6.9, debug@^2.2.0, debug@^2.3.3, debug@^2.6.0, debug@^2.6.9:
|
debug@2, debug@2.6.9, debug@^2.2.0, debug@^2.3.3, debug@^2.6.0, debug@^2.6.9:
|
||||||
version "2.6.9"
|
version "2.6.9"
|
||||||
resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f"
|
resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f"
|
||||||
|
|
Reference in New Issue