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
17512export 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