Skip to content

feat: open page #34 #48

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

Open
wants to merge 10 commits into
base: main
Choose a base branch
from
101 changes: 100 additions & 1 deletion apps/backend/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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 }],
Expand Down Expand Up @@ -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({
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this endpoint is doing too many DB calls. Can we make it more optimal?
Also cache this to 1 day intervals.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I already cached the data and revalidating every hour using ISR let me set it for a day

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;
Expand Down
36 changes: 36 additions & 0 deletions apps/web/app/open/page.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div className="min-h-screen bg-background py-20 px-4 sm:px-6 lg:px-8">
<div className="max-w-7xl mx-auto">
<Heading className="text-4xl font-bold text-foreground text-center mb-12 ">
Open Stats
</Heading>

<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-5 gap-6 mb-16">
<StatCards stats={statsData?.data || {}} />
</div>

<div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
<OpenCharts statsData={statsData?.data || []} />
</div>
</div>
</div>
);
}

export default OpenPage;
20 changes: 20 additions & 0 deletions apps/web/components/open/Heading.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<motion.h1
initial={{ opacity: 0, y: -20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, ease: "easeOut" }}
className={className || ""}
>
{children}
</motion.h1>
);
};

export default Heading;
15 changes: 15 additions & 0 deletions apps/web/components/open/Loader.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import React from "react";
import { motion } from "framer-motion";
const Loader = () => {
return (
<div className="min-h-screen flex items-center justify-center bg-background">
<motion.div
animate={{ rotate: 360 }}
transition={{ duration: 1, repeat: Infinity, ease: "linear" }}
className="w-12 h-12 border-4 border-primary border-t-transparent rounded-full"
/>
</div>
);
};

export default Loader;
21 changes: 21 additions & 0 deletions apps/web/components/open/OpenCardUI.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import React from "react";

interface Props {
className? : string,
children: React.ReactNode
}
export const Card:React.FC<Props> = ({ className, children }) => (
<div className={`rounded-lg border p-4 ${className}`}>{children}</div>
);

export const CardHeader:React.FC<Props> = ({ className, children }) => (
<div className={`pb-2 ${className}`}>{children}</div>
);

export const CardTitle:React.FC<Props> = ({ className, children }) => (
<h3 className={`text-lg font-semibold ${className}`}>{children}</h3>
);

export const CardContent:React.FC<Props> = ({ className, children }) => (
<div className={className}>{children}</div>
);
118 changes: 118 additions & 0 deletions apps/web/components/open/OpenCharts.tsx
Original file line number Diff line number Diff line change
@@ -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) => (
<motion.div
key={chart.title}
variants={chartVariants}
initial="hidden"
animate="visible"
>
<Card className="bg-background/80 border-border/30 backdrop-blur-sm">
<CardHeader>
<CardTitle className="text-lg font-semibold text-foreground">
{chart.title}
</CardTitle>
</CardHeader>
<CardContent>
<div className="h-80">
<ResponsiveContainer width="100%" height="100%">
<LineChart data={chart.data}>
<CartesianGrid
strokeDasharray="3 3"
stroke={theme === "dark" ? "#444" : "#ddd"}
/>
<XAxis
dataKey="date"
tickFormatter={(date) => format(new Date(date), "MMM dd")}
stroke={theme === "dark" ? "#ccc" : "#666"}
/>
<YAxis stroke={theme === "dark" ? "#ccc" : "#666"} />
<Tooltip
contentStyle={{
backgroundColor: theme === "dark" ? "#333" : "#fff",
border: "none",
borderRadius: "8px",
boxShadow: "0 4px 6px rgba(0,0,0,0.1)",
}}
/>
<Line
type="monotone"
dataKey={chart.key}
stroke={chart.color}
strokeWidth={2}
dot={{ r: 4 }}
activeDot={{ r: 6 }}
/>
</LineChart>
</ResponsiveContainer>
</div>
</CardContent>
</Card>
</motion.div>
));
};

export default OpenCharts;
Loading