# frontend
> Next.js App Router with React, TypeScript, shadcn/ui, and NextAuth.js patterns.
- Author: SriSomanaath
- Repository: SriSomanaath/claude-test
- Version: 20260106110946
- Stars: 0
- Forks: 0
- Last Updated: 2026-02-07
- Source: https://github.com/SriSomanaath/claude-test
- Web: https://mule.run/skillshub/@@SriSomanaath/claude-test~frontend:20260106110946
---
# Frontend - Next.js/React/TypeScript Patterns
Next.js App Router with React, TypeScript, shadcn/ui, and NextAuth.js patterns.
---
## Tech Stack
| Library | Purpose |
|---------|---------|
| Next.js 16+ | React framework with App Router |
| React 19 | UI library |
| TypeScript 5 | Type safety |
| Tailwind CSS v4 | Utility-first styling |
| shadcn/ui | Component library |
| Zod | Schema validation |
| NextAuth.js | Authentication |
| Zustand | Client state management |
| React Query | Server state management |
| React Hook Form | Form handling |
---
## Project Structure
```
src/
├── app/ # Next.js App Router
│ ├── (auth)/ # Auth route group (no layout)
│ │ ├── login/page.tsx
│ │ └── register/page.tsx
│ ├── (dashboard)/ # Dashboard route group
│ │ ├── layout.tsx
│ │ ├── page.tsx
│ │ └── employees/page.tsx
│ ├── api/ # API routes
│ │ └── auth/[...nextauth]/route.ts
│ ├── layout.tsx # Root layout
│ ├── page.tsx # Home page
│ └── globals.css
│
├── components/
│ ├── ui/ # shadcn/ui components
│ │ ├── button.tsx
│ │ ├── input.tsx
│ │ ├── card.tsx
│ │ ├── dialog.tsx
│ │ ├── form.tsx
│ │ └── ...
│ ├── common/ # Custom shared components
│ │ └── DataTable.tsx
│ ├── layout/ # Layout components
│ │ ├── Header.tsx
│ │ ├── Sidebar.tsx
│ │ └── Footer.tsx
│ └── features/ # Feature-specific components
│ ├── employees/
│ └── departments/
│
├── hooks/ # Custom React hooks
│ ├── useDebounce.ts
│ └── index.ts
│
├── services/ # API service layer
│ ├── api.ts
│ └── index.ts
│
├── stores/ # Zustand stores
│ ├── useAuthStore.ts
│ └── index.ts
│
├── types/ # TypeScript types
│ └── index.ts
│
├── lib/ # Utility libraries
│ ├── utils.ts # cn() helper
│ ├── auth.ts # NextAuth config
│ └── validations.ts # Zod schemas
│
└── providers/ # React providers
├── AuthProvider.tsx
└── QueryProvider.tsx
```
---
## shadcn/ui Setup
> **Reference Repository**: https://github.com/vercel/registry-starter
>
> Follow the file structure and component patterns from this official Vercel registry starter.
### Configuration (components.json)
```json
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "new-york",
"rsc": true,
"tsx": true,
"tailwind": {
"config": "",
"css": "src/app/globals.css",
"baseColor": "neutral",
"cssVariables": true,
"prefix": ""
},
"aliases": {
"components": "@/components",
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib",
"hooks": "@/hooks"
},
"iconLibrary": "lucide"
}
```
### Installation
```bash
npx shadcn@latest init -y -d
```
### Adding Components
```bash
# Core UI components (install all at once)
npx shadcn@latest add button card input label badge dialog dropdown-menu table tabs avatar alert form select checkbox textarea separator skeleton sonner tooltip popover command -y
# Layout components
npx shadcn@latest add sidebar sheet scroll-area breadcrumb navigation-menu -y
# Form components
npx shadcn@latest add switch radio-group calendar accordion -y
# Additional components
npx shadcn@latest add drawer collapsible context-menu hover-card menubar resizable slider toggle toggle-group aspect-ratio input-otp alert-dialog progress pagination -y
```
### Available UI Components
| Component | Description |
|-----------|-------------|
| `accordion` | Collapsible content sections |
| `alert` | Callout for user attention |
| `alert-dialog` | Modal confirmation dialog |
| `avatar` | User profile image |
| `badge` | Status indicator |
| `breadcrumb` | Navigation path |
| `button` | Clickable actions |
| `calendar` | Date picker calendar |
| `card` | Content container |
| `checkbox` | Multiple selection |
| `collapsible` | Expandable content |
| `command` | Command palette |
| `context-menu` | Right-click menu |
| `dialog` | Modal overlay |
| `drawer` | Slide-out panel |
| `dropdown-menu` | Action menu |
| `form` | Form with validation |
| `hover-card` | Hover popup |
| `input` | Text input |
| `input-otp` | OTP code input |
| `label` | Form label |
| `menubar` | Desktop menu bar |
| `navigation-menu` | Site navigation |
| `pagination` | Page navigation |
| `popover` | Popup content |
| `progress` | Progress indicator |
| `radio-group` | Single selection |
| `resizable` | Resizable panels |
| `scroll-area` | Custom scrollbar |
| `select` | Dropdown select |
| `separator` | Visual divider |
| `sheet` | Side panel |
| `sidebar` | App sidebar |
| `skeleton` | Loading placeholder |
| `slider` | Range input |
| `sonner` | Toast notifications |
| `switch` | Toggle switch |
| `table` | Data table |
| `tabs` | Tabbed content |
| `textarea` | Multi-line input |
| `toggle` | Toggle button |
| `toggle-group` | Button group |
| `tooltip` | Hover hint |
### cn() Utility (lib/utils.ts)
```tsx
import { type ClassValue, clsx } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
```
### Button Usage
```tsx
import { Button } from "@/components/ui/button";
// Variants
// Sizes
// With loading state
```
### Card Component
```tsx
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from "@/components/ui/card";
Employee Details
View and edit employee information
Content here
```
### Dialog (Modal)
```tsx
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
```
### Data Table
```tsx
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
Name
Email
Status
Actions
{employees.map((employee) => (
{employee.name}
{employee.email}
{employee.status}
))}
```
---
## Zod Validation Schemas
### Schema Definitions (lib/validations.ts)
```tsx
import { z } from "zod";
// Employee schema
export const employeeSchema = z.object({
firstName: z
.string()
.min(1, "First name is required")
.max(100, "First name is too long"),
lastName: z
.string()
.min(1, "Last name is required")
.max(100, "Last name is too long"),
email: z
.string()
.min(1, "Email is required")
.email("Invalid email address"),
departmentId: z
.string()
.min(1, "Department is required"),
position: z
.string()
.min(1, "Position is required"),
status: z.enum(["active", "inactive", "on_leave"]),
});
export type EmployeeFormData = z.infer;
// Login schema
export const loginSchema = z.object({
email: z
.string()
.min(1, "Email is required")
.email("Invalid email address"),
password: z
.string()
.min(1, "Password is required")
.min(8, "Password must be at least 8 characters"),
});
export type LoginFormData = z.infer;
// Register schema with password confirmation
export const registerSchema = z
.object({
name: z.string().min(1, "Name is required"),
email: z.string().email("Invalid email address"),
password: z.string().min(8, "Password must be at least 8 characters"),
confirmPassword: z.string(),
})
.refine((data) => data.password === data.confirmPassword, {
message: "Passwords don't match",
path: ["confirmPassword"],
});
export type RegisterFormData = z.infer;
// Leave request schema
export const leaveRequestSchema = z
.object({
type: z.enum(["annual", "sick", "personal", "unpaid"]),
startDate: z.string().min(1, "Start date is required"),
endDate: z.string().min(1, "End date is required"),
reason: z.string().optional(),
})
.refine(
(data) => new Date(data.endDate) >= new Date(data.startDate),
{
message: "End date must be after start date",
path: ["endDate"],
}
);
export type LeaveRequestFormData = z.infer;
```
### Form with Zod + React Hook Form + shadcn/ui
```tsx
"use client";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { Loader2 } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { employeeSchema, type EmployeeFormData } from "@/lib/validations";
interface EmployeeFormProps {
onSubmit: (data: EmployeeFormData) => Promise;
defaultValues?: Partial;
isLoading?: boolean;
}
export function EmployeeForm({
onSubmit,
defaultValues,
isLoading = false,
}: EmployeeFormProps) {
const form = useForm({
resolver: zodResolver(employeeSchema),
defaultValues: {
firstName: "",
lastName: "",
email: "",
departmentId: "",
position: "",
status: "active",
...defaultValues,
},
});
return (
);
}
```
---
## NextAuth.js Setup
### Installation
```bash
npm install next-auth
```
### Auth Configuration (lib/auth.ts)
```tsx
import { type NextAuthOptions } from "next-auth";
import CredentialsProvider from "next-auth/providers/credentials";
import GoogleProvider from "next-auth/providers/google";
export const authOptions: NextAuthOptions = {
providers: [
// Credentials provider (email/password)
CredentialsProvider({
name: "credentials",
credentials: {
email: { label: "Email", type: "email" },
password: { label: "Password", type: "password" },
},
async authorize(credentials) {
if (!credentials?.email || !credentials?.password) {
throw new Error("Invalid credentials");
}
// Call your backend API to verify credentials
const response = await fetch(
`${process.env.NEXT_PUBLIC_API_URL}/auth/login`,
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
email: credentials.email,
password: credentials.password,
}),
}
);
if (!response.ok) {
throw new Error("Invalid credentials");
}
const user = await response.json();
return user;
},
}),
// Google OAuth (optional)
GoogleProvider({
clientId: process.env.GOOGLE_CLIENT_ID!,
clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
}),
],
session: {
strategy: "jwt",
maxAge: 30 * 24 * 60 * 60, // 30 days
},
callbacks: {
async jwt({ token, user }) {
if (user) {
token.id = user.id;
token.role = user.role;
}
return token;
},
async session({ session, token }) {
if (session.user) {
session.user.id = token.id as string;
session.user.role = token.role as string;
}
return session;
},
},
pages: {
signIn: "/login",
error: "/login",
},
};
```
### API Route (app/api/auth/[...nextauth]/route.ts)
```tsx
import NextAuth from "next-auth";
import { authOptions } from "@/lib/auth";
const handler = NextAuth(authOptions);
export { handler as GET, handler as POST };
```
### Auth Types (types/next-auth.d.ts)
```tsx
import { DefaultSession, DefaultUser } from "next-auth";
declare module "next-auth" {
interface Session {
user: {
id: string;
role: "admin" | "manager" | "employee";
} & DefaultSession["user"];
}
interface User extends DefaultUser {
id: string;
role: "admin" | "manager" | "employee";
}
}
declare module "next-auth/jwt" {
interface JWT {
id: string;
role: "admin" | "manager" | "employee";
}
}
```
### Session Provider (providers/AuthProvider.tsx)
```tsx
"use client";
import { SessionProvider } from "next-auth/react";
import type { ReactNode } from "react";
interface AuthProviderProps {
children: ReactNode;
}
export function AuthProvider({ children }: AuthProviderProps) {
return {children};
}
```
### Root Layout with Provider
```tsx
// app/layout.tsx
import { AuthProvider } from "@/providers/AuthProvider";
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
{children}
);
}
```
### Login Page
```tsx
"use client";
import { useState } from "react";
import { signIn } from "next-auth/react";
import { useRouter } from "next/navigation";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { Loader2 } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { loginSchema, type LoginFormData } from "@/lib/validations";
export default function LoginPage() {
const router = useRouter();
const [error, setError] = useState(null);
const [isLoading, setIsLoading] = useState(false);
const form = useForm({
resolver: zodResolver(loginSchema),
defaultValues: {
email: "",
password: "",
},
});
const onSubmit = async (data: LoginFormData) => {
setIsLoading(true);
setError(null);
try {
const result = await signIn("credentials", {
email: data.email,
password: data.password,
redirect: false,
});
if (result?.error) {
setError("Invalid email or password");
return;
}
router.push("/dashboard");
router.refresh();
} catch {
setError("Something went wrong");
} finally {
setIsLoading(false);
}
};
return (
Login
Enter your credentials to access the HR Portal
);
}
```
### Protected Route Middleware (middleware.ts)
```tsx
import { withAuth } from "next-auth/middleware";
import { NextResponse } from "next/server";
export default withAuth(
function middleware(req) {
const token = req.nextauth.token;
const isAdminRoute = req.nextUrl.pathname.startsWith("/admin");
// Check role for admin routes
if (isAdminRoute && token?.role !== "admin") {
return NextResponse.redirect(new URL("/unauthorized", req.url));
}
return NextResponse.next();
},
{
callbacks: {
authorized: ({ token }) => !!token,
},
}
);
export const config = {
matcher: ["/dashboard/:path*", "/employees/:path*", "/admin/:path*"],
};
```
### useSession Hook Usage
```tsx
"use client";
import { useSession, signOut } from "next-auth/react";
export function UserNav() {
const { data: session, status } = useSession();
if (status === "loading") {
return Loading...
;
}
if (!session) {
return null;
}
return (
{session.user.name}
{session.user.role}
);
}
```
### Server Component Auth Check
```tsx
// app/dashboard/page.tsx
import { getServerSession } from "next-auth";
import { redirect } from "next/navigation";
import { authOptions } from "@/lib/auth";
export default async function DashboardPage() {
const session = await getServerSession(authOptions);
if (!session) {
redirect("/login");
}
return (
Welcome, {session.user.name}
Role: {session.user.role}
);
}
```
---
## Environment Variables
```env
# NextAuth.js
NEXTAUTH_URL=http://localhost:3000
NEXTAUTH_SECRET=your-secret-key-here
# API
NEXT_PUBLIC_API_URL=http://localhost:8000/api/v1
# OAuth (optional)
GOOGLE_CLIENT_ID=your-google-client-id
GOOGLE_CLIENT_SECRET=your-google-client-secret
```
---
## Import Order
```tsx
// 1. React/Next.js imports
import { useState, useEffect } from "react";
import { useRouter } from "next/navigation";
// 2. Third-party libraries
import { useSession } from "next-auth/react";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { Loader2, Plus, Edit, Trash } from "lucide-react";
// 3. shadcn/ui components
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
// 4. Custom components
import { DataTable } from "@/components/common/DataTable";
import { EmployeeForm } from "@/components/features/employees/EmployeeForm";
// 5. Hooks, services, stores
import { useDebounce } from "@/hooks";
import { employeeService } from "@/services";
import { useEmployeeStore } from "@/stores";
// 6. Types and utilities
import type { Employee } from "@/types";
import { cn } from "@/lib/utils";
import { employeeSchema } from "@/lib/validations";
```
---
## DO NOT
- Use `any` type - use `unknown` or proper types
- Skip Zod validation on forms
- Store sensitive data in client state
- Use inline styles - use Tailwind classes
- Skip loading/error states
- Hardcode API URLs - use environment variables
- Skip TypeScript strict mode
- Mix server and client components incorrectly
- Leave `console.log` in production
- Skip accessibility (aria-*, roles)