From c05eef96c12103b5de2153eb7332ea70496b4495 Mon Sep 17 00:00:00 2001 From: Matt Aitken Date: Fri, 28 Jun 2024 19:34:33 +0100 Subject: [PATCH] Added the usage bar for v3 --- .../app/components/billing/v3/UsageBar.tsx | 156 ++++++++++++++++++ .../route.tsx | 102 ++++++++---- 2 files changed, 222 insertions(+), 36 deletions(-) create mode 100644 apps/webapp/app/components/billing/v3/UsageBar.tsx diff --git a/apps/webapp/app/components/billing/v3/UsageBar.tsx b/apps/webapp/app/components/billing/v3/UsageBar.tsx new file mode 100644 index 000000000..843e60cb1 --- /dev/null +++ b/apps/webapp/app/components/billing/v3/UsageBar.tsx @@ -0,0 +1,156 @@ +import { cn } from "~/utils/cn"; +import { formatCurrency } from "~/utils/numberFormatter"; +import { Paragraph } from "../../primitives/Paragraph"; +import { SimpleTooltip } from "../../primitives/Tooltip"; +import { motion } from "framer-motion"; + +type UsageBarProps = { + current: number; + billingLimit?: number; + tierLimit?: number; + projectedUsage: number; +}; + +export function UsageBar({ current, billingLimit, tierLimit, projectedUsage }: UsageBarProps) { + const getLargestNumber = Math.max( + current, + tierLimit ?? -Infinity, + projectedUsage, + billingLimit ?? -Infinity + ); + //creates a maximum range for the progress bar, add 10% to the largest number so the bar doesn't reach the end + const maxRange = Math.round(getLargestNumber * 1.1); + const tierRunLimitPercentage = tierLimit ? Math.round((tierLimit / maxRange) * 100) : 0; + const projectedRunsPercentage = Math.round((projectedUsage / maxRange) * 100); + const billingLimitPercentage = + billingLimit !== undefined ? Math.round((billingLimit / maxRange) * 100) : 0; + const usagePercentage = Math.round((current / maxRange) * 100); + + //cap the usagePercentage to the freeRunLimitPercentage + const usageCappedToLimitPercentage = Math.min(usagePercentage, tierRunLimitPercentage); + + return ( +
+
+ {billingLimit && ( + + + + )} + {tierLimit && ( + + + + )} + {projectedUsage !== 0 && ( + + + + )} + tierLimit ? "bg-rose-600" : "bg-green-600" + )} + > + + + +
+
+ ); +} + +const positions = { + topRow1: "bottom-0 h-9", + topRow2: "bottom-0 h-14", + bottomRow1: "top-0 h-9 items-end", + bottomRow2: "top-0 h-14 items-end", +}; + +type LegendProps = { + text: string; + value: number | string; + percentage: number; + position: keyof typeof positions; + tooltipContent: string; +}; + +function Legend({ text, value, position, percentage, tooltipContent }: LegendProps) { + const flipLegendPositionValue = 80; + const flipLegendPosition = percentage > flipLegendPositionValue ? true : false; + return ( +
+ + {text} + {value} + + } + variant="dark" + side="top" + content={tooltipContent} + className="z-50 h-fit" + /> +
+ ); +} diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.v3.usage/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.v3.usage/route.tsx index c7f68c364..47a74e7db 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.v3.usage/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.v3.usage/route.tsx @@ -1,26 +1,19 @@ +import { ArrowRightIcon } from "@heroicons/react/24/solid"; import { LoaderFunctionArgs } from "@remix-run/server-runtime"; -import { - Bar, - BarChart, - CartesianGrid, - Label, - Legend, - Rectangle, - ResponsiveContainer, - Tooltip, - XAxis, - YAxis, -} from "recharts"; +import { Bar, BarChart, Rectangle, ResponsiveContainer, Tooltip, XAxis, YAxis } from "recharts"; import { redirect, typedjson, useTypedLoaderData } from "remix-typedjson"; +import { UsageBar } from "~/components/billing/v3/UsageBar"; import { PageBody, PageContainer } from "~/components/layout/AppLayout"; -import { Header3 } from "~/components/primitives/Headers"; +import { Header2, Header3 } from "~/components/primitives/Headers"; import { NavBar, PageTitle } from "~/components/primitives/PageHeader"; import { prisma } from "~/db.server"; import { featuresForRequest } from "~/features.server"; -import { getUsageSeries } from "~/services/platform.v3.server"; +import { getUsage, getUsageSeries } from "~/services/platform.v3.server"; import { requireUserId } from "~/services/session.server"; import { createTimeSeriesData } from "~/utils/graphs"; +import { formatCurrency } from "~/utils/numberFormatter"; import { OrganizationParamsSchema, organizationPath } from "~/utils/pathBuilder"; +import { useCurrentPlan } from "../_app.orgs.$organizationSlug/route"; export async function loader({ params, request }: LoaderFunctionArgs) { await requireUserId(request); @@ -40,31 +33,43 @@ export async function loader({ params, request }: LoaderFunctionArgs) { } //periods - const periodStart = new Date(); - periodStart.setDate(periodStart.getDate() - 30); - periodStart.setHours(0, 0, 0, 0); - const periodEnd = new Date(); - periodEnd.setDate(periodEnd.getDate() + 1); - periodEnd.setHours(23, 59, 59, 999); + const thirtyDaysAgo = new Date(); + thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30); + thirtyDaysAgo.setHours(0, 0, 0, 0); + const endOfToday = new Date(); + endOfToday.setDate(endOfToday.getDate() + 1); + endOfToday.setHours(23, 59, 59, 999); - const creditUsage = await getUsageSeries(organization.id, { - from: periodStart, - to: periodEnd, + const past30Days = await getUsageSeries(organization.id, { + from: thirtyDaysAgo, + to: endOfToday, window: "DAY", }); - console.log(JSON.stringify(creditUsage, null, 2)); + const startOfMonth = new Date(); + startOfMonth.setDate(1); + startOfMonth.setHours(0, 0, 0, 0); + + const now = new Date(); + const endOfMonth = new Date(now.getFullYear(), now.getMonth() + 1, 0, 23, 59, 59, 999); + + const usageData = await getUsage(organization.id, { from: startOfMonth, to: endOfMonth }); + + const current = (usageData?.cents ?? 0) / 100; + const percentageThroughMonth = new Date().getDate() / endOfMonth.getDate(); + const usage = { + current: current, + projected: current / percentageThroughMonth, + }; return typedjson({ - periodStart, - periodEnd, - creditUsage: creditUsage + past30Days: past30Days ? createTimeSeriesData({ - startDate: periodStart, - endDate: periodEnd, + startDate: thirtyDaysAgo, + endDate: endOfToday, window: "DAY", data: - creditUsage.data.map((period) => ({ + past30Days.data.map((period) => ({ date: new Date(period.windowStart), value: period.value, })) ?? [], @@ -73,6 +78,7 @@ export async function loader({ params, request }: LoaderFunctionArgs) { dollars: (period.value ?? 0) / 100, })) : [], + usage, }); } @@ -84,7 +90,7 @@ const dateFormatter = new Intl.DateTimeFormat("en-US", { const tooltipStyle = { display: "flex", alignItems: "center", - gap: "0.5rem", + gap: "0rem", borderRadius: "0.25rem", border: "1px solid #1A2434", backgroundColor: "#0B1018", @@ -94,7 +100,8 @@ const tooltipStyle = { }; export default function ChoosePlanPage() { - const { periodStart, periodEnd, creditUsage } = useTypedLoaderData(); + const { usage, past30Days } = useTypedLoaderData(); + const currentPlan = useCurrentPlan(); return ( @@ -102,15 +109,37 @@ export default function ChoosePlanPage() { -
- Usage (past 30 days) + This month +
+
+
+ Month-to-date +

+ {formatCurrency(usage.current, false)} +

+
+ +
+ Projected +

{formatCurrency(usage.projected, false)}

+
+
+ +
+ Past 30 days +
+ Usage @@ -148,6 +177,7 @@ export default function ChoosePlanPage() { return dateFormatter.format(new Date(dateString)); }} + formatter={(value, data) => [`$${value.toLocaleString()}`, ""]} /> } />