.claude/skills/nextjs/SKILL.md
Next.js 16 App Router patterns for FTC Metrics. Use when creating pages, layouts, components, API routes, or implementing authentication flows.
npx skillsauth add ftc8569/ftcmetrics nextjsInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Patterns for the FTC Metrics Next.js frontend at packages/web/.
cd packages/web
bun dev # http://localhost:3000
bun typecheck # Type check
bun lint # Lint
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
| 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 |
// 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>;
}
// 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>
);
}
// 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>;
}
// 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>
);
}
// 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>
);
}
// 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;
// types/next-auth.d.ts
import { DefaultSession } from "next-auth";
declare module "next-auth" {
interface Session {
user: { id: string } & DefaultSession["user"];
}
}
// 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>
);
}
// 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 />;
}
// 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} />;
}
// 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>
);
}
// 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.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.config.ts
const nextConfig = {
reactStrictMode: true,
transpilePackages: ["@ftcmetrics/shared"], // Monorepo packages
};
// 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";
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>
development
Configure TypeScript in a Bun/npm workspaces monorepo with shared base config, package-specific overrides, path aliases, and cross-package type sharing. Use when setting up tsconfig files, configuring path aliases, resolving module errors, or sharing types between packages.
development
Tailwind CSS styling for FTC Metrics with official FIRST colors and component patterns. Use when styling React components, creating responsive layouts, or implementing dark mode.
development
Configure and integrate Soketi WebSocket server with FTC Metrics for real-time updates. Use when setting up WebSocket connections, broadcasting scouting data changes, implementing presence channels for team collaboration, or debugging real-time features.
development
Create, structure, and optimize skills for the FTC Metrics project. Use when creating a new skill, improving an existing skill, or needing guidance on skill design patterns, triggers, frontmatter, and progressive disclosure.