diff --git a/apps/backend/index.ts b/apps/backend/index.ts
index 313b4ea..675b403 100644
--- a/apps/backend/index.ts
+++ b/apps/backend/index.ts
@@ -11,6 +11,11 @@ import { FalAIModel } from "./models/FalAIModel";
import cors from "cors";
import { authMiddleware } from "./middleware";
import dotenv from "dotenv";
+import {
+ ModelTrainingStatusEnum,
+ OutputImageStatusEnum,
+ TransactionStatus,
+} from "@prisma/client";
import paymentRoutes from "./routes/payment.routes";
import { router as webhookRouter } from "./routes/webhook.routes";
@@ -264,7 +269,7 @@ app.get("/image/bulk", authMiddleware, async (req, res) => {
});
});
-app.get("/models", authMiddleware, async (req, res) => {
+app.get("/models", async (req, res) => {
const models = await prismaClient.model.findMany({
where: {
OR: [{ userId: req.userId }, { open: true }],
@@ -441,6 +446,100 @@ app.post("/fal-ai/webhook/image", async (req, res) => {
});
});
+app.get("/open", async (req, res) => {
+ console.log("in open");
+ const userCount = await prismaClient.user.count();
+ const trainedModelCount = await prismaClient.model.count({
+ where: { trainingStatus: ModelTrainingStatusEnum.Generated },
+ });
+ const imagesCount = await prismaClient.outputImages.count({
+ where: { status: OutputImageStatusEnum.Generated },
+ });
+ const packCount = await prismaClient.packs.count();
+
+ const totalRevenue = await prismaClient.transaction.aggregate({
+ where: { status: TransactionStatus.SUCCESS },
+ _sum: { amount: true },
+ });
+
+ console.log("open stats", {
+ userCount,
+ trainedModelCount,
+ imagesCount,
+ packCount,
+ totalRevenue: totalRevenue._sum.amount,
+ });
+ const thirtyDaysAgo = new Date();
+ thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30);
+
+ const dailyUsers = await prismaClient.user.groupBy({
+ by: ["createdAt"],
+ where: { createdAt: { gte: thirtyDaysAgo } },
+ _count: { id: true },
+ orderBy: { createdAt: "asc" },
+ });
+
+ const dailyTrainedModels = await prismaClient.model.groupBy({
+ by: ["createdAt"],
+ where: {
+ createdAt: { gte: thirtyDaysAgo },
+ trainingStatus: ModelTrainingStatusEnum.Generated,
+ },
+ _count: { id: true },
+ orderBy: { createdAt: "asc" },
+ });
+
+ const dailyGeneratedImages = await prismaClient.outputImages.groupBy({
+ by: ["createdAt"],
+ where: {
+ status: OutputImageStatusEnum.Generated,
+ createdAt: { gte: thirtyDaysAgo },
+ },
+ _count: { id: true },
+ orderBy: { createdAt: "asc" },
+ });
+
+ const dailyRevenue = await prismaClient.transaction.groupBy({
+ by: ["createdAt"],
+ where: {
+ createdAt: { gte: thirtyDaysAgo },
+ status: TransactionStatus.SUCCESS,
+ },
+ _sum: { amount: true },
+ orderBy: { createdAt: "asc" },
+ });
+
+ const charts = {
+ dailyUsers: dailyUsers.map((user) => ({
+ date: user.createdAt,
+ count: user._count.id,
+ })),
+ dailyTrainedModels: dailyTrainedModels.map((model) => ({
+ date: model.createdAt,
+ count: model._count.id,
+ })),
+ dailyGeneratedImages: dailyGeneratedImages.map((image) => ({
+ date: image.createdAt,
+ count: image._count.id,
+ })),
+ dailyRevenue: dailyRevenue.map((entry) => ({
+ date: entry.createdAt,
+ amount: entry._sum.amount || 0,
+ })),
+ };
+
+ res.status(200).json({
+ data: {
+ userCount,
+ trainedModelCount,
+ imagesCount,
+ packCount,
+ totalRevenue: totalRevenue._sum.amount,
+ charts,
+ },
+ });
+});
+
app.get("/model/status/:modelId", authMiddleware, async (req, res) => {
try {
const modelId = req.params.modelId;
diff --git a/apps/web/app/open/page.tsx b/apps/web/app/open/page.tsx
new file mode 100644
index 0000000..3c26f3a
--- /dev/null
+++ b/apps/web/app/open/page.tsx
@@ -0,0 +1,36 @@
+import React from "react";
+import { BACKEND_URL } from "../config";
+import Heading from "@/components/open/Heading";
+import StatCards from "@/components/open/StatCards";
+import OpenCharts from "@/components/open/OpenCharts";
+
+async function getStatsData() {
+ const response = await fetch(`${BACKEND_URL}/open`, {
+ next: { revalidate: 60 * 60 * 24 },
+ });
+ return response.json();
+}
+
+async function OpenPage() {
+ const statsData = await getStatsData();
+
+ return (
+
+
+
+ Open Stats
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
+
+export default OpenPage;
diff --git a/apps/web/components/open/Heading.tsx b/apps/web/components/open/Heading.tsx
new file mode 100644
index 0000000..e134474
--- /dev/null
+++ b/apps/web/components/open/Heading.tsx
@@ -0,0 +1,20 @@
+"use client";
+import React, { ReactNode } from "react";
+import { motion } from "framer-motion";
+const Heading: React.FC<{ children: ReactNode; className?: string }> = ({
+ children,
+ className
+}) => {
+ return (
+
+ {children}
+
+ );
+};
+
+export default Heading;
diff --git a/apps/web/components/open/Loader.tsx b/apps/web/components/open/Loader.tsx
new file mode 100644
index 0000000..5cf76c8
--- /dev/null
+++ b/apps/web/components/open/Loader.tsx
@@ -0,0 +1,15 @@
+import React from "react";
+import { motion } from "framer-motion";
+const Loader = () => {
+ return (
+
+
+
+ );
+};
+
+export default Loader;
diff --git a/apps/web/components/open/OpenCardUI.tsx b/apps/web/components/open/OpenCardUI.tsx
new file mode 100644
index 0000000..6cb3c7b
--- /dev/null
+++ b/apps/web/components/open/OpenCardUI.tsx
@@ -0,0 +1,21 @@
+import React from "react";
+
+interface Props {
+ className? : string,
+ children: React.ReactNode
+}
+export const Card:React.FC = ({ className, children }) => (
+ {children}
+);
+
+export const CardHeader:React.FC = ({ className, children }) => (
+ {children}
+);
+
+export const CardTitle:React.FC = ({ className, children }) => (
+ {children}
+);
+
+export const CardContent:React.FC = ({ className, children }) => (
+ {children}
+);
diff --git a/apps/web/components/open/OpenCharts.tsx b/apps/web/components/open/OpenCharts.tsx
new file mode 100644
index 0000000..986614d
--- /dev/null
+++ b/apps/web/components/open/OpenCharts.tsx
@@ -0,0 +1,118 @@
+"use client";
+
+import { motion } from "framer-motion";
+import {
+ LineChart,
+ Line,
+ XAxis,
+ YAxis,
+ CartesianGrid,
+ Tooltip,
+ ResponsiveContainer,
+} from "recharts";
+import { format } from "date-fns";
+import { Card, CardContent, CardHeader, CardTitle } from "./OpenCardUI";
+import { useTheme } from "next-themes";
+import { StatsData } from "@/types";
+
+const chartVariants = {
+ hidden: { opacity: 0, scale: 0.95 },
+ visible: {
+ opacity: 1,
+ scale: 1,
+ transition: { duration: 0.7, ease: "easeOut" },
+ },
+};
+
+interface CharConfig {
+ title: string;
+ data:
+ | { date: string }[]
+ | { date: string; count?: number; amount?: number }[];
+ key: "count" | "amount";
+ color: string;
+}
+
+const OpenCharts: React.FC<{ statsData: StatsData }> = ({ statsData }) => {
+ const { theme } = useTheme();
+
+ const chartConfig: CharConfig[] = [
+ {
+ title: "Daily Users",
+ data: statsData.charts?.dailyUsers,
+ key: "count",
+ color: "#3b82f6",
+ },
+ {
+ title: "Daily Trained Models",
+ data: statsData.charts?.dailyTrainedModels,
+ key: "count",
+ color: "#10b981",
+ },
+ {
+ title: "Daily Generated Images",
+ data: statsData.charts?.dailyGeneratedImages,
+ key: "count",
+ color: "#f59e0b",
+ },
+ {
+ title: "Daily Revenue",
+ data: statsData.charts?.dailyRevenue,
+ key: "amount",
+ color: "#ef4444",
+ },
+ ];
+
+ return (chartConfig || []).map((chart: CharConfig) => (
+
+
+
+
+ {chart.title}
+
+
+
+
+
+
+
+ format(new Date(date), "MMM dd")}
+ stroke={theme === "dark" ? "#ccc" : "#666"}
+ />
+
+
+
+
+
+
+
+
+
+ ));
+};
+
+export default OpenCharts;
diff --git a/apps/web/components/open/StatCards.tsx b/apps/web/components/open/StatCards.tsx
new file mode 100644
index 0000000..ce1f675
--- /dev/null
+++ b/apps/web/components/open/StatCards.tsx
@@ -0,0 +1,76 @@
+"use client";
+
+import React from "react";
+import { motion } from "framer-motion";
+import { BarChart2, DollarSign, Image, Package, Users } from "lucide-react";
+import { Card, CardContent, CardHeader, CardTitle } from "./OpenCardUI";
+import { StatsData } from "@/types";
+
+interface StateProps {
+ stats: Pick<
+ StatsData,
+ | "userCount"
+ | "trainedModelCount"
+ | "imagesCount"
+ | "packCount"
+ | "totalRevenue"
+ >;
+}
+
+const statVariants = {
+ hidden: { opacity: 0, y: 20 },
+ visible: (i: number) => ({
+ opacity: 1,
+ y: 0,
+ transition: { delay: i * 0.2, duration: 0.5, ease: "easeOut" },
+ }),
+};
+
+const StatCard: React.FC = ({ stats }) => {
+ const statsData = [
+ { title: "User Count", value: stats?.userCount, icon: Users },
+ {
+ title: "Models Trained",
+ value: stats.trainedModelCount,
+ icon: BarChart2,
+ },
+ {
+ title: "Images Generated ",
+ value: stats.imagesCount,
+ icon: Image,
+ },
+ {
+ title: "Pack Requests",
+ value: stats.packCount,
+ icon: Package,
+ },
+ {
+ title: "Total Revenue",
+ value: `$${stats.totalRevenue?.toLocaleString() || 0}`,
+ icon: DollarSign,
+ },
+ ];
+ return statsData.map((stat, index) => (
+
+
+
+
+
+ {stat.title}
+
+
+
+ {stat.value}
+
+
+
+ ));
+};
+
+export default StatCard;
diff --git a/apps/web/next.config.js b/apps/web/next.config.js
index c5bb7c7..b806d79 100644
--- a/apps/web/next.config.js
+++ b/apps/web/next.config.js
@@ -2,6 +2,11 @@
const nextConfig = {
images: {
remotePatterns: [
+
+ {
+ protocol: "https",
+ hostname: "dummyimage.com",
+ },
{
protocol: "https",
hostname: "r2-us-west.photoai.com",
diff --git a/apps/web/package.json b/apps/web/package.json
index 2469cc0..b7acf47 100644
--- a/apps/web/package.json
+++ b/apps/web/package.json
@@ -43,6 +43,7 @@
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-hot-toast": "^2.5.1",
+ "recharts": "^2.15.1",
"tailwind-merge": "^3.0.1",
"tailwindcss": "^4.0.6",
"tailwindcss-animate": "^1.0.7"
diff --git a/apps/web/types/index.ts b/apps/web/types/index.ts
index b17afcd..9966eae 100644
--- a/apps/web/types/index.ts
+++ b/apps/web/types/index.ts
@@ -44,3 +44,18 @@ export interface Transaction {
createdAt: string;
updatedAt: string;
}
+
+export type StatsData = {
+ userCount: number;
+ trainedModelCount: number;
+ imagesCount: number;
+ packCount: number;
+ totalRevenue: number;
+ charts: {
+ dailyUsers: { date: string; count: number }[];
+ dailyTrainedModels: { date: string; count: number }[];
+ dailyGeneratedImages: { date: string; count: number }[];
+ dailyRevenue: { date: string; amount: number }[];
+ };
+};
+
diff --git a/packages/db/package.json b/packages/db/package.json
index c8687a0..3511669 100644
--- a/packages/db/package.json
+++ b/packages/db/package.json
@@ -5,14 +5,21 @@
"exports": {
".": "./index.ts"
},
+ "scripts": {
+ "seed": "bun run seed.ts"
+ },
"devDependencies": {
"@types/bun": "latest",
- "prisma": "^6.3.1"
+ "prisma": "^6.3.1",
+ "ts-node": "^10.9.1"
+
},
"peerDependencies": {
"typescript": "^5.0.0"
},
"dependencies": {
- "@prisma/client": "6.3.1"
+ "@prisma/client": "6.3.1",
+ "date-fns": "^4.1.0",
+ "uuid": "^11.0.0"
}
}
\ No newline at end of file
diff --git a/packages/db/seed.ts b/packages/db/seed.ts
new file mode 100644
index 0000000..b5fafa4
--- /dev/null
+++ b/packages/db/seed.ts
@@ -0,0 +1,168 @@
+import {
+ PrismaClient,
+ ModelTrainingStatusEnum,
+ OutputImageStatusEnum,
+ TransactionStatus,
+ ModelTypeEnum,
+ EthenecityEnum,
+ EyeColorEnum,
+ PlanType,
+} from "@prisma/client";
+import { subDays } from "date-fns";
+import { v4 as uuidv4 } from "uuid";
+
+const prisma = new PrismaClient();
+
+const modelTypes = [
+ ModelTypeEnum.Man,
+ ModelTypeEnum.Woman,
+ ModelTypeEnum.Others,
+];
+const ethnicities = [
+ EthenecityEnum.White,
+ EthenecityEnum.Black,
+ EthenecityEnum.Asian_American,
+ EthenecityEnum.East_Asian,
+ EthenecityEnum.South_East_Asian,
+ EthenecityEnum.South_Asian,
+ EthenecityEnum.Middle_Eastern,
+ EthenecityEnum.Pacific,
+ EthenecityEnum.Hispanic,
+];
+const eyeColors = [
+ EyeColorEnum.Brown,
+ EyeColorEnum.Blue,
+ EyeColorEnum.Hazel,
+ EyeColorEnum.Gray,
+];
+
+const dummyImage = (text: string) => {
+ return `https://dummyimage.com/100x100/3357ff/ffffff&text=${text}`;
+};
+async function seed() {
+ console.log("Starting database seeding...");
+
+ await prisma.transaction.deleteMany();
+ await prisma.userCredit.deleteMany();
+ await prisma.subscription.deleteMany();
+ await prisma.outputImages.deleteMany();
+ await prisma.packPrompts.deleteMany();
+ await prisma.model.deleteMany();
+ await prisma.packs.deleteMany();
+ await prisma.user.deleteMany();
+
+ const users = Array.from({ length: 19 }, (_, i) => ({
+ clerkId: `clerk_${uuidv4()}`,
+ createdAt: subDays(new Date(), Math.floor(Math.random() * 30)),
+ updatedAt: new Date(),
+ email: `user${i + 1}@example.com`,
+ name: `User ${i + 1}`,
+ profilePicture: dummyImage(`User ${i + 1}`),
+ }));
+ await prisma.user.createMany({ data: users });
+ console.log("Seeded 19 users");
+ const createdUsers = await prisma.user.findMany();
+
+ // -----------------------------------------------
+ const models = Array.from({ length: 15 }, (_, i) => ({
+ createdAt: subDays(new Date(), Math.floor(Math.random() * 30)),
+ updatedAt: new Date(),
+ trainingStatus: ModelTrainingStatusEnum.Generated,
+ name: `Model ${i + 1}`,
+ userId: createdUsers[Math.floor(Math.random() * createdUsers.length)].id,
+ type: modelTypes[Math.floor(Math.random() * modelTypes.length)],
+ age: Math.floor(Math.random() * 25) + 18,
+ ethinicity: ethnicities[Math.floor(Math.random() * ethnicities.length)],
+ eyeColor: eyeColors[Math.floor(Math.random() * eyeColors.length)],
+ bald: Math.random() > 0.5,
+ triggerWord: `trigger${i + 1}`,
+ tensorPath: `/tensor/model${i + 1}.pt`,
+ thumbnail: dummyImage(`Model ${i + 1}`),
+ falAiRequestId: `fal_${uuidv4()}`,
+ zipUrl: `https://example.com/zip${i + 1}.zip`,
+ open: Math.random() > 0.5,
+ }));
+ await prisma.model.createMany({ data: models });
+ console.log("Seeded 15 trained models");
+
+ const createdModels = await prisma.model.findMany();
+
+ // ======================
+ const images = Array.from({ length: 14 }, (_, i) => ({
+ createdAt: subDays(new Date(), Math.floor(Math.random() * 30)),
+ updatedAt: new Date(),
+ status: OutputImageStatusEnum.Generated,
+ imageUrl: dummyImage(`Image ${i + 1}`),
+ modelId: createdModels[Math.floor(Math.random() * createdModels.length)].id,
+ userId: createdUsers[Math.floor(Math.random() * createdUsers.length)].id,
+ prompt: `A photo of a person ${i + 1}`,
+ falAiRequestId: `fal_img_${uuidv4()}`,
+ }));
+ await prisma.outputImages.createMany({ data: images });
+ console.log("Seeded 14 generated images");
+
+ const packs = Array.from({ length: 12 }, (_, i) => ({
+ name: `Pack ${i + 1}`,
+ description: `Description for pack ${i + 1}`,
+ imageUrl1: dummyImage(`Pack ${i + 1}`),
+ imageUrl2: dummyImage(`Pack2 - ${i + 1}`),
+ }));
+ await prisma.packs.createMany({ data: packs });
+ console.log("Seeded 12 packs");
+ const createdPacks = await prisma.packs.findMany();
+
+
+
+ const packPrompts = Array.from({ length: 20 }, (_, i) => ({
+ prompt: `Prompt ${i + 1} for pack`,
+ packId: createdPacks[Math.floor(Math.random() * createdPacks.length)].id,
+ }));
+ await prisma.packPrompts.createMany({ data: packPrompts });
+ console.log("Seeded 20 pack prompts");
+
+ const plans = [PlanType.basic, PlanType.premium];
+ const subscriptions = Array.from({ length: 25 }, (_, i) => ({
+ userId: createdUsers[Math.floor(Math.random() * createdUsers.length)].id,
+ plan: plans[Math.floor(Math.random() * plans.length)],
+ paymentId: `pay_${uuidv4()}`,
+ orderId: `order_${uuidv4()}`,
+ createdAt: subDays(new Date(), Math.floor(Math.random() * 30)),
+ updatedAt: new Date(),
+ }));
+ await prisma.subscription.createMany({ data: subscriptions });
+ console.log("Seeded 25 subscriptions");
+
+ const userCredits = createdUsers.map((user) => ({
+ userId: user.id,
+ amount: Math.floor(Math.random() * 2500),
+ createdAt: subDays(new Date(), Math.floor(Math.random() * 30)),
+ updatedAt: new Date(),
+ }));
+ await prisma.userCredit.createMany({ data: userCredits });
+ console.log("Seeded 25 user credits");
+
+ const transactions = Array.from({ length: 10 }, (_, i) => ({
+ createdAt: subDays(new Date(), Math.floor(Math.random() * 30)),
+ updatedAt: new Date(),
+ status: TransactionStatus.SUCCESS,
+ amount: Math.random() < 0.5? 100 : 50 ,
+ userId: createdUsers[Math.floor(Math.random() * createdUsers.length)].id,
+ currency: "USD",
+ paymentId: `pay_${uuidv4()}`,
+ orderId: `order_${uuidv4()}`,
+ plan: plans[Math.floor(Math.random() * plans.length)],
+ }));
+ await prisma.transaction.createMany({ data: transactions });
+ console.log("Seeded 10 transactions");
+
+ console.log("Seeding completed!");
+}
+
+try {
+ await seed();
+} catch (e) {
+ console.error("Seeding failed:", e);
+ process.exit(1);
+} finally {
+ await prisma.$disconnect();
+}