Skip to content

Commit

Permalink
final changes
Browse files Browse the repository at this point in the history
  • Loading branch information
steven-tey committed Oct 5, 2022
1 parent fd42026 commit 9f3fc90
Show file tree
Hide file tree
Showing 22 changed files with 495 additions and 206 deletions.
152 changes: 152 additions & 0 deletions components/app/modals/upgrade-plan-modal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
import Modal from "@/components/shared/modal";
import {
useCallback,
useMemo,
useState,
Dispatch,
SetStateAction,
} from "react";
import BlurImage from "@/components/shared/blur-image";
import LoadingDots from "@/components/shared/icons/loading-dots";
import { getStripe } from "@/lib/stripe/client";
import { PRO_TIERS } from "@/lib/stripe/constants";
import { nFormatter } from "@/lib/utils";
import Slider from "@/components/shared/slider";
import Switch from "@/components/shared/switch";

function UpgradePlanModal({
showUpgradePlanModal,
setShowUpgradePlanModal,
}: {
showUpgradePlanModal: boolean;
setShowUpgradePlanModal: Dispatch<SetStateAction<boolean>>;
}) {
const [clicked, setClicked] = useState(false);
const [tier, setTier] = useState(0);
const [annualBilling, setAnnualBilling] = useState(true);
const period = useMemo(
() => (annualBilling ? "yearly" : "monthly"),
[annualBilling]
);
const env =
process.env.NEXT_PUBLIC_VERCEL_ENV === "production" ? "production" : "test";

return (
<Modal
showModal={showUpgradePlanModal}
setShowModal={setShowUpgradePlanModal}
>
<div className="inline-block w-full sm:max-w-md overflow-hidden align-middle transition-all transform bg-white sm:border sm:border-gray-200 shadow-xl sm:rounded-2xl">
<div className="flex flex-col justify-center items-center space-y-3 sm:px-16 px-4 pt-8 pb-24 border-b border-gray-200">
<BlurImage
src="/static/logo.png"
alt="Dub logo"
className="w-10 h-10 rounded-full border border-gray-200"
width={20}
height={20}
/>
<h3 className="font-medium text-xl">Upgrade to Pro</h3>
<div className="flex space-x-2 items-center">
<p className="text-sm text-gray-600">Billed Monthly</p>
<Switch setState={setAnnualBilling} />
<p className="text-sm text-gray-600">Billed Annually</p>
</div>
</div>

<div className="relative bg-white border border-gray-200 shadow-md rounded-lg max-w-sm mx-auto -mt-[5.1rem] -mb-[5.3rem]">
{annualBilling && (
<span className="bg-gradient-to-r from-blue-600 to-cyan-600 text-white px-2 py-0.5 absolute top-2 -right-0.5 text-xs rounded-l-md">
2 Free Months
</span>
)}
<div className="max-w-md flex justify-between items-center w-full p-5 pt-7">
<h3 className="text-2xl text-transparent bg-clip-text bg-gradient-to-r from-blue-600 to-cyan-600">
{PRO_TIERS[tier].name}
</h3>
<div className="flex items-center">
<p className="text-2xl font-semibold text-gray-700">
${PRO_TIERS[tier].price[period].amount}
</p>
<p className="text-sm text-gray-700">
/{annualBilling ? "yr" : "mo"}
</p>
</div>
</div>
<div className="w-full flex flex-col items-center space-y-1 bg-gray-50 text-center p-5 border-t border-gray-200 rounded-b-lg">
<Slider
value={tier}
setValue={setTier}
maxValue={PRO_TIERS.length - 1}
/>
<p className="text-sm text-gray-700">
Up to {nFormatter(PRO_TIERS[tier].quota)} link clicks/mo
</p>
</div>
</div>

<div className="flex flex-col items-center text-left bg-gray-50 sm:px-16 px-4 pt-28 pb-8">
<button
disabled={clicked}
onClick={() => {
setClicked(true);
fetch(
`/api/stripe/upgrade?priceId=${PRO_TIERS[tier].price[period].priceIds[env]}&usageLimit=${PRO_TIERS[tier].quota}`,
{
method: "POST",
}
)
.then(async (res) => {
const data = await res.json();
const { id: sessionId } = data;
const stripe = await getStripe();
stripe.redirectToCheckout({ sessionId });
})
.catch((err) => {
alert(err);
setClicked(false);
});
}}
className={`${
clicked
? "cursor-not-allowed bg-gray-100 border-gray-200 text-gray-400"
: "bg-blue-500 hover:bg-white hover:text-blue-500 border-blue-500 text-white"
} flex justify-center items-center w-full text-sm h-10 mb-2 rounded-md border transition-all focus:outline-none`}
>
{clicked ? (
<LoadingDots color="#808080" />
) : (
<p>Upgrade to {PRO_TIERS[tier].name}</p>
)}
</button>
<a
href="mailto:[email protected]?subject=Upgrade%20to%20Enterprise%20Plan"
className="text-gray-500 text-sm hover:text-gray-700 transition-all"
>
Or contact us for Enterprise
</a>
</div>
</div>
</Modal>
);
}

