🎯 Рекомендуемые коллекции
Балансированные коллекции примеров кода из различных категорий, которые вы можете исследовать
Примеры Next.js
Примеры фреймворка Next.js - React фреймворк для продакшена с рендерингом на стороне сервера, маршрутизацией и современными веб-возможностями
💻 Next.js Основы и Компоненты javascript
🟢 simple
⭐⭐
Базовые концепции Next.js включая компоненты, страницы и базовую маршрутизацию
⏱️ 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'
export default 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'
export default 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'
export default 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'
}
]
export default 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>
</>
)
}
💻 API маршруты Next.js и получение данных javascript
🟡 intermediate
⭐⭐⭐⭐
Создание RESTful API с маршрутами API Next.js, паттерны получения данных и обработка данных на стороне сервера
⏱️ 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)
export default 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)
export default 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'
export default 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
}
}
💻 Продвинутые возможности Next.js javascript
🔴 complex
⭐⭐⭐⭐⭐
Продвинутые концепции Next.js включая SSR, SSG, ISR, аутентификацию, middleware и развертывание
⏱️ 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'
export default 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'
export default 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
}
}
export default 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'
export default 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.'
}
}