Build and maintain Next.js 15 admin dashboards with App Router, Auth0, Shadcn UI, Tailwind CSS, and Drizzle ORM following laxai-fe project conventions
Build Next.js 15 admin dashboards with App Router architecture, Auth0 authentication, Shadcn UI components, Tailwind CSS styling, and Drizzle ORM for database management.
```
app/ # App Router pages and layouts
(dashboard)/ # Dashboard route group
uploads/page.tsx # File upload UI example
providers.tsx # Client-side context providers
api/ # API routes
stitch/ # Backend API proxy routes
lib/
auth.ts # Auth0 server utilities
stitcher-api.ts # Backend API endpoint config
db.ts # Database connection
public/ # Static assets and SVG icons
globals.css # Tailwind + custom CSS variables
.env.local # Environment variables (not committed)
```
**CRITICAL:** This project uses Auth0, NOT NextAuth. Always use `@auth0/nextjs-auth0`.
```tsx
// lib/auth.ts
import { getSession, getAccessToken } from '@auth0/nextjs-auth0/edge';
import { withPageAuthRequired } from '@auth0/nextjs-auth0';
export { getSession, getAccessToken, withPageAuthRequired };
// Usage in API route
import { getSession } from '@/lib/auth';
const session = await getSession();
```
```tsx
// app/(dashboard)/providers.tsx
'use client';
import { Auth0Provider } from '@auth0/nextjs-auth0/client';
import { TooltipProvider } from '@/components/ui/tooltip';
export default function Providers({ children }: { children: React.ReactNode }) {
return (
<Auth0Provider>
<TooltipProvider>
{children}
</TooltipProvider>
</Auth0Provider>
);
}
```
```bash
AUTH0_SECRET=<generated-secret>
AUTH0_BASE_URL=http://localhost:3000 # Production: https://your-domain.com
AUTH0_ISSUER_BASE_URL=https://your-tenant.auth0.com
AUTH0_CLIENT_ID=<auth0-client-id>
AUTH0_CLIENT_SECRET=<auth0-client-secret>
STITCHER_API_BASE_URL=https://backend-api.example.com
APP_BASE_URL=http://localhost:3000
POSTGRES_URL=<vercel-postgres-connection-string>
NEXT_PUBLIC_APP_NAME=YourAppName
```
**Never hardcode backend URLs.** Centralize all endpoints in `lib/stitcher-api.ts`.
```tsx
// lib/stitcher-api.ts
export const STITCHER_API_BASE_URL = process.env.STITCHER_API_BASE_URL || '';
export const STITCHER_API_ENDPOINTS = {
loadVideo: '/api/v1/video/load',
processVideo: '/api/v1/video/process',
// Add more endpoints here
} as const;
export function getStitcherApiUrl(endpoint: string): string {
return `${STITCHER_API_BASE_URL}${endpoint}`;
}
```
```tsx
// app/api/stitch/video/load/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { getStitcherApiUrl, STITCHER_API_ENDPOINTS } from '@/lib/stitcher-api';
import { getBackendIdToken } from '@/lib/auth';
export async function POST(request: NextRequest) {
try {
const body = await request.json();
const { tenant_id, video_path } = body;
// Get Auth0 token for backend
const idToken = await getBackendIdToken(process.env.STITCHER_API_BASE_URL!);
// Build backend URL
const backendUrl = getStitcherApiUrl(STITCHER_API_ENDPOINTS.loadVideo);
// Make authenticated request
const response = await fetch(backendUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${idToken}`
},
body: JSON.stringify({ tenant_id, video_path })
});
const data = await response.json();
return NextResponse.json(data);
} catch (error) {
return NextResponse.json(
{ error: 'Failed to load video' },
{ status: 500 }
);
}
}
```
**Always prefer Shadcn UI components over custom implementations.**
| Component | Use Case | Example |
|-----------|----------|---------|
| `Button` | All interactive elements | `<Button variant="default">Submit</Button>` |
| `Card` | Content containers | `<Card><CardHeader>Title</CardHeader></Card>` |
| `Dialog` | Modals and popups | `<Dialog><DialogTrigger>Open</DialogTrigger></Dialog>` |
| `Sheet` | Side panels | `<Sheet><SheetTrigger>Menu</SheetTrigger></Sheet>` |
| `Table` | Data display | `<Table><TableHeader>...</TableHeader></Table>` |
| `Input` | Form text fields | `<Input type="text" placeholder="Enter..." />` |
| `Badge` | Status indicators | `<Badge variant="success">Active</Badge>` |
| `Progress` | Loading states | `<Progress value={60} />` |
| `Alert` | Notifications | `<Alert><AlertTitle>Success</AlertTitle></Alert>` |
Custom colors and fonts are defined in `globals.css`. Use Tailwind utility classes that reference these variables:
```css
/* globals.css */
:root {
--background: 0 0% 100%;
--foreground: 222.2 84% 4.9%;
--primary: 221.2 83.2% 53.3%;
/* ... more variables */
}
```
```tsx
// Component usage
<div className="bg-background text-foreground">
<Button className="bg-primary text-primary-foreground">
Click Me
</Button>
</div>
```
When a TSX component exceeds ~200 lines, break it down:
**Before (monolithic):**
```
uploads/page.tsx (350 lines)
```
**After (organized):**
```
uploads/
page.tsx (80 lines - main page)
UploadForm.tsx (100 lines - form component)
UploadedFilesList.tsx (80 lines - file list)
uploadHelpers.ts (50 lines - utilities)
types.ts (40 lines - type definitions)
```
1. **Form Logic → Separate Components**
```tsx
// Before: 300-line page.tsx with embedded form
// After: page.tsx + VideoUploadForm.tsx + VideoUploadConfig.tsx
```
2. **Complex UI Sections → Dedicated Components**
```tsx
// Before: Inline 80-line table rendering
// After: DataTable.tsx component
```
3. **Shared Logic → Custom Hooks**
```tsx
// lib/hooks/useFileUpload.ts
export function useFileUpload() {
const [files, setFiles] = useState<File[]>([]);
const [uploading, setUploading] = useState(false);
// ... upload logic
return { files, uploading, uploadFile };
}
```
4. **File Naming Convention**
- `ComponentName.tsx` - Main component (PascalCase)
- `ComponentName.types.ts` - TypeScript interfaces/types
- `ComponentName.utils.ts` - Helper functions
- `useComponentName.ts` - Custom React hooks
```tsx
// uploads/page.tsx
'use client';
import { useDropzone } from 'react-dropzone';
import { Button } from '@/components/ui/button';
import { Card } from '@/components/ui/card';
export default function UploadsPage() {
const onDrop = useCallback((acceptedFiles: File[]) => {
// Handle file upload
console.log(acceptedFiles);
}, []);
const { getRootProps, getInputProps, isDragActive } = useDropzone({
onDrop,
accept: { 'video/*': ['.mp4', '.mov'] },
maxSize: 500 * 1024 * 1024 // 500MB
});
return (
<Card className="p-8">
<div
{...getRootProps()}
className={`border-2 border-dashed rounded-lg p-12 text-center cursor-pointer
${isDragActive ? 'border-primary bg-primary/5' : 'border-muted-foreground'}`}
>
<input {...getInputProps()} />
{isDragActive ? (
<p>Drop files here...</p>
) : (
<p>Drag & drop files, or click to select</p>
)}
</div>
</Card>
);
}
```
```tsx
// lib/db.ts
import { drizzle } from 'drizzle-orm/vercel-postgres';
import { sql } from '@vercel/postgres';
export const db = drizzle(sql);
// Usage in API route
import { db } from '@/lib/db';
import { videos } from '@/lib/db/schema';
export async function GET() {
const allVideos = await db.select().from(videos);
return NextResponse.json(allVideos);
}
```
1. **Install dependencies:**
```bash
pnpm install
```
2. **Set up environment:**
```bash
cp .env.example .env.local
# Fill in Auth0, database, and API credentials
```
3. **Start dev server:**
```bash
pnpm dev
```
4. **Database setup:**
- Use Vercel Postgres dashboard to run SQL schema
- Seed data: Uncomment `app/api/seed.ts` and visit `http://localhost:3000/api/seed`
5. **Deploy to Vercel:**
```bash
vercel link
vercel env pull
vercel deploy --prod
```
```tsx
// app/(dashboard)/reports/page.tsx
import { withPageAuthRequired } from '@/lib/auth';
import { Card } from '@/components/ui/card';
async function ReportsPage() {
return (
<div className="container mx-auto p-6">
<h1 className="text-3xl font-bold mb-6">Reports</h1>
<Card>
{/* Page content */}
</Card>
</div>
);
}
export default withPageAuthRequired(ReportsPage);
```
1. **Add endpoint to config:**
```tsx
// lib/stitcher-api.ts
export const STITCHER_API_ENDPOINTS = {
// ... existing
exportReport: '/api/v1/reports/export',
};
```
2. **Create API route:**
```tsx
// app/api/stitch/reports/export/route.ts
import { getStitcherApiUrl, STITCHER_API_ENDPOINTS } from '@/lib/stitcher-api';
export async function POST(request: NextRequest) {
const backendUrl = getStitcherApiUrl(STITCHER_API_ENDPOINTS.exportReport);
// ... fetch logic
}
```
```bash
npx shadcn-ui@latest add dialog
```
**Issue:** Auth0 callback fails locally
**Issue:** Backend API returns 401
**Issue:** Tailwind styles not applying
**Issue:** Build fails with Drizzle errors
Leave a review
No reviews yet. Be the first to review this skill!
# Download SKILL.md from killerskills.ai/api/skills/nextjs-15-app-router-auth0-development/raw