diff --git a/src/app/dashboard/page.tsx b/src/app/dashboard/page.tsx
index 1b2299c..49cd1b6 100644
--- a/src/app/dashboard/page.tsx
+++ b/src/app/dashboard/page.tsx
@@ -3,10 +3,13 @@
import { useSession } from "@/lib/auth-client";
import { UserProfile } from "@/components/auth/user-profile";
import { Button } from "@/components/ui/button";
+import { Lock } from "lucide-react";
+import { useDiagnostics } from "@/hooks/use-diagnostics";
import Link from "next/link";
export default function DashboardPage() {
const { data: session, isPending } = useSession();
+ const { isAiReady, loading: diagnosticsLoading } = useDiagnostics();
if (isPending) {
return (
@@ -19,7 +22,14 @@ export default function DashboardPage() {
if (!session) {
return (
-
+
+
+
+
Protected Page
+
+ You need to sign in to access the dashboard
+
+
@@ -38,9 +48,15 @@ export default function DashboardPage() {
Start a conversation with AI using the Vercel AI SDK
-
+ {(diagnosticsLoading || !isAiReady) ? (
+
+ ) : (
+
+ )}
diff --git a/src/app/layout.tsx b/src/app/layout.tsx
index 60afe66..bc4806d 100644
--- a/src/app/layout.tsx
+++ b/src/app/layout.tsx
@@ -14,8 +14,8 @@ const geistMono = Geist_Mono({
});
export const metadata: Metadata = {
- title: "Create Next App",
- description: "Generated by create next app",
+ title: "Next.js Full-Stack Boilerplate",
+ description: "Complete Next.js starter template with authentication, database, AI integration, and modern tooling by Leon van Zyl",
};
export default function RootLayout({
diff --git a/src/app/page.tsx b/src/app/page.tsx
index ff23768..98455ea 100644
--- a/src/app/page.tsx
+++ b/src/app/page.tsx
@@ -1,8 +1,12 @@
+"use client";
+
import Link from "next/link";
import { Button } from "@/components/ui/button";
import { SetupChecklist } from "@/components/setup-checklist";
+import { useDiagnostics } from "@/hooks/use-diagnostics";
export default function Home() {
+ const { isAuthReady, isAiReady, loading } = useDiagnostics();
return (
@@ -76,17 +80,42 @@ export default function Home() {
3. Try the features
-
-
+ {(loading || !isAuthReady) ? (
+
+ ) : (
+
+ )}
+ {(loading || !isAiReady) ? (
+
+ ) : (
+
+ )}
@@ -103,7 +132,21 @@ export default function Home() {
diff --git a/src/components/auth/sign-out-button.tsx b/src/components/auth/sign-out-button.tsx
index be1631d..484ea6b 100644
--- a/src/components/auth/sign-out-button.tsx
+++ b/src/components/auth/sign-out-button.tsx
@@ -1,33 +1,31 @@
-"use client"
+"use client";
-import { signOut, useSession } from "@/lib/auth-client"
-import { Button } from "@/components/ui/button"
+import { signOut, useSession } from "@/lib/auth-client";
+import { Button } from "@/components/ui/button";
+import { useRouter } from "next/navigation";
export function SignOutButton() {
- const { data: session, isPending } = useSession()
+ const { data: session, isPending } = useSession();
+ const router = useRouter();
if (isPending) {
- return
+ return ;
}
if (!session) {
- return null
+ return null;
}
return (
- )
-}
\ No newline at end of file
+ );
+}
diff --git a/src/components/auth/user-profile.tsx b/src/components/auth/user-profile.tsx
index ec8ff0a..56fecf4 100644
--- a/src/components/auth/user-profile.tsx
+++ b/src/components/auth/user-profile.tsx
@@ -21,25 +21,24 @@ export function UserProfile() {
}
return (
-
-
-
-
-
- {(
- session.user?.name?.[0] ||
- session.user?.email?.[0] ||
- "U"
- ).toUpperCase()}
-
-
-
{session.user?.name}
-
{session.user?.email}
-
+
+
+ Welcome {session.user?.name}
+
+
+
+
+ {(
+ session.user?.name?.[0] ||
+ session.user?.email?.[0] ||
+ "U"
+ ).toUpperCase()}
+
+
);
diff --git a/src/components/site-header.tsx b/src/components/site-header.tsx
index 21439d3..16b21b8 100644
--- a/src/components/site-header.tsx
+++ b/src/components/site-header.tsx
@@ -10,7 +10,7 @@ export function SiteHeader() {
href="/"
className="bg-clip-text text-transparent [background-image:linear-gradient(90deg,color-mix(in_oklab,var(--primary)_85%,white_0%),color-mix(in_oklab,var(--primary)_50%,white_0%))] hover:opacity-90"
>
- Next.js Starter Kit
+ Next.js Boilerplate
diff --git a/src/hooks/use-diagnostics.ts b/src/hooks/use-diagnostics.ts
new file mode 100644
index 0000000..606b34b
--- /dev/null
+++ b/src/hooks/use-diagnostics.ts
@@ -0,0 +1,65 @@
+"use client";
+
+import { useEffect, useState } from "react";
+
+type DiagnosticsResponse = {
+ timestamp: string;
+ env: {
+ DATABASE_URL: boolean;
+ BETTER_AUTH_SECRET: boolean;
+ GOOGLE_CLIENT_ID: boolean;
+ GOOGLE_CLIENT_SECRET: boolean;
+ OPENAI_API_KEY: boolean;
+ NEXT_PUBLIC_APP_URL: boolean;
+ };
+ database: {
+ connected: boolean;
+ schemaApplied: boolean;
+ error?: string;
+ };
+ auth: {
+ configured: boolean;
+ routeResponding: boolean | null;
+ };
+ ai: {
+ configured: boolean;
+ };
+ overallStatus: "ok" | "warn" | "error";
+};
+
+export function useDiagnostics() {
+ const [data, setData] = useState
(null);
+ const [loading, setLoading] = useState(true);
+ const [error, setError] = useState(null);
+
+ async function fetchDiagnostics() {
+ setLoading(true);
+ setError(null);
+ try {
+ const res = await fetch("/api/diagnostics", { cache: "no-store" });
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
+ const json = (await res.json()) as DiagnosticsResponse;
+ setData(json);
+ } catch (e) {
+ setError(e instanceof Error ? e.message : "Failed to load diagnostics");
+ } finally {
+ setLoading(false);
+ }
+ }
+
+ useEffect(() => {
+ fetchDiagnostics();
+ }, []);
+
+ const isAuthReady = data?.auth.configured && data?.database.connected && data?.database.schemaApplied;
+ const isAiReady = data?.ai.configured;
+
+ return {
+ data,
+ loading,
+ error,
+ refetch: fetchDiagnostics,
+ isAuthReady: Boolean(isAuthReady),
+ isAiReady: Boolean(isAiReady),
+ };
+}
\ No newline at end of file