Skip to content

Commit 6825500

Browse files
feat: Add Suspense boundaries
1 parent d370500 commit 6825500

File tree

4 files changed

+126
-45
lines changed

4 files changed

+126
-45
lines changed

cursor-docs/project-plan.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,7 @@
189189
- [x] Efficient data fetching
190190
- [x] Asset optimization
191191
- [x] Edge caching strategy
192+
- [x] Suspense for async operations to improve initial page load
192193

193194
### Security
194195
- [x] Authentication & authorization

src/app/layout.tsx

Lines changed: 54 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,16 @@
11
import type { Metadata } from "next";
22
import { Inter } from "next/font/google";
33
import "./globals.css";
4+
import { Suspense } from "react";
5+
import "server-only";
46

57
import { ThemeProvider } from "@/components/providers";
68
import { Toaster } from "@/components/ui/sonner";
7-
import { getSessionFromCookie } from "@/utils/auth";
89
import { TooltipProvider } from "@/components/ui/tooltip";
910
import { SITE_NAME, SITE_DESCRIPTION, SITE_URL } from "@/constants";
10-
import { getConfig } from "@/flags";
1111
import { StartupStudioStickyBanner } from "@/components/startup-studio-sticky-banner";
12+
import { getSessionFromCookie } from "@/utils/auth";
13+
import { getConfig } from "@/flags";
1214

1315
const inter = Inter({ subsets: ["latin"] });
1416

@@ -49,34 +51,65 @@ export const metadata: Metadata = {
4951
},
5052
};
5153

