Skip to content

Commit 2852bf1

Browse files
fix(#416): load consolidation suggestions in HomePage server data
- Add buildConsolidationSuggestions to page-data.ts and call it on the fetched subscriptions before returning, replacing the hardcoded empty array that caused the initial payload to always be empty. - Remove duplicate getInitialData definition and duplicate HomePage export from page.tsx; page now cleanly delegates to page-data.
1 parent 021cc71 commit 2852bf1

2 files changed

Lines changed: 43 additions & 171 deletions

File tree

client/app/page-data.ts

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { createClient } from '@/lib/supabase/server'
22
import { buildQueryWarning, type DataLoadWarning } from '@/lib/dashboard-bootstrap'
3+
import type { ConsolidationSuggestion } from '@/lib/types'
34

45
export type InitialPriceChange = {
56
id: string
@@ -78,6 +79,40 @@ function normalizePriceChange(
7879
}
7980
}
8081

82+
const FLAGGABLE_CATEGORIES = ['ai_tools', 'entertainment', 'productivity', 'design', 'music']
83+
84+
const BUNDLE_SUGGESTIONS: Record<string, string> = {
85+
ai_tools: 'one AI subscription',
86+
entertainment: 'a streaming bundle',
87+
productivity: 'a single productivity suite',
88+
design: 'one design tool',
89+
music: 'one music service',
90+
}
91+
92+
function buildConsolidationSuggestions(subscriptions: any[]): ConsolidationSuggestion[] {
93+
const byCategory: Record<string, any[]> = {}
94+
95+
for (const sub of subscriptions) {
96+
const cat = sub.category
97+
if (!cat || !FLAGGABLE_CATEGORIES.includes(cat)) continue
98+
;(byCategory[cat] ??= []).push(sub)
99+
}
100+
101+
return Object.entries(byCategory)
102+
.filter(([, group]) => group.length >= 2)
103+
.map(([category, group]) => {
104+
const total = group.reduce((sum: number, s: any) => sum + s.price, 0)
105+
const cheapest = Math.min(...group.map((s: any) => s.price))
106+
return {
107+
id: `consolidation_${category}`,
108+
category: category.replace('_', ' '),
109+
services: group.map((s: any) => s.name),
110+
suggestedBundle: BUNDLE_SUGGESTIONS[category] ?? 'a single plan',
111+
savings: (total - cheapest).toFixed(2),
112+
}
113+
})
114+
}
115+
81116
export async function getInitialData(): Promise<InitialDataResult> {
82117
const warnings: DataLoadWarning[] = []
83118

@@ -204,7 +239,7 @@ export async function getInitialData(): Promise<InitialDataResult> {
204239
emailAccounts,
205240
payments,
206241
priceChanges,
207-
consolidationSuggestions: [],
242+
consolidationSuggestions: buildConsolidationSuggestions(subscriptions),
208243
warnings,
209244
isDemo: false,
210245
}

client/app/page.tsx

Lines changed: 7 additions & 170 deletions
Original file line numberDiff line numberDiff line change
@@ -2,178 +2,15 @@
22
* Home page — server component.
33
*/
44

5-
import { Suspense } from "react";
6-
import { getInitialData } from "./page-data";
7-
import { AppClient } from "@/components/app/app-client";
8-
import { LoadingSpinner } from "@/components/ui/loading-spinner";
9-
import type { ConsolidationSuggestion } from "@/lib/types";
5+
import { Suspense } from 'react'
6+
import { getInitialData } from './page-data'
7+
import { AppClient } from '@/components/app/app-client'
8+
import { LoadingSpinner } from '@/components/ui/loading-spinner'
109

11-
interface DbSubscription {
12-
id: string;
13-
name: string;
14-
category: string | null;
15-
price: number;
16-
icon: string | null;
17-
renews_in: number | null;
18-
status: string;
19-
color: string | null;
20-
renewal_url: string | null;
21-
tags: string[] | null;
22-
date_added: string;
23-
email_account_id: string | null;
24-
last_used_at: string | null;
25-
has_api_key: boolean | null;
26-
is_trial: boolean | null;
27-
trial_ends_at: string | null;
28-
price_after_trial: number | null;
29-
source: string | null;
30-
manually_edited: boolean | null;
31-
edited_fields: string[] | null;
32-
pricing_type: string | null;
33-
billing_cycle: string | null;
34-
cancelled_at: string | null;
35-
active_until: string | null;
36-
paused_at: string | null;
37-
resumes_at: string | null;
38-
price_range: string | null;
39-
price_history: unknown;
40-
}
41-
42-
function transformSubscription(dbSub: DbSubscription) {
43-
return {
44-
id: dbSub.id,
45-
name: dbSub.name,
46-
category: dbSub.category,
47-
price: dbSub.price,
48-
icon: dbSub.icon ?? "🔗",
49-
renewsIn: dbSub.renews_in,
50-
status: dbSub.status,
51-
color: dbSub.color ?? "#000000",
52-
renewalUrl: dbSub.renewal_url,
53-
tags: dbSub.tags ?? [],
54-
dateAdded: dbSub.date_added,
55-
emailAccountId: dbSub.email_account_id,
56-
lastUsedAt: dbSub.last_used_at,
57-
hasApiKey: dbSub.has_api_key ?? false,
58-
isTrial: dbSub.is_trial ?? false,
59-
trialEndsAt: dbSub.trial_ends_at,
60-
priceAfterTrial: dbSub.price_after_trial,
61-
source: dbSub.source ?? "manual",
62-
manuallyEdited: dbSub.manually_edited ?? false,
63-
editedFields: dbSub.edited_fields ?? [],
64-
pricingType: dbSub.pricing_type ?? "fixed",
65-
billingCycle: dbSub.billing_cycle ?? "monthly",
66-
cancelledAt: dbSub.cancelled_at,
67-
activeUntil: dbSub.active_until,
68-
pausedAt: dbSub.paused_at,
69-
resumesAt: dbSub.resumes_at,
70-
priceRange: dbSub.price_range,
71-
priceHistory: dbSub.price_history,
72-
};
73-
}
74-
75-
type AppSubscription = ReturnType<typeof transformSubscription>;
76-
77-
const FLAGGABLE_CATEGORIES = ["ai_tools", "entertainment", "productivity", "design", "music"];
78-
79-
const BUNDLE_SUGGESTIONS: Record<string, string> = {
80-
ai_tools: "one AI subscription",
81-
entertainment: "a streaming bundle",
82-
productivity: "a single productivity suite",
83-
design: "one design tool",
84-
music: "one music service",
85-
};
86-
87-
function buildConsolidationSuggestions(subscriptions: AppSubscription[]): ConsolidationSuggestion[] {
88-
const byCategory: Record<string, AppSubscription[]> = {};
89-
90-
for (const sub of subscriptions) {
91-
const cat = sub.category;
92-
if (!cat || !FLAGGABLE_CATEGORIES.includes(cat)) continue;
93-
(byCategory[cat] ??= []).push(sub);
94-
}
95-
96-
return Object.entries(byCategory)
97-
.filter(([, group]) => group.length >= 2)
98-
.map(([category, group]) => {
99-
const total = group.reduce((sum, s) => sum + s.price, 0);
100-
const cheapest = Math.min(...group.map((s) => s.price));
101-
return {
102-
id: `consolidation_${category}`,
103-
category: category.replace("_", " "),
104-
services: group.map((s) => s.name),
105-
suggestedBundle: BUNDLE_SUGGESTIONS[category] ?? "a single plan",
106-
savings: `$${(total - cheapest).toFixed(2)}`,
107-
};
108-
});
109-
}
110-
111-
async function getInitialData() {
112-
try {
113-
const supabase = await createClient();
114-
const { data: { user } } = await supabase.auth.getUser();
115-
116-
if (!user) {
117-
return {
118-
subscriptions: [] as AppSubscription[],
119-
emailAccounts: [],
120-
payments: [],
121-
priceChanges: [],
122-
consolidationSuggestions: [] as ConsolidationSuggestion[],
123-
};
124-
}
125-
126-
const [subscriptionsResult, emailAccountsResult, paymentsResult] = await Promise.all([
127-
supabase.from("subscriptions").select("*").eq("user_id", user.id).order("date_added", { ascending: false }),
128-
supabase.from("email_accounts").select("*").eq("user_id", user.id),
129-
supabase.from("payments").select("*").eq("user_id", user.id).order("created_at", { ascending: false }),
130-
]);
131-
132-
const subscriptions = ((subscriptionsResult.data ?? []) as DbSubscription[]).map(transformSubscription);
133-
134-
return {
135-
subscriptions,
136-
emailAccounts: emailAccountsResult.data ?? [],
137-
payments: paymentsResult.data ?? [],
138-
priceChanges: [],
139-
consolidationSuggestions: buildConsolidationSuggestions(subscriptions),
140-
};
141-
} catch {
142-
return {
143-
subscriptions: [] as AppSubscription[],
144-
emailAccounts: [],
145-
payments: [],
146-
priceChanges: [],
147-
consolidationSuggestions: [] as ConsolidationSuggestion[],
148-
};
149-
}
150-
}
151-
152-
export default async function HomePage() {
153-
const initialData = await getInitialData();
154-
155-
return (
156-
<Suspense
157-
fallback={
158-
<div className="min-h-screen bg-[#F9F6F2] dark:bg-[#1E2A35] flex items-center justify-center">
159-
<LoadingSpinner size="lg" darkMode={false} />
160-
</div>
161-
}
162-
>
163-
<AppClient
164-
initialSubscriptions={initialData.subscriptions}
165-
initialEmailAccounts={initialData.emailAccounts}
166-
initialPayments={initialData.payments}
167-
initialPriceChanges={initialData.priceChanges}
168-
initialConsolidationSuggestions={initialData.consolidationSuggestions}
169-
/>
170-
</Suspense>
171-
);
172-
}
173-
export const dynamic = "force-dynamic";
10+
export const dynamic = 'force-dynamic'
17411

17512
export default async function HomePage() {
176-
const initialData = await getInitialData();
13+
const initialData = await getInitialData()
17714

17815
return (
17916
<Suspense
@@ -193,5 +30,5 @@ export default async function HomePage() {
19330
isDemo={initialData.isDemo}
19431
/>
19532
</Suspense>
196-
);
33+
)
19734
}

0 commit comments

Comments
 (0)