Agent skill
nextjs
Next.js 16 App Router patterns for FTC Metrics. Use when creating pages, layouts, components, API routes, or implementing authentication flows.
Install this agent skill to your Project
npx add-skill https://github.com/majiayu000/claude-skill-registry/tree/main/skills/other/other/nextjs-ftc8569-ftcmetrics
Metadata
Additional technical details for this skill
- author
- ftcmetrics
- version
- 1.0.0
- category
- frontend
SKILL.md
Next.js 16 App Router Skill
Patterns for the FTC Metrics Next.js frontend at packages/web/.
Quick Start
cd packages/web
bun dev # http://localhost:3000
bun typecheck # Type check
bun lint # Lint
Project Structure
packages/web/src/
app/ # App Router pages and layouts
layout.tsx # Root layout with Providers
page.tsx # Landing page (Server Component)
dashboard/layout.tsx # Auth-protected layout
analytics/page.tsx # Client Component with Suspense
analytics/team/[teamNumber]/ # Dynamic route
api/auth/[...nextauth]/ # Auth API routes
components/
providers.tsx # Client-side providers wrapper
header.tsx # Navigation header
lib/
auth.ts # NextAuth v5 configuration
api.ts # API client utilities
types/next-auth.d.ts # Session type augmentation
Key Concepts
| Concept | Pattern | Location |
|---|---|---|
| Server Components | Default, no directive | app/page.tsx |
| Client Components | "use client" directive |
components/header.tsx |
| Protected Routes | auth() check in layout |
app/dashboard/layout.tsx |
| Dynamic Routes | [param] folder naming |
app/analytics/team/[teamNumber]/ |
| API Routes | Route handlers | app/api/auth/[...nextauth]/route.ts |
Common Patterns
1. Root Layout with Providers
// app/layout.tsx
import { Providers } from "@/components/providers";
import "./globals.css";
export const metadata = {
title: "FTC Metrics",
description: "Scouting platform for FIRST Tech Challenge",
};
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body className="min-h-screen bg-background antialiased">
<Providers>{children}</Providers>
</body>
</html>
);
}
// components/providers.tsx
"use client";
import { SessionProvider } from "next-auth/react";
export function Providers({ children }: { children: React.ReactNode }) {
return <SessionProvider>{children}</SessionProvider>;
}
2. Protected Layout with Auth Check
// app/dashboard/layout.tsx
import { auth } from "@/lib/auth";
import { redirect } from "next/navigation";
import { Header } from "@/components/header";
export default async function DashboardLayout({ children }: { children: React.ReactNode }) {
const session = await auth();
if (!session?.user) redirect("/login");
return (
<div className="min-h-screen bg-gray-50 dark:bg-gray-950">
<Header />
<main className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">{children}</main>
</div>
);
}
3. Server Component with Auth
// app/dashboard/page.tsx
import { auth } from "@/lib/auth";
export default async function DashboardPage() {
const session = await auth();
return <h1>Welcome, {session?.user?.name?.split(" ")[0] || "Scout"}</h1>;
}
4. Client Component with useSearchParams (Suspense Required)
// app/analytics/page.tsx
"use client";
import { Suspense } from "react";
import { useSearchParams, useRouter } from "next/navigation";
function AnalyticsContent() {
const searchParams = useSearchParams();
const router = useRouter();
const eventCode = searchParams.get("event") || "";
const handleEventChange = (event: string) => {
router.push(`/analytics?event=${event}`, { scroll: false });
};
return <div>{/* Content */}</div>;
}
export default function AnalyticsPage() {
return (
<Suspense fallback={<LoadingSpinner />}>
<AnalyticsContent />
</Suspense>
);
}
5. Dynamic Route with Params
// app/analytics/team/[teamNumber]/page.tsx
"use client";
import { useParams, useSearchParams } from "next/navigation";
import { Suspense } from "react";
function TeamContent() {
const params = useParams();
const searchParams = useSearchParams();
const teamNumber = parseInt(params.teamNumber as string, 10);
const eventCode = searchParams.get("event") || "";
return <h1>Team {teamNumber}</h1>;
}
export default function TeamPage() {
return (
<Suspense fallback={<LoadingSpinner />}>
<TeamContent />
</Suspense>
);
}
6. NextAuth v5 Configuration
// lib/auth.ts
import NextAuth from "next-auth";
import { PrismaAdapter } from "@auth/prisma-adapter";
import Google from "next-auth/providers/google";
import { prisma } from "@ftcmetrics/db";
export const { handlers, auth, signIn, signOut } = NextAuth({
adapter: PrismaAdapter(prisma),
providers: [
Google({
clientId: process.env.GOOGLE_CLIENT_ID!,
clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
}),
],
pages: { signIn: "/login", error: "/login" },
callbacks: {
async session({ session, user }) {
if (session.user) session.user.id = user.id;
return session;
},
},
session: { strategy: "database" },
});
// app/api/auth/[...nextauth]/route.ts
import { handlers } from "@/lib/auth";
export const { GET, POST } = handlers;
7. Session Type Augmentation
// types/next-auth.d.ts
import { DefaultSession } from "next-auth";
declare module "next-auth" {
interface Session {
user: { id: string } & DefaultSession["user"];
}
}
8. Client Component with useSession
// components/header.tsx
"use client";
import { useSession, signOut } from "next-auth/react";
import Link from "next/link";
export function Header() {
const { data: session } = useSession();
return (
<header>
{session?.user ? (
<button onClick={() => signOut({ callbackUrl: "/" })}>Sign out</button>
) : (
<Link href="/login">Sign in</Link>
)}
</header>
);
}
9. Redirect Based on Auth State
// app/page.tsx
import { auth } from "@/lib/auth";
import { redirect } from "next/navigation";
export default async function Home() {
const session = await auth();
if (session?.user) redirect("/dashboard");
return <LandingPage />;
}
Anti-Patterns
Using auth() in Client Components
// Server Component - uses auth()
export default async function Page() {
const session = await auth();
return <Display user={session?.user} />;
}
// Client Component - uses useSession hook
"use client";
import { useSession } from "next-auth/react";
export function UserProfile() {
const { data: session, status } = useSession();
if (status === "loading") return <Skeleton />;
return <Profile user={session?.user} />;
}
useSearchParams Without Suspense
// WRONG: Will cause hydration errors
"use client";
export default function SearchPage() {
const params = useSearchParams(); // No Suspense wrapper
return <Results query={params.get("q")} />;
}
// CORRECT: Wrap in Suspense
"use client";
function SearchContent() {
const params = useSearchParams();
return <Results query={params.get("q")} />;
}
export default function SearchPage() {
return (
<Suspense fallback={<Loading />}>
<SearchContent />
</Suspense>
);
}
Importing signIn/signOut Wrong
// Server actions - import from lib/auth
import { signIn, signOut } from "@/lib/auth";
// Client components - import from next-auth/react
"use client";
import { signIn, signOut } from "next-auth/react";
Tailwind Configuration
// tailwind.config.ts
const config = {
content: ["./src/**/*.{js,ts,jsx,tsx,mdx}"],
theme: {
extend: {
colors: {
ftc: {
orange: "#f57e25", // Primary: text-ftc-orange, bg-ftc-orange
blue: "#0066b3", // Secondary: text-ftc-blue, bg-ftc-blue
dark: "#1a1a2e", // Dark: bg-ftc-dark
},
},
},
},
};
Next.js Configuration
// next.config.ts
const nextConfig = {
reactStrictMode: true,
transpilePackages: ["@ftcmetrics/shared"], // Monorepo packages
};
Monorepo Integration
// Workspace packages
import { prisma } from "@ftcmetrics/db";
import { TeamType } from "@ftcmetrics/shared";
// Local imports with @/ alias
import { auth } from "@/lib/auth";
import { Header } from "@/components/header";
Loading Spinner Pattern
Used throughout the codebase:
<div className="flex items-center justify-center py-12">
<div className="animate-spin rounded-full h-8 w-8 border-2 border-ftc-orange border-t-transparent" />
</div>
References
Recommended Agent Skills
Expand your agent's capabilities with these related and highly-rated skills.
agent-ops-spec
Manage specification documents in .agent/specs/. Use when user provides requirements, acceptance criteria, or feature descriptions that need to be tracked and validated against implementation.
agent-ops-state
Maintain .agent state files. Use at session start, after meaningful steps, and before concluding: read/update constitution/memory/focus/issues/baseline consistently.
agent-ops-spec
Manage specification documents in .agent/specs/. Use when user provides requirements, acceptance criteria, or feature descriptions that need to be tracked and validated against implementation.
agent-ops-testing
Test strategy, execution, and coverage analysis. Use when designing tests, running test suites, or analyzing test results beyond baseline checks.
agent-ops-testing
Test strategy, execution, and coverage analysis. Use when designing tests, running test suites, or analyzing test results beyond baseline checks.
agent-ops-state
Maintain .agent state files. Use at session start, after meaningful steps, and before concluding: read/update constitution/memory/focus/issues/baseline consistently.
Didn't find tool you were looking for?