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(); +}