1- "use client" ;
1+ import Link from "next/link" ;
2+ import { redirect } from "next/navigation" ;
3+ import AccountUrlFilter from "@/components/account-url-filter" ;
4+ import StatCard from "@/components/stat-card" ;
5+ import StatusBadge from "@/components/status-badge" ;
6+ import { getDashboardStats } from "@/lib/server/stats" ;
7+ import { getCurrentWorkspaceContext } from "@/lib/workspace-access" ;
28
39/**
4- * Dashboard Home Page
10+ * Dashboard Home Page (Server Component)
511 *
6- * Overview cards, 7-day chart, and recent activity feed.
12+ * Renders the stats tiles, 7-day chart, keywords, and recent activity directly
13+ * from the database — no client fetch, no JSON round-trip. The account filter
14+ * lives in the URL (`?instagramAccountId=`), so switching accounts is a plain
15+ * navigation that re-renders this component; the only client island is the
16+ * account `<select>`.
717 */
818
9- import { useState } from "react" ;
10- import AccountSelect , { type AccountOption } from "@/components/account-select" ;
11- import StatCard from "@/components/stat-card" ;
12- import StatusBadge from "@/components/status-badge" ;
13- import { useCachedFetch } from "@/lib/client-cache" ;
14-
15- interface DashboardStats {
16- userName : string | null ;
17- contactsCount : number ;
18- totalAutomations : number ;
19- activeAutomations : number ;
20- dmsSentToday : number ;
21- dmsSentWeek : number ;
22- dmsSentMonth : number ;
23- dmsSkippedMonth : number ;
24- dmsFailedMonth : number ;
25- totalDMs : number ;
26- clicksThisMonth : number ;
27- totalClicks : number ;
28- ctrThisMonth : number ;
29- instagramAccounts : AccountOption [ ] ;
30- selectedInstagramAccountId : string | null ;
31- topKeywords : { keyword : string ; count : number } [ ] ;
32- dailyDMs : { date : string ; count : number } [ ] ;
33- recentLogs : Array < {
34- id : string ;
35- commenterName : string | null ;
36- commentText : string ;
37- status : string ;
38- createdAt : string ;
39- automation : { name : string } ;
40- instagramAccount ?: { username : string } ;
41- } > ;
42- }
19+ export const dynamic = "force-dynamic" ;
4320
44- export default function DashboardPage ( ) {
45- const [ selectedAccountId , setSelectedAccountId ] = useState ( "all" ) ;
21+ export default async function DashboardPage ( props : PageProps < "/dashboard" > ) {
22+ // One auth() session lookup + one membership query covers both the
23+ // workspace and the user — the previous code did two separate auth()
24+ // round trips (getCurrentWorkspaceId + getCurrentUserId).
25+ const context = await getCurrentWorkspaceContext ( ) ;
26+ if ( ! context ) redirect ( "/login" ) ;
4627
47- // Cached copy paints instantly on return visits; a background fetch keeps it
48- // fresh. 30s max age is plenty for dashboard tiles.
49- const statsFetch = useCachedFetch < DashboardStats > (
50- `dash:stats:${ selectedAccountId } ` ,
51- async ( ) => {
52- const params = new URLSearchParams ( ) ;
53- if ( selectedAccountId !== "all" ) {
54- params . set ( "instagramAccountId" , selectedAccountId ) ;
55- }
28+ const searchParams = await props . searchParams ;
29+ const selectedAccountId =
30+ typeof searchParams . instagramAccountId === "string"
31+ ? searchParams . instagramAccountId
32+ : "all" ;
5633
57- const res = await fetch (
58- `/api/dashboard/stats${ params . size ? `?${ params } ` : "" } `
59- ) ;
60- const data = await res . json ( ) ;
61- if ( ! data . success ) {
62- throw new Error ( data . error ?? "Failed to load stats" ) ;
63- }
64- return data . data as DashboardStats ;
65- } ,
66- { maxAgeMs : 30_000 }
34+ const stats = await getDashboardStats (
35+ context . workspaceId ,
36+ context . userId ,
37+ selectedAccountId === "all" ? null : selectedAccountId
6738 ) ;
68- const stats = statsFetch . data ;
69-
70- function handleAccountChange ( accountId : string ) {
71- setSelectedAccountId ( accountId ) ;
72- }
73-
74- if ( statsFetch . loading && ! stats ) {
75- return (
76- < div className = "space-y-6" >
77- < div className = "grid grid-cols-2 lg:grid-cols-3 xl:grid-cols-6 gap-3 sm:gap-4" >
78- { [ ...Array ( 6 ) ] . map ( ( _ , i ) => (
79- < div key = { i } className = "panel rounded p-5 h-32" >
80- < div className = "w-10 h-10 rounded bg-surface-hover" />
81- < div className = "mt-4 h-6 w-16 bg-surface-hover rounded" />
82- < div className = "mt-2 h-4 w-24 bg-surface-hover/60 rounded" />
83- </ div >
84- ) ) }
85- </ div >
86- </ div >
87- ) ;
88- }
89-
90- const maxDM = Math . max ( ...( stats ?. dailyDMs . map ( ( d ) => d . count ) ?? [ 1 ] ) , 1 ) ;
9139
92- const connectedCount = stats ?. instagramAccounts . length ?? 0 ;
40+ const maxDM = Math . max ( ...stats . dailyDMs . map ( ( d ) => d . count ) , 1 ) ;
41+ const connectedCount = stats . instagramAccounts . length ;
9342
9443 return (
9544 < div className = "space-y-8" >
9645 { /* Greeting header */ }
9746 < div className = "flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between" >
9847 < div >
9948 < h1 className = "text-2xl font-bold text-foreground sm:text-3xl" >
100- Hello, { stats ? .userName ?? "there" } !
49+ Hello, { stats . userName ?? "there" } !
10150 </ h1 >
10251 < p className = "mt-1 text-sm text-muted" >
10352 { connectedCount } connected{ " " }
10453 { connectedCount === 1 ? "account" : "accounts" }
10554 { " · " }
106- { stats ? .contactsCount ?? 0 } { " " }
107- { stats ? .contactsCount === 1 ? "contact" : "contacts" }
55+ { stats . contactsCount } { " " }
56+ { stats . contactsCount === 1 ? "contact" : "contacts" }
10857 { " · " }
109- < a href = "/logs" className = "text-accent hover:underline" >
58+ < Link href = "/logs" className = "text-accent hover:underline" >
11059 See activity
111- </ a >
60+ </ Link >
11261 </ p >
11362 </ div >
114- { stats && stats . instagramAccounts . length > 1 && (
115- < AccountSelect
63+ { stats . instagramAccounts . length > 1 && (
64+ < AccountUrlFilter
11665 accounts = { stats . instagramAccounts }
11766 value = { selectedAccountId }
118- onChange = { handleAccountChange }
11967 />
12068 ) }
12169 </ div >
12270
12371 { /* Stat Cards */ }
12472 < div className = "grid grid-cols-2 lg:grid-cols-3 xl:grid-cols-6 gap-3 sm:gap-4" >
125- < StatCard
126- label = "Active Campaigns"
127- value = { stats ?. activeAutomations ?? 0 }
128- />
129- < StatCard label = "DMs Sent" value = { stats ?. dmsSentMonth ?? 0 } />
130- < StatCard label = "Skipped" value = { stats ?. dmsSkippedMonth ?? 0 } />
131- < StatCard label = "Failed" value = { stats ?. dmsFailedMonth ?? 0 } />
132- < StatCard label = "Clicks" value = { stats ?. clicksThisMonth ?? 0 } />
133- < StatCard label = "CTR" value = { `${ stats ?. ctrThisMonth ?? 0 } %` } />
73+ < StatCard label = "Active Campaigns" value = { stats . activeAutomations } />
74+ < StatCard label = "DMs Sent" value = { stats . dmsSentMonth } />
75+ < StatCard label = "Skipped" value = { stats . dmsSkippedMonth } />
76+ < StatCard label = "Failed" value = { stats . dmsFailedMonth } />
77+ < StatCard label = "Clicks" value = { stats . clicksThisMonth } />
78+ < StatCard label = "CTR" value = { `${ stats . ctrThisMonth } %` } />
13479 </ div >
13580
13681 { /* Chart + Recent Activity */ }
@@ -139,7 +84,7 @@ export default function DashboardPage() {
13984 < div className = "lg:col-span-3 panel rounded p-4 sm:p-6" >
14085 < h2 className = "text-sm font-semibold text-foreground mb-6" > DMs — Last 7 Days</ h2 >
14186 < div className = "flex items-end gap-1.5 h-40 sm:gap-2" >
142- { stats ? .dailyDMs . map ( ( day ) => (
87+ { stats . dailyDMs . map ( ( day ) => (
14388 < div key = { day . date } className = "min-w-0 flex-1 flex flex-col items-center gap-2" >
14489 < span className = "text-xs text-muted font-medium" > { day . count } </ span >
14590 < div
@@ -159,10 +104,10 @@ export default function DashboardPage() {
159104 < div className = "lg:col-span-1 panel rounded p-4 sm:p-6" >
160105 < h2 className = "text-sm font-semibold text-foreground mb-4" > Top Keywords</ h2 >
161106 < div className = "space-y-3" >
162- { stats ? .topKeywords . length === 0 && (
107+ { stats . topKeywords . length === 0 && (
163108 < p className = "text-sm text-muted py-8" > No keyword matches yet</ p >
164109 ) }
165- { stats ? .topKeywords . map ( ( keyword ) => (
110+ { stats . topKeywords . map ( ( keyword ) => (
166111 < div key = { keyword . keyword } className = "flex items-center justify-between gap-3" >
167112 < span className = "truncate text-sm font-medium text-foreground" >
168113 { keyword . keyword }
@@ -177,10 +122,10 @@ export default function DashboardPage() {
177122 < div className = "lg:col-span-2 panel rounded p-4 sm:p-6" >
178123 < h2 className = "text-sm font-semibold text-foreground mb-4" > Recent Activity</ h2 >
179124 < div className = "space-y-3 max-h-60 overflow-y-auto" >
180- { stats ? .recentLogs . length === 0 && (
125+ { stats . recentLogs . length === 0 && (
181126 < p className = "text-sm text-muted text-center py-8" > No activity yet</ p >
182127 ) }
183- { stats ? .recentLogs . map ( ( log ) => (
128+ { stats . recentLogs . map ( ( log ) => (
184129 < div
185130 key = { log . id }
186131 className = "flex items-center justify-between gap-3 py-2 border-b border-border last:border-0"
0 commit comments