Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(saas): Admin Dashboard #20

Merged
merged 12 commits into from
May 3, 2024
Merged
4,332 changes: 1,556 additions & 2,776 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions starterkits/saas/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@
"react-dom": "18.2.0",
"react-hook-form": "^7.51.3",
"react-wrap-balancer": "^1.1.0",
"recharts": "^2.12.6",
"rehype-prism-plus": "^2.0.0",
"remark": "^15.0.1",
"resend": "^3.2.0",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import {
cancelPlan,
pausePlan,
resumePlan,
} from "@/server/actions/plans/mutations";
} from "@/server/actions/subscription/mutations";
import { useMutation } from "@tanstack/react-query";
import { useRouter } from "next/navigation";
import { toast } from "sonner";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,11 @@ import { Button, type ButtonProps } from "@/components/ui/button";
import { Icons } from "@/components/ui/icons";
import { siteUrls } from "@/config/urls";
import { useAwaitableTransition } from "@/hooks/use-awaitable-transition";
import { changePlan } from "@/server/actions/plans/mutations";
import { changePlan } from "@/server/actions/subscription/mutations";
import {
getCheckoutURL,
getOrgSubscription,
} from "@/server/actions/plans/query";
} from "@/server/actions/subscription/query";
import { useMutation } from "@tanstack/react-query";
import { useRouter } from "next/navigation";
import { toast } from "sonner";
Expand Down
2 changes: 1 addition & 1 deletion starterkits/saas/src/app/(app)/(user)/org/billing/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { AvailablePlans } from "@/app/(app)/(user)/org/billing/_components/avail
import { CurrentPlan } from "@/app/(app)/(user)/org/billing/_components/current-plan";
import { orgBillingPageConfig } from "@/app/(app)/(user)/org/billing/_constants/page-config";
import { AppPageShell } from "@/app/(app)/_components/page-shell";
import { getOrgSubscription } from "@/server/actions/plans/query";
import { getOrgSubscription } from "@/server/actions/subscription/query";

export const dynamic = "force-dynamic";

