@@ -173,7 +174,7 @@ export function RecentTransactions() {
)}
>
{isCredit ? "+" : "−"}
- {formatEurFromCents(Math.abs(t.amount))}
+ {formatCredits(Math.abs(t.amount), locale)}
diff --git a/convex/_generated/api.d.ts b/convex/_generated/api.d.ts
index fd12472..77559b8 100644
--- a/convex/_generated/api.d.ts
+++ b/convex/_generated/api.d.ts
@@ -17,6 +17,7 @@ import type * as auth from "../auth.js";
import type * as batch_validation_utils from "../batch_validation_utils.js";
import type * as canvases from "../canvases.js";
import type * as credits from "../credits.js";
+import type * as dashboard from "../dashboard.js";
import type * as edges from "../edges.js";
import type * as export_ from "../export.js";
import type * as freepik from "../freepik.js";
@@ -48,6 +49,7 @@ declare const fullApi: ApiFromModules<{
batch_validation_utils: typeof batch_validation_utils;
canvases: typeof canvases;
credits: typeof credits;
+ dashboard: typeof dashboard;
edges: typeof edges;
export: typeof export_;
freepik: typeof freepik;
diff --git a/convex/credits.ts b/convex/credits.ts
index 5644adc..dcf3d1d 100644
--- a/convex/credits.ts
+++ b/convex/credits.ts
@@ -3,6 +3,7 @@ import { v, ConvexError } from "convex/values";
import { optionalAuth, requireAuth } from "./helpers";
import { internal } from "./_generated/api";
import { MONTHLY_TIER_CREDITS, normalizeBillingTier } from "../lib/tier-credits";
+import { prioritizeRecentCreditTransactions } from "../lib/credits-activity";
// ============================================================================
// Tier-Konfiguration
@@ -239,12 +240,15 @@ export const getRecentTransactions = query({
return [];
}
const limit = args.limit ?? 10;
+ const readLimit = Math.min(Math.max(limit * 4, 20), 100);
- return await ctx.db
+ const transactions = await ctx.db
.query("creditTransactions")
.withIndex("by_user", (q) => q.eq("userId", user.userId))
.order("desc")
- .take(limit);
+ .take(readLimit);
+
+ return prioritizeRecentCreditTransactions(transactions, limit);
},
});
diff --git a/convex/dashboard.ts b/convex/dashboard.ts
new file mode 100644
index 0000000..054aa85
--- /dev/null
+++ b/convex/dashboard.ts
@@ -0,0 +1,102 @@
+import { query } from "./_generated/server";
+
+import { optionalAuth } from "./helpers";
+import { prioritizeRecentCreditTransactions } from "../lib/credits-activity";
+import { MONTHLY_TIER_CREDITS, normalizeBillingTier } from "../lib/tier-credits";
+
+const DEFAULT_TIER = "free" as const;
+const DEFAULT_SUBSCRIPTION_STATUS = "active" as const;
+
+export const getSnapshot = query({
+ args: {},
+ handler: async (ctx) => {
+ const user = await optionalAuth(ctx);
+
+ if (!user) {
+ return {
+ balance: { balance: 0, reserved: 0, available: 0, monthlyAllocation: 0 },
+ subscription: {
+ tier: DEFAULT_TIER,
+ status: DEFAULT_SUBSCRIPTION_STATUS,
+ currentPeriodEnd: undefined,
+ },
+ usageStats: {
+ monthlyUsage: 0,
+ totalGenerations: 0,
+ monthlyCredits: MONTHLY_TIER_CREDITS[DEFAULT_TIER],
+ },
+ recentTransactions: [],
+ canvases: [],
+ generatedAt: Date.now(),
+ };
+ }
+
+ const [balanceRow, subscriptionRow, usageTransactions, recentTransactionsRaw, canvases] =
+ await Promise.all([
+ ctx.db
+ .query("creditBalances")
+ .withIndex("by_user", (q) => q.eq("userId", user.userId))
+ .unique(),
+ ctx.db
+ .query("subscriptions")
+ .withIndex("by_user", (q) => q.eq("userId", user.userId))
+ .order("desc")
+ .first(),
+ ctx.db
+ .query("creditTransactions")
+ .withIndex("by_user_type", (q) => q.eq("userId", user.userId).eq("type", "usage"))
+ .order("desc")
+ .collect(),
+ ctx.db
+ .query("creditTransactions")
+ .withIndex("by_user", (q) => q.eq("userId", user.userId))
+ .order("desc")
+ .take(80),
+ ctx.db
+ .query("canvases")
+ .withIndex("by_owner_updated", (q) => q.eq("ownerId", user.userId))
+ .order("desc")
+ .collect(),
+ ]);
+
+ const tier = normalizeBillingTier(subscriptionRow?.tier);
+ const monthStart = new Date(new Date().getFullYear(), new Date().getMonth(), 1).getTime();
+ let monthlyUsage = 0;
+ let totalGenerations = 0;
+
+ for (const transaction of usageTransactions) {
+ if (transaction._creationTime < monthStart) {
+ break;
+ }
+
+ if (transaction.status === "committed") {
+ monthlyUsage += Math.abs(transaction.amount);
+ totalGenerations += 1;
+ }
+ }
+
+ const balance = {
+ balance: balanceRow?.balance ?? 0,
+ reserved: balanceRow?.reserved ?? 0,
+ available: (balanceRow?.balance ?? 0) - (balanceRow?.reserved ?? 0),
+ monthlyAllocation: balanceRow?.monthlyAllocation ?? MONTHLY_TIER_CREDITS[tier],
+ };
+
+ return {
+ balance,
+ subscription: {
+ tier: subscriptionRow?.tier ?? DEFAULT_TIER,
+ status: subscriptionRow?.status ?? DEFAULT_SUBSCRIPTION_STATUS,
+ currentPeriodEnd: subscriptionRow?.currentPeriodEnd,
+ },
+ usageStats: {
+ monthlyUsage,
+ totalGenerations,
+ monthlyCredits: MONTHLY_TIER_CREDITS[tier],
+ },
+ recentTransactions: prioritizeRecentCreditTransactions(recentTransactionsRaw, 20),
+ canvases,
+ generatedAt: Date.now(),
+ };
+ },
+});
diff --git a/hooks/use-dashboard-snapshot.ts b/hooks/use-dashboard-snapshot.ts
new file mode 100644
index 0000000..abd89eb
--- /dev/null
+++ b/hooks/use-dashboard-snapshot.ts
@@ -0,0 +1,60 @@
+"use client";
+
+import { useEffect, useMemo } from "react";
+import type { FunctionReturnType } from "convex/server";
+
+import { api } from "@/convex/_generated/api";
+import { useAuthQuery } from "@/hooks/use-auth-query";
+import {
+ clearDashboardSnapshotCache,
+ readDashboardSnapshotCache,
+ writeDashboardSnapshotCache,
+} from "@/lib/dashboard-snapshot-cache";
+
+export type DashboardSnapshot = FunctionReturnType