diff --git a/packages/dashboard-v2/package.json b/packages/dashboard-v2/package.json index 7b620128..7b561154 100644 --- a/packages/dashboard-v2/package.json +++ b/packages/dashboard-v2/package.json @@ -23,6 +23,7 @@ "@fontsource/source-sans-pro": "^4.5.3", "classnames": "^2.3.1", "copy-text-to-clipboard": "^3.0.1", + "dayjs": "^1.10.8", "gatsby": "^4.6.2", "gatsby-plugin-postcss": "^5.7.0", "http-status-codes": "^2.2.0", diff --git a/packages/dashboard-v2/src/components/Button/Button.js b/packages/dashboard-v2/src/components/Button/Button.js index 165935a2..95ecbede 100644 --- a/packages/dashboard-v2/src/components/Button/Button.js +++ b/packages/dashboard-v2/src/components/Button/Button.js @@ -6,7 +6,7 @@ import styled from "styled-components"; */ export const Button = styled.button.attrs(({ $primary }) => ({ 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"}`, }))``; Button.propTypes = { diff --git a/packages/dashboard-v2/src/components/CurrentPlan/CurrentPlan.js b/packages/dashboard-v2/src/components/CurrentPlan/CurrentPlan.js new file mode 100644 index 00000000..c5cdee36 --- /dev/null +++ b/packages/dashboard-v2/src/components/CurrentPlan/CurrentPlan.js @@ -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 +
Loading...
+ ); + } + + if (userError || plansError) { + return ( +
+

An error occurred while loading this data.

+

We'll retry automatically.

+
+ ); + } + + return ( +
+

{activePlan.name}

+
+ {activePlan.price === 0 &&

100GB without paying a dime! 🎉

} + {activePlan.price !== 0 && + (user.subscriptionCancelAtPeriodEnd ? ( +

Your subscription expires {dayjs(user.subscribedUntil).fromNow()}

+ ) : ( +

{dayjs(user.subscribedUntil).fromNow(true)} until the next payment

+ ))} + + +
+
+ ); +}; + +export default CurrentPlan; diff --git a/packages/dashboard-v2/src/components/CurrentPlan/LatestPayment.js b/packages/dashboard-v2/src/components/CurrentPlan/LatestPayment.js new file mode 100644 index 00000000..8ca2ab9e --- /dev/null +++ b/packages/dashboard-v2/src/components/CurrentPlan/LatestPayment.js @@ -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 }) => ( +
+
+ Latest payment + + {dayjs(user.subscribedUntil).subtract(1, "month").format("MM/DD/YYYY")} + +
+
+ Success +
+
+); + +export default LatestPayment; diff --git a/packages/dashboard-v2/src/components/CurrentPlan/SuggestedPlan.js b/packages/dashboard-v2/src/components/CurrentPlan/SuggestedPlan.js new file mode 100644 index 00000000..21aa9b48 --- /dev/null +++ b/packages/dashboard-v2/src/components/CurrentPlan/SuggestedPlan.js @@ -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 ( +
+

Discover {nextPlan.name}

+

{nextPlan.description}

+ +
+ ); +}; + +export default SuggestedPlan; diff --git a/packages/dashboard-v2/src/components/CurrentPlan/index.js b/packages/dashboard-v2/src/components/CurrentPlan/index.js new file mode 100644 index 00000000..20390eab --- /dev/null +++ b/packages/dashboard-v2/src/components/CurrentPlan/index.js @@ -0,0 +1,3 @@ +import CurrentPlan from "./CurrentPlan"; + +export default CurrentPlan; diff --git a/packages/dashboard-v2/src/contexts/user/UserContext.js b/packages/dashboard-v2/src/contexts/user/UserContext.js new file mode 100644 index 00000000..e97723a3 --- /dev/null +++ b/packages/dashboard-v2/src/contexts/user/UserContext.js @@ -0,0 +1,6 @@ +import { createContext } from "react"; + +export const UserContext = createContext({ + user: null, + error: null, +}); diff --git a/packages/dashboard-v2/src/contexts/user/UserProvider.js b/packages/dashboard-v2/src/contexts/user/UserProvider.js new file mode 100644 index 00000000..4d1efac5 --- /dev/null +++ b/packages/dashboard-v2/src/contexts/user/UserProvider.js @@ -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 {children}; +}; diff --git a/packages/dashboard-v2/src/contexts/user/index.js b/packages/dashboard-v2/src/contexts/user/index.js new file mode 100644 index 00000000..311416e7 --- /dev/null +++ b/packages/dashboard-v2/src/contexts/user/index.js @@ -0,0 +1,2 @@ +export * from "./UserProvider"; +export * from "./useUser"; diff --git a/packages/dashboard-v2/src/contexts/user/useUser.js b/packages/dashboard-v2/src/contexts/user/useUser.js new file mode 100644 index 00000000..2b077961 --- /dev/null +++ b/packages/dashboard-v2/src/contexts/user/useUser.js @@ -0,0 +1,5 @@ +import { useContext } from "react"; + +import { UserContext } from "./UserContext"; + +export const useUser = () => useContext(UserContext); diff --git a/packages/dashboard-v2/src/hooks/useSubscriptionPlans.js b/packages/dashboard-v2/src/hooks/useSubscriptionPlans.js new file mode 100644 index 00000000..26658df8 --- /dev/null +++ b/packages/dashboard-v2/src/hooks/useSubscriptionPlans.js @@ -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, + }; +} diff --git a/packages/dashboard-v2/src/layouts/DashboardLayout.js b/packages/dashboard-v2/src/layouts/DashboardLayout.js index 32156549..07f4eabf 100644 --- a/packages/dashboard-v2/src/layouts/DashboardLayout.js +++ b/packages/dashboard-v2/src/layouts/DashboardLayout.js @@ -7,8 +7,9 @@ import { authenticatedOnly } from "../lib/swrConfig"; import { PageContainer } from "../components/PageContainer"; import { NavBar } from "../components/Navbar"; 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", })` background-image: url(/images/dashboard-bg.svg); @@ -16,17 +17,35 @@ const Layout = styled.div.attrs({ background-repeat: no-repeat; `; +const Layout = ({ children }) => { + const { user } = useUser(); + + // Prevent from flashing the dashboard screen to unauthenticated users. + return ( + + {!user && ( +
+

Loading...

{/* TODO: Do something nicer here */} +
+ )} + {user && <>{children}} +
+ ); +}; + const DashboardLayout = ({ children }) => { return ( <> - - - -
{children}
-
-