Expand Down
6 changes: 3 additions & 3 deletions starterkits/saas/src/app/(app)/_components/page-shell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,10 @@ export function AppPageShell({
const Container = as ?? "main";

return (
<Container className="w-full space-y-8 pl-8">
<div className="w-full space-y-8 pl-8">
<PageHeader title={title} description={description} />
<div className="space-y-8 pb-8">{children}</div>
</Container>
<Container className="space-y-8 pb-8">{children}</Container>
</div>
);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
"use client";

import { LineChart } from "@/components/charts";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { thousandToK } from "@/lib/utils";

type RevenueChartProps = {
data: {
Date: string;
RevenueCount: number;
}[];
};

export function RevenueChart({ data }: RevenueChartProps) {
return (
<Card>
<CardHeader>
<CardTitle>Revenue Analytics</CardTitle>
<CardDescription>
Count of revenue each month for last 6 months
</CardDescription>
</CardHeader>
<CardContent>
<LineChart
data={data}
xAxisDataKey="Date"
yAxisDataKey="RevenueCount"
lineDataKeys={["RevenueCount"]}
lineProps={[{ stroke: "hsl(var(--primary))" }]}
yAxisProps={{
tickFormatter: (value) => {
if (value >= 10000) {
return `${thousandToK(Number(value)).toFixed(1)}k`;
} else {
return `${value}`;
}
},
}}
/>
</CardContent>
</Card>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import type { IconProps } from "@/components/ui/icons";

type StatsCardProps = {
title: string;
value: string | number;
subText: string;
Icon: React.ComponentType<IconProps>;
};

export function StatsCard({ title, value, Icon, subText }: StatsCardProps) {
return (
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">{title}</CardTitle>
<Icon className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{value}</div>
<p className="text-xs text-muted-foreground">{subText}</p>
</CardContent>
</Card>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
"use client";

import { LineChart } from "@/components/charts";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { thousandToK } from "@/lib/utils";

type SubsChartProps = {
data: {
Date: string;
SubsCount: number;
}[];
};

export function SubsChart({ data }: SubsChartProps) {
return (
<Card>
<CardHeader>
<CardTitle>Subscription Analytics</CardTitle>
<CardDescription>
Count of subscriptions each month for last 6 months
</CardDescription>
</CardHeader>
<CardContent>
<LineChart
data={data}
xAxisDataKey="Date"
yAxisDataKey="SubsCount"
lineDataKeys={["SubsCount"]}
lineProps={[{ stroke: "hsl(var(--primary))" }]}
yAxisProps={{
tickFormatter: (value) => {
if (value >= 10000) {
return `${thousandToK(Number(value)).toFixed(1)}k`;
} else {
return value as string;
}
},
}}
/>
</CardContent>
</Card>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
"use client";

import { LineChart } from "@/components/charts";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { thousandToK } from "@/lib/utils";

type UsersChartProps = {
data: {
Date: string;
UsersCount: number;
}[];
};

export function UsersChart({ data }: UsersChartProps) {
return (
<Card>
<CardHeader>
<CardTitle>Users Analytics</CardTitle>
<CardDescription>
Count of users joined each month for last 6 months
</CardDescription>
</CardHeader>
<CardContent>
<LineChart
data={data}
xAxisDataKey="Date"
yAxisDataKey="UsersCount"
lineDataKeys={["UsersCount"]}
lineProps={[{ stroke: "hsl(var(--primary))" }]}
yAxisProps={{
tickFormatter: (value) => {
if (value >= 10000) {
return `${thousandToK(Number(value)).toFixed(1)}k`;
} else {
return value as string;
}
},
}}
/>
</CardContent>
</Card>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
/**
* This file contains the page configuration for the users page.
* This is used to generate the page title and description.
*/

export const adminDashConfig = {
title: "Admin Dashboard",
description:
"View insights and analytics to monitor your app's performance and user behavior.",
} as const;
14 changes: 14 additions & 0 deletions starterkits/saas/src/app/(app)/admin/dashboard/loading.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { AppPageLoading } from "@/app/(app)/_components/page-loading";
import { adminDashConfig } from "@/app/(app)/admin/dashboard/_constants/page-config";
import { Skeleton } from "@/components/ui/skeleton";

export default function AdminFeedbackPageLoading() {
return (
<AppPageLoading
title={adminDashConfig.title}
description={adminDashConfig.description}
>
<Skeleton className="h-96 w-full" />
</AppPageLoading>
);
}
101 changes: 94 additions & 7 deletions starterkits/saas/src/app/(app)/admin/dashboard/page.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,100 @@
import { getUser } from "@/server/auth";
import { AppPageShell } from "@/app/(app)/_components/page-shell";
import { RevenueChart } from "@/app/(app)/admin/dashboard/_components/revenue-chart";
import { StatsCard } from "@/app/(app)/admin/dashboard/_components/stats-card";
import { SubsChart } from "@/app/(app)/admin/dashboard/_components/subs-chart";
import { UsersChart } from "@/app/(app)/admin/dashboard/_components/users-chart";
import { adminDashConfig } from "@/app/(app)/admin/dashboard/_constants/page-config";
import { buttonVariants } from "@/components/ui/button";
import { siteUrls } from "@/config/urls";
import { cn } from "@/lib/utils";
import {
getRevenueCount,
getSubscriptionsCount,
} from "@/server/actions/subscription/query";
import { getUsersCount } from "@/server/actions/user/queries";
import {
DollarSignIcon,
UserRoundCheckIcon,
UserRoundPlusIcon,
Users2Icon,
} from "lucide-react";
import Link from "next/link";

export default async function AdminDashPage() {
const user = await getUser();
const usersCountData = await getUsersCount();
const usersChartData = usersCountData.usersCountByMonth;

const subscriptionsCountData = await getSubscriptionsCount({});

const activeSubscriptionsCountData = await getSubscriptionsCount({
status: "active",
});
const subsChartData = subscriptionsCountData.subscriptionsCountByMonth;

const revenueCountData = await getRevenueCount();
const revenueChartData = revenueCountData.revenueCountByMonth;

return (
<div>
<h1>Admin Dashboard</h1>
<p>Welcome {user?.name}</p>
<p>{user?.isNewUser ? "Yes" : "No"}</p>
</div>
<AppPageShell
title={adminDashConfig.title}
description={adminDashConfig.description}
>
<div className="grid w-full gap-8">
<p className="text-sm">
This a simple dashboard with Analytics, to see detailed
Analytics go to{" "}
<Link
href={siteUrls.admin.analytics}
className={cn(
buttonVariants({
variant: "link",
size: "default",
className: "px-0 underline",
}),
)}
>
PostHog Dashboard
</Link>
</p>

<div className="grid grid-cols-4 gap-4">
<StatsCard
title="Users"
value={String(usersCountData.totalCount)}
Icon={Users2Icon}
subText="Total users joined"
/>

<StatsCard
title="Revenue"
value={revenueCountData.totalRevenue}
Icon={DollarSignIcon}
subText="Total revenue generated"
/>

<StatsCard
title="Subscriptions"
value={String(subscriptionsCountData.totalCount)}
Icon={UserRoundPlusIcon}
subText="Total subscriptions made"
/>

<StatsCard
title="Active Subscriptions"
value={String(activeSubscriptionsCountData.totalCount)}
Icon={UserRoundCheckIcon}
subText="Current active subscriptions"
/>
</div>

<div className="grid grid-cols-2 gap-4">
<UsersChart data={usersChartData} />

<SubsChart data={subsChartData} />

<RevenueChart data={revenueChartData} />
</div>
</div>
</AppPageShell>
);
}
2 changes: 1 addition & 1 deletion starterkits/saas/src/app/api/lemonsqueezy/webhook/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { webhookHasMeta } from "@/validations/lemonsqueezy";
import {
processWebhookEvent,
storeWebhookEvent,
} from "@/server/actions/plans/mutations";
} from "@/server/actions/subscription/mutations";

export async function POST(request: Request) {
const rawBody = await request.text();
Expand Down
Loading
Loading