Skip to content

Commit

Permalink
feat(saas): Admin Dashboard (#20)
Browse files Browse the repository at this point in the history
## Feat(saas): Admin Dashboard with Analytics

This pull request introduces a new admin dashboard to the RapidLaunch SaaS platform, providing administrators with valuable insights and management capabilities. 

**Key features:**

* **Comprehensive dashboard**:  Provides a central hub for administrators to monitor key metrics and manage various aspects of the platform.
* **Analytics integration**:  Includes detailed analytics on user activity, subscription data, and other relevant metrics, empowering data-driven decision making.
  • Loading branch information
alifarooq9 authored May 3, 2024
2 parents 23eee85 + 4c8d65f commit e2bd6e9
Show file tree
Hide file tree
Showing 23 changed files with 2,268 additions and 2,901 deletions.
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

0 comments on commit e2bd6e9

Please sign in to comment.