52-
export default async function BaseLayout({
54+
// This component will be wrapped in Suspense in the BaseLayout
55+
async function SessionProvider({ children }: { children: React.ReactNode }) {
56+
// These async operations will be handled by Suspense in the parent component
57+
const session = await getSessionFromCookie();
58+
const config = await getConfig();
59+
60+
return (
61+
<ThemeProvider
62+
attribute="class"
63+
defaultTheme="system"
64+
enableSystem
65+
session={session}
66+
config={config}
67+
>
68+
<TooltipProvider
69+
delayDuration={100}
70+
skipDelayDuration={50}
71+
>
72+
{children}
73+
</TooltipProvider>
74+
</ThemeProvider>
75+
);
76+
}
77+
78+
export default function BaseLayout({
5379
children,
5480
}: Readonly<{
5581
children: React.ReactNode;
5682
}>) {
57-
const session = await getSessionFromCookie();
58-
const config = await getConfig();
59-
6083
return (
6184
<html lang="en">
6285
<body className={inter.className}>
63-
<ThemeProvider
64-
attribute="class"
65-
defaultTheme="system"
66-
enableSystem
67-
session={session}
68-
config={config}
69-
>
70-
<TooltipProvider
71-
delayDuration={100}
72-
skipDelayDuration={50}
73-
>
86+
<Suspense fallback={<ThemeProviderFallback>{children}</ThemeProviderFallback>}>
87+
<SessionProvider>
7488
{children}
75-
<Toaster richColors closeButton position="top-right" expand duration={7000} />
76-
<StartupStudioStickyBanner />
77-
</TooltipProvider>
78-
</ThemeProvider>
89+
</SessionProvider>
90+
</Suspense>
91+
<Toaster richColors closeButton position="top-right" expand duration={7000} />
92+
<StartupStudioStickyBanner />
7993
</body>
8094
</html>
8195
);
8296
}
97+
98+
function ThemeProviderFallback({ children }: { children: React.ReactNode }) {
99+
return (
100+
<ThemeProvider
101+
attribute="class"
102+
defaultTheme="system"
103+
enableSystem
104+
session={null}
105+
config={{ isGoogleSSOEnabled: false, isTurnstileEnabled: false }}
106+
>
107+
<TooltipProvider
108+
delayDuration={100}
109+
skipDelayDuration={50}
110+
>
111+
{children}
112+
</TooltipProvider>
113+
</ThemeProvider>
114+
);
115+
}

src/components/footer.tsx

Lines changed: 43 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,9 @@ import { GITHUB_REPO_URL, SITE_NAME } from "@/constants";
55
import { Button } from "./ui/button";
66
import StartupStudioLogo from "./startupstudio-logo";
77
import { getGithubStars } from "@/utils/stats";
8+
import { Suspense } from "react";
89

9-
export async function Footer() {
10-
const starsCount = await getGithubStars();
11-
10+
export function Footer() {
1211
return (
1312
<footer className="border-t dark:bg-muted/30 bg-muted/60 shadow">
1413
<div className="max-w-7xl mx-auto px-4 md:px-6 lg:px-8">
@@ -79,19 +78,9 @@ export async function Footer() {
7978

8079
<div className="flex flex-col md:flex-row items-center gap-4 md:space-x-4">
8180
{GITHUB_REPO_URL && (
82-
<Button variant="outline" size="sm" className="w-full md:w-auto h-9" asChild>
83-
<Link
84-
href={GITHUB_REPO_URL}
85-
target="_blank"
86-
rel="noopener noreferrer"
87-
className="flex items-center justify-center space-x-2"
88-
>
89-
<GithubIcon className="h-4 w-4" />
90-
<span className="whitespace-nowrap">
91-
{starsCount ? `Fork on Github (${starsCount} Stars)` : "Fork on Github"}
92-
</span>
93-
</Link>
94-
</Button>
81+
<Suspense fallback={<GithubButtonFallback />}>
82+
<GithubButton />
83+
</Suspense>
9584
)}
9685

9786
<div className="flex items-center gap-4">
@@ -115,3 +104,41 @@ export async function Footer() {
115104
</footer>
116105
);
117106
}
107+
108+
// This component will be wrapped in Suspense
109+
async function GithubButton() {
110+
const starsCount = await getGithubStars();
111+
112+
return (
113+
<Button variant="outline" size="sm" className="w-full md:w-auto h-9" asChild>
114+
<Link
115+
href={GITHUB_REPO_URL!}
116+
target="_blank"
117+
rel="noopener noreferrer"
118+
className="flex items-center justify-center space-x-2"
119+
>
120+
<GithubIcon className="h-4 w-4" />
121+
<span className="whitespace-nowrap">
122+
{starsCount ? `Fork on Github (${starsCount} Stars)` : "Fork on Github"}
123+
</span>
124+
</Link>
125+
</Button>
126+
);
127+
}
128+
129+
// Fallback while loading stars count
130+
function GithubButtonFallback() {
131+
return (
132+
<Button variant="outline" size="sm" className="w-full md:w-auto h-9" asChild>
133+
<Link
134+
href={GITHUB_REPO_URL!}
135+
target="_blank"
136+
rel="noopener noreferrer"
137+
className="flex items-center justify-center space-x-2"
138+
>
139+
<GithubIcon className="h-4 w-4" />
140+
<span className="whitespace-nowrap">Fork on Github</span>
141+
</Link>
142+
</Button>
143+
);
144+
}

src/components/landing/hero.tsx

Lines changed: 28 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,10 @@ import { GITHUB_REPO_URL } from "@/constants";
33
import Link from "next/link";
44
import ShinyButton from "@/components/ui/shiny-button";
55
import { getTotalUsers } from "@/utils/stats";
6+
import { Suspense } from "react";
7+
import { Skeleton } from "@/components/ui/skeleton";
68

7-
export async function Hero() {
8-
const totalUsers = await getTotalUsers();
9-
9+
export function Hero() {
1010
return (
1111
<div className="relative isolate pt-14 dark:bg-gray-900">
1212
<div className="pt-20 pb-24 sm:pt-20 sm:pb-32 lg:pb-40">
@@ -16,11 +16,9 @@ export async function Hero() {
1616
<ShinyButton className="rounded-full bg-indigo-500/10 text-indigo-600 dark:text-indigo-400 ring-1 ring-inset ring-indigo-500/20">
1717
100% Free & Open Source
1818
</ShinyButton>
19-
{Boolean(totalUsers) && (
20-
<ShinyButton className="rounded-full bg-purple-500/10 text-purple-600 dark:text-purple-400 ring-1 ring-inset ring-purple-500/20">
21-
{totalUsers} Users & Growing
22-
</ShinyButton>
23-
)}
19+
<Suspense fallback={<TotalUsersButtonSkeleton />}>
20+
<TotalUsersButton />
21+
</Suspense>
2422
</div>
2523
<h1 className="text-4xl font-bold tracking-tight sm:text-6xl bg-gradient-to-r from-indigo-500 to-purple-500 bg-clip-text text-transparent">
2624
Production-Ready SaaS Template
@@ -47,3 +45,25 @@ export async function Hero() {
4745
</div>
4846
);
4947
}
48+
49+
// This component will be wrapped in Suspense
50+
async function TotalUsersButton() {
51+
const totalUsers = await getTotalUsers();
52+
53+
if (!totalUsers) return null;
54+
55+
return (
56+
<ShinyButton className="rounded-full bg-purple-500/10 text-purple-600 dark:text-purple-400 ring-1 ring-inset ring-purple-500/20">
57+
{totalUsers} Users & Growing
58+
</ShinyButton>
59+
);
60+
}
61+
62+
// Skeleton fallback for the TotalUsersButton
63+
function TotalUsersButtonSkeleton() {
64+
return (
65+
<div className="rounded-full bg-purple-500/10 ring-1 ring-inset ring-purple-500/20 px-4 py-1.5 text-sm font-medium">
66+
<Skeleton className="w-32 h-5" />
67+
</div>
68+
);
69+
}

0 commit comments

Comments
 (0)