A modern, responsive admin dashboard built with Next.js 14, TypeScript, and Tailwind CSS. Features authentication, lazy loading, and a modular architecture for easy extensibility.
- π Authentication System - Role-based access control with admin credentials
- π± Responsive Design - Mobile-first approach with Tailwind CSS
- β‘ Lazy Loading - Optimized performance with code splitting
- π¨ Modern UI - Clean, professional interface with shadcn/ui components
- π Data Management - CRUD operations for various data types
- π‘οΈ Error Handling - Comprehensive error boundaries and validation
- π Performance Monitoring - Built-in performance tracking
- π§ Modular Architecture - Easy to extend with new modules
src/
βββ app/ # Next.js App Router
β βββ (guest)/ # Public routes (login, signup, etc.)
β β βββ login/page.tsx
β β βββ signup/page.tsx
β β βββ forgot/page.tsx
β βββ admin/ # Protected admin routes
β β βββ layout.tsx # Admin layout with sidebar & topbar
β β βββ page.tsx # Dashboard
β β βββ users/page.tsx
β β βββ products/page.tsx
β β βββ faqs/page.tsx
β β βββ ... # Other admin pages
β βββ api/ # API routes
β βββ auth/
βββ components/ # Reusable components
β βββ admin/ # Admin-specific components
β βββ auth/ # Authentication components
β βββ modules/ # Feature modules
β β βββ faqs/ # FAQ module example
β βββ ui/ # Base UI components
βββ features/ # Redux slices for state management
βββ lib/ # Utilities and configurations
βββ data/ # Static JSON data
βββ store/ # Redux store configuration
- Framework: Next.js 14 (App Router)
- Language: TypeScript
- Styling: Tailwind CSS
- UI Components: shadcn/ui
- State Management: Redux Toolkit
- Icons: Lucide React
- Authentication: Cookie-based with role validation
- Node.js 18+
- npm or yarn
-
Clone the repository
git clone <repository-url> cd myapp
-
Install dependencies
npm install # or yarn install
-
Run the development server
npm run dev
# or
yarn dev
- Open your browser Navigate to http://localhost:3000
For development, use these credentials to access the admin panel:
- Email:
admin@yopmail.com
- Password:
Admin@123
The application uses a role-based authentication system:
- Login: Navigate to
/login
and enter admin credentials - Access Control: Only users with admin role can access
/admin/*
routes - Logout: Use the profile menu in the top-right corner
- Public Routes: Login, signup, forgot password
- Admin Routes: Dashboard, users, products, FAQs, settings, etc.
- Sidebar Navigation: Collapsible sidebar with organized menu items
This section shows how to add a new module to the application. We'll create a "Blog Posts" module as an example.
Create a new JSON file in src/data/
:
// src/data/blogPosts.json
[
{
"id": 1,
"title": "Getting Started with Next.js",
"content": "Learn the basics of Next.js development...",
"author": "John Doe",
"status": "Published",
"createdAt": "2024-01-15T10:00:00Z",
"updatedAt": "2024-01-15T10:00:00Z"
}
]
Define TypeScript interfaces:
// src/components/modules/blogPosts/types.ts
export interface BlogPost {
id: number
title: string
content: string
author: string
status: "Draft" | "Published" | "Archived"
createdAt: string
updatedAt: string
}
export interface CreateBlogPostPayload {
title: string
content: string
author: string
status: "Draft" | "Published"
}
// src/features/blogPosts/blogPostsSlice.ts
import { createSlice, PayloadAction } from "@reduxjs/toolkit"
import type { BlogPost, CreateBlogPostPayload } from "@/components/modules/blogPosts/types"
interface BlogPostsState {
items: BlogPost[]
loading: boolean
error: string | null
}
const initialState: BlogPostsState = {
items: [],
loading: false,
error: null
}
const blogPostsSlice = createSlice({
name: "blogPosts",
initialState,
reducers: {
fetchBlogPosts: (state) => {
state.loading = true
state.error = null
},
fetchBlogPostsSuccess: (state, action: PayloadAction<BlogPost[]>) => {
state.loading = false
state.items = action.payload
},
createBlogPost: (state, action: PayloadAction<CreateBlogPostPayload>) => {
const newPost: BlogPost = {
id: Date.now(),
...action.payload,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString()
}
state.items.unshift(newPost)
},
updateBlogPost: (state, action: PayloadAction<BlogPost>) => {
const index = state.items.findIndex(post => post.id === action.payload.id)
if (index !== -1) {
state.items[index] = { ...action.payload, updatedAt: new Date().toISOString() }
}
},
deleteBlogPost: (state, action: PayloadAction<number>) => {
state.items = state.items.filter(post => post.id !== action.payload)
}
}
})
export const {
fetchBlogPosts,
fetchBlogPostsSuccess,
createBlogPost,
updateBlogPost,
deleteBlogPost
} = blogPostsSlice.actions
export const selectBlogPosts = (state: { blogPosts: BlogPostsState }) => state.blogPosts.items
export const selectBlogPostsLoading = (state: { blogPosts: BlogPostsState }) => state.blogPosts.loading
export default blogPostsSlice.reducer
// src/components/modules/blogPosts/BlogPostsTable.tsx
"use client"
import { useState } from "react"
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
import { Button } from "@/components/ui/button"
import { Badge } from "@/components/ui/badge"
import { Edit, Trash, Eye } from "lucide-react"
import type { BlogPost } from "./types"
interface BlogPostsTableProps {
posts: BlogPost[]
onEdit: (post: BlogPost) => void
onDelete: (id: number) => void
onView: (post: BlogPost) => void
}
export default function BlogPostsTable({ posts, onEdit, onDelete, onView }: BlogPostsTableProps) {
const getStatusVariant = (status: string) => {
switch (status) {
case "Published": return "default"
case "Draft": return "secondary"
case "Archived": return "outline"
default: return "secondary"
}
}
return (
<div className="rounded-md border">
<Table>
<TableHeader>
<TableRow>
<TableHead>Title</TableHead>
<TableHead>Author</TableHead>
<TableHead>Status</TableHead>
<TableHead>Created</TableHead>
<TableHead className="text-right">Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{posts.map((post) => (
<TableRow key={post.id}>
<TableCell className="font-medium">{post.title}</TableCell>
<TableCell>{post.author}</TableCell>
<TableCell>
<Badge variant={getStatusVariant(post.status)}>
{post.status}
</Badge>
</TableCell>
<TableCell>
{new Date(post.createdAt).toLocaleDateString()}
</TableCell>
<TableCell className="text-right">
<div className="flex justify-end gap-2">
<Button variant="outline" size="sm" onClick={() => onView(post)}>
<Eye className="h-4 w-4" />
</Button>
<Button variant="outline" size="sm" onClick={() => onEdit(post)}>
<Edit className="h-4 w-4" />
</Button>
<Button variant="outline" size="sm" onClick={() => onDelete(post.id)}>
<Trash className="h-4 w-4" />
</Button>
</div>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
)
}
// src/app/admin/blog-posts/page.tsx
"use client"
import { useEffect, useState } from "react"
import { useAppDispatch, useAppSelector } from "@/store/hooks"
import { fetchBlogPosts, selectBlogPosts, createBlogPost, deleteBlogPost } from "@/features/blogPosts/blogPostsSlice"
import { Button } from "@/components/ui/button"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import { Plus } from "lucide-react"
import BlogPostsTable from "@/components/modules/blogPosts/BlogPostsTable"
import type { BlogPost } from "@/components/modules/blogPosts/types"
export default function BlogPostsPage() {
const dispatch = useAppDispatch()
const posts = useAppSelector(selectBlogPosts)
const [editing, setEditing] = useState<BlogPost | null>(null)
useEffect(() => {
// Load data from JSON file (in real app, this would be an API call)
const loadData = async () => {
const response = await fetch('/data/blogPosts.json')
const data = await response.json()
dispatch(fetchBlogPostsSuccess(data))
}
loadData()
}, [dispatch])
const handleDelete = (id: number) => {
if (confirm("Are you sure you want to delete this blog post?")) {
dispatch(deleteBlogPost(id))
}
}
return (
<div className="space-y-6">
<div className="flex items-center justify-between">
<h1 className="text-3xl font-bold">Blog Posts</h1>
<Button>
<Plus className="mr-2 h-4 w-4" />
New Post
</Button>
</div>
<Card>
<CardHeader>
<CardTitle>All Posts</CardTitle>
</CardHeader>
<CardContent>
<BlogPostsTable
posts={posts}
onEdit={setEditing}
onDelete={handleDelete}
onView={(post) => console.log("View post:", post)}
/>
</CardContent>
</Card>
</div>
)
}
// src/store/store.ts
import { configureStore } from "@reduxjs/toolkit"
import blogPostsReducer from "@/features/blogPosts/blogPostsSlice"
// ... other imports
export const store = configureStore({
reducer: {
blogPosts: blogPostsReducer,
// ... other reducers
},
})
// src/components/admin/sidebar.tsx
// Add to the navigation items:
{
title: "Blog Posts",
href: "/admin/blog-posts",
icon: FileText,
}
The application uses Tailwind CSS with a custom design system. Key customization points:
- Colors: Defined in
tailwind.config.js
- Components: Based on shadcn/ui, located in
src/components/ui/
- Layout: Responsive grid system with breakpoints
-
Use the shadcn/ui CLI to add new components:
npx shadcn-ui@latest add [component-name]
-
Or create custom components in
src/components/ui/
The application uses Redux Toolkit for state management. Each feature has its own slice:
- Actions: Define what can happen
- Reducers: Handle state changes
- Selectors: Extract data from state
- Async Thunks: Handle API calls
npm run build
Create a .env.local
file:
# API Configuration (for future backend integration)
NEXT_PUBLIC_API_URL=http://localhost:8000/api
API_KEY=your_api_key_here
# Authentication
NEXTAUTH_SECRET=your_secret_here
NEXTAUTH_URL=http://localhost:3000
- Push your code to GitHub
- Connect your repository to Vercel
- Deploy automatically
The application is prepared for backend integration:
- Client-side authentication with hardcoded credentials
- Static JSON data files
- Placeholder API routes
- Replace
src/lib/auth.ts
functions with API calls - Update Redux slices to use
createAsyncThunk
- Implement real API endpoints in
src/app/api/
-
Authentication not working
- Check if cookies are enabled
- Verify credentials match exactly
- Clear browser cache and cookies
-
Lazy loading not working
- Ensure components are properly exported
- Check for TypeScript errors
- Verify Suspense boundaries are in place
-
Styling issues
- Check Tailwind CSS configuration
- Verify component imports
- Clear Next.js cache:
rm -rf .next
- Fork the repository
- Create a feature branch
- Make your changes
- Add tests if applicable
- Submit a pull request
This project is licensed under the MIT License.
For support and questions:
- Create an issue in the repository
- Check the documentation
- Review the code examples
Happy coding! π