A Next.js-based training management platform with role-based authentication built with React, TypeScript, Tailwind CSS, and Supabase.
- Getting Started
- Tech Stack
- Project Structure
- Next.js Folder Structure Conventions
- Key Concepts
- Development Workflow
- Styling Guide
- Authentication
- Best Practices
- Node.js 18+ installed
- npm or yarn package manager
- Supabase project credentials (.env.local)
- Clone the repository:
git clone <repository-url>
cd training-den- Install dependencies:
npm install-
Set up environment variables: Create a
.env.localfile in the root directory with your Supabase credentials: -
Run the development server:
npm run dev- Open http://localhost:3000 in your browser.
npm run dev- Start development servernpm run build- Build for productionnpm start- Start production servernpm run lint- Run ESLint
- Framework: Next.js 15 (App Router)
- Language: TypeScript
- Styling: Tailwind CSS v4
- Authentication: Supabase Auth
- Database: Supabase (PostgreSQL)
- UI Library: React 19
training-den/
├── public/ # Static assets
│ └── logo.png # App logo and images
├── src/ # Source code
│ ├── app/ # Next.js App Router directory (CRITICAL)
│ │ ├── layout.js # Root layout wrapper
│ │ ├── page.js # Home page (/)
│ │ ├── globals.css # Global styles and CSS variables
│ │ ├── login/ # Login page route
│ │ │ └── page.tsx # Login page component
│ │ ├── register/ # Register page route
│ │ │ └── page.tsx # Register page component
│ │ ├── dashboard/ # Dashboard page route
│ │ │ └── page.tsx # Dashboard page component
│ │ └── api/ # API routes
│ │ └── test/
│ │ └── route.ts # API endpoint
│ └── lib/ # Shared utilities and libraries
│ ├── supabase.ts # Supabase client configuration
│ └── auth.ts # Authentication helpers
├── tsconfig.json # TypeScript configuration
├── package.json # Dependencies and scripts
├── next.config.mjs # Next.js configuration
├── postcss.config.mjs # PostCSS configuration
└── eslint.config.mjs # ESLint configuration
Next.js 13+ uses the App Router, where the folder structure directly maps to your application's routes. This is a convention-based system where folder names become URL paths.
| File/Folder Path | URL Route | Purpose |
|---|---|---|
app/page.js |
/ |
Home page |
app/login/page.tsx |
/login |
Login page |
app/register/page.tsx |
/register |
Register page |
app/dashboard/page.tsx |
/dashboard |
Dashboard page |
app/api/test/route.ts |
/api/test |
API endpoint |
-
page.tsx/js- Creates a publicly accessible route- Example:
app/dashboard/page.tsx→/dashboard - This is the ONLY file that makes a route accessible
- Example:
-
layout.tsx/js- Shared UI wrapper for routes- Example:
app/layout.jswraps all pages - Layouts are nested and preserved during navigation
- Great for headers, footers, navigation
- Example:
-
route.ts/js- API route handlers- Example:
app/api/test/route.ts→/api/test - Used for backend endpoints (GET, POST, etc.)
- Example:
-
loading.tsx/js- Loading UI (we don't have this yet)- Automatically shown while page loads
-
error.tsx/js- Error handling UI (we don't have this yet)- Catches errors in the route segment
-
not-found.tsx/js- 404 page (we don't have this yet)
CRITICAL: In Next.js App Router, the folder structure IS your routing system. You cannot arbitrarily organize files - each folder name becomes part of the URL.
Examples:
Creating a new page:
app/
training/
page.tsx → This creates route: /training
Creating nested routes:
app/
training/
page.tsx → /training
[id]/
page.tsx → /training/:id (dynamic route)
Common Mistake: Creating a folder without page.tsx will NOT create a route. The folder will be invisible to the router.
Next.js 15 uses Server Components by default. This is a new React paradigm.
Server Components (default)
- Rendered on the server
- No JavaScript sent to client
- Can directly access databases
- CANNOT use hooks like
useState,useEffect - CANNOT use browser APIs
Client Components (opt-in with "use client")
- Rendered in the browser
- Can use React hooks
- Can handle user interactions
- Example: login/page.tsx
"use client"; // This directive makes it a client component
import { useState } from "react";
export default function LoginPage() {
const [email, setEmail] = useState(""); // Now hooks work!
// ...
}Rule of thumb: Use Client Components for:
- Forms with state
- Interactive UI
- Browser APIs (localStorage, etc.)
- Event handlers (onClick, onChange)
The folder structure in app/ defines your routes automatically:
app/
login/page.tsx → /login
dashboard/page.tsx → /dashboard
api/test/route.ts → /api/test
No need for react-router or manual route configuration!
Path aliases are configured in tsconfig.json:
"paths": {
"@/*": ["./src/*"]
}This allows clean imports:
import { supabase } from "@/lib/supabase"; // Instead of ../../lib/supabase- Create a new folder in
src/app/with the desired route name - Add a
page.tsxfile inside that folder - Decide if you need client-side features (if yes, add
"use client")
Example - Creating /profile page:
// src/app/profile/page.tsx
"use client";
export default function ProfilePage() {
return (
<div>
<h1>Profile Page</h1>
</div>
);
}- Create a folder in
src/app/api/with your endpoint name - Add a
route.tsfile - Export HTTP method handlers
Example:
// src/app/api/users/route.ts
export async function GET() {
return Response.json({ users: [] });
}
export async function POST(request: Request) {
const body = await request.json();
return Response.json({ success: true });
}Place reusable code in src/lib/:
- Utilities:
src/lib/utils.ts - API clients:
src/lib/supabase.ts - Authentication:
src/lib/auth.ts - Types:
src/lib/types.ts
We use Tailwind CSS with custom brand colors defined in globals.css.
Custom properties are defined at the root:
:root {
--primary: #4c1f7a; /* Brand purple */
--secondary: #eb5b00; /* Brand orange */
--background: #fafbff; /* Page background */
--foreground: #0e0e0e; /* Text color */
}Pre-built component classes for consistency:
Buttons:
<button className="btn-primary">Primary Action</button>
<button className="btn-secondary">Secondary Action</button>Inputs:
<input className="input-primary" type="text" />Cards:
<div className="card">Card content</div>You can use Tailwind utility classes alongside our custom classes:
<div className="flex items-center justify-between p-4 bg-primary text-white rounded-lg">
<h2 className="text-xl font-bold">Title</h2>
<button className="btn-secondary">Action</button>
</div>Tailwind is configured to use our brand colors:
<div className="bg-primary">Purple background</div>
<div className="bg-secondary">Orange background</div>
<div className="text-primary">Purple text</div>Authentication is handled via Supabase. Key functions are in lib/auth.ts:
import { signIn, signUp, signOut, getCurrentUser } from "@/lib/auth";
// Sign up
const { user, error } = await signUp(email, password, {
first_name: "John",
last_name: "Doe",
role: "employee",
});
// Sign in
const { user, error } = await signIn(email, password);
// Get current user
const user = await getCurrentUser(); // Returns AuthUser or null
// Sign out
await signOut();The app supports role-based access:
- admin - Full system access
- manager - Team management
- trainer - Training content creation
- employee - Basic access
To protect a page, check authentication status:
"use client";
import { useEffect, useState } from "react";
import { useRouter } from "next/navigation";
import { getCurrentUser } from "@/lib/auth";
export default function ProtectedPage() {
const [user, setUser] = useState(null);
const router = useRouter();
useEffect(() => {
async function checkAuth() {
const currentUser = await getCurrentUser();
if (!currentUser) {
router.push("/login");
return;
}
setUser(currentUser);
}
checkAuth();
}, [router]);
if (!user) return <div>Loading...</div>;
return <div>Protected content for {user.email}</div>;
}See dashboard/page.tsx for a complete example.
- Pages: Only routing logic and page composition
- Components: Reusable UI in
src/components/(create this folder as needed) - Business Logic: Keep in
src/lib/
Always define TypeScript types for:
- Component props
- API responses
- User data
Example:
interface UserCardProps {
name: string;
email: string;
role: "admin" | "manager" | "trainer" | "employee";
}
export function UserCard({ name, email, role }: UserCardProps) {
return (
<div>
{name} - {email}
</div>
);
}Always handle errors in async operations:
const [error, setError] = useState<string | null>(null);
try {
const result = await someAsyncOperation();
} catch (err) {
setError(err.message);
}- Server Components CANNOT import Client Components directly
- Client Components CAN import Server Components as children
- Keep server-side logic (database calls) separate from client code
Provide feedback during async operations:
const [isLoading, setIsLoading] = useState(false);
async function handleSubmit() {
setIsLoading(true);
await someOperation();
setIsLoading(false);
}
return (
<button disabled={isLoading}>{isLoading ? "Loading..." : "Submit"}</button>
);- Use
page.tsxfor routes (notindex.tsx) - Use
layout.tsxfor shared layouts - Use
route.tsfor API endpoints - Place static assets in
public/
-
Forgot "use client"?
- Error: "You're importing a component that needs useState..."
- Solution: Add
"use client"at the top of the file
-
Wrong import path?
- Use
@/alias:import { supabase } from "@/lib/supabase" - Not relative:
import { supabase } from "../../lib/supabase"
- Use
-
Folder without page.tsx?
- The route won't exist! Every route needs a
page.tsxfile
- The route won't exist! Every route needs a
-
Styling not working?
- Check if globals.css is imported in
layout.js - Verify Tailwind classes are spelled correctly
- Check if globals.css is imported in
-
API route not working?
- Must be in
app/api/folder - Must be named
route.ts(notpage.ts) - Must export named functions:
GET,POST, etc.
- Must be in