Exemples Next.js
Exemples de framework Next.js - Framework React pour Production avec rendu côté serveur, routage et fonctionnalités web modernes
Key Facts
- Category
- Web Frameworks
- Items
- 3
- Format Families
- sample
Sample Overview
Exemples de framework Next.js - Framework React pour Production avec rendu côté serveur, routage et fonctionnalités web modernes This sample set belongs to Web Frameworks and can be used to test related workflows inside Elysia Tools.
💻 Next.js Bases et Composants javascript
🟢 simple
⭐⭐
Concepts de base Next.js incluant composants, pages et routage de base
⏱️ 25 min
🏷️ nextjs, react, frontend, components, routing
Prerequisites:
React basics, JavaScript ES6+, HTML/CSS
// Next.js Basics and Components
// 1. Basic Next.js App Structure
/*
my-next-app/
├── pages/
│ ├── index.js # Home page
│ ├── about.js # About page
│ └── api/
│ └── users.js # API route
├── components/
│ ├── Header.js # Reusable header
│ └── Footer.js # Reusable footer
├── styles/
│ └── globals.css # Global styles
├── public/ # Static assets
└── package.json
*/
// 2. Basic Page Component (pages/index.js)
import Head from 'next/head'
import Link from 'next/link'
import Header from '../components/Header'
import Footer from '../components/Footer'
function HomePage() {
return (
<>
<Head>
<title>My Next.js App</title>
<meta name="description" content="Welcome to my Next.js application" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" href="/favicon.ico" />
</Head>
<div className="container">
<Header />
<main>
<h1>Welcome to Next.js!</h1>
<p>This is a basic Next.js application.</p>
<section className="features">
<h2>Features</h2>
<ul>
<li>Server-Side Rendering (SSR)</li>
<li>Static Site Generation (SSG)</li>
<li>API Routes</li>
<li>Image Optimization</li>
<li>Automatic Code Splitting</li>
</ul>
</section>
<section className="navigation">
<h2>Navigation</h2>
<nav>
<Link href="/about">
<a>About Us</a>
</Link>
<Link href="/blog">
<a>Blog</a>
</Link>
<Link href="/api/users">
<a>Users API</a>
</Link>
</nav>
</section>
</main>
<Footer />
</div>
<style jsx>{`
.container {
max-width: 1200px;
margin: 0 auto;
padding: 0 20px;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}
main {
padding: 40px 0;
}
h1 {
color: #333;
margin-bottom: 20px;
}
.features {
margin: 40px 0;
}
.features ul {
list-style-type: none;
padding: 0;
}
.features li {
padding: 10px 0;
border-bottom: 1px solid #eee;
}
.features li:before {
content: "✓";
color: #4CAF50;
font-weight: bold;
margin-right: 10px;
}
.navigation {
margin: 40px 0;
}
.navigation nav {
display: flex;
gap: 20px;
}
.navigation a {
padding: 10px 20px;
background-color: #0070f3;
color: white;
text-decoration: none;
border-radius: 5px;
transition: background-color 0.3s;
}
.navigation a:hover {
background-color: #0051cc;
}
`}</style>
</>
)
}
// 3. Reusable Header Component (components/Header.js)
import Link from 'next/link'
import { useState } from 'react'
function Header() {
const [isMenuOpen, setIsMenuOpen] = useState(false)
return (
<header className="header">
<div className="header-content">
<Link href="/">
<a className="logo">MyApp</a>
</Link>
<nav className={`nav ${isMenuOpen ? 'open' : ''}`}>
<Link href="/">
<a>Home</a>
</Link>
<Link href="/about">
<a>About</a>
</Link>
<Link href="/blog">
<a>Blog</a>
</Link>
<Link href="/contact">
<a>Contact</a>
</Link>
</nav>
<button
className="menu-toggle"
onClick={() => setIsMenuOpen(!isMenuOpen)}
>
{isMenuOpen ? '✕' : '☰'}
</button>
</div>
<style jsx>{`
.header {
background-color: #fff;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
position: sticky;
top: 0;
z-index: 1000;
}
.header-content {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1rem 2rem;
max-width: 1200px;
margin: 0 auto;
}
.logo {
font-size: 1.5rem;
font-weight: bold;
color: #0070f3;
text-decoration: none;
}
.nav {
display: flex;
gap: 2rem;
}
.nav a {
color: #333;
text-decoration: none;
font-weight: 500;
transition: color 0.3s;
}
.nav a:hover {
color: #0070f3;
}
.menu-toggle {
display: none;
background: none;
border: none;
font-size: 1.5rem;
cursor: pointer;
}
@media (max-width: 768px) {
.nav {
position: absolute;
top: 100%;
left: 0;
right: 0;
background: white;
flex-direction: column;
padding: 1rem;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
display: none;
}
.nav.open {
display: flex;
}
.menu-toggle {
display: block;
}
}
`}</style>
</>
)
}
// 4. About Page with Dynamic Data (pages/about.js)
import Head from 'next/head'
import { useState, useEffect } from 'react'
function AboutPage() {
const [team, setTeam] = useState([])
const [loading, setLoading] = useState(true)
useEffect(() => {
// Simulate API call
fetchTeamData()
}, [])
async function fetchTeamData() {
try {
// In real app, this would be an actual API call
setTimeout(() => {
setTeam([
{ id: 1, name: 'John Doe', role: 'CEO', bio: 'Visionary leader with 10+ years experience' },
{ id: 2, name: 'Jane Smith', role: 'CTO', bio: 'Tech expert specializing in modern web technologies' },
{ id: 3, name: 'Mike Johnson', role: 'Designer', bio: 'Creative mind behind our beautiful designs' }
])
setLoading(false)
}, 1000)
} catch (error) {
console.error('Error fetching team data:', error)
setLoading(false)
}
}
return (
<>
<Head>
<title>About Us - My Next.js App</title>
<meta name="description" content="Learn more about our company and team" />
</Head>
<div className="about-page">
<section className="hero">
<h1>About Our Company</h1>
<p>We are passionate about creating amazing web experiences using Next.js and modern technologies.</p>
</section>
<section className="mission">
<h2>Our Mission</h2>
<p>To build fast, scalable, and user-friendly web applications that make a difference in people's lives.</p>
<div className="values">
<div className="value">
<h3>Innovation</h3>
<p>We constantly explore new technologies and approaches</p>
</div>
<div className="value">
<h3>Quality</h3>
<p>We never compromise on code quality and user experience</p>
</div>
<div className="value">
<h3>Collaboration</h3>
<p>We believe in the power of teamwork and open communication</p>
</div>
</div>
</section>
<section className="team">
<h2>Meet Our Team</h2>
{loading ? (
<div className="loading">Loading team members...</div>
) : (
<div className="team-grid">
{team.map(member => (
<div key={member.id} className="team-member">
<div className="member-avatar">
{member.name.split(' ').map(n => n[0]).join('')}
</div>
<h3>{member.name}</h3>
<p className="role">{member.role}</p>
<p className="bio">{member.bio}</p>
</div>
))}
</div>
)}
</section>
</div>
<style jsx>{`
.about-page {
max-width: 1200px;
margin: 0 auto;
padding: 40px 20px;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}
.hero {
text-align: center;
margin-bottom: 60px;
}
.hero h1 {
font-size: 2.5rem;
color: #333;
margin-bottom: 20px;
}
.hero p {
font-size: 1.2rem;
color: #666;
max-width: 600px;
margin: 0 auto;
}
.mission {
margin-bottom: 60px;
}
.mission h2 {
font-size: 2rem;
color: #333;
margin-bottom: 20px;
}
.values {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 30px;
margin-top: 40px;
}
.value {
text-align: center;
padding: 30px;
background: #f8f9fa;
border-radius: 10px;
}
.value h3 {
color: #0070f3;
margin-bottom: 15px;
}
.team h2 {
font-size: 2rem;
color: #333;
text-align: center;
margin-bottom: 40px;
}
.loading {
text-align: center;
font-size: 1.1rem;
color: #666;
}
.team-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 30px;
}
.team-member {
text-align: center;
padding: 30px;
background: white;
border-radius: 10px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
transition: transform 0.3s, box-shadow 0.3s;
}
.team-member:hover {
transform: translateY(-5px);
box-shadow: 0 4px 20px rgba(0,0,0,0.15);
}
.member-avatar {
width: 80px;
height: 80px;
border-radius: 50%;
background: #0070f3;
color: white;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.5rem;
font-weight: bold;
margin: 0 auto 20px;
}
.team-member h3 {
color: #333;
margin-bottom: 10px;
}
.role {
color: #0070f3;
font-weight: 500;
margin-bottom: 15px;
}
.bio {
color: #666;
line-height: 1.6;
}
`}</style>
</>
)
}
// 5. Blog List Page (pages/blog/index.js)
import Head from 'next/head'
import Link from 'next/link'
import { useState, useEffect } from 'react'
// Mock blog posts data
const blogPosts = [
{
id: 1,
title: 'Getting Started with Next.js',
excerpt: 'Learn the basics of Next.js and how to build your first application.',
author: 'John Doe',
date: '2024-01-15',
tags: ['Next.js', 'React', 'Tutorial'],
readTime: '5 min'
},
{
id: 2,
title: 'Server-Side Rendering vs Static Site Generation',
excerpt: 'Understanding the differences between SSR and SSG in Next.js.',
author: 'Jane Smith',
date: '2024-01-10',
tags: ['SSR', 'SSG', 'Performance'],
readTime: '8 min'
},
{
id: 3,
title: 'Building API Routes in Next.js',
excerpt: 'Create powerful backend APIs using Next.js API routes.',
author: 'Mike Johnson',
date: '2024-01-05',
tags: ['API', 'Backend', 'Full-stack'],
readTime: '10 min'
}
]
function BlogListPage() {
const [posts, setPosts] = useState([])
const [filteredPosts, setFilteredPosts] = useState([])
const [selectedTag, setSelectedTag] = useState('all')
useEffect(() => {
// Simulate fetching blog posts
setTimeout(() => {
setPosts(blogPosts)
setFilteredPosts(blogPosts)
}, 500)
}, [])
useEffect(() => {
if (selectedTag === 'all') {
setFilteredPosts(posts)
} else {
setFilteredPosts(posts.filter(post => post.tags.includes(selectedTag)))
}
}, [selectedTag, posts])
const getAllTags = () => {
const tags = ['all', ...new Set(posts.flatMap(post => post.tags))]
return tags
}
return (
<>
<Head>
<title>Blog - My Next.js App</title>
<meta name="description" content="Read our latest articles about web development" />
</Head>
<div className="blog-container">
<header className="blog-header">
<h1>Our Blog</h1>
<p>Insights, tutorials, and news about web development</p>
</header>
<div className="blog-content">
<aside className="sidebar">
<div className="tags-section">
<h3>Filter by Tags</h3>
<div className="tags">
{getAllTags().map(tag => (
<button
key={tag}
className={`tag ${selectedTag === tag ? 'active' : ''}`}
onClick={() => setSelectedTag(tag)}
>
{tag}
</button>
))}
</div>
</div>
<div className="recent-posts">
<h3>Recent Posts</h3>
<ul>
{posts.slice(0, 3).map(post => (
<li key={post.id}>
<Link href={`/blog/${post.id}`}>
<a>{post.title}</a>
</Link>
</li>
))}
</ul>
</div>
</aside>
<main className="blog-main">
{filteredPosts.length === 0 ? (
<div className="no-posts">
<h3>No posts found</h3>
<p>Try selecting a different tag.</p>
</div>
) : (
<div className="posts-grid">
{filteredPosts.map(post => (
<article key={post.id} className="post-card">
<Link href={`/blog/${post.id}`}>
<a>
<div className="post-meta">
<span className="date">{post.date}</span>
<span className="read-time">{post.readTime} read</span>
</div>
<h2>{post.title}</h2>
<p className="excerpt">{post.excerpt}</p>
<div className="post-footer">
<span className="author">By {post.author}</span>
<div className="tags">
{post.tags.map(tag => (
<span key={tag} className="tag">{tag}</span>
))}
</div>
</div>
</a>
</Link>
</article>
))}
</div>
)}
</main>
</div>
</div>
<style jsx>{`
.blog-container {
max-width: 1200px;
margin: 0 auto;
padding: 40px 20px;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}
.blog-header {
text-align: center;
margin-bottom: 60px;
}
.blog-header h1 {
font-size: 2.5rem;
color: #333;
margin-bottom: 10px;
}
.blog-header p {
font-size: 1.2rem;
color: #666;
}
.blog-content {
display: grid;
grid-template-columns: 250px 1fr;
gap: 40px;
}
.sidebar {
position: sticky;
top: 20px;
height: fit-content;
}
.tags-section h3,
.recent-posts h3 {
color: #333;
margin-bottom: 15px;
font-size: 1.1rem;
}
.tags {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.tag {
padding: 5px 12px;
background: #f0f0f0;
border: 1px solid #ddd;
border-radius: 20px;
font-size: 0.9rem;
cursor: pointer;
transition: all 0.3s;
}
.tag:hover {
background: #0070f3;
color: white;
border-color: #0070f3;
}
.tag.active {
background: #0070f3;
color: white;
border-color: #0070f3;
}
.recent-posts ul {
list-style: none;
padding: 0;
}
.recent-posts li {
margin-bottom: 10px;
}
.recent-posts a {
color: #333;
text-decoration: none;
font-size: 0.95rem;
line-height: 1.4;
}
.recent-posts a:hover {
color: #0070f3;
}
.no-posts {
text-align: center;
padding: 60px 20px;
color: #666;
}
.posts-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(350px, 1fr));
gap: 30px;
}
.post-card {
background: white;
border-radius: 10px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
overflow: hidden;
transition: transform 0.3s, box-shadow 0.3s;
}
.post-card:hover {
transform: translateY(-5px);
box-shadow: 0 4px 20px rgba(0,0,0,0.15);
}
.post-card a {
display: block;
padding: 30px;
text-decoration: none;
color: inherit;
}
.post-meta {
display: flex;
justify-content: space-between;
align-items: center;
font-size: 0.9rem;
color: #666;
margin-bottom: 15px;
}
.post-card h2 {
color: #333;
font-size: 1.3rem;
margin-bottom: 15px;
line-height: 1.3;
}
.excerpt {
color: #666;
line-height: 1.6;
margin-bottom: 20px;
}
.post-footer {
display: flex;
justify-content: space-between;
align-items: center;
font-size: 0.9rem;
}
.author {
color: #666;
}
.post-footer .tags {
display: flex;
gap: 5px;
}
.post-footer .tag {
font-size: 0.8rem;
padding: 3px 8px;
}
@media (max-width: 768px) {
.blog-content {
grid-template-columns: 1fr;
}
.sidebar {
position: static;
margin-bottom: 40px;
}
.posts-grid {
grid-template-columns: 1fr;
}
}
`}</style>
</>
)
}
💻 Routes API et Récupération de Données Next.js javascript
🟡 intermediate
⭐⭐⭐⭐
Construction d'APIs RESTful avec routes API Next.js, patterns de récupération de données et traitement de données côté serveur
⏱️ 30 min
🏷️ nextjs, api, backend, database, swr
Prerequisites:
Next.js basics, REST APIs, JavaScript async/await
// Next.js API Routes and Data Fetching
// 1. Simple API Route (pages/api/users.js)
function handler(req, res) {
// Handle different HTTP methods
switch (req.method) {
case 'GET':
return handleGet(req, res)
case 'POST':
return handlePost(req, res)
case 'PUT':
return handlePut(req, res)
case 'DELETE':
return handleDelete(req, res)
default:
res.setHeader('Allow', ['GET', 'POST', 'PUT', 'DELETE'])
return res.status(405).end(`Method ${req.method} Not Allowed`)
}
}
// Mock user data (in real app, this would come from a database)
let users = [
{ id: 1, name: 'John Doe', email: '[email protected]', age: 30 },
{ id: 2, name: 'Jane Smith', email: '[email protected]', age: 25 },
{ id: 3, name: 'Mike Johnson', email: '[email protected]', age: 35 }
]
let nextId = 4
function handleGet(req, res) {
const { id } = req.query
if (id) {
// Get single user
const user = users.find(u => u.id === parseInt(id))
if (!user) {
return res.status(404).json({ error: 'User not found' })
}
return res.status(200).json(user)
} else {
// Get all users with optional filtering
const { minAge, maxAge } = req.query
let filteredUsers = users
if (minAge) {
filteredUsers = filteredUsers.filter(u => u.age >= parseInt(minAge))
}
if (maxAge) {
filteredUsers = filteredUsers.filter(u => u.age <= parseInt(maxAge))
}
return res.status(200).json({
users: filteredUsers,
total: filteredUsers.length
})
}
}
function handlePost(req, res) {
try {
const { name, email, age } = req.body
// Validation
if (!name || !email || !age) {
return res.status(400).json({
error: 'Missing required fields: name, email, age'
})
}
if (typeof age !== 'number' || age < 0 || age > 150) {
return res.status(400).json({
error: 'Age must be a number between 0 and 150'
})
}
// Check if email already exists
if (users.some(u => u.email === email)) {
return res.status(400).json({
error: 'Email already exists'
})
}
// Create new user
const newUser = {
id: nextId++,
name,
email,
age
}
users.push(newUser)
res.status(201).json({
message: 'User created successfully',
user: newUser
})
} catch (error) {
res.status(500).json({ error: 'Internal server error' })
}
}
function handlePut(req, res) {
try {
const { id } = req.query
const { name, email, age } = req.body
if (!id) {
return res.status(400).json({ error: 'User ID is required' })
}
const userIndex = users.findIndex(u => u.id === parseInt(id))
if (userIndex === -1) {
return res.status(404).json({ error: 'User not found' })
}
// Update user
if (name) users[userIndex].name = name
if (email) users[userIndex].email = email
if (age !== undefined) users[userIndex].age = age
res.status(200).json({
message: 'User updated successfully',
user: users[userIndex]
})
} catch (error) {
res.status(500).json({ error: 'Internal server error' })
}
}
function handleDelete(req, res) {
try {
const { id } = req.query
if (!id) {
return res.status(400).json({ error: 'User ID is required' })
}
const userIndex = users.findIndex(u => u.id === parseInt(id))
if (userIndex === -1) {
return res.status(404).json({ error: 'User not found' })
}
const deletedUser = users.splice(userIndex, 1)[0]
res.status(200).json({
message: 'User deleted successfully',
user: deletedUser
})
} catch (error) {
res.status(500).json({ error: 'Internal server error' })
}
}
// 2. Advanced API Route with Database Integration (pages/api/products.js)
import { MongoClient, ObjectId } from 'mongodb'
const uri = process.env.MONGODB_URI
const client = new MongoClient(uri)
async function handler(req, res) {
try {
await client.connect()
const database = client.db('myapp')
const collection = database.collection('products')
switch (req.method) {
case 'GET':
return await getProducts(collection, req, res)
case 'POST':
return await createProduct(collection, req, res)
case 'PUT':
return await updateProduct(collection, req, res)
case 'DELETE':
return await deleteProduct(collection, req, res)
default:
res.setHeader('Allow', ['GET', 'POST', 'PUT', 'DELETE'])
return res.status(405).end(`Method ${req.method} Not Allowed`)
}
} catch (error) {
console.error('Database error:', error)
res.status(500).json({ error: 'Database connection failed' })
} finally {
await client.close()
}
}
async function getProducts(collection, req, res) {
const { page = 1, limit = 10, category, minPrice, maxPrice, sortBy = 'createdAt' } = req.query
try {
// Build query
const query = {}
if (category) query.category = category
if (minPrice || maxPrice) {
query.price = {}
if (minPrice) query.price.$gte = parseFloat(minPrice)
if (maxPrice) query.price.$lte = parseFloat(maxPrice)
}
// Build sort options
const sortOptions = {}
if (sortBy) {
sortOptions[sortBy] = -1 // descending order
}
const skip = (parseInt(page) - 1) * parseInt(limit)
const products = await collection
.find(query)
.sort(sortOptions)
.skip(skip)
.limit(parseInt(limit))
.toArray()
const total = await collection.countDocuments(query)
res.status(200).json({
products,
pagination: {
currentPage: parseInt(page),
totalPages: Math.ceil(total / parseInt(limit)),
totalItems: total,
itemsPerPage: parseInt(limit)
}
})
} catch (error) {
res.status(500).json({ error: 'Failed to fetch products' })
}
}
async function createProduct(collection, req, res) {
try {
const { name, description, price, category, inStock = true, images = [] } = req.body
// Validation
if (!name || !price || !category) {
return res.status(400).json({
error: 'Missing required fields: name, price, category'
})
}
const product = {
name,
description,
price: parseFloat(price),
category,
inStock,
images,
createdAt: new Date(),
updatedAt: new Date()
}
const result = await collection.insertOne(product)
res.status(201).json({
message: 'Product created successfully',
product: { ...product, _id: result.insertedId }
})
} catch (error) {
res.status(500).json({ error: 'Failed to create product' })
}
}
// 3. Client-side Data Fetching Component (components/UserList.js)
import { useState, useEffect } from 'react'
function UserList() {
const [users, setUsers] = useState([])
const [loading, setLoading] = useState(true)
const [error, setError] = useState(null)
const [filters, setFilters] = useState({
minAge: '',
maxAge: ''
})
useEffect(() => {
fetchUsers()
}, [filters.minAge, filters.maxAge])
async function fetchUsers() {
try {
setLoading(true)
setError(null)
const queryParams = new URLSearchParams()
if (filters.minAge) queryParams.append('minAge', filters.minAge)
if (filters.maxAge) queryParams.append('maxAge', filters.maxAge)
const response = await fetch(`/api/users?${queryParams}`)
const data = await response.json()
if (!response.ok) {
throw new Error(data.error || 'Failed to fetch users')
}
setUsers(data.users)
} catch (err) {
setError(err.message)
} finally {
setLoading(false)
}
}
const handleFilterChange = (field, value) => {
setFilters(prev => ({ ...prev, [field]: value }))
}
const deleteUser = async (userId) => {
if (!confirm('Are you sure you want to delete this user?')) {
return
}
try {
const response = await fetch(`/api/users?id=${userId}`, {
method: 'DELETE'
})
const data = await response.json()
if (!response.ok) {
throw new Error(data.error || 'Failed to delete user')
}
// Remove user from local state
setUsers(prev => prev.filter(user => user.id !== userId))
alert('User deleted successfully')
} catch (err) {
alert(`Error deleting user: ${err.message}`)
}
}
if (loading) {
return <div className="loading">Loading users...</div>
}
if (error) {
return <div className="error">Error: {error}</div>
}
return (
<div className="user-list">
<h2>User Management</h2>
{/* Filters */}
<div className="filters">
<h3>Filters</h3>
<div className="filter-group">
<label>
Min Age:
<input
type="number"
value={filters.minAge}
onChange={(e) => handleFilterChange('minAge', e.target.value)}
min="0"
max="150"
/>
</label>
<label>
Max Age:
<input
type="number"
value={filters.maxAge}
onChange={(e) => handleFilterChange('maxAge', e.target.value)}
min="0"
max="150"
/>
</label>
</div>
</div>
{/* Users Table */}
<div className="table-container">
<table>
<thead>
<tr>
<th>ID</th>
<th>Name</th>
<th>Email</th>
<th>Age</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{users.map(user => (
<tr key={user.id}>
<td>{user.id}</td>
<td>{user.name}</td>
<td>{user.email}</td>
<td>{user.age}</td>
<td>
<button
onClick={() => deleteUser(user.id)}
className="delete-btn"
>
Delete
</button>
</td>
</tr>
))}
</tbody>
</table>
</div>
{users.length === 0 && (
<div className="no-users">
<p>No users found matching the current filters.</p>
</div>
)}
<style jsx>{`
.user-list {
max-width: 1200px;
margin: 0 auto;
padding: 20px;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}
h2 {
color: #333;
margin-bottom: 30px;
}
.loading, .error {
text-align: center;
padding: 40px;
font-size: 1.1rem;
}
.error {
color: #e74c3c;
}
.filters {
background: #f8f9fa;
padding: 20px;
border-radius: 8px;
margin-bottom: 30px;
}
.filters h3 {
margin-top: 0;
margin-bottom: 15px;
color: #333;
}
.filter-group {
display: flex;
gap: 20px;
flex-wrap: wrap;
}
.filter-group label {
display: flex;
flex-direction: column;
font-weight: 500;
color: #555;
}
.filter-group input {
margin-top: 5px;
padding: 8px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 14px;
}
.table-container {
overflow-x: auto;
background: white;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
table {
width: 100%;
border-collapse: collapse;
}
th, td {
padding: 12px;
text-align: left;
border-bottom: 1px solid #eee;
}
th {
background-color: #f8f9fa;
font-weight: 600;
color: #333;
}
tr:hover {
background-color: #f8f9fa;
}
.delete-btn {
background-color: #e74c3c;
color: white;
border: none;
padding: 6px 12px;
border-radius: 4px;
cursor: pointer;
font-size: 12px;
transition: background-color 0.3s;
}
.delete-btn:hover {
background-color: #c0392b;
}
.no-users {
text-align: center;
padding: 40px;
color: #666;
}
@media (max-width: 768px) {
.filter-group {
flex-direction: column;
}
.table-container {
font-size: 14px;
}
th, td {
padding: 8px;
}
}
`}</style>
</div>
)
}
// 4. SWR Hook for Data Fetching (hooks/useUsers.js)
import useSWR from 'swr'
const fetcher = async (url) => {
const response = await fetch(url)
const data = await response.json()
if (!response.ok) {
throw new Error(data.error || 'An error occurred while fetching the data.')
}
return data
}
export function useUsers(filters = {}) {
const queryParams = new URLSearchParams()
if (filters.minAge) queryParams.append('minAge', filters.minAge)
if (filters.maxAge) queryParams.append('maxAge', filters.maxAge)
const url = queryParams.toString() ? `/api/users?${queryParams}` : '/api/users'
const { data, error, isLoading, mutate } = useSWR(url, fetcher, {
revalidateOnFocus: false,
dedupingInterval: 5000
})
return {
users: data?.users || [],
total: data?.total || 0,
isLoading,
error,
mutate
}
}
export function useUser(id) {
const { data, error, isLoading, mutate } = useSWR(
id ? `/api/users?id=${id}` : null,
fetcher
)
return {
user: data,
isLoading,
error,
mutate
}
}
// 5. Custom Hook with SWR (hooks/useUserActions.js)
import { useUsers } from './useUsers'
export function useUserActions() {
const { mutate } = useUsers()
const createUser = async (userData) => {
try {
const response = await fetch('/api/users', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(userData),
})
const data = await response.json()
if (!response.ok) {
throw new Error(data.error || 'Failed to create user')
}
// Update local cache with new user
mutate(currentData => ({
...currentData,
users: [...(currentData?.users || []), data.user],
total: (currentData?.total || 0) + 1
}), false)
return data
} catch (error) {
throw error
}
}
const updateUser = async (id, userData) => {
try {
const response = await fetch(`/api/users?id=${id}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(userData),
})
const data = await response.json()
if (!response.ok) {
throw new Error(data.error || 'Failed to update user')
}
// Update local cache
mutate(currentData => ({
...currentData,
users: (currentData?.users || []).map(user =>
user.id === parseInt(id) ? data.user : user
)
}), false)
return data
} catch (error) {
throw error
}
}
const deleteUser = async (id) => {
try {
const response = await fetch(`/api/users?id=${id}`, {
method: 'DELETE',
})
const data = await response.json()
if (!response.ok) {
throw new Error(data.error || 'Failed to delete user')
}
// Update local cache
mutate(currentData => ({
...currentData,
users: (currentData?.users || []).filter(user => user.id !== parseInt(id)),
total: (currentData?.total || 0) - 1
}), false)
return data
} catch (error) {
throw error
}
}
return {
createUser,
updateUser,
deleteUser
}
}
💻 Fonctionnalités Avancées Next.js javascript
🔴 complex
⭐⭐⭐⭐⭐
Concepts avancés Next.js incluant SSR, SSG, ISR, authentification, middleware et déploiement
⏱️ 40 min
🏷️ nextjs, ssr, ssg, auth, performance
Prerequisites:
Advanced Next.js, React hooks, Authentication concepts, MongoDB
// Next.js Advanced Features
// 1. Server-Side Rendering (SSR) with getServerSideProps (pages/posts/[id].js)
import { MongoClient, ObjectId } from 'mongodb'
import Head from 'next/head'
import Link from 'next/link'
import { useRouter } from 'next/router'
function PostPage({ post, relatedPosts }) {
const router = useRouter()
if (router.isFallback) {
return <div>Loading...</div>
}
return (
<>
<Head>
<title>{post.title} - My Blog</title>
<meta name="description" content={post.excerpt} />
<meta property="og:title" content={post.title} />
<meta property="og:description" content={post.excerpt} />
<meta property="og:image" content={post.coverImage} />
</Head>
<article className="post">
<header className="post-header">
<nav className="breadcrumb">
<Link href="/">
<a>Home</a>
</Link>
{' / '}
<Link href="/blog">
<a>Blog</a>
</Link>
{' / '}
<span>{post.title}</span>
</nav>
<h1>{post.title}</h1>
<div className="post-meta">
<span className="author">By {post.author.name}</span>
<span className="date">
{new Date(post.createdAt).toLocaleDateString()}
</span>
<span className="read-time">{post.readTime} min read</span>
</div>
<div className="tags">
{post.tags.map(tag => (
<span key={tag} className="tag">{tag}</span>
))}
</div>
</header>
{post.coverImage && (
<div className="cover-image">
<img src={post.coverImage} alt={post.title} />
</div>
)}
<div
className="post-content"
dangerouslySetInnerHTML={{ __html: post.content }}
/>
<footer className="post-footer">
<div className="author-bio">
<img
src={post.author.avatar}
alt={post.author.name}
className="author-avatar"
/>
<div className="author-info">
<h4>About {post.author.name}</h4>
<p>{post.author.bio}</p>
</div>
</div>
</footer>
</article>
<section className="related-posts">
<h2>Related Posts</h2>
<div className="posts-grid">
{relatedPosts.map(relatedPost => (
<Link key={relatedPost.id} href={`/posts/${relatedPost.id}`}>
<a>
<div className="post-card">
{relatedPost.coverImage && (
<img
src={relatedPost.coverImage}
alt={relatedPost.title}
className="card-image"
/>
)}
<div className="card-content">
<h3>{relatedPost.title}</h3>
<p>{relatedPost.excerpt}</p>
<div className="card-meta">
<span>{new Date(relatedPost.createdAt).toLocaleDateString()}</span>
</div>
</div>
</div>
</a>
</Link>
))}
</div>
</section>
<style jsx>{`
.post {
max-width: 800px;
margin: 0 auto;
padding: 40px 20px;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
line-height: 1.6;
}
.breadcrumb {
margin-bottom: 20px;
font-size: 0.9rem;
color: #666;
}
.breadcrumb a {
color: #0070f3;
text-decoration: none;
}
.post-header h1 {
font-size: 2.5rem;
color: #333;
margin-bottom: 20px;
line-height: 1.2;
}
.post-meta {
display: flex;
gap: 20px;
margin-bottom: 20px;
font-size: 0.9rem;
color: #666;
}
.tags {
display: flex;
gap: 8px;
margin-bottom: 40px;
}
.tag {
padding: 4px 12px;
background: #f0f0f0;
border-radius: 20px;
font-size: 0.8rem;
color: #555;
}
.cover-image {
margin-bottom: 40px;
}
.cover-image img {
width: 100%;
height: auto;
border-radius: 8px;
}
.post-content {
font-size: 1.1rem;
line-height: 1.8;
}
.post-content h2 {
margin-top: 40px;
margin-bottom: 20px;
color: #333;
}
.post-content p {
margin-bottom: 20px;
}
.post-footer {
margin-top: 60px;
padding-top: 40px;
border-top: 1px solid #eee;
}
.author-bio {
display: flex;
align-items: center;
gap: 20px;
}
.author-avatar {
width: 60px;
height: 60px;
border-radius: 50%;
object-fit: cover;
}
.author-info h4 {
margin: 0 0 5px 0;
color: #333;
}
.author-info p {
margin: 0;
color: #666;
font-size: 0.9rem;
}
.related-posts {
margin-top: 80px;
}
.related-posts h2 {
font-size: 1.8rem;
color: #333;
margin-bottom: 30px;
text-align: center;
}
.posts-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 30px;
}
.post-card {
background: white;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
overflow: hidden;
transition: transform 0.3s;
}
.post-card:hover {
transform: translateY(-5px);
}
.post-card a {
display: block;
text-decoration: none;
color: inherit;
}
.card-image {
width: 100%;
height: 200px;
object-fit: cover;
}
.card-content {
padding: 20px;
}
.card-content h3 {
margin: 0 0 10px 0;
color: #333;
font-size: 1.1rem;
}
.card-content p {
margin: 0 0 15px 0;
color: #666;
font-size: 0.9rem;
}
.card-meta {
color: #888;
font-size: 0.8rem;
}
`}</style>
</>
)
}
export async function getServerSideProps(context) {
const { id } = context.params
const { req } = context
try {
// Connect to database
const client = new MongoClient(process.env.MONGODB_URI)
await client.connect()
const database = client.db('myblog')
const postsCollection = database.collection('posts')
// Fetch the specific post
const post = await postsCollection.findOne({
_id: new ObjectId(id),
published: true
})
if (!post) {
return {
notFound: true
}
}
// Convert ObjectId to string for JSON serialization
post._id = post._id.toString()
// Fetch author information
const usersCollection = database.collection('users')
const author = await usersCollection.findOne(
{ _id: new ObjectId(post.authorId) },
{ projection: { name: 1, avatar: 1, bio: 1 } }
)
post.author = author ? {
...author,
_id: author._id.toString()
} : { name: 'Anonymous' }
// Fetch related posts
const relatedPosts = await postsCollection
.find({
_id: { $ne: new ObjectId(id) },
published: true,
$or: [
{ category: post.category },
{ tags: { $in: post.tags } }
]
})
.sort({ createdAt: -1 })
.limit(3)
.toArray()
// Convert ObjectIds to strings
const formattedRelatedPosts = relatedPosts.map(relatedPost => ({
...relatedPost,
_id: relatedPost._id.toString()
}))
await client.close()
return {
props: {
post,
relatedPosts: formattedRelatedPosts
}
}
} catch (error) {
console.error('Error fetching post:', error)
return {
props: {
post: null,
relatedPosts: []
}
}
}
}
// 2. Static Site Generation (SSG) with getStaticProps (pages/docs/[slug].js)
import { promises as fs } from 'fs'
import { join } from 'path'
import matter from 'gray-matter'
import { serialize } from 'next-mdx-remote/serialize'
function DocumentationPage({ frontMatter, content, tableOfContents }) {
return (
<>
<div className="docs-container">
<nav className="table-of-contents">
<h3>Table of Contents</h3>
<ul>
{tableOfContents.map((item, index) => (
<li key={index}>
<a href={`#${item.anchor}`}>{item.title}</a>
</li>
))}
</ul>
</nav>
<main className="docs-content">
<h1>{frontMatter.title}</h1>
<div className="meta">
<span className="date">
{new Date(frontMatter.date).toLocaleDateString()}
</span>
<span className="author">By {frontMatter.author}</span>
<span className="reading-time">{frontMatter.readingTime} min read</span>
</div>
<div className="content">
{content}
</div>
</main>
</div>
<style jsx>{`
.docs-container {
display: grid;
grid-template-columns: 250px 1fr;
max-width: 1200px;
margin: 0 auto;
padding: 40px 20px;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}
.table-of-contents {
position: sticky;
top: 20px;
height: fit-content;
}
.table-of-contents h3 {
margin-bottom: 15px;
color: #333;
font-size: 1.1rem;
}
.table-of-contents ul {
list-style: none;
padding: 0;
}
.table-of-contents li {
margin-bottom: 8px;
}
.table-of-contents a {
color: #0070f3;
text-decoration: none;
font-size: 0.9rem;
line-height: 1.4;
}
.table-of-contents a:hover {
text-decoration: underline;
}
.docs-content {
max-width: 800px;
}
.docs-content h1 {
color: #333;
margin-bottom: 10px;
font-size: 2rem;
}
.meta {
display: flex;
gap: 20px;
margin-bottom: 40px;
font-size: 0.9rem;
color: #666;
}
.content {
line-height: 1.6;
}
.content h2 {
color: #333;
margin: 40px 0 20px 0;
font-size: 1.5rem;
}
.content h3 {
color: #333;
margin: 30px 0 15px 0;
font-size: 1.2rem;
}
.content p {
margin-bottom: 20px;
}
.content pre {
background: #f6f8fa;
padding: 20px;
border-radius: 8px;
overflow-x: auto;
margin-bottom: 20px;
}
.content code {
background: #f6f8fa;
padding: 2px 6px;
border-radius: 4px;
font-size: 0.9rem;
}
@media (max-width: 768px) {
.docs-container {
grid-template-columns: 1fr;
}
.table-of-contents {
position: static;
margin-bottom: 40px;
}
}
`}</style>
</>
)
}
export async function getStaticPaths() {
const postsDirectory = join(process.cwd(), 'docs')
try {
const fileNames = await fs.readdir(postsDirectory)
const paths = fileNames
.filter(name => name.endsWith('.mdx'))
.map(name => ({
params: {
slug: name.replace(/\.mdx$/, '')
}
}))
return {
paths,
fallback: false
}
} catch (error) {
console.error('Error reading docs directory:', error)
return {
paths: [],
fallback: false
}
}
}
export async function getStaticProps({ params }) {
const { slug } = params
const fullPath = join(process.cwd(), 'docs', `${slug}.mdx`)
try {
const fileContents = await fs.readFile(fullPath, 'utf8')
const { data, content } = matter(fileContents)
// Generate table of contents
const headings = content.match(/^###\s+(.+)$/gm) || []
const tableOfContents = headings.map((heading, index) => {
const title = heading.replace(/^###\s+/, '')
const anchor = title.toLowerCase().replace(/\s+/g, '-').replace(/[^a-z0-9-]/g, '')
return { title, anchor }
})
// Serialize MDX content
const mdxSource = await serialize(content)
return {
props: {
frontMatter: data,
content: mdxSource,
tableOfContents
}
}
} catch (error) {
console.error(`Error reading file ${fullPath}:`, error)
return {
props: {
frontMatter: {},
content: null,
tableOfContents: []
}
}
}
}
// 3. Authentication Middleware (middleware.js)
import { NextResponse } from 'next/server'
import { getToken } from 'next-auth/jwt'
// This function can be marked `async` if using `await` inside
export async function middleware(req) {
const { pathname } = req.next
// Skip middleware for API routes and static files
if (pathname.startsWith('/api/') ||
pathname.startsWith('/_next/') ||
pathname.includes('.')) {
return NextResponse.next()
}
// Public routes that don't require authentication
const publicRoutes = ['/login', '/register', '/forgot-password']
if (publicRoutes.includes(pathname)) {
return NextResponse.next()
}
// Protected routes that require authentication
const protectedRoutes = ['/dashboard', '/profile', '/settings', '/admin']
const isProtectedRoute = protectedRoutes.some(route =>
pathname.startsWith(route)
)
if (isProtectedRoute) {
const token = await getToken({ req })
if (!token) {
// Redirect to login page with return URL
const loginUrl = new URL('/login', req.url)
loginUrl.searchParams.set('returnUrl', pathname)
return NextResponse.redirect(loginUrl)
}
// Admin routes require admin role
if (pathname.startsWith('/admin')) {
try {
// Verify admin role (this would involve checking the token payload)
// const user = await verifyToken(token)
// if (user.role !== 'admin') {
// return NextResponse.redirect('/unauthorized')
// }
} catch (error) {
return NextResponse.redirect('/login')
}
}
}
return NextResponse.next()
}
export const config = {
matcher: [
/*
* Match all request paths except for the ones starting with:
* - api (API routes)
* - _next/static (static files)
* - _next/image (image optimization files)
* - favicon.ico (favicon file)
*/
'/((?!api|_next/static|_next/image|favicon.ico).*)',
],
}
// 4. Custom App Component (pages/_app.js)
import { SessionProvider } from 'next-auth/react'
import { QueryClient, QueryClientProvider } from 'react-query'
import { ReactQueryDevtools } from 'react-query/devtools'
import '../styles/globals.css'
// Create a client
function getQueryClient() {
if (typeof window === 'undefined') {
// Server: always make a new query client
return new QueryClient({
defaultOptions: {
queries: {
staleTime: 60 * 1000,
refetchOnWindowFocus: false,
},
},
})
} else {
// Browser: make a new query client if we don't already have one
if (!global.queryClient) global.queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 60 * 1000,
refetchOnWindowFocus: false,
},
},
})
return global.queryClient
}
}
function App({
Component,
pageProps: { session, ...pageProps },
}) {
const queryClient = getQueryClient()
return (
<SessionProvider session={session}>
<QueryClientProvider client={queryClient}>
<Component {...pageProps} />
{process.env.NODE_ENV === 'development' && (
<ReactQueryDevtools initialIsOpen={false} />
)}
</QueryClientProvider>
</SessionProvider>
)
}
// 5. Error Handling (pages/_error.js)
import Head from 'next/head'
import Link from 'next/link'
function Error({ statusCode, err }) {
const title = statusCode === 404 ? 'Page Not Found' : 'An Error Occurred'
const message = getErrorMessage(statusCode, err)
return (
<>
<Head>
<title>
{statusCode}: {title}
</title>
</Head>
<div className="error-container">
<div className="error-content">
<h1>{statusCode}</h1>
<h2>{title}</h2>
<p>{message}</p>
<div className="error-actions">
<Link href="/">
<a className="btn-primary">Go Home</a>
</Link>
<button onClick={() => window.history.back()} className="btn-secondary">
Go Back
</button>
</div>
{process.env.NODE_ENV === 'development' && err && (
<details className="error-details">
<summary>Error Details</summary>
<pre>{err.stack}</pre>
</details>
)}
</div>
</div>
<style jsx>{`
.error-container {
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}
.error-content {
text-align: center;
background: white;
padding: 60px;
border-radius: 12px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
max-width: 500px;
}
.error-content h1 {
font-size: 6rem;
font-weight: bold;
color: #e74c3c;
margin: 0 0 20px 0;
}
.error-content h2 {
font-size: 1.8rem;
color: #333;
margin: 0 0 20px 0;
}
.error-content p {
color: #666;
margin: 0 0 30px 0;
line-height: 1.6;
}
.error-actions {
display: flex;
gap: 15px;
justify-content: center;
}
.btn-primary,
.btn-secondary {
padding: 12px 24px;
border: none;
border-radius: 6px;
font-size: 1rem;
font-weight: 500;
cursor: pointer;
transition: all 0.3s;
text-decoration: none;
display: inline-block;
}
.btn-primary {
background-color: #0070f3;
color: white;
}
.btn-primary:hover {
background-color: #0051cc;
}
.btn-secondary {
background-color: #6c757d;
color: white;
}
.btn-secondary:hover {
background-color: #5a6268;
}
.error-details {
margin-top: 30px;
text-align: left;
}
.error-details summary {
cursor: pointer;
font-weight: 500;
margin-bottom: 10px;
padding: 10px;
background: #f8f9fa;
border-radius: 4px;
}
.error-details pre {
background: #f1f3f4;
padding: 15px;
border-radius: 4px;
overflow-x: auto;
font-size: 0.8rem;
color: #333;
}
@media (max-width: 768px) {
.error-content {
margin: 20px;
padding: 40px 20px;
}
.error-content h1 {
font-size: 4rem;
}
.error-content h2 {
font-size: 1.5rem;
}
.error-actions {
flex-direction: column;
}
}
`}</style>
</>
)
}
function getErrorMessage(statusCode, err) {
switch (statusCode) {
case 404:
return 'Sorry, the page you are looking for could not be found.'
case 500:
return 'Sorry, something went wrong on our end. Please try again later.'
default:
return 'An unexpected error occurred.'
}
}