export function useUpgradePlanModal() {
const [showUpgradePlanModal, setShowUpgradePlanModal] = useState(false);

const UpgradePlanModalCallback = useCallback(() => {
return (
<UpgradePlanModal
showUpgradePlanModal={showUpgradePlanModal}
setShowUpgradePlanModal={setShowUpgradePlanModal}
/>
);
}, [showUpgradePlanModal, setShowUpgradePlanModal]);

return useMemo(
() => ({
setShowUpgradePlanModal,
UpgradePlanModal: UpgradePlanModalCallback,
}),
[setShowUpgradePlanModal, UpgradePlanModalCallback]
);
}
51 changes: 17 additions & 34 deletions components/app/settings/plan-usage.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import useSWR from "swr";
import { useMemo, useState } from "react";
import { nFormatter } from "@/lib/utils";
import { getFirstAndLastDay, nFormatter } from "@/lib/utils";
import { useRouter } from "next/router";
import Tooltip, { ProTiers } from "@/components/shared/tooltip";
import {
Expand All @@ -10,41 +9,43 @@ import {
LoadingDots,
} from "@/components/shared/icons";
import { motion } from "framer-motion";
import { getStripe } from "@/lib/stripe/client";
import useUsage from "@/lib/swr/use-usage";
import { PLAN_FROM_USAGE_LIMIT } from "@/lib/constants";
import { PRO_TIERS } from "@/lib/stripe/constants";
import { useUpgradePlanModal } from "@/components/app/modals/upgrade-plan-modal";

export default function PlanUsage() {
const router = useRouter();
const {
data: { usage, usageLimit, lastBilled, projectCount } = {},
data: { usage, usageLimit, billingCycleStart, projectCount } = {},
loading,
} = useUsage({ settingsPage: true });

const [clicked, setClicked] = useState(false);

const [billingStart, billingEnd] = useMemo(() => {
if (lastBilled) {
const lastBilledDate = new Date(lastBilled);
const start = lastBilledDate.toLocaleDateString("en-us", {
if (billingCycleStart) {
const { firstDay, lastDay } = getFirstAndLastDay(billingCycleStart);
const start = firstDay.toLocaleDateString("en-us", {
month: "short",
day: "numeric",
});
const end = new Date(
lastBilledDate.setDate(lastBilledDate.getDate() + 30)
).toLocaleDateString("en-us", {
const end = lastDay.toLocaleDateString("en-us", {
month: "short",
day: "numeric",
});
return [start, end];
}
return [];
}, [lastBilled]);
}, [billingCycleStart]);

const plan = usageLimit && PLAN_FROM_USAGE_LIMIT[usageLimit];
const plan =
usageLimit && PRO_TIERS.find((tier) => tier.quota === usageLimit).name;

const { UpgradePlanModal, setShowUpgradePlanModal } = useUpgradePlanModal();

return (
<div className="bg-white rounded-lg border border-gray-200">
<UpgradePlanModal />
<div className="flex flex-col space-y-3 p-10">
<h2 className="text-xl font-medium">Plan &amp; Usage</h2>
<p className="text-gray-500 text-sm">
Expand Down Expand Up @@ -156,29 +157,11 @@ export default function PlanUsage() {
plan === "Free" ? (
<button
onClick={() => {
setClicked(true);
fetch(`/api/stripe/upgrade`, {
method: "POST",
})
.then(async (res) => {
const data = await res.json();
const { id: sessionId } = data;
const stripe = await getStripe();
stripe.redirectToCheckout({ sessionId });
})
.catch((err) => {
alert(err);
setClicked(false);
});
setShowUpgradePlanModal(true);
}}
disabled={clicked}
className={`${
clicked
? "cursor-not-allowed bg-gray-100 border-gray-200"
: "bg-blue-500 text-white border-blue-500 hover:text-blue-500 hover:bg-white"
} h-9 w-24 text-sm border rounded-md focus:outline-none transition-all ease-in-out duration-150`}
className="bg-blue-500 text-white border-blue-500 hover:text-blue-500 hover:bg-white h-9 w-24 text-sm border rounded-md focus:outline-none transition-all ease-in-out duration-150"
>
{clicked ? <LoadingDots /> : "Upgrade"}
Upgrade
</button>
) : (
<div className="flex space-x-3">
Expand Down
9 changes: 6 additions & 3 deletions components/home/pricing.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import Tooltip, { OGImageProxy } from "@/components/shared/tooltip";
import { useState } from "react";
import Slider from "@/components/shared/slider";
import { nFormatter } from "@/lib/utils";
import { PRO_TIERS } from "@/lib/constants";
import { PRO_TIERS } from "@/lib/stripe/constants";

const pricingItems = [
{
Expand All @@ -23,7 +23,7 @@ const pricingItems = [
"Just bring any domain you own and turn it into a custom domain link shortener for free.",
},
{ text: "Unlimited branded links" },
{ text: "1 project" },
{ text: "5 project" },
{
text: "Root domain redirect",
footnote:
Expand Down Expand Up @@ -131,7 +131,10 @@ const Pricing = () => {
) : (
<div className="my-5 flex justify-center">
<p className="text-6xl font-display font-semibold">
${plan === "Pro" ? PRO_TIERS[tier].price : 0}
$
{plan === "Pro"
? PRO_TIERS[tier].price.monthly.amount
: 0}
</p>
</div>
)}
Expand Down
32 changes: 32 additions & 0 deletions components/shared/switch.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import * as SwitchPrimitive from "@radix-ui/react-switch";
import cx from "classnames";
import { Dispatch, SetStateAction } from "react";

const Switch = ({
setState,
}: {
setState: Dispatch<SetStateAction<boolean>>;
}) => {
return (
<SwitchPrimitive.Root
defaultChecked={true}
onCheckedChange={(checked) => setState(checked)}
className={cx(
"radix-state-checked:bg-blue-500",
"radix-state-unchecked:bg-gray-200 dark:radix-state-unchecked:bg-gray-800",
"relative inline-flex h-4 w-8 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out",
"focus:outline-none focus-visible:ring focus-visible:ring-blue-500 focus-visible:ring-opacity-75"
)}
>
<SwitchPrimitive.Thumb
className={cx(
"radix-state-checked:translate-x-4",
"radix-state-unchecked:translate-x-0",
"pointer-events-none inline-block h-3 w-3 transform rounded-full bg-white shadow-lg ring-0 transition duration-200 ease-in-out"
)}
/>
</SwitchPrimitive.Root>
);
};

export default Switch;
6 changes: 3 additions & 3 deletions components/shared/tooltip.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
import { ReactNode, useEffect, useRef, useState } from "react";
import { motion, AnimatePresence, useAnimation } from "framer-motion";
import { PLAN_FROM_USAGE_LIMIT, PRO_TIERS } from "@/lib/constants";
import { PRO_TIERS } from "@/lib/stripe/constants";
import Slider from "@/components/shared/slider";

export default function Tooltip({
Expand Down Expand Up @@ -168,11 +168,11 @@ export function ProTiers({ usageLimit }: { usageLimit?: number }) {
<div className="w-full rounded-md">
<div className="max-w-md flex justify-between items-center w-full p-5">
<h3 className="text-2xl text-transparent bg-clip-text bg-gradient-to-r from-blue-600 to-cyan-600">
{PLAN_FROM_USAGE_LIMIT[PRO_TIERS[tier].quota]}
{PRO_TIERS[tier].name}
</h3>
<div className="flex items-center">
<p className="text-2xl font-semibold text-gray-700">
${PRO_TIERS[tier].price}
${PRO_TIERS[tier].price.monthly.amount}
</p>
<p className="text-sm text-gray-700">/mo</p>
</div>
Expand Down
2 changes: 1 addition & 1 deletion lib/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,7 @@ const withUserAuth =
})) as UserProps;

const freePlan = user.usageLimit === 1000;
if (needProSubscription && freePlan && user.projects.length === 1) {
if (needProSubscription && freePlan && user.projects.length >= 5) {
return res
.status(403)
.end("Unauthorized: Can't add more projects, need pro subscription");
Expand Down
38 changes: 0 additions & 38 deletions lib/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,44 +13,6 @@ export const FRAMER_MOTION_LIST_ITEM_VARIANTS = {
show: { scale: 1, opacity: 1, transition: { type: "spring" } },
};

export const PLAN_FROM_USAGE_LIMIT = {
1000: "Free",
10000: "Pro 10K",
25000: "Pro 25K",
50000: "Pro 50K",
100000: "Pro 100K",
500000: "Pro 500K",
1000000: "Pro 1M",
5000000: "Enterprise 5M",
};

export const PRO_TIERS = [
{
price: 9,
quota: 10000,
},
{
price: 19,
quota: 25000,
},
{
price: 29,
quota: 50000,
},
{
price: 49,
quota: 100000,
},
{
price: 79,
quota: 500000,
},
{
price: 99,
quota: 1000000,
},
];

export const HOME_HOSTNAMES = new Set([
// comment for better diffs
"dub.sh",
Expand Down
Loading

0 comments on commit 9f3fc90

Please sign in to comment.