π Module Project: Multi-Page Blog Application
Congratulations on mastering React Router! You've learned the fundamentals of routing, navigation, dynamic parameters, protected routes, query parameters, and sophisticated layout systems. Now it's time to bring it all together in a comprehensive multi-page application. In this module project, you'll build a feature-rich blog platform that showcases professional routing architecture, multiple layouts, search functionality, and seamless navigation. This is your opportunity to create a production-quality application that demonstrates real-world routing patterns! π
π― Project Objectives
By completing this project, you will:
- Build a complete multi-page blog platform with React Router and TypeScript
- Implement multiple layouts for different sections (public, auth, dashboard)
- Create dynamic routes with parameters for posts, categories, and authors
- Build protected routes with authentication simulation
- Implement search and filtering with query parameters
- Add breadcrumb navigation that updates based on location
- Handle nested routing for author profiles and settings
- Create loading states and error boundaries for routes
- Build a responsive navigation system with mobile support
- Implement programmatic navigation and redirects
Estimated Time: 4-6 hours
Difficulty: Advanced - applies all Module 6 concepts
π‘ What You'll Build
A production-ready blog application featuring:
- π Public homepage with featured posts
- π Blog listing with categories and search
- π Individual post pages with comments
- π€ Author profile pages with their posts
- π Login/register pages with authentication
- π Dashboard for managing posts (protected)
- βοΈ Settings section with nested routes
- π Search with query parameters and filters
- π Dynamic breadcrumb navigation
- π¨ Multiple layouts (public, auth, dashboard)
- π± Responsive navigation with mobile menu
- β‘ Loading states and error handling
- 404 Not Found page with helpful navigation
- π Smooth transitions between routes
π Project Sections
π Project Requirements & Features
Let's break down the complete feature set we're building. This isn't just a simple blogβit's a sophisticated multi-page application that demonstrates professional routing patterns you'll use in real-world applications. Each feature builds on the routing concepts you've learned throughout Module 6.
Core Features Breakdown
Essential Functionality
1οΈβ£ Public Pages (No Authentication Required)
| Page | Route | Features |
|---|---|---|
| Home | / |
Featured posts, recent posts, category links |
| Blog List | /blog |
All posts with search, filter by category, pagination |
| Post Detail | /blog/:postId |
Full post content, author info, related posts |
| Category | /category/:categoryName |
Posts filtered by category |
| Author Profile | /author/:authorId |
Author bio, their posts, statistics |
| About | /about |
About the blog platform |
2οΈβ£ Authentication Pages
| Page | Route | Features |
|---|---|---|
| Login | /login |
Email/password login, remember me, redirects |
| Register | /register |
Create new account, validation, auto-login |
| Forgot Password | /forgot-password |
Password reset email simulation |
3οΈβ£ Protected Dashboard Pages (Authentication Required)
| Page | Route | Features |
|---|---|---|
| Dashboard | /dashboard |
Overview, stats, recent activity |
| My Posts | /dashboard/posts |
List of user's posts, edit/delete actions |
| Create Post | /dashboard/posts/new |
Form to create new blog post |
| Edit Post | /dashboard/posts/:postId/edit |
Edit existing post |
| Settings (nested) | /dashboard/settings |
Index page with overview of settings |
| ββ Profile | /dashboard/settings/profile |
Edit profile information |
| ββ Account | /dashboard/settings/account |
Email, password, security |
| ββ Preferences | /dashboard/settings/preferences |
Theme, notifications, display options |
Technical Requirements
π§ Implementation Standards
Routing Requirements:
- β React Router v6+ with TypeScript
- β Multiple layout components (PublicLayout, AuthLayout, DashboardLayout)
- β Nested routes with proper Outlet placement
- β Dynamic route parameters (postId, authorId, categoryName)
- β Query parameters for search and filters
- β Protected routes with redirect logic
- β 404 Not Found page with helpful navigation
- β Loading states during route transitions
- β Error boundaries for route errors
Navigation Requirements:
- β Responsive navigation bar in each layout
- β Active link highlighting with NavLink
- β Breadcrumb navigation showing current path
- β Programmatic navigation after actions
- β Back navigation where appropriate
- β Mobile-friendly hamburger menu
State Management:
- β Authentication state (simulated with localStorage)
- β Mock data for posts, authors, categories
- β Search and filter state in URL query params
- β Form state for create/edit operations
TypeScript Requirements:
- β All components properly typed
- β Route parameter types defined
- β Props interfaces for all components
- β Data model interfaces (Post, Author, Category)
- β
No use of
anytype
User Experience Requirements
π User Stories
As a visitor, I want to:
- Browse blog posts on the homepage and blog listing page
- Search for posts by title or content
- Filter posts by category
- Read individual blog posts with full content
- View author profiles and their posts
- Navigate easily with clear breadcrumbs
- See helpful error messages if a page doesn't exist
As an authenticated user, I want to:
- Log in to access my dashboard
- Create new blog posts
- Edit and delete my existing posts
- Manage my profile settings
- Customize my account preferences
- See my post statistics
- Be redirected to my intended destination after login
As a developer, I want to:
- Have a clear, maintainable routing structure
- Easily add new routes and features
- Reuse layout components across sections
- Handle errors gracefully at the route level
- Type-safe route parameters and navigation
β οΈ Important Project Notes
- Authentication is simulated - We'll use localStorage to simulate login state. This is NOT production-ready authentication but demonstrates routing patterns.
- Data is mocked - We'll use mock data arrays instead of real API calls. The focus is on routing, not data fetching.
- Build incrementally - This is a large project. Build it section by section, testing each part before moving on.
- Focus on routing - While we'll create nice UI, the main learning goal is mastering React Router patterns.
- Responsive design - Test on different screen sizes as you build, especially the navigation.
Success Criteria Checklist
β Your Project is Complete When:
- β All routes are defined and working
- β Three distinct layouts are implemented
- β Navigation works on all screen sizes
- β Protected routes redirect to login
- β Login redirects back to intended page
- β Search and filters update URL params
- β Breadcrumbs show current location
- β 404 page handles invalid routes
- β Loading states show during navigation
- β Dynamic routes load correct data
- β Nested settings routes work
- β Active links are highlighted
- β All TypeScript types are defined
- β Code is organized and readable
- β App is tested on mobile and desktop
π οΈ Project Setup & Planning
Before diving into code, let's set up our project properly and create a clear plan. A well-organized project structure makes development smoother and your code more maintainable.
Step 1: Create the Project
If you haven't already, create a new React + TypeScript project using Vite:
# Create new project
npm create vite@latest blog-app -- --template react-ts
# Navigate to project
cd blog-app
# Install dependencies
npm install
# Install React Router
npm install react-router-dom
# Install TypeScript types for React Router (if needed)
npm install -D @types/react-router-dom
π‘ Project Structure Preview
We'll organize our code like this:
blog-app/
βββ src/
β βββ components/ # Reusable UI components
β β βββ Breadcrumbs.tsx
β β βββ PostCard.tsx
β β βββ Navbar.tsx
β β βββ LoadingSpinner.tsx
β βββ layouts/ # Layout components
β β βββ PublicLayout.tsx
β β βββ AuthLayout.tsx
β β βββ DashboardLayout.tsx
β βββ pages/ # Page components (routes)
β β βββ public/
β β β βββ HomePage.tsx
β β β βββ BlogListPage.tsx
β β β βββ PostDetailPage.tsx
β β β βββ AuthorProfilePage.tsx
β β β βββ CategoryPage.tsx
β β βββ auth/
β β β βββ LoginPage.tsx
β β β βββ RegisterPage.tsx
β β β βββ ForgotPasswordPage.tsx
β β βββ dashboard/
β β βββ DashboardHomePage.tsx
β β βββ MyPostsPage.tsx
β β βββ CreatePostPage.tsx
β β βββ EditPostPage.tsx
β β βββ settings/
β β βββ SettingsLayout.tsx
β β βββ ProfileSettingsPage.tsx
β β βββ AccountSettingsPage.tsx
β β βββ PreferencesPage.tsx
β βββ data/ # Mock data
β β βββ posts.ts
β β βββ authors.ts
β β βββ categories.ts
β βββ types/ # TypeScript type definitions
β β βββ index.ts
β βββ utils/ # Utility functions
β β βββ auth.ts
β βββ App.tsx # Main app with routing
β βββ main.tsx # Entry point
β βββ index.css # Global styles
βββ package.json
Step 2: Clean Up Default Files
Remove unnecessary default files and code:
- Delete
src/App.css(we'll use our own styles) - Clean up
src/App.tsx- remove the default content - Clean up
src/index.css- we'll add our own base styles - Update
src/main.tsxif needed
Step 3: Plan the Development Order
We'll build the project in this order to ensure a logical progression:
β Pro Tips for Success
- Test frequently - Run
npm run devand check your work after each major addition - Use TypeScript - Define types early and let TypeScript catch errors
- Component first - Build small, focused components before assembling pages
- Start simple - Get basic routing working before adding complex features
- Console log - Use console.log to verify params, location, and navigation state
- Read errors carefully - React Router error messages are usually very helpful
- Commit often - Use Git to save your progress at each milestone
Step 4: Install Additional Dependencies (Optional)
You might want these helpful packages:
# For better date formatting (if needed)
npm install date-fns
# For unique IDs (if needed)
npm install uuid
npm install -D @types/uuid
# For icons (optional)
npm install react-icons
β οΈ Keep It Simple
While these packages are nice, they're not required for this project. The focus is on React Router, not on external libraries. You can build everything with just React, TypeScript, and React Router!
Development Workflow
Follow this workflow as you build:
- Read the section - Understand what you're building
- Look at the code - Study the example implementations
- Type it yourself - Don't copy-paste, type it to learn
- Test it - Verify it works in the browser
- Experiment - Try variations and see what happens
- Move forward - Once it works, proceed to the next section
π― Ready to Build?
You have your project set up and a clear plan. Now let's start building! We'll begin with the foundation: defining our data models and types.
π Data Modeling & Mock Data
Before we build any UI or routing, we need to define our data structures. Good TypeScript types and mock data will make the rest of development much smoother. Let's create the data foundation for our blog application.
Step 1: Define TypeScript Interfaces
Create src/types/index.ts with all our data models:
// src/types/index.ts
// User/Author types
export interface Author {
id: string;
name: string;
email: string;
avatar: string;
bio: string;
website?: string;
social: {
twitter?: string;
github?: string;
linkedin?: string;
};
postCount: number;
joinedDate: string;
}
// Blog post types
export interface Post {
id: string;
title: string;
slug: string;
excerpt: string;
content: string;
author: Author;
category: Category;
tags: string[];
publishedDate: string;
updatedDate?: string;
readTime: number; // in minutes
views: number;
likes: number;
featured: boolean;
coverImage: string;
}
// Category types
export interface Category {
id: string;
name: string;
slug: string;
description: string;
postCount: number;
color: string;
}
// Comment types (for future use)
export interface Comment {
id: string;
postId: string;
author: {
name: string;
avatar: string;
};
content: string;
createdDate: string;
likes: number;
}
// Authentication types
export interface User {
id: string;
email: string;
name: string;
avatar: string;
role: 'admin' | 'author' | 'reader';
}
export interface AuthState {
isAuthenticated: boolean;
user: User | null;
}
// Form types
export interface LoginFormData {
email: string;
password: string;
rememberMe: boolean;
}
export interface RegisterFormData {
name: string;
email: string;
password: string;
confirmPassword: string;
}
export interface PostFormData {
title: string;
excerpt: string;
content: string;
categoryId: string;
tags: string[];
coverImage: string;
featured: boolean;
}
// Filter and search types
export interface BlogFilters {
search?: string;
category?: string;
author?: string;
tag?: string;
sortBy?: 'date' | 'views' | 'likes';
sortOrder?: 'asc' | 'desc';
}
// Statistics types
export interface UserStats {
totalPosts: number;
totalViews: number;
totalLikes: number;
draftPosts: number;
publishedPosts: number;
}
π‘ Why These Types Matter
These TypeScript interfaces provide:
- Type safety - Catch errors at compile time
- Autocomplete - VS Code will suggest properties
- Documentation - Types show what data looks like
- Refactoring safety - Rename properties with confidence
Step 2: Create Mock Categories
Create src/data/categories.ts:
// src/data/categories.ts
import { Category } from '../types';
export const categories: Category[] = [
{
id: 'cat-1',
name: 'Web Development',
slug: 'web-development',
description: 'Articles about HTML, CSS, JavaScript, and web frameworks',
postCount: 15,
color: '#3B82F6', // Blue
},
{
id: 'cat-2',
name: 'React',
slug: 'react',
description: 'Deep dives into React, hooks, and the React ecosystem',
postCount: 12,
color: '#06B6D4', // Cyan
},
{
id: 'cat-3',
name: 'TypeScript',
slug: 'typescript',
description: 'Type-safe JavaScript development with TypeScript',
postCount: 8,
color: '#3178C6', // TypeScript Blue
},
{
id: 'cat-4',
name: 'Career',
slug: 'career',
description: 'Tips for developers to grow their careers',
postCount: 6,
color: '#10B981', // Green
},
{
id: 'cat-5',
name: 'Tutorials',
slug: 'tutorials',
description: 'Step-by-step guides and how-to articles',
postCount: 10,
color: '#8B5CF6', // Purple
},
{
id: 'cat-6',
name: 'Best Practices',
slug: 'best-practices',
description: 'Industry standards and coding best practices',
postCount: 7,
color: '#EF4444', // Red
},
];
// Helper function to find category by slug
export function getCategoryBySlug(slug: string): Category | undefined {
return categories.find(cat => cat.slug === slug);
}
// Helper function to find category by ID
export function getCategoryById(id: string): Category | undefined {
return categories.find(cat => cat.id === id);
}
Step 3: Create Mock Authors
Create src/data/authors.ts:
// src/data/authors.ts
import { Author } from '../types';
export const authors: Author[] = [
{
id: 'author-1',
name: 'Sarah Johnson',
email: 'sarah@example.com',
avatar: 'https://i.pravatar.cc/150?img=1',
bio: 'Full-stack developer with a passion for React and TypeScript. Love teaching others through writing.',
website: 'https://sarahjohnson.dev',
social: {
twitter: '@sarahj_dev',
github: 'sarahjohnson',
linkedin: 'sarah-johnson-dev',
},
postCount: 12,
joinedDate: '2023-01-15',
},
{
id: 'author-2',
name: 'Michael Chen',
email: 'michael@example.com',
avatar: 'https://i.pravatar.cc/150?img=12',
bio: 'Senior software engineer specializing in frontend architecture and performance optimization.',
website: 'https://michaelchen.tech',
social: {
twitter: '@mchen_tech',
github: 'michaelchen',
},
postCount: 8,
joinedDate: '2023-03-20',
},
{
id: 'author-3',
name: 'Emily Rodriguez',
email: 'emily@example.com',
avatar: 'https://i.pravatar.cc/150?img=5',
bio: 'JavaScript enthusiast and tech writer. Making complex concepts simple and accessible.',
social: {
twitter: '@emily_codes',
github: 'emilyrodriguez',
linkedin: 'emily-rodriguez',
},
postCount: 15,
joinedDate: '2022-11-10',
},
{
id: 'author-4',
name: 'David Kim',
email: 'david@example.com',
avatar: 'https://i.pravatar.cc/150?img=8',
bio: 'React consultant and open source contributor. Building better web experiences one component at a time.',
website: 'https://davidkim.io',
social: {
github: 'davidkim',
linkedin: 'david-kim-dev',
},
postCount: 6,
joinedDate: '2023-05-01',
},
];
// Helper function to find author by ID
export function getAuthorById(id: string): Author | undefined {
return authors.find(author => author.id === id);
}
// Helper function to get author by name (for search)
export function getAuthorByName(name: string): Author | undefined {
return authors.find(
author => author.name.toLowerCase() === name.toLowerCase()
);
}
Step 4: Create Mock Posts
Create src/data/posts.ts with a comprehensive set of blog posts:
// src/data/posts.ts
import { Post } from '../types';
import { authors } from './authors';
import { categories } from './categories';
export const posts: Post[] = [
{
id: 'post-1',
title: 'Getting Started with React Router v6',
slug: 'getting-started-react-router-v6',
excerpt: 'Learn the fundamentals of React Router v6 and how to implement routing in your React applications.',
content: `# Getting Started with React Router v6
React Router is the standard routing library for React applications. In this comprehensive guide, we'll explore the fundamentals of React Router v6 and learn how to implement routing in your applications.
## What is React Router?
React Router is a collection of navigational components that compose declaratively with your application. Whether you want to have bookmarkable URLs for your web app or a composable way to navigate in React Native, React Router works wherever React is rendering.
## Installation
First, install React Router in your project:
\`\`\`bash
npm install react-router-dom
\`\`\`
## Basic Setup
Here's a simple example of setting up React Router:
\`\`\`tsx
import { BrowserRouter, Routes, Route } from 'react-router-dom';
function App() {
return (
<BrowserRouter>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
<Route path="/contact" element={<Contact />} />
</Routes>
</BrowserRouter>
);
}
\`\`\`
## Navigation
Use the Link component to navigate between routes:
\`\`\`tsx
import { Link } from 'react-router-dom';
function Navigation() {
return (
<nav>
<Link to="/">Home</Link>
<Link to="/about">About</Link>
<Link to="/contact">Contact</Link>
</nav>
);
}
\`\`\`
## Conclusion
React Router v6 provides a powerful and flexible way to handle routing in your React applications. Start with these basics and explore more advanced features as you build!`,
author: authors[0],
category: categories[1], // React
tags: ['react', 'routing', 'tutorial'],
publishedDate: '2024-01-15',
readTime: 8,
views: 1250,
likes: 89,
featured: true,
coverImage: 'https://images.unsplash.com/photo-1633356122544-f134324a6cee?w=800',
},
{
id: 'post-2',
title: 'TypeScript Best Practices for 2024',
slug: 'typescript-best-practices-2024',
excerpt: 'Discover the latest TypeScript best practices to write cleaner, more maintainable code.',
content: `# TypeScript Best Practices for 2024
TypeScript has become an essential tool for modern web development. Here are the best practices you should follow in 2024.
## Use Strict Mode
Always enable strict mode in your \`tsconfig.json\`:
\`\`\`json
{
"compilerOptions": {
"strict": true
}
}
\`\`\`
## Avoid the 'any' Type
The \`any\` type defeats the purpose of TypeScript. Instead, use proper types or \`unknown\` when the type is truly unknown.
## Leverage Utility Types
TypeScript provides powerful utility types:
\`\`\`typescript
type Partial<T> // Makes all properties optional
type Required<T> // Makes all properties required
type Pick<T, K> // Pick specific properties
type Omit<T, K> // Omit specific properties
\`\`\`
## Conclusion
Following these best practices will help you write more robust TypeScript code!`,
author: authors[1],
category: categories[2], // TypeScript
tags: ['typescript', 'best-practices', 'coding'],
publishedDate: '2024-01-20',
readTime: 6,
views: 980,
likes: 67,
featured: true,
coverImage: 'https://images.unsplash.com/photo-1516116216624-53e697fedbea?w=800',
},
{
id: 'post-3',
title: 'Building Responsive Layouts with CSS Grid',
slug: 'responsive-layouts-css-grid',
excerpt: 'Master CSS Grid to create beautiful, responsive layouts for modern web applications.',
content: `# Building Responsive Layouts with CSS Grid
CSS Grid is a powerful layout system that makes creating complex, responsive designs much easier. Let's explore how to use it effectively.
## Grid Basics
Define a grid container:
\`\`\`css
.container {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 20px;
}
\`\`\`
## Responsive Grids
Use \`auto-fit\` and \`minmax\` for responsive layouts:
\`\`\`css
.container {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 20px;
}
\`\`\`
This creates a grid that automatically adjusts the number of columns based on available space!
## Grid Areas
Name your grid areas for cleaner code:
\`\`\`css
.layout {
display: grid;
grid-template-areas:
"header header header"
"sidebar main main"
"footer footer footer";
}
.header { grid-area: header; }
.sidebar { grid-area: sidebar; }
.main { grid-area: main; }
.footer { grid-area: footer; }
\`\`\`
## Conclusion
CSS Grid simplifies responsive layout creation. Start using it in your projects today!`,
author: authors[2],
category: categories[0], // Web Development
tags: ['css', 'grid', 'responsive', 'layout'],
publishedDate: '2024-01-18',
readTime: 10,
views: 1420,
likes: 102,
featured: false,
coverImage: 'https://images.unsplash.com/photo-1507721999472-8ed4421c4af2?w=800',
},
{
id: 'post-4',
title: 'React Hooks: A Complete Guide',
slug: 'react-hooks-complete-guide',
excerpt: 'Everything you need to know about React Hooks, from basics to advanced patterns.',
content: `# React Hooks: A Complete Guide
React Hooks revolutionized how we write React components. This guide covers everything from basics to advanced patterns.
## useState
The most basic hook for managing state:
\`\`\`tsx
const [count, setCount] = useState(0);
\`\`\`
## useEffect
Handle side effects in your components:
\`\`\`tsx
useEffect(() => {
document.title = \`Count: \${count}\`;
}, [count]);
\`\`\`
## Custom Hooks
Create reusable logic:
\`\`\`tsx
function useFetch(url: string) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetch(url)
.then(res => res.json())
.then(data => {
setData(data);
setLoading(false);
});
}, [url]);
return { data, loading };
}
\`\`\`
## Conclusion
Hooks make React code more reusable and easier to understand. Master them to become a better React developer!`,
author: authors[0],
category: categories[1], // React
tags: ['react', 'hooks', 'tutorial', 'javascript'],
publishedDate: '2024-01-22',
readTime: 12,
views: 2100,
likes: 156,
featured: true,
coverImage: 'https://images.unsplash.com/photo-1633356122102-3fe601e05bd2?w=800',
},
{
id: 'post-5',
title: 'Career Tips for Junior Developers',
slug: 'career-tips-junior-developers',
excerpt: 'Essential advice for junior developers starting their career in tech.',
content: `# Career Tips for Junior Developers
Starting your career as a developer can be overwhelming. Here are essential tips to help you succeed.
## 1. Focus on Fundamentals
Master JavaScript, HTML, and CSS before jumping to frameworks. A solid foundation will serve you throughout your career.
## 2. Build Projects
Create real projects, not just tutorial follow-alongs. Put them on GitHub and deploy them.
## 3. Learn to Debug
Debugging is a crucial skill. Get comfortable with browser DevTools and learn to read error messages.
## 4. Ask Questions
Don't be afraid to ask for help. Every senior developer was once a junior.
## 5. Contribute to Open Source
Start with small contributions. It's great for learning and building your resume.
## 6. Network
Attend meetups, join online communities, and connect with other developers.
## Conclusion
Your first year as a developer is about learning and growth. Be patient with yourself and keep coding!`,
author: authors[3],
category: categories[3], // Career
tags: ['career', 'beginners', 'advice'],
publishedDate: '2024-01-25',
readTime: 7,
views: 890,
likes: 54,
featured: false,
coverImage: 'https://images.unsplash.com/photo-1522071820081-009f0129c71c?w=800',
},
{
id: 'post-6',
title: 'Advanced TypeScript: Generics',
slug: 'advanced-typescript-generics',
excerpt: 'Deep dive into TypeScript generics and how to use them effectively.',
content: `# Advanced TypeScript: Generics
Generics are one of TypeScript's most powerful features. They allow you to write reusable, type-safe code.
## Basic Generics
A simple generic function:
\`\`\`typescript
function identity<T>(arg: T): T {
return arg;
}
const result = identity<string>("hello"); // Type is string
\`\`\`
## Generic Constraints
Constrain what types can be used:
\`\`\`typescript
interface HasLength {
length: number;
}
function logLength<T extends HasLength>(arg: T): void {
console.log(arg.length);
}
\`\`\`
## Generic Interfaces
\`\`\`typescript
interface Container<T> {
value: T;
getValue: () => T;
setValue: (value: T) => void;
}
\`\`\`
## Conclusion
Mastering generics will make you a much more effective TypeScript developer!`,
author: authors[1],
category: categories[2], // TypeScript
tags: ['typescript', 'generics', 'advanced'],
publishedDate: '2024-01-28',
readTime: 9,
views: 1150,
likes: 78,
featured: false,
coverImage: 'https://images.unsplash.com/photo-1555066931-4365d14bab8c?w=800',
},
{
id: 'post-7',
title: 'Modern JavaScript Features You Should Know',
slug: 'modern-javascript-features',
excerpt: 'Explore the latest JavaScript features that will improve your code.',
content: `# Modern JavaScript Features You Should Know
JavaScript continues to evolve. Here are modern features every developer should know.
## Optional Chaining
Safely access nested properties:
\`\`\`javascript
const name = user?.profile?.name;
\`\`\`
## Nullish Coalescing
Use ?? instead of ||:
\`\`\`javascript
const value = input ?? 'default';
\`\`\`
## Array Methods
Powerful array manipulation:
\`\`\`javascript
const doubled = numbers.map(n => n * 2);
const evens = numbers.filter(n => n % 2 === 0);
const sum = numbers.reduce((acc, n) => acc + n, 0);
\`\`\`
## Async/Await
Clean asynchronous code:
\`\`\`javascript
async function fetchData() {
try {
const response = await fetch(url);
const data = await response.json();
return data;
} catch (error) {
console.error(error);
}
}
\`\`\`
## Conclusion
These features make JavaScript code cleaner and more maintainable. Use them in your projects!`,
author: authors[2],
category: categories[0], // Web Development
tags: ['javascript', 'es6', 'modern', 'features'],
publishedDate: '2024-02-01',
readTime: 8,
views: 1680,
likes: 112,
featured: true,
coverImage: 'https://images.unsplash.com/photo-1579468118864-1b9ea3c0db4a?w=800',
},
{
id: 'post-8',
title: 'Testing React Components with Testing Library',
slug: 'testing-react-components',
excerpt: 'Learn how to write effective tests for your React components.',
content: `# Testing React Components with Testing Library
Testing is crucial for maintainable applications. Learn how to test React components effectively.
## Setup
Install the necessary packages:
\`\`\`bash
npm install --save-dev @testing-library/react @testing-library/jest-dom
\`\`\`
## Basic Test
Test a simple component:
\`\`\`tsx
import { render, screen } from '@testing-library/react';
import Button from './Button';
test('renders button with text', () => {
render(<Button>Click me</Button>);
expect(screen.getByText('Click me')).toBeInTheDocument();
});
\`\`\`
## Testing User Interactions
\`\`\`tsx
import { render, screen, fireEvent } from '@testing-library/react';
test('button click calls handler', () => {
const handleClick = jest.fn();
render(<Button onClick={handleClick}>Click</Button>);
fireEvent.click(screen.getByText('Click'));
expect(handleClick).toHaveBeenCalledTimes(1);
});
\`\`\`
## Conclusion
Good tests give you confidence to refactor and add features. Make testing a habit!`,
author: authors[0],
category: categories[4], // Tutorials
tags: ['testing', 'react', 'tutorial', 'quality'],
publishedDate: '2024-02-03',
readTime: 11,
views: 950,
likes: 71,
featured: false,
coverImage: 'https://images.unsplash.com/photo-1516116216624-53e697fedbea?w=800',
},
];
// Helper functions for working with posts
export function getPostBySlug(slug: string): Post | undefined {
return posts.find(post => post.slug === slug);
}
export function getPostById(id: string): Post | undefined {
return posts.find(post => post.id === id);
}
export function getPostsByCategory(categorySlug: string): Post[] {
return posts.filter(post => post.category.slug === categorySlug);
}
export function getPostsByAuthor(authorId: string): Post[] {
return posts.filter(post => post.author.id === authorId);
}
export function getFeaturedPosts(): Post[] {
return posts.filter(post => post.featured);
}
export function searchPosts(query: string): Post[] {
const lowerQuery = query.toLowerCase();
return posts.filter(
post =>
post.title.toLowerCase().includes(lowerQuery) ||
post.excerpt.toLowerCase().includes(lowerQuery) ||
post.tags.some(tag => tag.toLowerCase().includes(lowerQuery))
);
}
export function getRecentPosts(limit: number = 5): Post[] {
return [...posts]
.sort((a, b) =>
new Date(b.publishedDate).getTime() - new Date(a.publishedDate).getTime()
)
.slice(0, limit);
}
export function getPopularPosts(limit: number = 5): Post[] {
return [...posts]
.sort((a, b) => b.views - a.views)
.slice(0, limit);
}
β Data Structure Benefits
Notice how our mock data provides:
- Rich content - Realistic blog posts with full content
- Relationships - Posts linked to authors and categories
- Variety - Different featured status, dates, and popularity
- Helper functions - Easy ways to query and filter data
- Type safety - All data matches our TypeScript interfaces
Step 5: Create Authentication Utility
Create src/utils/auth.ts for authentication helpers:
// src/utils/auth.ts
import { User, AuthState } from '../types';
const AUTH_KEY = 'blog_auth';
const USER_KEY = 'blog_user';
// Mock user for authentication
const mockUser: User = {
id: 'user-1',
email: 'demo@example.com',
name: 'Demo User',
avatar: 'https://i.pravatar.cc/150?img=68',
role: 'author',
};
// Get current authentication state
export function getAuthState(): AuthState {
const isAuthenticated = localStorage.getItem(AUTH_KEY) === 'true';
const userJson = localStorage.getItem(USER_KEY);
const user = userJson ? JSON.parse(userJson) : null;
return {
isAuthenticated,
user,
};
}
// Check if user is authenticated
export function isAuthenticated(): boolean {
return localStorage.getItem(AUTH_KEY) === 'true';
}
// Get current user
export function getCurrentUser(): User | null {
const userJson = localStorage.getItem(USER_KEY);
return userJson ? JSON.parse(userJson) : null;
}
// Login function (simulated)
export function login(email: string, password: string): boolean {
// In a real app, this would call an API
// For demo purposes, accept any email/password
if (email && password) {
localStorage.setItem(AUTH_KEY, 'true');
localStorage.setItem(USER_KEY, JSON.stringify(mockUser));
return true;
}
return false;
}
// Register function (simulated)
export function register(name: string, email: string, password: string): boolean {
// In a real app, this would call an API
if (name && email && password) {
const newUser: User = {
...mockUser,
id: `user-${Date.now()}`,
name,
email,
};
localStorage.setItem(AUTH_KEY, 'true');
localStorage.setItem(USER_KEY, JSON.stringify(newUser));
return true;
}
return false;
}
// Logout function
export function logout(): void {
localStorage.removeItem(AUTH_KEY);
localStorage.removeItem(USER_KEY);
}
// Update user profile
export function updateUser(updates: Partial<User>): void {
const currentUser = getCurrentUser();
if (currentUser) {
const updatedUser = { ...currentUser, ...updates };
localStorage.setItem(USER_KEY, JSON.stringify(updatedUser));
}
}
β οΈ Authentication Notice
This authentication system is for demonstration only. It uses localStorage and accepts any credentials. In a production app, you would:
- Use a real authentication API
- Validate credentials securely on the server
- Use JWT tokens or session cookies
- Implement proper security measures
- Handle token refresh and expiration
For this project, we're focusing on routing patterns, not authentication implementation.
Testing Your Data
Before moving forward, let's verify your data is set up correctly. Create a temporary test file:
// src/test-data.ts (temporary file for testing)
import { posts, getPostBySlug, getFeaturedPosts } from './data/posts';
import { authors, getAuthorById } from './data/authors';
import { categories, getCategoryBySlug } from './data/categories';
console.log('Total posts:', posts.length);
console.log('Total authors:', authors.length);
console.log('Total categories:', categories.length);
console.log('\nFeatured posts:', getFeaturedPosts().length);
console.log('\nFirst post:', getPostBySlug('getting-started-react-router-v6'));
console.log('\nReact category:', getCategoryBySlug('react'));
console.log('\nFirst author:', getAuthorById('author-1'));
Import this in your App.tsx temporarily to verify the data loads correctly in the browser console.
π Data Foundation Complete!
You now have:
- β TypeScript interfaces for all data types
- β Mock categories (6 categories)
- β Mock authors (4 authors)
- β Mock posts (8 detailed blog posts)
- β Helper functions for querying data
- β Authentication utilities
With this foundation in place, we're ready to start building our routing architecture!
πΊοΈ Route Architecture Design
Before writing any routing code, let's design our complete route structure. A well-planned architecture makes implementation much smoother and creates a maintainable application.
Complete Route Structure
Here's our full route hierarchy visualized:
Route Configuration Table
| Path | Component | Layout | Protected | Description |
|---|---|---|---|---|
| π Public Routes | ||||
/ |
HomePage | PublicLayout | β | Landing page with featured posts |
/blog |
BlogListPage | PublicLayout | β | List all posts with search |
/blog/:postId |
PostDetailPage | PublicLayout | β | Individual post view |
/category/:categoryName |
CategoryPage | PublicLayout | β | Posts filtered by category |
/author/:authorId |
AuthorProfilePage | PublicLayout | β | Author bio and their posts |
/about |
AboutPage | PublicLayout | β | About the blog |
| π Authentication Routes | ||||
/login |
LoginPage | AuthLayout | β | User login form |
/register |
RegisterPage | AuthLayout | β | User registration form |
/forgot-password |
ForgotPasswordPage | AuthLayout | β | Password reset |
| π Protected Dashboard Routes | ||||
/dashboard |
DashboardHomePage | DashboardLayout | β | Dashboard overview |
/dashboard/posts |
MyPostsPage | DashboardLayout | β | User's posts list |
/dashboard/posts/new |
CreatePostPage | DashboardLayout | β | Create new post |
/dashboard/posts/:postId/edit |
EditPostPage | DashboardLayout | β | Edit existing post |
/dashboard/settings |
SettingsLayout (index) | DashboardLayout | β | Settings overview |
/dashboard/settings/profile |
ProfileSettingsPage | DashboardLayout | β | Edit profile |
/dashboard/settings/account |
AccountSettingsPage | DashboardLayout | β | Account settings |
/dashboard/settings/preferences |
PreferencesPage | DashboardLayout | β | User preferences |
| β Error Routes | ||||
* |
NotFoundPage | PublicLayout | β | 404 Not Found |
Layout Responsibilities
1. PublicLayout
Purpose: General public pages accessible to everyone
Features:
- Main navigation bar with links to Home, Blog, About
- Category menu
- Search bar in header
- Login/Register buttons (if not authenticated)
- User menu (if authenticated)
- Breadcrumb navigation
- Footer with links and information
2. AuthLayout
Purpose: Authentication pages (login, register)
Features:
- Minimal header with logo
- Centered form container
- No navigation or footer
- Clean, focused design
- Links to switch between login/register
3. DashboardLayout
Purpose: Protected pages for authenticated users
Features:
- Sidebar navigation with dashboard links
- Top bar with user info and logout
- Breadcrumb navigation
- Main content area with Outlet
- Mobile-responsive sidebar (collapsible)
- Settings sub-navigation (for nested settings routes)
Routing Strategy
π― Key Routing Decisions
1. Layout Routes
We use layout routes to wrap groups of pages with consistent UI:
<Route element={<PublicLayout />}>
<Route path="/" element={<HomePage />} />
<Route path="/blog" element={<BlogListPage />} />
{/* More public routes */}
</Route>
2. Protected Routes
We create a ProtectedRoute component that checks authentication:
<Route element={<ProtectedRoute><DashboardLayout /></ProtectedRoute>}>
<Route path="/dashboard" element={<DashboardHome />} />
{/* More protected routes */}
</Route>
3. Nested Routes
Settings uses nested routing with its own layout:
<Route path="/dashboard/settings" element={<SettingsLayout />}>
<Route index element={<SettingsIndex />} />
<Route path="profile" element={<ProfileSettings />} />
<Route path="account" element={<AccountSettings />} />
</Route>
4. Dynamic Routes
We use URL parameters for dynamic content:
<Route path="/blog/:postId" element={<PostDetail />} />
<Route path="/author/:authorId" element={<AuthorProfile />} />
<Route path="/category/:categoryName" element={<Category />} />
β Architecture Checklist
Before coding, verify you understand:
- β The three main layouts and their purposes
- β Which routes belong in each layout
- β Which routes require authentication
- β How nested routes work (settings example)
- β Which routes use dynamic parameters
- β How breadcrumbs will work across layouts
- β Where the 404 page fits in
π Ready to Code!
You have a complete route architecture designed. You understand:
- β All routes and their paths
- β Three distinct layouts
- β Protected vs public routes
- β Nested routing structure
- β Dynamic route parameters
In the next section, we'll start implementing this architecture with React Router!
βοΈ Setting Up React Router
Now that we have our data and architecture planned, let's set up React Router and create the foundation of our routing system. We'll start with the main App component and configure all our routes.
Step 1: Update main.tsx
First, make sure your entry point is set up correctly. Update src/main.tsx:
// src/main.tsx
import React from 'react';
import ReactDOM from 'react-dom/client';
import { BrowserRouter } from 'react-router-dom';
import App from './App';
import './index.css';
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<BrowserRouter>
<App />
</BrowserRouter>
</React.StrictMode>
);
π‘ BrowserRouter Placement
We wrap our entire app with BrowserRouter in main.tsx so that routing is available everywhere. This is the standard pattern for React Router applications.
Step 2: Create ProtectedRoute Component
Before building our route structure, we need a component to protect authenticated routes. Create src/components/ProtectedRoute.tsx:
// src/components/ProtectedRoute.tsx
import { Navigate, useLocation } from 'react-router-dom';
import { isAuthenticated } from '../utils/auth';
interface ProtectedRouteProps {
children: React.ReactNode;
}
export function ProtectedRoute({ children }: ProtectedRouteProps) {
const location = useLocation();
const isAuth = isAuthenticated();
if (!isAuth) {
// Redirect to login but save the location they were trying to go to
return <Navigate to="/login" state={{ from: location }} replace />;
}
return <>{children}</>;
}
β How ProtectedRoute Works
- Check if the user is authenticated using our
isAuthenticated()function - If not authenticated, redirect to
/login - Save the current location in
state.fromso we can redirect back after login - If authenticated, render the children (the protected content)
Step 3: Configure Routes in App.tsx
Now let's build our complete route configuration. Update src/App.tsx:
// src/App.tsx
import { Routes, Route } from 'react-router-dom';
import { ProtectedRoute } from './components/ProtectedRoute';
// Layout components (we'll create these next)
import { PublicLayout } from './layouts/PublicLayout';
import { AuthLayout } from './layouts/AuthLayout';
import { DashboardLayout } from './layouts/DashboardLayout';
// Public pages (we'll create these later)
import { HomePage } from './pages/public/HomePage';
import { BlogListPage } from './pages/public/BlogListPage';
import { PostDetailPage } from './pages/public/PostDetailPage';
import { CategoryPage } from './pages/public/CategoryPage';
import { AuthorProfilePage } from './pages/public/AuthorProfilePage';
import { AboutPage } from './pages/public/AboutPage';
// Auth pages
import { LoginPage } from './pages/auth/LoginPage';
import { RegisterPage } from './pages/auth/RegisterPage';
import { ForgotPasswordPage } from './pages/auth/ForgotPasswordPage';
// Dashboard pages
import { DashboardHomePage } from './pages/dashboard/DashboardHomePage';
import { MyPostsPage } from './pages/dashboard/MyPostsPage';
import { CreatePostPage } from './pages/dashboard/CreatePostPage';
import { EditPostPage } from './pages/dashboard/EditPostPage';
// Settings pages and layout
import { SettingsLayout } from './pages/dashboard/settings/SettingsLayout';
import { SettingsIndexPage } from './pages/dashboard/settings/SettingsIndexPage';
import { ProfileSettingsPage } from './pages/dashboard/settings/ProfileSettingsPage';
import { AccountSettingsPage } from './pages/dashboard/settings/AccountSettingsPage';
import { PreferencesPage } from './pages/dashboard/settings/PreferencesPage';
// Error page
import { NotFoundPage } from './pages/NotFoundPage';
function App() {
return (
<Routes>
{/* Public routes with PublicLayout */}
<Route element={<PublicLayout />}>
<Route index element={<HomePage />} />
<Route path="/blog" element={<BlogListPage />} />
<Route path="/blog/:postId" element={<PostDetailPage />} />
<Route path="/category/:categoryName" element={<CategoryPage />} />
<Route path="/author/:authorId" element={<AuthorProfilePage />} />
<Route path="/about" element={<AboutPage />} />
</Route>
{/* Auth routes with AuthLayout */}
<Route element={<AuthLayout />}>
<Route path="/login" element={<LoginPage />} />
<Route path="/register" element={<RegisterPage />} />
<Route path="/forgot-password" element={<ForgotPasswordPage />} />
</Route>
{/* Protected dashboard routes with DashboardLayout */}
<Route
element={
<ProtectedRoute>
<DashboardLayout />
</ProtectedRoute>
}
>
<Route path="/dashboard" element={<DashboardHomePage />} />
<Route path="/dashboard/posts" element={<MyPostsPage />} />
<Route path="/dashboard/posts/new" element={<CreatePostPage />} />
<Route path="/dashboard/posts/:postId/edit" element={<EditPostPage />} />
{/* Nested settings routes */}
<Route path="/dashboard/settings" element={<SettingsLayout />}>
<Route index element={<SettingsIndexPage />} />
<Route path="profile" element={<ProfileSettingsPage />} />
<Route path="account" element={<AccountSettingsPage />} />
<Route path="preferences" element={<PreferencesPage />} />
</Route>
</Route>
{/* 404 Not Found - catch all */}
<Route path="*" element={<NotFoundPage />} />
</Routes>
);
}
export default App;
π― Route Structure Highlights
- Layout grouping - Routes are grouped by their layout component
- Index route - The homepage uses
indexinstead ofpath="/" - Protected wrapper - Dashboard routes wrapped in
ProtectedRoute - Nested settings - Settings has its own layout with nested routes
- Catch-all -
path="*"catches any unmatched routes (404)
Step 4: Add Basic Global Styles
Update src/index.css with base styles for our application:
/* src/index.css */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
:root {
/* Colors */
--primary: #667eea;
--primary-dark: #5568d3;
--secondary: #764ba2;
--success: #48bb78;
--danger: #f56565;
--warning: #ed8936;
--info: #4299e1;
/* Neutrals */
--gray-50: #f9fafb;
--gray-100: #f3f4f6;
--gray-200: #e5e7eb;
--gray-300: #d1d5db;
--gray-400: #9ca3af;
--gray-500: #6b7280;
--gray-600: #4b5563;
--gray-700: #374151;
--gray-800: #1f2937;
--gray-900: #111827;
/* Layout */
--max-width: 1200px;
--sidebar-width: 250px;
--header-height: 64px;
/* Typography */
--font-sans: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
--font-mono: 'SF Mono', Monaco, 'Cascadia Code', monospace;
/* Spacing */
--spacing-xs: 0.25rem;
--spacing-sm: 0.5rem;
--spacing-md: 1rem;
--spacing-lg: 1.5rem;
--spacing-xl: 2rem;
/* Border radius */
--radius-sm: 0.25rem;
--radius-md: 0.5rem;
--radius-lg: 0.75rem;
/* Shadows */
--shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
--shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
--shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1);
}
body {
font-family: var(--font-sans);
line-height: 1.6;
color: var(--gray-900);
background-color: var(--gray-50);
}
/* Typography */
h1, h2, h3, h4, h5, h6 {
line-height: 1.2;
font-weight: 700;
margin-bottom: 1rem;
}
h1 { font-size: 2.5rem; }
h2 { font-size: 2rem; }
h3 { font-size: 1.5rem; }
h4 { font-size: 1.25rem; }
h5 { font-size: 1.125rem; }
h6 { font-size: 1rem; }
p {
margin-bottom: 1rem;
}
a {
color: var(--primary);
text-decoration: none;
transition: color 0.2s;
}
a:hover {
color: var(--primary-dark);
}
/* Buttons */
button {
font-family: inherit;
font-size: 1rem;
cursor: pointer;
border: none;
outline: none;
transition: all 0.2s;
}
.btn {
padding: 0.5rem 1rem;
border-radius: var(--radius-md);
font-weight: 500;
display: inline-flex;
align-items: center;
gap: 0.5rem;
}
.btn-primary {
background: var(--primary);
color: white;
}
.btn-primary:hover {
background: var(--primary-dark);
}
.btn-secondary {
background: var(--gray-200);
color: var(--gray-800);
}
.btn-secondary:hover {
background: var(--gray-300);
}
/* Forms */
input, textarea, select {
font-family: inherit;
font-size: 1rem;
padding: 0.5rem;
border: 1px solid var(--gray-300);
border-radius: var(--radius-md);
width: 100%;
transition: border-color 0.2s;
}
input:focus, textarea:focus, select:focus {
outline: none;
border-color: var(--primary);
}
label {
display: block;
margin-bottom: 0.25rem;
font-weight: 500;
color: var(--gray-700);
}
/* Cards */
.card {
background: white;
border-radius: var(--radius-lg);
padding: var(--spacing-lg);
box-shadow: var(--shadow-sm);
margin-bottom: var(--spacing-lg);
}
/* Container */
.container {
max-width: var(--max-width);
margin: 0 auto;
padding: 0 var(--spacing-lg);
}
/* Utility classes */
.text-center { text-align: center; }
.text-right { text-align: right; }
.mt-1 { margin-top: var(--spacing-sm); }
.mt-2 { margin-top: var(--spacing-md); }
.mt-3 { margin-top: var(--spacing-lg); }
.mb-1 { margin-bottom: var(--spacing-sm); }
.mb-2 { margin-bottom: var(--spacing-md); }
.mb-3 { margin-bottom: var(--spacing-lg); }
/* Responsive */
@media (max-width: 768px) {
h1 { font-size: 2rem; }
h2 { font-size: 1.75rem; }
h3 { font-size: 1.25rem; }
.container {
padding: 0 var(--spacing-md);
}
}
β CSS Variables Benefits
Using CSS custom properties gives us:
- Consistency - Colors and spacing are uniform across the app
- Easy theming - Change variables to update the entire app
- Maintainability - Update values in one place
- Readability -
var(--primary)is clearer than#667eea
Step 5: Create Placeholder Components
Before we can test our routes, we need placeholder components for each page. Let's create simple placeholders that we'll build out later.
Create a helper component for placeholders in src/components/PagePlaceholder.tsx:
// src/components/PagePlaceholder.tsx
interface PagePlaceholderProps {
title: string;
description?: string;
}
export function PagePlaceholder({ title, description }: PagePlaceholderProps) {
return (
<div className="container" style={{ padding: '2rem', textAlign: 'center' }}>
<h1>{title}</h1>
{description && <p style={{ color: 'var(--gray-600)' }}>{description}</p>}
<p style={{
marginTop: '2rem',
padding: '1rem',
background: 'var(--gray-100)',
borderRadius: 'var(--radius-md)'
}}>
This page is under construction. We'll build it in the next sections!
</p>
</div>
);
}
β οΈ Don't Create All Pages Yet!
We'll create placeholder files now just to test routing, but we'll build the actual page content in later sections. This lets us verify our routing structure works before investing time in building pages.
Testing Your Route Setup
After creating the layouts and placeholder pages (which we'll do in the next section), you can test your routes by:
- Start the dev server:
npm run dev - Visit different URLs manually to test routing
- Try public routes:
/,/blog,/about - Try auth routes:
/login,/register - Try protected routes (should redirect):
/dashboard - Try invalid routes (should show 404):
/random-page
π Route Configuration Complete!
You've set up:
- β React Router in main.tsx
- β ProtectedRoute component for auth
- β Complete route structure in App.tsx
- β Global CSS variables and base styles
- β PagePlaceholder utility component
Next, we'll build the three layout components that wrap our pages!
ποΈ Creating Layout Components
Layouts are the backbone of our application's structure. Each layout provides consistent navigation, headers, and structure for its group of pages. Let's build all three layouts: PublicLayout, AuthLayout, and DashboardLayout.
Layout 1: PublicLayout
The PublicLayout wraps all public pages and provides the main navigation. Create src/layouts/PublicLayout.tsx:
// src/layouts/PublicLayout.tsx
import { Outlet, Link, NavLink } from 'react-router-dom';
import { isAuthenticated, getCurrentUser, logout } from '../utils/auth';
import { categories } from '../data/categories';
import './PublicLayout.css';
export function PublicLayout() {
const isAuth = isAuthenticated();
const user = getCurrentUser();
const handleLogout = () => {
logout();
window.location.href = '/'; // Reload to update auth state
};
return (
<div className="public-layout">
{/* Header */}
<header className="public-header">
<div className="container">
<div className="header-content">
{/* Logo */}
<Link to="/" className="logo">
<span className="logo-icon">π</span>
<span className="logo-text">DevBlog</span>
</Link>
{/* Navigation */}
<nav className="main-nav">
<NavLink
to="/"
className={({ isActive }) => isActive ? 'nav-link active' : 'nav-link'}
>
Home
</NavLink>
<NavLink
to="/blog"
className={({ isActive }) => isActive ? 'nav-link active' : 'nav-link'}
>
Blog
</NavLink>
{/* Categories dropdown */}
<div className="dropdown">
<button className="nav-link">
Categories βΎ
</button>
<div className="dropdown-content">
{categories.map(cat => (
<Link
key={cat.id}
to={`/category/${cat.slug}`}
className="dropdown-item"
>
{cat.name}
</Link>
))}
</div>
</div>
<NavLink
to="/about"
className={({ isActive }) => isActive ? 'nav-link active' : 'nav-link'}
>
About
</NavLink>
</nav>
{/* Auth section */}
<div className="auth-section">
{isAuth && user ? (
<div className="user-menu">
<Link to="/dashboard" className="btn btn-primary">
Dashboard
</Link>
<span className="user-name">{user.name}</span>
<button onClick={handleLogout} className="btn btn-secondary">
Logout
</button>
</div>
) : (
<div className="auth-buttons">
<Link to="/login" className="btn btn-secondary">
Login
</Link>
<Link to="/register" className="btn btn-primary">
Sign Up
</Link>
</div>
)}
</div>
</div>
</div>
</header>
{/* Main content area */}
<main className="public-main">
<Outlet />
</main>
{/* Footer */}
<footer className="public-footer">
<div className="container">
<div className="footer-content">
<div className="footer-section">
<h4>DevBlog</h4>
<p>Sharing knowledge about web development, React, and TypeScript.</p>
</div>
<div className="footer-section">
<h4>Quick Links</h4>
<Link to="/blog">All Posts</Link>
<Link to="/about">About Us</Link>
</div>
<div className="footer-section">
<h4>Categories</h4>
{categories.slice(0, 4).map(cat => (
<Link key={cat.id} to={`/category/${cat.slug}`}>
{cat.name}
</Link>
))}
</div>
</div>
<div className="footer-bottom">
<p>© 2024 DevBlog. Built with React and TypeScript.</p>
</div>
</div>
</footer>
</div>
);
}
PublicLayout Styles
Create src/layouts/PublicLayout.css:
/* src/layouts/PublicLayout.css */
.public-layout {
min-height: 100vh;
display: flex;
flex-direction: column;
}
/* Header */
.public-header {
background: white;
border-bottom: 1px solid var(--gray-200);
padding: 1rem 0;
position: sticky;
top: 0;
z-index: 100;
box-shadow: var(--shadow-sm);
}
.header-content {
display: flex;
align-items: center;
justify-content: space-between;
gap: 2rem;
}
/* Logo */
.logo {
display: flex;
align-items: center;
gap: 0.5rem;
font-size: 1.5rem;
font-weight: 700;
color: var(--primary);
text-decoration: none;
}
.logo-icon {
font-size: 2rem;
}
/* Navigation */
.main-nav {
display: flex;
align-items: center;
gap: 1.5rem;
flex: 1;
}
.nav-link {
color: var(--gray-700);
font-weight: 500;
padding: 0.5rem 1rem;
border-radius: var(--radius-md);
transition: all 0.2s;
background: transparent;
border: none;
cursor: pointer;
}
.nav-link:hover {
color: var(--primary);
background: var(--gray-100);
}
.nav-link.active {
color: var(--primary);
background: var(--gray-100);
}
/* Dropdown */
.dropdown {
position: relative;
}
.dropdown-content {
display: none;
position: absolute;
top: 100%;
left: 0;
background: white;
border-radius: var(--radius-md);
box-shadow: var(--shadow-lg);
padding: 0.5rem;
min-width: 200px;
margin-top: 0.5rem;
}
.dropdown:hover .dropdown-content {
display: block;
}
.dropdown-item {
display: block;
padding: 0.5rem 1rem;
color: var(--gray-700);
border-radius: var(--radius-sm);
transition: all 0.2s;
}
.dropdown-item:hover {
background: var(--gray-100);
color: var(--primary);
}
/* Auth section */
.auth-section {
display: flex;
align-items: center;
gap: 1rem;
}
.user-menu {
display: flex;
align-items: center;
gap: 1rem;
}
.user-name {
color: var(--gray-700);
font-weight: 500;
}
.auth-buttons {
display: flex;
gap: 0.5rem;
}
/* Main content */
.public-main {
flex: 1;
padding: 2rem 0;
}
/* Footer */
.public-footer {
background: var(--gray-900);
color: white;
padding: 3rem 0 1rem;
margin-top: 4rem;
}
.footer-content {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 2rem;
margin-bottom: 2rem;
}
.footer-section h4 {
margin-bottom: 1rem;
color: white;
}
.footer-section a {
display: block;
color: var(--gray-400);
margin-bottom: 0.5rem;
}
.footer-section a:hover {
color: white;
}
.footer-section p {
color: var(--gray-400);
margin-bottom: 0;
}
.footer-bottom {
text-align: center;
padding-top: 2rem;
border-top: 1px solid var(--gray-800);
color: var(--gray-500);
}
/* Responsive */
@media (max-width: 768px) {
.header-content {
flex-direction: column;
gap: 1rem;
}
.main-nav {
flex-wrap: wrap;
justify-content: center;
}
.auth-section {
width: 100%;
justify-content: center;
}
.footer-content {
grid-template-columns: 1fr;
}
}
π‘ PublicLayout Features
- Sticky header - Stays at top when scrolling
- Dynamic auth UI - Shows different buttons based on login state
- Category dropdown - Hover to see all categories
- Active link highlighting - Current page is highlighted
- Responsive design - Works on mobile and desktop
- Footer with links - Consistent across all public pages
Layout 2: AuthLayout
The AuthLayout is minimal and focused, perfect for login and registration forms. Create src/layouts/AuthLayout.tsx:
// src/layouts/AuthLayout.tsx
import { Outlet, Link } from 'react-router-dom';
import './AuthLayout.css';
export function AuthLayout() {
return (
<div className="auth-layout">
{/* Simple header with logo */}
<header className="auth-header">
<Link to="/" className="auth-logo">
<span className="logo-icon">π</span>
<span className="logo-text">DevBlog</span>
</Link>
</header>
{/* Centered content area */}
<main className="auth-main">
<div className="auth-container">
<Outlet />
</div>
</main>
{/* Simple footer */}
<footer className="auth-footer">
<p>© 2024 DevBlog. All rights reserved.</p>
</footer>
</div>
);
}
AuthLayout Styles
Create src/layouts/AuthLayout.css:
/* src/layouts/AuthLayout.css */
.auth-layout {
min-height: 100vh;
display: flex;
flex-direction: column;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}
/* Header */
.auth-header {
padding: 2rem;
text-align: center;
}
.auth-logo {
display: inline-flex;
align-items: center;
gap: 0.5rem;
font-size: 2rem;
font-weight: 700;
color: white;
text-decoration: none;
}
/* Main content */
.auth-main {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
padding: 2rem;
}
.auth-container {
width: 100%;
max-width: 450px;
background: white;
border-radius: var(--radius-lg);
padding: 2rem;
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1);
}
/* Footer */
.auth-footer {
text-align: center;
padding: 2rem;
color: white;
opacity: 0.9;
}
Layout 3: DashboardLayout
The DashboardLayout provides a sidebar navigation for authenticated users. Create src/layouts/DashboardLayout.tsx:
// src/layouts/DashboardLayout.tsx
import { Outlet, Link, NavLink, useNavigate } from 'react-router-dom';
import { getCurrentUser, logout } from '../utils/auth';
import { useState } from 'react';
import './DashboardLayout.css';
export function DashboardLayout() {
const user = getCurrentUser();
const navigate = useNavigate();
const [sidebarOpen, setSidebarOpen] = useState(true);
const handleLogout = () => {
logout();
navigate('/');
};
return (
<div className={`dashboard-layout ${sidebarOpen ? 'sidebar-open' : 'sidebar-closed'}`}>
{/* Sidebar */}
<aside className="dashboard-sidebar">
<div className="sidebar-header">
<Link to="/" className="sidebar-logo">
<span className="logo-icon">π</span>
<span className="logo-text">DevBlog</span>
</Link>
<button
className="sidebar-toggle"
onClick={() => setSidebarOpen(!sidebarOpen)}
aria-label="Toggle sidebar"
>
{sidebarOpen ? 'β' : 'βΆ'}
</button>
</div>
<nav className="sidebar-nav">
<NavLink
to="/dashboard"
end
className={({ isActive }) => `sidebar-link ${isActive ? 'active' : ''}`}
>
<span className="link-icon">π</span>
<span className="link-text">Dashboard</span>
</NavLink>
<NavLink
to="/dashboard/posts"
className={({ isActive }) => `sidebar-link ${isActive ? 'active' : ''}`}
>
<span className="link-icon">π</span>
<span className="link-text">My Posts</span>
</NavLink>
<NavLink
to="/dashboard/posts/new"
className={({ isActive }) => `sidebar-link ${isActive ? 'active' : ''}`}
>
<span className="link-icon">β</span>
<span className="link-text">New Post</span>
</NavLink>
<NavLink
to="/dashboard/settings"
className={({ isActive }) => `sidebar-link ${isActive ? 'active' : ''}`}
>
<span className="link-icon">βοΈ</span>
<span className="link-text">Settings</span>
</NavLink>
<div className="sidebar-divider"></div>
<Link to="/blog" className="sidebar-link">
<span className="link-icon">π</span>
<span className="link-text">View Blog</span>
</Link>
</nav>
</aside>
{/* Main content area */}
<div className="dashboard-main">
{/* Top bar */}
<header className="dashboard-header">
<div className="header-left">
<h2>Welcome back, {user?.name}!</h2>
</div>
<div className="header-right">
<div className="user-info">
<img
src={user?.avatar}
alt={user?.name}
className="user-avatar"
/>
<span className="user-name">{user?.name}</span>
</div>
<button onClick={handleLogout} className="btn btn-secondary">
Logout
</button>
</div>
</header>
{/* Page content */}
<main className="dashboard-content">
<Outlet />
</main>
</div>
</div>
);
}
DashboardLayout Styles
Create src/layouts/DashboardLayout.css:
/* src/layouts/DashboardLayout.css */
.dashboard-layout {
display: flex;
min-height: 100vh;
background: var(--gray-50);
}
/* Sidebar */
.dashboard-sidebar {
width: var(--sidebar-width);
background: var(--gray-900);
color: white;
display: flex;
flex-direction: column;
position: fixed;
left: 0;
top: 0;
bottom: 0;
transition: width 0.3s;
z-index: 100;
}
.sidebar-closed .dashboard-sidebar {
width: 60px;
}
.sidebar-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 1rem;
border-bottom: 1px solid var(--gray-800);
}
.sidebar-logo {
display: flex;
align-items: center;
gap: 0.5rem;
color: white;
font-weight: 700;
font-size: 1.25rem;
text-decoration: none;
}
.sidebar-closed .logo-text,
.sidebar-closed .link-text {
display: none;
}
.sidebar-toggle {
background: transparent;
color: white;
border: none;
cursor: pointer;
padding: 0.5rem;
font-size: 1rem;
transition: transform 0.3s;
}
.sidebar-toggle:hover {
transform: scale(1.1);
}
/* Sidebar navigation */
.sidebar-nav {
flex: 1;
padding: 1rem 0;
overflow-y: auto;
}
.sidebar-link {
display: flex;
align-items: center;
gap: 0.75rem;
padding: 0.75rem 1rem;
color: var(--gray-400);
text-decoration: none;
transition: all 0.2s;
border-left: 3px solid transparent;
}
.sidebar-link:hover {
background: var(--gray-800);
color: white;
}
.sidebar-link.active {
background: var(--gray-800);
color: white;
border-left-color: var(--primary);
}
.link-icon {
font-size: 1.25rem;
flex-shrink: 0;
}
.sidebar-divider {
height: 1px;
background: var(--gray-800);
margin: 1rem 0;
}
/* Main area */
.dashboard-main {
flex: 1;
margin-left: var(--sidebar-width);
transition: margin-left 0.3s;
}
.sidebar-closed .dashboard-main {
margin-left: 60px;
}
/* Header */
.dashboard-header {
background: white;
border-bottom: 1px solid var(--gray-200);
padding: 1rem 2rem;
display: flex;
align-items: center;
justify-content: space-between;
position: sticky;
top: 0;
z-index: 50;
}
.header-left h2 {
margin: 0;
font-size: 1.5rem;
color: var(--gray-900);
}
.header-right {
display: flex;
align-items: center;
gap: 1rem;
}
.user-info {
display: flex;
align-items: center;
gap: 0.5rem;
}
.user-avatar {
width: 40px;
height: 40px;
border-radius: 50%;
object-fit: cover;
}
.user-name {
font-weight: 500;
color: var(--gray-700);
}
/* Content */
.dashboard-content {
padding: 2rem;
}
/* Responsive */
@media (max-width: 768px) {
.dashboard-sidebar {
width: 60px;
}
.sidebar-closed .dashboard-sidebar {
width: 0;
}
.dashboard-main {
margin-left: 60px;
}
.sidebar-closed .dashboard-main {
margin-left: 0;
}
.sidebar-closed .logo-text,
.sidebar-closed .link-text {
display: none;
}
.logo-text,
.link-text {
display: none;
}
.dashboard-header {
padding: 1rem;
}
.header-left h2 {
font-size: 1.25rem;
}
.user-name {
display: none;
}
.dashboard-content {
padding: 1rem;
}
}
β DashboardLayout Features
- Collapsible sidebar - Toggle button to show/hide labels
- Fixed sidebar - Stays in place while scrolling content
- Active link highlighting - Current page clearly marked
- Sticky top bar - Header stays visible when scrolling
- User info display - Shows avatar and name
- Mobile responsive - Collapses on small screens
Testing All Three Layouts
Now that all three layouts are complete, let's create simple placeholder pages to test them:
π Quick Test Checklist
Create these placeholder files using the PagePlaceholder component:
- Public pages: HomePage, BlogListPage, PostDetailPage, CategoryPage, AuthorProfilePage, AboutPage
- Auth pages: LoginPage, RegisterPage, ForgotPasswordPage
- Dashboard pages: DashboardHomePage, MyPostsPage, CreatePostPage, EditPostPage
- Settings pages: SettingsLayout, SettingsIndexPage, ProfileSettingsPage, AccountSettingsPage, PreferencesPage
- Error page: NotFoundPage
Example Placeholder Page
Here's a quick example of how to create a placeholder page (create similar files for all pages):
// src/pages/public/HomePage.tsx
import { PagePlaceholder } from '../../components/PagePlaceholder';
export function HomePage() {
return (
<PagePlaceholder
title="Home Page"
description="Featured posts and latest content will appear here"
/>
);
}
// src/pages/auth/LoginPage.tsx
import { PagePlaceholder } from '../../components/PagePlaceholder';
export function LoginPage() {
return (
<PagePlaceholder
title="Login Page"
description="Login form will appear here"
/>
);
}
// src/pages/dashboard/DashboardHomePage.tsx
import { PagePlaceholder } from '../../components/PagePlaceholder';
export function DashboardHomePage() {
return (
<PagePlaceholder
title="Dashboard Home"
description="User statistics and overview will appear here"
/>
);
}
// src/pages/NotFoundPage.tsx
export function NotFoundPage() {
return (
<div style={{ textAlign: 'center', padding: '4rem 2rem' }}>
<h1 style={{ fontSize: '4rem', marginBottom: '1rem' }}>404</h1>
<h2>Page Not Found</h2>
<p>The page you're looking for doesn't exist.</p>
<a href="/" style={{
display: 'inline-block',
marginTop: '2rem',
padding: '0.75rem 1.5rem',
background: 'var(--primary)',
color: 'white',
borderRadius: 'var(--radius-md)',
textDecoration: 'none'
}}>
Go Home
</a>
</div>
);
}
β οΈ Settings Layout Note
The SettingsLayout is special because it's a nested layout within the DashboardLayout. Here's a simple version:
// src/pages/dashboard/settings/SettingsLayout.tsx
import { Outlet, NavLink } from 'react-router-dom';
export function SettingsLayout() {
return (
<div>
<h1>Settings</h1>
<nav style={{
display: 'flex',
gap: '1rem',
marginBottom: '2rem',
borderBottom: '1px solid var(--gray-200)',
paddingBottom: '1rem'
}}>
<NavLink
to="/dashboard/settings/profile"
style={({ isActive }) => ({
padding: '0.5rem 1rem',
borderRadius: 'var(--radius-md)',
background: isActive ? 'var(--primary)' : 'transparent',
color: isActive ? 'white' : 'var(--gray-700)'
})}
>
Profile
</NavLink>
<NavLink
to="/dashboard/settings/account"
style={({ isActive }) => ({
padding: '0.5rem 1rem',
borderRadius: 'var(--radius-md)',
background: isActive ? 'var(--primary)' : 'transparent',
color: isActive ? 'white' : 'var(--gray-700)'
})}
>
Account
</NavLink>
<NavLink
to="/dashboard/settings/preferences"
style={({ isActive }) => ({
padding: '0.5rem 1rem',
borderRadius: 'var(--radius-md)',
background: isActive ? 'var(--primary)' : 'transparent',
color: isActive ? 'white' : 'var(--gray-700)'
})}
>
Preferences
</NavLink>
</nav>
<Outlet />
</div>
);
}
Verification Steps
After creating all placeholder pages, test your layouts:
- Start dev server:
npm run dev - Test PublicLayout:
- Visit
/- Should see HomePage with header/footer - Click "Blog" in nav - Should navigate to BlogListPage
- Hover "Categories" - Dropdown should appear
- Check responsive - Resize browser window
- Visit
- Test AuthLayout:
- Visit
/login- Should see centered card with gradient background - Visit
/register- Same layout - Notice minimal header (just logo)
- Visit
- Test DashboardLayout:
- Visit
/dashboard- Should redirect to login (not authenticated) - After "logging in" (we'll build this next), dashboard should show sidebar
- Click sidebar toggle - Sidebar should collapse/expand
- Click different sidebar links - Active state should change
- Visit
- Test 404:
- Visit
/random-page- Should show NotFoundPage
- Visit
π All Three Layouts Complete!
You now have:
- β PublicLayout with full navigation, categories, and footer
- β AuthLayout with minimal, centered design
- β DashboardLayout with collapsible sidebar
- β ProtectedRoute guarding dashboard pages
- β Complete routing structure in App.tsx
- β PagePlaceholder for quick testing
The foundation is solid! Next, we'll build out the actual page content, starting with the public pages.
π Building Public Pages
Now that our layouts are working, let's build the public pages with real content. We'll start with the HomePage, then create the blog listing, post detail pages, and more. These pages will use our mock data to display realistic content.
Page 1: HomePage
The HomePage is the landing page for our blog. It showcases featured posts, recent posts, and categories. Update src/pages/public/HomePage.tsx:
// src/pages/public/HomePage.tsx
import { Link } from 'react-router-dom';
import { getFeaturedPosts, getRecentPosts } from '../../data/posts';
import { categories } from '../../data/categories';
import './HomePage.css';
export function HomePage() {
const featuredPosts = getFeaturedPosts();
const recentPosts = getRecentPosts(6);
return (
<div className="home-page">
{/* Hero section */}
<section className="hero">
<div className="container">
<h1 className="hero-title">Welcome to DevBlog</h1>
<p className="hero-subtitle">
Exploring web development, React, TypeScript, and modern JavaScript
</p>
<div className="hero-actions">
<Link to="/blog" className="btn btn-primary btn-large">
Browse Articles
</Link>
<Link to="/about" className="btn btn-secondary btn-large">
Learn More
</Link>
</div>
</div>
</section>
{/* Featured posts */}
<section className="featured-section">
<div className="container">
<h2 className="section-title">Featured Posts</h2>
<div className="featured-grid">
{featuredPosts.map(post => (
<article key={post.id} className="featured-card">
<img
src={post.coverImage}
alt={post.title}
className="featured-image"
/>
<div className="featured-content">
<span
className="post-category"
style={{ background: post.category.color }}
>
{post.category.name}
</span>
<h3 className="post-title">
<Link to={`/blog/${post.id}`}>{post.title}</Link>
</h3>
<p className="post-excerpt">{post.excerpt}</p>
<div className="post-meta">
<Link to={`/author/${post.author.id}`} className="author-link">
<img
src={post.author.avatar}
alt={post.author.name}
className="author-avatar"
/>
{post.author.name}
</Link>
<span className="post-date">
{new Date(post.publishedDate).toLocaleDateString()}
</span>
</div>
</div>
</article>
))}
</div>
</div>
</section>
{/* Categories section */}
<section className="categories-section">
<div className="container">
<h2 className="section-title">Browse by Category</h2>
<div className="categories-grid">
{categories.map(category => (
<Link
key={category.id}
to={`/category/${category.slug}`}
className="category-card"
style={{ borderTopColor: category.color }}
>
<h3>{category.name}</h3>
<p>{category.description}</p>
<span className="category-count">
{category.postCount} articles
</span>
</Link>
))}
</div>
</div>
</section>
{/* Recent posts */}
<section className="recent-section">
<div className="container">
<div className="section-header">
<h2 className="section-title">Recent Posts</h2>
<Link to="/blog" className="view-all">View all β</Link>
</div>
<div className="recent-grid">
{recentPosts.map(post => (
<article key={post.id} className="post-card">
<img
src={post.coverImage}
alt={post.title}
className="post-image"
/>
<div className="post-body">
<span
className="post-category small"
style={{ background: post.category.color }}
>
{post.category.name}
</span>
<h3 className="post-title small">
<Link to={`/blog/${post.id}`}>{post.title}</Link>
</h3>
<p className="post-excerpt small">{post.excerpt}</p>
<div className="post-footer">
<span>{post.readTime} min read</span>
<span>{post.views} views</span>
</div>
</div>
</article>
))}
</div>
</div>
</section>
</div>
);
}
HomePage Styles
Create src/pages/public/HomePage.css:
/* src/pages/public/HomePage.css */
/* Hero section */
.hero {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 6rem 0;
text-align: center;
}
.hero-title {
font-size: 3rem;
margin-bottom: 1rem;
color: white;
}
.hero-subtitle {
font-size: 1.25rem;
margin-bottom: 2rem;
opacity: 0.95;
}
.hero-actions {
display: flex;
gap: 1rem;
justify-content: center;
}
.btn-large {
padding: 0.75rem 2rem;
font-size: 1.125rem;
}
/* Section styling */
.featured-section,
.categories-section,
.recent-section {
padding: 4rem 0;
}
.section-title {
font-size: 2rem;
margin-bottom: 2rem;
color: var(--gray-900);
}
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 2rem;
}
.view-all {
color: var(--primary);
font-weight: 500;
}
/* Featured grid */
.featured-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(350px, 1fr));
gap: 2rem;
}
.featured-card {
background: white;
border-radius: var(--radius-lg);
overflow: hidden;
box-shadow: var(--shadow-md);
transition: transform 0.2s, box-shadow 0.2s;
}
.featured-card:hover {
transform: translateY(-4px);
box-shadow: var(--shadow-lg);
}
.featured-image {
width: 100%;
height: 200px;
object-fit: cover;
}
.featured-content {
padding: 1.5rem;
}
.post-category {
display: inline-block;
padding: 0.25rem 0.75rem;
border-radius: var(--radius-sm);
color: white;
font-size: 0.875rem;
font-weight: 500;
margin-bottom: 1rem;
}
.post-category.small {
padding: 0.2rem 0.5rem;
font-size: 0.75rem;
}
.post-title a {
color: var(--gray-900);
text-decoration: none;
}
.post-title a:hover {
color: var(--primary);
}
.post-excerpt {
color: var(--gray-600);
line-height: 1.6;
margin: 1rem 0;
}
.post-meta {
display: flex;
align-items: center;
justify-content: space-between;
padding-top: 1rem;
border-top: 1px solid var(--gray-200);
}
.author-link {
display: flex;
align-items: center;
gap: 0.5rem;
color: var(--gray-700);
text-decoration: none;
}
.author-avatar {
width: 32px;
height: 32px;
border-radius: 50%;
}
.post-date {
color: var(--gray-500);
font-size: 0.875rem;
}
/* Categories grid */
.categories-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 1.5rem;
}
.category-card {
background: white;
padding: 1.5rem;
border-radius: var(--radius-lg);
border-top: 4px solid var(--primary);
box-shadow: var(--shadow-sm);
text-decoration: none;
transition: transform 0.2s, box-shadow 0.2s;
}
.category-card:hover {
transform: translateY(-2px);
box-shadow: var(--shadow-md);
}
.category-card h3 {
color: var(--gray-900);
margin-bottom: 0.5rem;
}
.category-card p {
color: var(--gray-600);
font-size: 0.875rem;
margin-bottom: 1rem;
}
.category-count {
display: inline-block;
padding: 0.25rem 0.75rem;
background: var(--gray-100);
color: var(--gray-700);
border-radius: var(--radius-sm);
font-size: 0.875rem;
}
/* Recent posts grid */
.recent-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 1.5rem;
}
.post-card {
background: white;
border-radius: var(--radius-lg);
overflow: hidden;
box-shadow: var(--shadow-sm);
transition: transform 0.2s, box-shadow 0.2s;
}
.post-card:hover {
transform: translateY(-2px);
box-shadow: var(--shadow-md);
}
.post-image {
width: 100%;
height: 180px;
object-fit: cover;
}
.post-body {
padding: 1.25rem;
}
.post-title.small {
font-size: 1.125rem;
margin: 0.5rem 0;
}
.post-excerpt.small {
font-size: 0.875rem;
margin: 0.75rem 0;
}
.post-footer {
display: flex;
justify-content: space-between;
padding-top: 1rem;
border-top: 1px solid var(--gray-200);
font-size: 0.875rem;
color: var(--gray-500);
}
/* Responsive */
@media (max-width: 768px) {
.hero {
padding: 4rem 0;
}
.hero-title {
font-size: 2rem;
}
.hero-subtitle {
font-size: 1rem;
}
.hero-actions {
flex-direction: column;
align-items: stretch;
}
.featured-grid,
.categories-grid,
.recent-grid {
grid-template-columns: 1fr;
}
}
β HomePage Features
- Hero section - Eye-catching gradient with CTAs
- Featured posts - Showcases important articles
- Category grid - Browse by topic
- Recent posts - Latest content at a glance
- Responsive design - Works on all screen sizes
- Hover effects - Interactive and engaging
Page 2: BlogListPage
The BlogListPage displays all blog posts with search and filtering capabilities. Create src/pages/public/BlogListPage.tsx:
// src/pages/public/BlogListPage.tsx
import { useState, useMemo } from 'react';
import { Link, useSearchParams } from 'react-router-dom';
import { posts, searchPosts } from '../../data/posts';
import { categories } from '../../data/categories';
import './BlogListPage.css';
export function BlogListPage() {
const [searchParams, setSearchParams] = useSearchParams();
const [searchQuery, setSearchQuery] = useState(searchParams.get('q') || '');
// Get filter values from URL
const categoryFilter = searchParams.get('category');
const sortBy = searchParams.get('sort') || 'date';
// Filter and sort posts
const filteredPosts = useMemo(() => {
let result = [...posts];
// Apply search
if (searchQuery) {
result = searchPosts(searchQuery);
}
// Apply category filter
if (categoryFilter) {
result = result.filter(post => post.category.slug === categoryFilter);
}
// Sort posts
switch (sortBy) {
case 'views':
result.sort((a, b) => b.views - a.views);
break;
case 'likes':
result.sort((a, b) => b.likes - a.likes);
break;
case 'date':
default:
result.sort((a, b) =>
new Date(b.publishedDate).getTime() - new Date(a.publishedDate).getTime()
);
}
return result;
}, [searchQuery, categoryFilter, sortBy]);
const handleSearch = (e: React.FormEvent) => {
e.preventDefault();
if (searchQuery) {
setSearchParams({ q: searchQuery, ...(categoryFilter && { category: categoryFilter }) });
} else {
setSearchParams(categoryFilter ? { category: categoryFilter } : {});
}
};
const handleCategoryFilter = (slug: string) => {
if (slug === categoryFilter) {
// Remove filter if clicking same category
setSearchParams(searchQuery ? { q: searchQuery } : {});
} else {
setSearchParams({
...(searchQuery && { q: searchQuery }),
category: slug
});
}
};
const handleSortChange = (newSort: string) => {
setSearchParams({
...(searchQuery && { q: searchQuery }),
...(categoryFilter && { category: categoryFilter }),
sort: newSort
});
};
return (
<div className="blog-list-page">
<div className="container">
{/* Header */}
<header className="page-header">
<h1>All Blog Posts</h1>
<p>Explore articles about web development, React, TypeScript, and more</p>
</header>
{/* Search and filters */}
<div className="filters-section">
{/* Search bar */}
<form onSubmit={handleSearch} className="search-form">
<input
type="text"
placeholder="Search posts..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="search-input"
/>
<button type="submit" className="btn btn-primary">
Search
</button>
</form>
{/* Category filters */}
<div className="category-filters">
<button
onClick={() => handleCategoryFilter('')}
className={`filter-btn ${!categoryFilter ? 'active' : ''}`}
>
All
</button>
{categories.map(cat => (
<button
key={cat.id}
onClick={() => handleCategoryFilter(cat.slug)}
className={`filter-btn ${categoryFilter === cat.slug ? 'active' : ''}`}
style={{
borderColor: categoryFilter === cat.slug ? cat.color : undefined,
color: categoryFilter === cat.slug ? cat.color : undefined
}}
>
{cat.name}
</button>
))}
</div>
{/* Sort options */}
<div className="sort-section">
<label>Sort by:</label>
<select
value={sortBy}
onChange={(e) => handleSortChange(e.target.value)}
className="sort-select"
>
<option value="date">Latest</option>
<option value="views">Most Viewed</option>
<option value="likes">Most Liked</option>
</select>
</div>
</div>
{/* Results info */}
<div className="results-info">
<p>
Showing {filteredPosts.length} {filteredPosts.length === 1 ? 'post' : 'posts'}
{searchQuery && <> for "{searchQuery}"</>}
{categoryFilter && <> in {categories.find(c => c.slug === categoryFilter)?.name}</>}
</p>
{(searchQuery || categoryFilter) && (
<button
onClick={() => {
setSearchQuery('');
setSearchParams({});
}}
className="clear-filters"
>
Clear filters
</button>
)}
</div>
{/* Posts grid */}
{filteredPosts.length > 0 ? (
<div className="posts-grid">
{filteredPosts.map(post => (
<article key={post.id} className="blog-post-card">
<Link to={`/blog/${post.id}`}>
<img
src={post.coverImage}
alt={post.title}
className="post-image"
/>
</Link>
<div className="post-content">
<Link
to={`/category/${post.category.slug}`}
className="post-category"
style={{ background: post.category.color }}
>
{post.category.name}
</Link>
<h2 className="post-title">
<Link to={`/blog/${post.id}`}>{post.title}</Link>
</h2>
<p className="post-excerpt">{post.excerpt}</p>
<div className="post-meta">
<Link to={`/author/${post.author.id}`} className="author-info">
<img
src={post.author.avatar}
alt={post.author.name}
className="author-avatar"
/>
<span>{post.author.name}</span>
</Link>
<div className="post-stats">
<span>π
{new Date(post.publishedDate).toLocaleDateString()}</span>
<span>β±οΈ {post.readTime} min</span>
<span>ποΈ {post.views}</span>
</div>
</div>
</div>
</article>
))}
</div>
) : (
<div className="no-results">
<h3>No posts found</h3>
<p>Try adjusting your search or filters</p>
</div>
)}
</div>
</div>
);
}
BlogListPage Styles
Create src/pages/public/BlogListPage.css:
/* src/pages/public/BlogListPage.css */
.blog-list-page {
padding: 2rem 0;
}
/* Page header */
.page-header {
text-align: center;
margin-bottom: 3rem;
}
.page-header h1 {
font-size: 2.5rem;
margin-bottom: 0.5rem;
}
.page-header p {
color: var(--gray-600);
font-size: 1.125rem;
}
/* Filters section */
.filters-section {
background: white;
padding: 1.5rem;
border-radius: var(--radius-lg);
box-shadow: var(--shadow-sm);
margin-bottom: 2rem;
}
/* Search form */
.search-form {
display: flex;
gap: 0.5rem;
margin-bottom: 1.5rem;
}
.search-input {
flex: 1;
padding: 0.75rem 1rem;
border: 1px solid var(--gray-300);
border-radius: var(--radius-md);
font-size: 1rem;
}
.search-input:focus {
outline: none;
border-color: var(--primary);
}
/* Category filters */
.category-filters {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
margin-bottom: 1.5rem;
}
.filter-btn {
padding: 0.5rem 1rem;
background: var(--gray-100);
border: 2px solid transparent;
border-radius: var(--radius-md);
color: var(--gray-700);
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
}
.filter-btn:hover {
background: var(--gray-200);
}
.filter-btn.active {
background: white;
border-color: var(--primary);
color: var(--primary);
}
/* Sort section */
.sort-section {
display: flex;
align-items: center;
gap: 0.5rem;
}
.sort-section label {
margin: 0;
color: var(--gray-700);
font-weight: 500;
}
.sort-select {
padding: 0.5rem 1rem;
border: 1px solid var(--gray-300);
border-radius: var(--radius-md);
background: white;
cursor: pointer;
}
/* Results info */
.results-info {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1.5rem;
padding-bottom: 1rem;
border-bottom: 2px solid var(--gray-200);
}
.results-info p {
margin: 0;
color: var(--gray-600);
}
.clear-filters {
padding: 0.5rem 1rem;
background: transparent;
border: 1px solid var(--gray-300);
border-radius: var(--radius-md);
color: var(--gray-700);
cursor: pointer;
transition: all 0.2s;
}
.clear-filters:hover {
background: var(--gray-100);
}
/* Posts grid */
.posts-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(350px, 1fr));
gap: 2rem;
}
.blog-post-card {
background: white;
border-radius: var(--radius-lg);
overflow: hidden;
box-shadow: var(--shadow-sm);
transition: transform 0.2s, box-shadow 0.2s;
}
.blog-post-card:hover {
transform: translateY(-4px);
box-shadow: var(--shadow-lg);
}
.blog-post-card .post-image {
width: 100%;
height: 220px;
object-fit: cover;
}
.blog-post-card .post-content {
padding: 1.5rem;
}
.blog-post-card .post-category {
display: inline-block;
padding: 0.25rem 0.75rem;
border-radius: var(--radius-sm);
color: white;
font-size: 0.875rem;
font-weight: 500;
margin-bottom: 1rem;
text-decoration: none;
}
.blog-post-card .post-title {
font-size: 1.5rem;
margin: 0.5rem 0 1rem;
}
.blog-post-card .post-title a {
color: var(--gray-900);
text-decoration: none;
}
.blog-post-card .post-title a:hover {
color: var(--primary);
}
.blog-post-card .post-excerpt {
color: var(--gray-600);
line-height: 1.6;
margin-bottom: 1rem;
}
.blog-post-card .post-meta {
display: flex;
justify-content: space-between;
align-items: center;
padding-top: 1rem;
border-top: 1px solid var(--gray-200);
}
.author-info {
display: flex;
align-items: center;
gap: 0.5rem;
color: var(--gray-700);
text-decoration: none;
font-weight: 500;
}
.author-info:hover {
color: var(--primary);
}
.author-avatar {
width: 32px;
height: 32px;
border-radius: 50%;
}
.post-stats {
display: flex;
gap: 1rem;
font-size: 0.875rem;
color: var(--gray-500);
}
/* No results */
.no-results {
text-align: center;
padding: 4rem 2rem;
background: white;
border-radius: var(--radius-lg);
}
.no-results h3 {
color: var(--gray-700);
margin-bottom: 0.5rem;
}
.no-results p {
color: var(--gray-500);
}
/* Responsive */
@media (max-width: 768px) {
.page-header h1 {
font-size: 2rem;
}
.search-form {
flex-direction: column;
}
.results-info {
flex-direction: column;
align-items: flex-start;
gap: 1rem;
}
.posts-grid {
grid-template-columns: 1fr;
}
}
π‘ BlogListPage Features
- Search functionality - Search posts by title or content
- Category filtering - Filter by specific categories
- Multiple sort options - Sort by date, views, or likes
- URL state management - Filters stored in query parameters
- Clear filters button - Easy reset to default view
- Results count - Shows how many posts match
- Responsive grid - Adapts to screen size
Page 3: PostDetailPage
The PostDetailPage shows the full blog post content. Create src/pages/public/PostDetailPage.tsx:
// src/pages/public/PostDetailPage.tsx
import { useParams, Link, useNavigate } from 'react-router-dom';
import { getPostById, getPostsByCategory } from '../../data/posts';
import './PostDetailPage.css';
export function PostDetailPage() {
const { postId } = useParams<{ postId: string }>();
const navigate = useNavigate();
const post = postId ? getPostById(postId) : undefined;
if (!post) {
return (
<div className="container" style={{ padding: '4rem 2rem', textAlign: 'center' }}>
<h1>Post Not Found</h1>
<p>The post you're looking for doesn't exist.</p>
<button onClick={() => navigate('/blog')} className="btn btn-primary">
Back to Blog
</button>
</div>
);
}
// Get related posts from the same category
const relatedPosts = getPostsByCategory(post.category.slug)
.filter(p => p.id !== post.id)
.slice(0, 3);
return (
<div className="post-detail-page">
{/* Post header */}
<header className="post-header">
<div className="container">
<Link
to={`/category/${post.category.slug}`}
className="post-category"
style={{ background: post.category.color }}
>
{post.category.name}
</Link>
<h1 className="post-title">{post.title}</h1>
<p className="post-excerpt">{post.excerpt}</p>
<div className="post-meta">
<Link to={`/author/${post.author.id}`} className="author-section">
<img
src={post.author.avatar}
alt={post.author.name}
className="author-avatar-large"
/>
<div>
<div className="author-name">{post.author.name}</div>
<div className="post-date">
{new Date(post.publishedDate).toLocaleDateString('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric'
})}
</div>
</div>
</Link>
<div className="post-stats">
<span>β±οΈ {post.readTime} min read</span>
<span>ποΈ {post.views} views</span>
<span>β€οΈ {post.likes} likes</span>
</div>
</div>
</div>
</header>
{/* Featured image */}
<div className="featured-image-container">
<img
src={post.coverImage}
alt={post.title}
className="featured-image"
/>
</div>
{/* Post content */}
<article className="post-content">
<div className="container">
<div className="content-wrapper">
<div className="post-body">
{/* Render markdown-style content */}
<div dangerouslySetInnerHTML={{
__html: post.content
.replace(/\n\n/g, '</p><p>')
.replace(/^# (.+)$/gm, '<h2>$1</h2>')
.replace(/^## (.+)$/gm, '<h3>$1</h3>')
.replace(/```(\w+)?\n([\s\S]*?)```/g, '<pre><code>$2</code></pre>')
.replace(/`([^`]+)`/g, '<code>$1</code>')
}} />
</div>
{/* Sidebar */}
<aside className="post-sidebar">
{/* Tags */}
<div className="sidebar-section">
<h3>Tags</h3>
<div className="tags">
{post.tags.map(tag => (
<span key={tag} className="tag">
#{tag}
</span>
))}
</div>
</div>
{/* Share */}
<div className="sidebar-section">
<h3>Share</h3>
<div className="share-buttons">
<button className="share-btn">π¦ Twitter</button>
<button className="share-btn">π Facebook</button>
<button className="share-btn">π LinkedIn</button>
</div>
</div>
</aside>
</div>
</div>
</article>
{/* Author bio */}
<section className="author-bio-section">
<div className="container">
<div className="author-bio-card">
<img
src={post.author.avatar}
alt={post.author.name}
className="author-avatar-large"
/>
<div className="author-bio-content">
<h3>About {post.author.name}</h3>
<p>{post.author.bio}</p>
<Link to={`/author/${post.author.id}`} className="btn btn-secondary">
View Profile
</Link>
</div>
</div>
</div>
</section>
{/* Related posts */}
{relatedPosts.length > 0 && (
<section className="related-posts-section">
<div className="container">
<h2>Related Posts</h2>
<div className="related-posts-grid">
{relatedPosts.map(relatedPost => (
<article key={relatedPost.id} className="related-post-card">
<Link to={`/blog/${relatedPost.id}`}>
<img
src={relatedPost.coverImage}
alt={relatedPost.title}
className="related-post-image"
/>
</Link>
<div className="related-post-content">
<h3>
<Link to={`/blog/${relatedPost.id}`}>
{relatedPost.title}
</Link>
</h3>
<p>{relatedPost.excerpt}</p>
</div>
</article>
))}
</div>
</div>
</section>
)}
</div>
);
}
PostDetailPage Styles
Create src/pages/public/PostDetailPage.css:
/* src/pages/public/PostDetailPage.css */
/* Post header */
.post-header {
background: white;
padding: 3rem 0 2rem;
border-bottom: 1px solid var(--gray-200);
}
.post-header .post-category {
display: inline-block;
padding: 0.25rem 0.75rem;
border-radius: var(--radius-sm);
color: white;
font-size: 0.875rem;
font-weight: 500;
margin-bottom: 1rem;
text-decoration: none;
}
.post-header .post-title {
font-size: 3rem;
line-height: 1.2;
margin-bottom: 1rem;
}
.post-header .post-excerpt {
font-size: 1.25rem;
color: var(--gray-600);
margin-bottom: 2rem;
}
.post-header .post-meta {
display: flex;
justify-content: space-between;
align-items: center;
}
.author-section {
display: flex;
align-items: center;
gap: 1rem;
text-decoration: none;
}
.author-section:hover .author-name {
color: var(--primary);
}
.author-avatar-large {
width: 60px;
height: 60px;
border-radius: 50%;
}
.author-name {
font-weight: 600;
color: var(--gray-900);
font-size: 1.125rem;
}
.post-date {
color: var(--gray-500);
font-size: 0.875rem;
}
.post-stats {
display: flex;
gap: 1.5rem;
color: var(--gray-600);
}
/* Featured image */
.featured-image-container {
width: 100%;
max-width: 1200px;
margin: 0 auto;
padding: 2rem;
}
.featured-image {
width: 100%;
height: 500px;
object-fit: cover;
border-radius: var(--radius-lg);
}
/* Post content */
.post-content {
padding: 2rem 0;
}
.content-wrapper {
display: grid;
grid-template-columns: 1fr 300px;
gap: 3rem;
}
.post-body {
background: white;
padding: 2rem;
border-radius: var(--radius-lg);
line-height: 1.8;
}
.post-body h2 {
margin-top: 2rem;
margin-bottom: 1rem;
color: var(--gray-900);
}
.post-body h3 {
margin-top: 1.5rem;
margin-bottom: 0.75rem;
color: var(--gray-800);
}
.post-body p {
margin-bottom: 1.25rem;
color: var(--gray-700);
}
.post-body code {
background: var(--gray-100);
padding: 0.2rem 0.4rem;
border-radius: var(--radius-sm);
font-family: var(--font-mono);
font-size: 0.9em;
}
.post-body pre {
background: var(--gray-900);
color: var(--gray-100);
padding: 1.5rem;
border-radius: var(--radius-md);
overflow-x: auto;
margin: 1.5rem 0;
}
.post-body pre code {
background: none;
padding: 0;
color: inherit;
}
/* Sidebar */
.post-sidebar {
position: sticky;
top: 100px;
height: fit-content;
}
.sidebar-section {
background: white;
padding: 1.5rem;
border-radius: var(--radius-lg);
margin-bottom: 1.5rem;
}
.sidebar-section h3 {
margin-bottom: 1rem;
font-size: 1.125rem;
}
.tags {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
}
.tag {
padding: 0.25rem 0.75rem;
background: var(--gray-100);
color: var(--gray-700);
border-radius: var(--radius-sm);
font-size: 0.875rem;
}
.share-buttons {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.share-btn {
padding: 0.5rem;
background: var(--gray-100);
border: none;
border-radius: var(--radius-md);
cursor: pointer;
transition: background 0.2s;
}
.share-btn:hover {
background: var(--gray-200);
}
/* Author bio section */
.author-bio-section {
background: var(--gray-100);
padding: 3rem 0;
margin-top: 3rem;
}
.author-bio-card {
background: white;
padding: 2rem;
border-radius: var(--radius-lg);
display: flex;
gap: 2rem;
align-items: center;
}
.author-bio-content h3 {
margin-bottom: 0.5rem;
}
.author-bio-content p {
color: var(--gray-600);
margin-bottom: 1rem;
}
/* Related posts */
.related-posts-section {
padding: 3rem 0;
}
.related-posts-section h2 {
margin-bottom: 2rem;
font-size: 2rem;
}
.related-posts-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 1.5rem;
}
.related-post-card {
background: white;
border-radius: var(--radius-lg);
overflow: hidden;
box-shadow: var(--shadow-sm);
transition: transform 0.2s;
}
.related-post-card:hover {
transform: translateY(-4px);
}
.related-post-image {
width: 100%;
height: 180px;
object-fit: cover;
}
.related-post-content {
padding: 1.5rem;
}
.related-post-content h3 {
font-size: 1.25rem;
margin-bottom: 0.5rem;
}
.related-post-content h3 a {
color: var(--gray-900);
text-decoration: none;
}
.related-post-content h3 a:hover {
color: var(--primary);
}
.related-post-content p {
color: var(--gray-600);
font-size: 0.875rem;
margin: 0;
}
/* Responsive */
@media (max-width: 768px) {
.post-header .post-title {
font-size: 2rem;
}
.post-header .post-meta {
flex-direction: column;
align-items: flex-start;
gap: 1rem;
}
.featured-image {
height: 300px;
}
.content-wrapper {
grid-template-columns: 1fr;
}
.post-sidebar {
position: static;
}
.author-bio-card {
flex-direction: column;
text-align: center;
}
.related-posts-grid {
grid-template-columns: 1fr;
}
}
β PostDetailPage Features
- Full post content - Displays complete article with formatting
- Author information - Bio section with link to profile
- Related posts - Shows other posts from same category
- Sticky sidebar - Tags and share buttons stay visible
- Markdown rendering - Basic markdown-to-HTML conversion
- Not found handling - Graceful error for missing posts
- Rich metadata - Views, likes, read time displayed
π Three Major Pages Complete!
You've built:
- β HomePage with hero, featured posts, categories, and recent posts
- β BlogListPage with search, filters, and sorting
- β PostDetailPage with full content, author bio, and related posts
These are the core pages of your blog! In the next part, we'll build CategoryPage, AuthorProfilePage, AboutPage, and start on the authentication pages.
ποΈ More Public Pages
Let's complete the remaining public pages: CategoryPage, AuthorProfilePage, and AboutPage. These pages round out the public-facing portion of our blog application.
Page 4: CategoryPage
The CategoryPage shows all posts from a specific category. Create src/pages/public/CategoryPage.tsx:
// src/pages/public/CategoryPage.tsx
import { useParams, Link, useNavigate } from 'react-router-dom';
import { getCategoryBySlug } from '../../data/categories';
import { getPostsByCategory } from '../../data/posts';
import './CategoryPage.css';
export function CategoryPage() {
const { categoryName } = useParams<{ categoryName: string }>();
const navigate = useNavigate();
const category = categoryName ? getCategoryBySlug(categoryName) : undefined;
const posts = category ? getPostsByCategory(category.slug) : [];
if (!category) {
return (
<div className="container" style={{ padding: '4rem 2rem', textAlign: 'center' }}>
<h1>Category Not Found</h1>
<p>The category you're looking for doesn't exist.</p>
<button onClick={() => navigate('/blog')} className="btn btn-primary">
Back to Blog
</button>
</div>
);
}
return (
<div className="category-page">
{/* Category header */}
<header
className="category-header"
style={{ borderTopColor: category.color }}
>
<div className="container">
<div className="breadcrumb-nav">
<Link to="/blog">Blog</Link>
<span> / </span>
<span>{category.name}</span>
</div>
<h1 className="category-title">{category.name}</h1>
<p className="category-description">{category.description}</p>
<div className="category-stats">
<span style={{ color: category.color }}>
{posts.length} {posts.length === 1 ? 'post' : 'posts'}
</span>
</div>
</div>
</header>
{/* Posts grid */}
<section className="category-posts">
<div className="container">
{posts.length > 0 ? (
<div className="posts-grid">
{posts.map(post => (
<article key={post.id} className="post-card">
<Link to={`/blog/${post.id}`}>
<img
src={post.coverImage}
alt={post.title}
className="post-image"
/>
</Link>
<div className="post-content">
{post.featured && (
<span className="featured-badge">β Featured</span>
)}
<h2 className="post-title">
<Link to={`/blog/${post.id}`}>{post.title}</Link>
</h2>
<p className="post-excerpt">{post.excerpt}</p>
<div className="post-meta">
<Link to={`/author/${post.author.id}`} className="author-info">
<img
src={post.author.avatar}
alt={post.author.name}
className="author-avatar"
/>
<span>{post.author.name}</span>
</Link>
<div className="post-stats">
<span>π
{new Date(post.publishedDate).toLocaleDateString()}</span>
<span>β±οΈ {post.readTime} min</span>
</div>
</div>
</div>
</article>
))}
</div>
) : (
<div className="no-posts">
<h3>No posts yet</h3>
<p>Check back later for content in this category.</p>
</div>
)}
</div>
</section>
</div>
);
}
CategoryPage Styles
Create src/pages/public/CategoryPage.css:
/* src/pages/public/CategoryPage.css */
/* Category header */
.category-header {
background: white;
padding: 3rem 0 2rem;
border-top: 6px solid var(--primary);
margin-bottom: 3rem;
}
.breadcrumb-nav {
color: var(--gray-600);
margin-bottom: 1rem;
font-size: 0.875rem;
}
.breadcrumb-nav a {
color: var(--primary);
text-decoration: none;
}
.breadcrumb-nav a:hover {
text-decoration: underline;
}
.category-title {
font-size: 3rem;
margin-bottom: 1rem;
}
.category-description {
font-size: 1.25rem;
color: var(--gray-600);
margin-bottom: 1.5rem;
}
.category-stats {
font-size: 1.125rem;
font-weight: 600;
}
/* Posts section */
.category-posts {
padding: 2rem 0;
}
.posts-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(350px, 1fr));
gap: 2rem;
}
.post-card {
background: white;
border-radius: var(--radius-lg);
overflow: hidden;
box-shadow: var(--shadow-sm);
transition: transform 0.2s, box-shadow 0.2s;
}
.post-card:hover {
transform: translateY(-4px);
box-shadow: var(--shadow-lg);
}
.post-image {
width: 100%;
height: 220px;
object-fit: cover;
}
.post-content {
padding: 1.5rem;
}
.featured-badge {
display: inline-block;
padding: 0.25rem 0.75rem;
background: linear-gradient(135deg, #ffd700, #ffed4e);
color: var(--gray-900);
font-size: 0.875rem;
font-weight: 600;
border-radius: var(--radius-sm);
margin-bottom: 1rem;
}
.post-title {
font-size: 1.5rem;
margin: 0.5rem 0 1rem;
}
.post-title a {
color: var(--gray-900);
text-decoration: none;
}
.post-title a:hover {
color: var(--primary);
}
.post-excerpt {
color: var(--gray-600);
line-height: 1.6;
margin-bottom: 1rem;
}
.post-meta {
display: flex;
justify-content: space-between;
align-items: center;
padding-top: 1rem;
border-top: 1px solid var(--gray-200);
}
.author-info {
display: flex;
align-items: center;
gap: 0.5rem;
color: var(--gray-700);
text-decoration: none;
font-weight: 500;
}
.author-info:hover {
color: var(--primary);
}
.author-avatar {
width: 32px;
height: 32px;
border-radius: 50%;
}
.post-stats {
display: flex;
gap: 1rem;
font-size: 0.875rem;
color: var(--gray-500);
}
/* No posts */
.no-posts {
text-align: center;
padding: 4rem 2rem;
background: white;
border-radius: var(--radius-lg);
}
.no-posts h3 {
color: var(--gray-700);
margin-bottom: 0.5rem;
}
.no-posts p {
color: var(--gray-500);
}
/* Responsive */
@media (max-width: 768px) {
.category-title {
font-size: 2rem;
}
.posts-grid {
grid-template-columns: 1fr;
}
}
Page 5: AuthorProfilePage
The AuthorProfilePage displays an author's bio and their posts. Create src/pages/public/AuthorProfilePage.tsx:
// src/pages/public/AuthorProfilePage.tsx
import { useParams, Link, useNavigate } from 'react-router-dom';
import { getAuthorById } from '../../data/authors';
import { getPostsByAuthor } from '../../data/posts';
import './AuthorProfilePage.css';
export function AuthorProfilePage() {
const { authorId } = useParams<{ authorId: string }>();
const navigate = useNavigate();
const author = authorId ? getAuthorById(authorId) : undefined;
const posts = author ? getPostsByAuthor(author.id) : [];
if (!author) {
return (
<div className="container" style={{ padding: '4rem 2rem', textAlign: 'center' }}>
<h1>Author Not Found</h1>
<p>The author you're looking for doesn't exist.</p>
<button onClick={() => navigate('/blog')} className="btn btn-primary">
Back to Blog
</button>
</div>
);
}
return (
<div className="author-profile-page">
{/* Author hero */}
<section className="author-hero">
<div className="container">
<div className="author-card">
<img
src={author.avatar}
alt={author.name}
className="author-avatar-xlarge"
/>
<div className="author-info">
<h1 className="author-name">{author.name}</h1>
<p className="author-bio">{author.bio}</p>
{/* Social links */}
{(author.social.twitter || author.social.github || author.social.linkedin) && (
<div className="social-links">
{author.social.twitter && (
<a
href={`https://twitter.com/${author.social.twitter.replace('@', '')}`}
target="_blank"
rel="noopener noreferrer"
className="social-link"
>
π¦ Twitter
</a>
)}
{author.social.github && (
<a
href={`https://github.com/${author.social.github}`}
target="_blank"
rel="noopener noreferrer"
className="social-link"
>
π» GitHub
</a>
)}
{author.social.linkedin && (
<a
href={`https://linkedin.com/in/${author.social.linkedin}`}
target="_blank"
rel="noopener noreferrer"
className="social-link"
>
πΌ LinkedIn
</a>
)}
</div>
)}
{author.website && (
<a
href={author.website}
target="_blank"
rel="noopener noreferrer"
className="author-website"
>
π {author.website}
</a>
)}
</div>
</div>
{/* Author stats */}
<div className="author-stats">
<div className="stat-item">
<div className="stat-value">{author.postCount}</div>
<div className="stat-label">Posts</div>
</div>
<div className="stat-item">
<div className="stat-value">
{posts.reduce((sum, post) => sum + post.views, 0).toLocaleString()}
</div>
<div className="stat-label">Total Views</div>
</div>
<div className="stat-item">
<div className="stat-value">
{posts.reduce((sum, post) => sum + post.likes, 0)}
</div>
<div className="stat-label">Total Likes</div>
</div>
<div className="stat-item">
<div className="stat-value">
{new Date(author.joinedDate).toLocaleDateString('en-US', {
year: 'numeric',
month: 'short'
})}
</div>
<div className="stat-label">Joined</div>
</div>
</div>
</div>
</section>
{/* Author's posts */}
<section className="author-posts">
<div className="container">
<h2>Posts by {author.name}</h2>
{posts.length > 0 ? (
<div className="posts-grid">
{posts.map(post => (
<article key={post.id} className="post-card">
<Link to={`/blog/${post.id}`}>
<img
src={post.coverImage}
alt={post.title}
className="post-image"
/>
</Link>
<div className="post-content">
<Link
to={`/category/${post.category.slug}`}
className="post-category"
style={{ background: post.category.color }}
>
{post.category.name}
</Link>
<h3 className="post-title">
<Link to={`/blog/${post.id}`}>{post.title}</Link>
</h3>
<p className="post-excerpt">{post.excerpt}</p>
<div className="post-footer">
<span>π
{new Date(post.publishedDate).toLocaleDateString()}</span>
<span>β±οΈ {post.readTime} min</span>
<span>ποΈ {post.views}</span>
</div>
</div>
</article>
))}
</div>
) : (
<div className="no-posts">
<h3>No posts yet</h3>
<p>This author hasn't published any posts.</p>
</div>
)}
</div>
</section>
</div>
);
}
AuthorProfilePage Styles
Create src/pages/public/AuthorProfilePage.css:
/* src/pages/public/AuthorProfilePage.css */
/* Author hero */
.author-hero {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 4rem 0;
}
.author-card {
display: flex;
gap: 2rem;
align-items: center;
margin-bottom: 3rem;
}
.author-avatar-xlarge {
width: 150px;
height: 150px;
border-radius: 50%;
border: 4px solid white;
box-shadow: var(--shadow-lg);
}
.author-info {
flex: 1;
}
.author-name {
font-size: 2.5rem;
margin-bottom: 1rem;
color: white;
}
.author-bio {
font-size: 1.25rem;
margin-bottom: 1.5rem;
opacity: 0.95;
}
.social-links {
display: flex;
gap: 1rem;
margin-bottom: 1rem;
}
.social-link {
padding: 0.5rem 1rem;
background: rgba(255, 255, 255, 0.2);
color: white;
text-decoration: none;
border-radius: var(--radius-md);
transition: background 0.2s;
}
.social-link:hover {
background: rgba(255, 255, 255, 0.3);
color: white;
}
.author-website {
display: inline-block;
color: white;
text-decoration: underline;
opacity: 0.9;
}
.author-website:hover {
opacity: 1;
color: white;
}
/* Author stats */
.author-stats {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
gap: 1.5rem;
}
.stat-item {
background: rgba(255, 255, 255, 0.2);
padding: 1.5rem;
border-radius: var(--radius-lg);
text-align: center;
}
.stat-value {
font-size: 2rem;
font-weight: 700;
margin-bottom: 0.25rem;
}
.stat-label {
opacity: 0.9;
font-size: 0.875rem;
}
/* Author's posts */
.author-posts {
padding: 4rem 0;
}
.author-posts h2 {
font-size: 2rem;
margin-bottom: 2rem;
}
.posts-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(350px, 1fr));
gap: 2rem;
}
.post-card {
background: white;
border-radius: var(--radius-lg);
overflow: hidden;
box-shadow: var(--shadow-sm);
transition: transform 0.2s, box-shadow 0.2s;
}
.post-card:hover {
transform: translateY(-4px);
box-shadow: var(--shadow-lg);
}
.post-image {
width: 100%;
height: 220px;
object-fit: cover;
}
.post-content {
padding: 1.5rem;
}
.post-category {
display: inline-block;
padding: 0.25rem 0.75rem;
border-radius: var(--radius-sm);
color: white;
font-size: 0.875rem;
font-weight: 500;
margin-bottom: 1rem;
text-decoration: none;
}
.post-title {
font-size: 1.5rem;
margin: 0.5rem 0 1rem;
}
.post-title a {
color: var(--gray-900);
text-decoration: none;
}
.post-title a:hover {
color: var(--primary);
}
.post-excerpt {
color: var(--gray-600);
line-height: 1.6;
margin-bottom: 1rem;
}
.post-footer {
display: flex;
justify-content: space-between;
padding-top: 1rem;
border-top: 1px solid var(--gray-200);
font-size: 0.875rem;
color: var(--gray-500);
}
/* No posts */
.no-posts {
text-align: center;
padding: 4rem 2rem;
background: white;
border-radius: var(--radius-lg);
}
.no-posts h3 {
color: var(--gray-700);
margin-bottom: 0.5rem;
}
.no-posts p {
color: var(--gray-500);
}
/* Responsive */
@media (max-width: 768px) {
.author-card {
flex-direction: column;
text-align: center;
}
.author-name {
font-size: 2rem;
}
.social-links {
justify-content: center;
}
.author-stats {
grid-template-columns: repeat(2, 1fr);
}
.posts-grid {
grid-template-columns: 1fr;
}
}
Page 6: AboutPage
Finally, let's create a simple AboutPage. Create src/pages/public/AboutPage.tsx:
// src/pages/public/AboutPage.tsx
import { Link } from 'react-router-dom';
import { authors } from '../../data/authors';
import { categories } from '../../data/categories';
import { posts } from '../../data/posts';
import './AboutPage.css';
export function AboutPage() {
const totalViews = posts.reduce((sum, post) => sum + post.views, 0);
const totalLikes = posts.reduce((sum, post) => sum + post.likes, 0);
return (
<div className="about-page">
{/* Hero section */}
<section className="about-hero">
<div className="container">
<h1>About DevBlog</h1>
<p className="hero-subtitle">
A platform for developers to learn, share, and grow together
</p>
</div>
</section>
{/* Mission section */}
<section className="mission-section">
<div className="container">
<div className="mission-content">
<h2>Our Mission</h2>
<p>
DevBlog was created to be a hub for web developers to share knowledge,
learn new skills, and stay up-to-date with the latest in React, TypeScript,
and modern web development.
</p>
<p>
We believe in the power of community and the importance of accessible,
high-quality technical content. Whether you're just starting your coding
journey or you're a seasoned professional, there's something here for you.
</p>
</div>
</div>
</section>
{/* Stats section */}
<section className="stats-section">
<div className="container">
<h2>By the Numbers</h2>
<div className="stats-grid">
<div className="stat-card">
<div className="stat-number">{posts.length}</div>
<div className="stat-label">Articles Published</div>
</div>
<div className="stat-card">
<div className="stat-number">{authors.length}</div>
<div className="stat-label">Contributing Authors</div>
</div>
<div className="stat-card">
<div className="stat-number">{totalViews.toLocaleString()}</div>
<div className="stat-label">Total Views</div>
</div>
<div className="stat-card">
<div className="stat-number">{categories.length}</div>
<div className="stat-label">Categories</div>
</div>
</div>
</div>
</section>
{/* Authors section */}
<section className="authors-section">
<div className="container">
<h2>Meet Our Authors</h2>
<div className="authors-grid">
{authors.map(author => (
<Link
key={author.id}
to={`/author/${author.id}`}
className="author-card-link"
>
<img
src={author.avatar}
alt={author.name}
className="author-avatar"
/>
<h3>{author.name}</h3>
<p>{author.bio}</p>
<div className="author-stats">
{author.postCount} {author.postCount === 1 ? 'post' : 'posts'}
</div>
</Link>
))}
</div>
</div>
</section>
{/* Topics section */}
<section className="topics-section">
<div className="container">
<h2>What We Write About</h2>
<div className="topics-grid">
{categories.map(category => (
<Link
key={category.id}
to={`/category/${category.slug}`}
className="topic-card"
style={{ borderLeftColor: category.color }}
>
<h3>{category.name}</h3>
<p>{category.description}</p>
</Link>
))}
</div>
</div>
</section>
{/* CTA section */}
<section className="cta-section">
<div className="container">
<h2>Ready to Start Reading?</h2>
<p>Explore our collection of articles and tutorials</p>
<div className="cta-buttons">
<Link to="/blog" className="btn btn-primary btn-large">
Browse Articles
</Link>
<Link to="/register" className="btn btn-secondary btn-large">
Create Account
</Link>
</div>
</div>
</section>
</div>
);
}
AboutPage Styles
Create src/pages/public/AboutPage.css:
/* src/pages/public/AboutPage.css */
/* Hero section */
.about-hero {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 5rem 0;
text-align: center;
}
.about-hero h1 {
font-size: 3rem;
margin-bottom: 1rem;
color: white;
}
.hero-subtitle {
font-size: 1.5rem;
opacity: 0.95;
}
/* Mission section */
.mission-section {
padding: 4rem 0;
background: white;
}
.mission-content {
max-width: 800px;
margin: 0 auto;
}
.mission-content h2 {
font-size: 2.5rem;
margin-bottom: 2rem;
text-align: center;
}
.mission-content p {
font-size: 1.125rem;
line-height: 1.8;
color: var(--gray-700);
margin-bottom: 1.5rem;
}
/* Stats section */
.stats-section {
padding: 4rem 0;
background: var(--gray-50);
}
.stats-section h2 {
font-size: 2.5rem;
text-align: center;
margin-bottom: 3rem;
}
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 2rem;
}
.stat-card {
background: white;
padding: 2rem;
border-radius: var(--radius-lg);
text-align: center;
box-shadow: var(--shadow-sm);
}
.stat-number {
font-size: 3rem;
font-weight: 700;
color: var(--primary);
margin-bottom: 0.5rem;
}
.stat-label {
color: var(--gray-600);
font-size: 1rem;
}
/* Authors section */
.authors-section {
padding: 4rem 0;
background: white;
}
.authors-section h2 {
font-size: 2.5rem;
text-align: center;
margin-bottom: 3rem;
}
.authors-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 2rem;
}
.author-card-link {
background: var(--gray-50);
padding: 2rem;
border-radius: var(--radius-lg);
text-align: center;
text-decoration: none;
transition: transform 0.2s, box-shadow 0.2s;
display: block;
}
.author-card-link:hover {
transform: translateY(-4px);
box-shadow: var(--shadow-lg);
}
.author-card-link .author-avatar {
width: 100px;
height: 100px;
border-radius: 50%;
margin-bottom: 1rem;
}
.author-card-link h3 {
color: var(--gray-900);
margin-bottom: 0.5rem;
}
.author-card-link p {
color: var(--gray-600);
font-size: 0.875rem;
margin-bottom: 1rem;
}
.author-card-link .author-stats {
color: var(--primary);
font-weight: 600;
}
/* Topics section */
.topics-section {
padding: 4rem 0;
background: var(--gray-50);
}
.topics-section h2 {
font-size: 2.5rem;
text-align: center;
margin-bottom: 3rem;
}
.topics-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 1.5rem;
}
.topic-card {
background: white;
padding: 1.5rem;
border-left: 4px solid var(--primary);
border-radius: var(--radius-md);
text-decoration: none;
transition: transform 0.2s, box-shadow 0.2s;
display: block;
}
.topic-card:hover {
transform: translateX(4px);
box-shadow: var(--shadow-md);
}
.topic-card h3 {
color: var(--gray-900);
margin-bottom: 0.5rem;
}
.topic-card p {
color: var(--gray-600);
font-size: 0.875rem;
margin: 0;
}
/* CTA section */
.cta-section {
padding: 5rem 0;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
text-align: center;
}
.cta-section h2 {
font-size: 2.5rem;
margin-bottom: 1rem;
color: white;
}
.cta-section p {
font-size: 1.25rem;
margin-bottom: 2rem;
opacity: 0.95;
}
.cta-buttons {
display: flex;
gap: 1rem;
justify-content: center;
}
.btn-large {
padding: 1rem 2rem;
font-size: 1.125rem;
}
/* Responsive */
@media (max-width: 768px) {
.about-hero h1 {
font-size: 2rem;
}
.hero-subtitle {
font-size: 1.125rem;
}
.mission-content h2,
.stats-section h2,
.authors-section h2,
.topics-section h2,
.cta-section h2 {
font-size: 2rem;
}
.stats-grid {
grid-template-columns: repeat(2, 1fr);
}
.authors-grid,
.topics-grid {
grid-template-columns: 1fr;
}
.cta-buttons {
flex-direction: column;
align-items: stretch;
}
}
β All Public Pages Complete!
- HomePage - Featured posts, categories, recent content β
- BlogListPage - Search, filter, sort functionality β
- PostDetailPage - Full article view β
- CategoryPage - Category-filtered posts β
- AuthorProfilePage - Author bio and their posts β
- AboutPage - Platform information β
π Public Section Complete!
All six public pages are now built and styled! You have a fully functional blog front-end with:
- β Beautiful, responsive layouts
- β Dynamic routing with URL parameters
- β Search and filtering
- β Author profiles and category pages
- β Related posts and social links
Next, we'll build the authentication pages (Login, Register, Forgot Password) and then move on to the protected dashboard pages!
π Authentication Pages
Now let's build the authentication pages that use our AuthLayout. These pages handle user login, registration, and password recovery. Remember, our authentication is simulated for demo purposes.
Page 7: LoginPage
The LoginPage allows users to log in and redirects them back to where they were trying to go. Create src/pages/auth/LoginPage.tsx:
// src/pages/auth/LoginPage.tsx
import { useState, FormEvent } from 'react';
import { Link, useNavigate, useLocation } from 'react-router-dom';
import { login } from '../../utils/auth';
import './AuthPages.css';
export function LoginPage() {
const navigate = useNavigate();
const location = useLocation();
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [rememberMe, setRememberMe] = useState(false);
const [error, setError] = useState('');
const [loading, setLoading] = useState(false);
// Get the page they were trying to visit
const from = (location.state as any)?.from?.pathname || '/dashboard';
const handleSubmit = async (e: FormEvent) => {
e.preventDefault();
setError('');
setLoading(true);
// Simulate API call delay
setTimeout(() => {
const success = login(email, password);
if (success) {
// Redirect to the page they were trying to visit
navigate(from, { replace: true });
} else {
setError('Invalid email or password');
setLoading(false);
}
}, 1000);
};
return (
<div className="auth-page">
<div className="auth-card">
<h1>Welcome Back</h1>
<p className="auth-subtitle">Sign in to your account</p>
{error && (
<div className="error-message">
β οΈ {error}
</div>
)}
<form onSubmit={handleSubmit} className="auth-form">
<div className="form-group">
<label htmlFor="email">Email Address</label>
<input
type="email"
id="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="you@example.com"
required
disabled={loading}
/>
</div>
<div className="form-group">
<label htmlFor="password">Password</label>
<input
type="password"
id="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder="Enter your password"
required
disabled={loading}
/>
</div>
<div className="form-row">
<label className="checkbox-label">
<input
type="checkbox"
checked={rememberMe}
onChange={(e) => setRememberMe(e.target.checked)}
disabled={loading}
/>
<span>Remember me</span>
</label>
<Link to="/forgot-password" className="forgot-link">
Forgot password?
</Link>
</div>
<button
type="submit"
className="btn btn-primary btn-full"
disabled={loading}
>
{loading ? 'Signing in...' : 'Sign In'}
</button>
</form>
<div className="auth-divider">
<span>or</span>
</div>
<div className="auth-footer">
<p>
Don't have an account?{' '}
<Link to="/register" className="auth-link">
Sign up
</Link>
</p>
</div>
<div className="demo-info">
<p>π‘ Demo mode: Any email/password will work!</p>
</div>
</div>
</div>
);
}
Page 8: RegisterPage
The RegisterPage allows new users to create an account. Create src/pages/auth/RegisterPage.tsx:
// src/pages/auth/RegisterPage.tsx
import { useState, FormEvent } from 'react';
import { Link, useNavigate } from 'react-router-dom';
import { register } from '../../utils/auth';
import './AuthPages.css';
export function RegisterPage() {
const navigate = useNavigate();
const [name, setName] = useState('');
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [confirmPassword, setConfirmPassword] = useState('');
const [agreeToTerms, setAgreeToTerms] = useState(false);
const [error, setError] = useState('');
const [loading, setLoading] = useState(false);
const handleSubmit = async (e: FormEvent) => {
e.preventDefault();
setError('');
// Validation
if (password !== confirmPassword) {
setError('Passwords do not match');
return;
}
if (password.length < 6) {
setError('Password must be at least 6 characters');
return;
}
if (!agreeToTerms) {
setError('You must agree to the terms and conditions');
return;
}
setLoading(true);
// Simulate API call delay
setTimeout(() => {
const success = register(name, email, password);
if (success) {
// Redirect to dashboard after registration
navigate('/dashboard', { replace: true });
} else {
setError('Registration failed. Please try again.');
setLoading(false);
}
}, 1000);
};
return (
<div className="auth-page">
<div className="auth-card">
<h1>Create Account</h1>
<p className="auth-subtitle">Join DevBlog and start writing</p>
{error && (
<div className="error-message">
β οΈ {error}
</div>
)}
<form onSubmit={handleSubmit} className="auth-form">
<div className="form-group">
<label htmlFor="name">Full Name</label>
<input
type="text"
id="name"
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="John Doe"
required
disabled={loading}
/>
</div>
<div className="form-group">
<label htmlFor="email">Email Address</label>
<input
type="email"
id="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="you@example.com"
required
disabled={loading}
/>
</div>
<div className="form-group">
<label htmlFor="password">Password</label>
<input
type="password"
id="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder="At least 6 characters"
required
disabled={loading}
minLength={6}
/>
</div>
<div className="form-group">
<label htmlFor="confirmPassword">Confirm Password</label>
<input
type="password"
id="confirmPassword"
value={confirmPassword}
onChange={(e) => setConfirmPassword(e.target.value)}
placeholder="Repeat your password"
required
disabled={loading}
minLength={6}
/>
</div>
<div className="form-group">
<label className="checkbox-label">
<input
type="checkbox"
checked={agreeToTerms}
onChange={(e) => setAgreeToTerms(e.target.checked)}
disabled={loading}
/>
<span>
I agree to the{' '}
<a href="#" onClick={(e) => e.preventDefault()}>
Terms and Conditions
</a>
</span>
</label>
</div>
<button
type="submit"
className="btn btn-primary btn-full"
disabled={loading}
>
{loading ? 'Creating account...' : 'Create Account'}
</button>
</form>
<div className="auth-divider">
<span>or</span>
</div>
<div className="auth-footer">
<p>
Already have an account?{' '}
<Link to="/login" className="auth-link">
Sign in
</Link>
</p>
</div>
</div>
</div>
);
}
Page 9: ForgotPasswordPage
The ForgotPasswordPage simulates password recovery. Create src/pages/auth/ForgotPasswordPage.tsx:
// src/pages/auth/ForgotPasswordPage.tsx
import { useState, FormEvent } from 'react';
import { Link } from 'react-router-dom';
import './AuthPages.css';
export function ForgotPasswordPage() {
const [email, setEmail] = useState('');
const [submitted, setSubmitted] = useState(false);
const [loading, setLoading] = useState(false);
const handleSubmit = async (e: FormEvent) => {
e.preventDefault();
setLoading(true);
// Simulate API call delay
setTimeout(() => {
setSubmitted(true);
setLoading(false);
}, 1000);
};
if (submitted) {
return (
<div className="auth-page">
<div className="auth-card">
<div className="success-message">
<div className="success-icon">β
</div>
<h1>Check Your Email</h1>
<p>
We've sent a password reset link to <strong>{email}</strong>.
Please check your inbox and follow the instructions.
</p>
<p className="text-muted">
Didn't receive the email? Check your spam folder or try again.
</p>
<Link to="/login" className="btn btn-primary btn-full">
Back to Login
</Link>
</div>
</div>
</div>
);
}
return (
<div className="auth-page">
<div className="auth-card">
<h1>Reset Password</h1>
<p className="auth-subtitle">
Enter your email address and we'll send you a link to reset your password.
</p>
<form onSubmit={handleSubmit} className="auth-form">
<div className="form-group">
<label htmlFor="email">Email Address</label>
<input
type="email"
id="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="you@example.com"
required
disabled={loading}
/>
</div>
<button
type="submit"
className="btn btn-primary btn-full"
disabled={loading}
>
{loading ? 'Sending...' : 'Send Reset Link'}
</button>
</form>
<div className="auth-divider">
<span>or</span>
</div>
<div className="auth-footer">
<p>
Remember your password?{' '}
<Link to="/login" className="auth-link">
Sign in
</Link>
</p>
</div>
</div>
</div>
);
}
Shared Authentication Styles
Create src/pages/auth/AuthPages.css for all auth pages:
/* src/pages/auth/AuthPages.css */
.auth-page {
padding: 2rem;
}
.auth-card {
max-width: 450px;
margin: 0 auto;
}
.auth-card h1 {
font-size: 2rem;
text-align: center;
margin-bottom: 0.5rem;
color: var(--gray-900);
}
.auth-subtitle {
text-align: center;
color: var(--gray-600);
margin-bottom: 2rem;
}
/* Form styles */
.auth-form {
margin-bottom: 1.5rem;
}
.form-group {
margin-bottom: 1.5rem;
}
.form-group label {
display: block;
margin-bottom: 0.5rem;
color: var(--gray-700);
font-weight: 500;
}
.form-group input {
width: 100%;
padding: 0.75rem;
border: 2px solid var(--gray-300);
border-radius: var(--radius-md);
font-size: 1rem;
transition: border-color 0.2s;
}
.form-group input:focus {
outline: none;
border-color: var(--primary);
}
.form-group input:disabled {
background: var(--gray-100);
cursor: not-allowed;
}
/* Form row (for remember me / forgot password) */
.form-row {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1.5rem;
}
/* Checkbox label */
.checkbox-label {
display: flex;
align-items: center;
gap: 0.5rem;
cursor: pointer;
margin: 0;
font-weight: normal;
}
.checkbox-label input[type="checkbox"] {
width: auto;
cursor: pointer;
}
.checkbox-label span {
color: var(--gray-700);
}
.checkbox-label a {
color: var(--primary);
text-decoration: none;
}
.checkbox-label a:hover {
text-decoration: underline;
}
/* Links */
.forgot-link,
.auth-link {
color: var(--primary);
text-decoration: none;
font-weight: 500;
}
.forgot-link:hover,
.auth-link:hover {
text-decoration: underline;
}
/* Button */
.btn-full {
width: 100%;
padding: 0.875rem;
font-size: 1rem;
font-weight: 600;
}
.btn:disabled {
opacity: 0.6;
cursor: not-allowed;
}
/* Divider */
.auth-divider {
text-align: center;
margin: 1.5rem 0;
position: relative;
}
.auth-divider::before {
content: '';
position: absolute;
left: 0;
right: 0;
top: 50%;
height: 1px;
background: var(--gray-300);
}
.auth-divider span {
position: relative;
background: white;
padding: 0 1rem;
color: var(--gray-500);
font-size: 0.875rem;
}
/* Footer */
.auth-footer {
text-align: center;
}
.auth-footer p {
color: var(--gray-600);
margin: 0;
}
/* Messages */
.error-message {
background: #fee;
color: #c33;
padding: 1rem;
border-radius: var(--radius-md);
margin-bottom: 1.5rem;
border-left: 4px solid #c33;
}
.success-message {
text-align: center;
}
.success-icon {
font-size: 4rem;
margin-bottom: 1rem;
}
.success-message h1 {
color: var(--success);
margin-bottom: 1rem;
}
.success-message p {
color: var(--gray-700);
margin-bottom: 1rem;
line-height: 1.6;
}
.text-muted {
color: var(--gray-500);
font-size: 0.875rem;
}
/* Demo info */
.demo-info {
background: var(--gray-100);
padding: 1rem;
border-radius: var(--radius-md);
margin-top: 1.5rem;
text-align: center;
}
.demo-info p {
margin: 0;
color: var(--gray-600);
font-size: 0.875rem;
}
/* Responsive */
@media (max-width: 480px) {
.auth-card h1 {
font-size: 1.5rem;
}
.form-row {
flex-direction: column;
align-items: flex-start;
gap: 1rem;
}
}
π‘ Authentication Pages Features
- LoginPage - Remembers where user was trying to go and redirects back
- RegisterPage - Validates password match and length
- ForgotPasswordPage - Two-step flow with success confirmation
- Form validation - Client-side validation with error messages
- Loading states - Disabled inputs during submission
- Simulated delays - Realistic API call experience
- Demo indicator - Reminds users this is demo mode
Testing Authentication Flow
Let's verify the authentication system works correctly:
Test Scenarios
- Protected route redirect:
- Navigate to
/dashboardwhile logged out - Should redirect to
/login - After logging in, should redirect back to
/dashboard
- Navigate to
- Registration flow:
- Click "Sign up" from login page
- Fill out registration form
- Submit with non-matching passwords (should error)
- Fix and submit successfully
- Should redirect to dashboard and be logged in
- Forgot password:
- Click "Forgot password?" from login
- Enter email and submit
- Should show success message
- Click "Back to Login" to return
- Navigation while authenticated:
- Log in successfully
- Navigate to public pages (should show dashboard link)
- Click logout (should return to home)
- Try accessing dashboard (should redirect to login)
β οΈ Authentication Reminder
Remember, this authentication is for demonstration purposes only:
- Any email/password combination will work
- Data is stored in localStorage (not secure)
- No password hashing or encryption
- No real API calls
- No token management
In a production app, you would use a real authentication service like Auth0, Firebase Auth, or a backend API with JWT tokens.
β Authentication Pages Complete!
- β LoginPage with remember me and redirect logic
- β RegisterPage with validation
- β ForgotPasswordPage with success state
- β Shared styling for all auth pages
- β Error handling and loading states
- β AuthLayout providing centered, gradient design
π Major Milestone Reached!
You've completed:
- β All 6 public pages
- β All 3 authentication pages
- β Complete routing structure
- β Protected route system
- β Three distinct layouts
Project is approximately 70% complete! Next up: Dashboard pages where authenticated users can manage their content.
π Dashboard Pages
Now let's build the protected dashboard pages where authenticated users can manage their content. These pages use the DashboardLayout and demonstrate nested routing with the settings section.
Page 10: DashboardHomePage
The dashboard home shows an overview and statistics. Create src/pages/dashboard/DashboardHomePage.tsx:
// src/pages/dashboard/DashboardHomePage.tsx
import { Link } from 'react-router-dom';
import { getCurrentUser } from '../../utils/auth';
import { posts } from '../../data/posts';
import './DashboardPages.css';
export function DashboardHomePage() {
const user = getCurrentUser();
// In a real app, these would be filtered by the current user
const userPosts = posts.slice(0, 3);
const totalViews = userPosts.reduce((sum, post) => sum + post.views, 0);
const totalLikes = userPosts.reduce((sum, post) => sum + post.likes, 0);
return (
<div className="dashboard-home">
{/* Welcome section */}
<div className="dashboard-header">
<div>
<h1>Dashboard</h1>
<p>Welcome back, {user?.name}! Here's what's happening with your blog.</p>
</div>
<Link to="/dashboard/posts/new" className="btn btn-primary">
β New Post
</Link>
</div>
{/* Stats cards */}
<div className="stats-grid">
<div className="stat-card">
<div className="stat-icon" style={{ background: '#667eea' }}>π</div>
<div className="stat-content">
<div className="stat-value">{userPosts.length}</div>
<div className="stat-label">Total Posts</div>
</div>
</div>
<div className="stat-card">
<div className="stat-icon" style={{ background: '#48bb78' }}>ποΈ</div>
<div className="stat-content">
<div className="stat-value">{totalViews.toLocaleString()}</div>
<div className="stat-label">Total Views</div>
</div>
</div>
<div className="stat-card">
<div className="stat-icon" style={{ background: '#ed8936' }}>β€οΈ</div>
<div className="stat-content">
<div className="stat-value">{totalLikes}</div>
<div className="stat-label">Total Likes</div>
</div>
</div>
<div className="stat-card">
<div className="stat-icon" style={{ background: '#9f7aea' }}>π</div>
<div className="stat-content">
<div className="stat-value">
{Math.round(totalViews / userPosts.length || 0)}
</div>
<div className="stat-label">Avg. Views/Post</div>
</div>
</div>
</div>
{/* Recent posts */}
<div className="dashboard-section">
<div className="section-header">
<h2>Recent Posts</h2>
<Link to="/dashboard/posts" className="view-all-link">
View all β
</Link>
</div>
<div className="posts-list">
{userPosts.map(post => (
<div key={post.id} className="post-item">
<img
src={post.coverImage}
alt={post.title}
className="post-thumbnail"
/>
<div className="post-info">
<h3>
<Link to={`/blog/${post.id}`}>{post.title}</Link>
</h3>
<p>{post.excerpt}</p>
<div className="post-meta">
<span>π
{new Date(post.publishedDate).toLocaleDateString()}</span>
<span>ποΈ {post.views} views</span>
<span>β€οΈ {post.likes} likes</span>
</div>
</div>
<div className="post-actions">
<Link
to={`/dashboard/posts/${post.id}/edit`}
className="btn btn-secondary btn-sm"
>
Edit
</Link>
</div>
</div>
))}
</div>
</div>
{/* Quick actions */}
<div className="dashboard-section">
<h2>Quick Actions</h2>
<div className="quick-actions">
<Link to="/dashboard/posts/new" className="action-card">
<div className="action-icon">βοΈ</div>
<h3>Write New Post</h3>
<p>Share your knowledge with the community</p>
</Link>
<Link to="/dashboard/posts" className="action-card">
<div className="action-icon">π</div>
<h3>Manage Posts</h3>
<p>Edit or delete your existing posts</p>
</Link>
<Link to="/dashboard/settings" className="action-card">
<div className="action-icon">βοΈ</div>
<h3>Settings</h3>
<p>Update your profile and preferences</p>
</Link>
<Link to="/blog" className="action-card">
<div className="action-icon">π</div>
<h3>View Blog</h3>
<p>See how your blog looks to visitors</p>
</Link>
</div>
</div>
</div>
);
}
Page 11: MyPostsPage
This page lists all of the user's posts. Create src/pages/dashboard/MyPostsPage.tsx:
// src/pages/dashboard/MyPostsPage.tsx
import { Link } from 'react-router-dom';
import { posts } from '../../data/posts';
import './DashboardPages.css';
export function MyPostsPage() {
// In a real app, filter by current user
const userPosts = posts;
return (
<div className="my-posts-page">
<div className="dashboard-header">
<div>
<h1>My Posts</h1>
<p>Manage all your published articles</p>
</div>
<Link to="/dashboard/posts/new" className="btn btn-primary">
β New Post
</Link>
</div>
<div className="posts-table">
<table>
<thead>
<tr>
<th>Post</th>
<th>Category</th>
<th>Published</th>
<th>Views</th>
<th>Likes</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{userPosts.map(post => (
<tr key={post.id}>
<td>
<div className="post-cell">
<img
src={post.coverImage}
alt={post.title}
className="post-thumb"
/>
<div>
<Link to={`/blog/${post.id}`} className="post-title-link">
{post.title}
</Link>
{post.featured && (
<span className="featured-badge-sm">β</span>
)}
</div>
</div>
</td>
<td>
<span
className="category-badge"
style={{ background: post.category.color }}
>
{post.category.name}
</span>
</td>
<td>{new Date(post.publishedDate).toLocaleDateString()}</td>
<td>{post.views}</td>
<td>{post.likes}</td>
<td>
<div className="table-actions">
<Link
to={`/dashboard/posts/${post.id}/edit`}
className="btn btn-secondary btn-sm"
>
Edit
</Link>
<button className="btn btn-danger btn-sm">
Delete
</button>
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
);
}
Page 12: CreatePostPage
This page provides a form to create new posts. Create src/pages/dashboard/CreatePostPage.tsx:
// src/pages/dashboard/CreatePostPage.tsx
import { useState, FormEvent } from 'react';
import { useNavigate } from 'react-router-dom';
import { categories } from '../../data/categories';
import './DashboardPages.css';
export function CreatePostPage() {
const navigate = useNavigate();
const [title, setTitle] = useState('');
const [excerpt, setExcerpt] = useState('');
const [content, setContent] = useState('');
const [categoryId, setCategoryId] = useState('');
const [coverImage, setCoverImage] = useState('');
const [tags, setTags] = useState('');
const [featured, setFeatured] = useState(false);
const handleSubmit = (e: FormEvent) => {
e.preventDefault();
// In a real app, send to API
console.log('Creating post:', {
title,
excerpt,
content,
categoryId,
coverImage,
tags: tags.split(',').map(t => t.trim()),
featured
});
// Redirect to posts list
navigate('/dashboard/posts');
};
return (
<div className="create-post-page">
<div className="dashboard-header">
<div>
<h1>Create New Post</h1>
<p>Share your knowledge with the community</p>
</div>
</div>
<form onSubmit={handleSubmit} className="post-form">
<div className="form-grid">
<div className="form-main">
<div className="form-group">
<label htmlFor="title">Post Title</label>
<input
type="text"
id="title"
value={title}
onChange={(e) => setTitle(e.target.value)}
placeholder="Enter a catchy title..."
required
/>
</div>
<div className="form-group">
<label htmlFor="excerpt">Excerpt</label>
<textarea
id="excerpt"
value={excerpt}
onChange={(e) => setExcerpt(e.target.value)}
placeholder="Brief description of your post..."
rows={3}
required
/>
</div>
<div className="form-group">
<label htmlFor="content">Content</label>
<textarea
id="content"
value={content}
onChange={(e) => setContent(e.target.value)}
placeholder="Write your post content here... (Markdown supported)"
rows={15}
required
/>
</div>
</div>
<div className="form-sidebar">
<div className="sidebar-card">
<h3>Post Settings</h3>
<div className="form-group">
<label htmlFor="category">Category</label>
<select
id="category"
value={categoryId}
onChange={(e) => setCategoryId(e.target.value)}
required
>
<option value="">Select category...</option>
{categories.map(cat => (
<option key={cat.id} value={cat.id}>
{cat.name}
</option>
))}
</select>
</div>
<div className="form-group">
<label htmlFor="coverImage">Cover Image URL</label>
<input
type="url"
id="coverImage"
value={coverImage}
onChange={(e) => setCoverImage(e.target.value)}
placeholder="https://..."
/>
</div>
<div className="form-group">
<label htmlFor="tags">Tags</label>
<input
type="text"
id="tags"
value={tags}
onChange={(e) => setTags(e.target.value)}
placeholder="react, typescript, tutorial"
/>
<small>Separate tags with commas</small>
</div>
<div className="form-group">
<label className="checkbox-label">
<input
type="checkbox"
checked={featured}
onChange={(e) => setFeatured(e.target.checked)}
/>
<span>Mark as featured</span>
</label>
</div>
</div>
<div className="form-actions">
<button type="submit" className="btn btn-primary btn-full">
Publish Post
</button>
<button
type="button"
className="btn btn-secondary btn-full"
onClick={() => navigate('/dashboard/posts')}
>
Cancel
</button>
</div>
</div>
</div>
</form>
</div>
);
}
Page 13: EditPostPage
Similar to create, but pre-populated. Create src/pages/dashboard/EditPostPage.tsx:
// src/pages/dashboard/EditPostPage.tsx
import { useState, useEffect, FormEvent } from 'react';
import { useParams, useNavigate } from 'react-router-dom';
import { getPostById } from '../../data/posts';
import { categories } from '../../data/categories';
import './DashboardPages.css';
export function EditPostPage() {
const { postId } = useParams<{ postId: string }>();
const navigate = useNavigate();
const post = postId ? getPostById(postId) : undefined;
const [title, setTitle] = useState('');
const [excerpt, setExcerpt] = useState('');
const [content, setContent] = useState('');
const [categoryId, setCategoryId] = useState('');
const [coverImage, setCoverImage] = useState('');
const [tags, setTags] = useState('');
const [featured, setFeatured] = useState(false);
useEffect(() => {
if (post) {
setTitle(post.title);
setExcerpt(post.excerpt);
setContent(post.content);
setCategoryId(post.category.id);
setCoverImage(post.coverImage);
setTags(post.tags.join(', '));
setFeatured(post.featured);
}
}, [post]);
if (!post) {
return (
<div className="container" style={{ padding: '2rem', textAlign: 'center' }}>
<h1>Post Not Found</h1>
<button onClick={() => navigate('/dashboard/posts')} className="btn btn-primary">
Back to Posts
</button>
</div>
);
}
const handleSubmit = (e: FormEvent) => {
e.preventDefault();
// In a real app, send to API
console.log('Updating post:', {
id: postId,
title,
excerpt,
content,
categoryId,
coverImage,
tags: tags.split(',').map(t => t.trim()),
featured
});
navigate('/dashboard/posts');
};
return (
<div className="edit-post-page">
<div className="dashboard-header">
<div>
<h1>Edit Post</h1>
<p>Update your post content</p>
</div>
</div>
<form onSubmit={handleSubmit} className="post-form">
<div className="form-grid">
<div className="form-main">
<div className="form-group">
<label htmlFor="title">Post Title</label>
<input
type="text"
id="title"
value={title}
onChange={(e) => setTitle(e.target.value)}
required
/>
</div>
<div className="form-group">
<label htmlFor="excerpt">Excerpt</label>
<textarea
id="excerpt"
value={excerpt}
onChange={(e) => setExcerpt(e.target.value)}
rows={3}
required
/>
</div>
<div className="form-group">
<label htmlFor="content">Content</label>
<textarea
id="content"
value={content}
onChange={(e) => setContent(e.target.value)}
rows={15}
required
/>
</div>
</div>
<div className="form-sidebar">
<div className="sidebar-card">
<h3>Post Settings</h3>
<div className="form-group">
<label htmlFor="category">Category</label>
<select
id="category"
value={categoryId}
onChange={(e) => setCategoryId(e.target.value)}
required
>
{categories.map(cat => (
<option key={cat.id} value={cat.id}>
{cat.name}
</option>
))}
</select>
</div>
<div className="form-group">
<label htmlFor="coverImage">Cover Image URL</label>
<input
type="url"
id="coverImage"
value={coverImage}
onChange={(e) => setCoverImage(e.target.value)}
/>
</div>
<div className="form-group">
<label htmlFor="tags">Tags</label>
<input
type="text"
id="tags"
value={tags}
onChange={(e) => setTags(e.target.value)}
/>
</div>
<div className="form-group">
<label className="checkbox-label">
<input
type="checkbox"
checked={featured}
onChange={(e) => setFeatured(e.target.checked)}
/>
<span>Mark as featured</span>
</label>
</div>
</div>
<div className="form-actions">
<button type="submit" className="btn btn-primary btn-full">
Update Post
</button>
<button
type="button"
className="btn btn-secondary btn-full"
onClick={() => navigate('/dashboard/posts')}
>
Cancel
</button>
</div>
</div>
</div>
</form>
</div>
);
}
Dashboard Pages Styles
Create src/pages/dashboard/DashboardPages.css:
/* src/pages/dashboard/DashboardPages.css */
/* Dashboard header */
.dashboard-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 2rem;
}
.dashboard-header h1 {
font-size: 2rem;
margin-bottom: 0.25rem;
}
.dashboard-header p {
color: var(--gray-600);
margin: 0;
}
/* Stats grid */
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 1.5rem;
margin-bottom: 2rem;
}
.stat-card {
background: white;
padding: 1.5rem;
border-radius: var(--radius-lg);
box-shadow: var(--shadow-sm);
display: flex;
align-items: center;
gap: 1rem;
}
.stat-icon {
width: 60px;
height: 60px;
border-radius: var(--radius-md);
display: flex;
align-items: center;
justify-content: center;
font-size: 2rem;
color: white;
}
.stat-value {
font-size: 2rem;
font-weight: 700;
color: var(--gray-900);
}
.stat-label {
color: var(--gray-600);
font-size: 0.875rem;
}
/* Dashboard section */
.dashboard-section {
background: white;
padding: 1.5rem;
border-radius: var(--radius-lg);
box-shadow: var(--shadow-sm);
margin-bottom: 2rem;
}
.dashboard-section h2 {
margin-bottom: 1.5rem;
}
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1.5rem;
}
.section-header h2 {
margin: 0;
}
.view-all-link {
color: var(--primary);
font-weight: 500;
text-decoration: none;
}
.view-all-link:hover {
text-decoration: underline;
}
/* Posts list */
.posts-list {
display: flex;
flex-direction: column;
gap: 1rem;
}
.post-item {
display: flex;
gap: 1rem;
padding: 1rem;
border: 1px solid var(--gray-200);
border-radius: var(--radius-md);
transition: box-shadow 0.2s;
}
.post-item:hover {
box-shadow: var(--shadow-sm);
}
.post-thumbnail {
width: 120px;
height: 80px;
object-fit: cover;
border-radius: var(--radius-md);
flex-shrink: 0;
}
.post-info {
flex: 1;
min-width: 0;
}
.post-info h3 {
margin: 0 0 0.5rem;
font-size: 1.125rem;
}
.post-info h3 a {
color: var(--gray-900);
text-decoration: none;
}
.post-info h3 a:hover {
color: var(--primary);
}
.post-info p {
color: var(--gray-600);
font-size: 0.875rem;
margin: 0 0 0.5rem;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.post-meta {
display: flex;
gap: 1rem;
font-size: 0.875rem;
color: var(--gray-500);
}
.post-actions {
display: flex;
flex-direction: column;
gap: 0.5rem;
flex-shrink: 0;
}
.btn-sm {
padding: 0.5rem 1rem;
font-size: 0.875rem;
}
.btn-danger {
background: var(--danger);
color: white;
}
.btn-danger:hover {
background: #e53e3e;
}
/* Quick actions */
.quick-actions {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
gap: 1.5rem;
}
.action-card {
background: var(--gray-50);
padding: 2rem;
border-radius: var(--radius-lg);
text-align: center;
text-decoration: none;
transition: transform 0.2s, box-shadow 0.2s;
}
.action-card:hover {
transform: translateY(-4px);
box-shadow: var(--shadow-md);
}
.action-icon {
font-size: 3rem;
margin-bottom: 1rem;
}
.action-card h3 {
color: var(--gray-900);
margin-bottom: 0.5rem;
}
.action-card p {
color: var(--gray-600);
font-size: 0.875rem;
margin: 0;
}
/* Posts table */
.posts-table {
background: white;
border-radius: var(--radius-lg);
overflow: hidden;
box-shadow: var(--shadow-sm);
}
.posts-table table {
width: 100%;
border-collapse: collapse;
}
.posts-table th {
background: var(--gray-100);
padding: 1rem;
text-align: left;
font-weight: 600;
color: var(--gray-700);
border-bottom: 2px solid var(--gray-200);
}
.posts-table td {
padding: 1rem;
border-bottom: 1px solid var(--gray-200);
}
.posts-table tbody tr:hover {
background: var(--gray-50);
}
.post-cell {
display: flex;
align-items: center;
gap: 1rem;
}
.post-thumb {
width: 60px;
height: 40px;
object-fit: cover;
border-radius: var(--radius-sm);
}
.post-title-link {
color: var(--gray-900);
text-decoration: none;
font-weight: 500;
}
.post-title-link:hover {
color: var(--primary);
}
.featured-badge-sm {
margin-left: 0.5rem;
font-size: 0.875rem;
}
.category-badge {
display: inline-block;
padding: 0.25rem 0.75rem;
border-radius: var(--radius-sm);
color: white;
font-size: 0.875rem;
font-weight: 500;
}
.table-actions {
display: flex;
gap: 0.5rem;
}
/* Post form */
.post-form {
background: white;
padding: 2rem;
border-radius: var(--radius-lg);
box-shadow: var(--shadow-sm);
}
.form-grid {
display: grid;
grid-template-columns: 1fr 320px;
gap: 2rem;
}
.form-main {
display: flex;
flex-direction: column;
gap: 1.5rem;
}
.form-sidebar {
display: flex;
flex-direction: column;
gap: 1.5rem;
}
.sidebar-card {
background: var(--gray-50);
padding: 1.5rem;
border-radius: var(--radius-lg);
}
.sidebar-card h3 {
margin: 0 0 1.5rem;
font-size: 1.125rem;
}
.form-group {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.form-group label {
font-weight: 500;
color: var(--gray-700);
}
.form-group input,
.form-group textarea,
.form-group select {
padding: 0.75rem;
border: 1px solid var(--gray-300);
border-radius: var(--radius-md);
font-family: inherit;
}
.form-group textarea {
resize: vertical;
font-family: var(--font-mono);
}
.form-group small {
color: var(--gray-500);
font-size: 0.875rem;
}
.checkbox-label {
display: flex;
align-items: center;
gap: 0.5rem;
cursor: pointer;
}
.checkbox-label input[type="checkbox"] {
width: auto;
cursor: pointer;
}
.form-actions {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.btn-full {
width: 100%;
}
/* Responsive */
@media (max-width: 1024px) {
.form-grid {
grid-template-columns: 1fr;
}
.form-sidebar {
order: -1;
}
}
@media (max-width: 768px) {
.dashboard-header {
flex-direction: column;
align-items: flex-start;
gap: 1rem;
}
.stats-grid {
grid-template-columns: repeat(2, 1fr);
}
.post-item {
flex-direction: column;
}
.post-thumbnail {
width: 100%;
height: 160px;
}
.posts-table {
overflow-x: auto;
}
.quick-actions {
grid-template-columns: 1fr;
}
}
β Dashboard Pages Features
- DashboardHome - Stats overview, recent posts, quick actions
- MyPosts - Table view of all posts with edit/delete
- CreatePost - Full post creation form with sidebar settings
- EditPost - Pre-populated form for editing
- Responsive tables - Mobile-friendly data display
- Form validation - Required fields and proper inputs
π Dashboard Core Complete!
You've built the main dashboard functionality! Only one section remaining: the nested Settings pages.
βοΈ Settings Pages (Nested Routing)
Now let's implement the settings section with nested routing. This demonstrates how to have a layout within a layout, with its own navigation tabs. The settings section already has its SettingsLayout created earlier, now we'll build the actual settings pages.
Settings Index Page
The settings index shows an overview. Create src/pages/dashboard/settings/SettingsIndexPage.tsx:
// src/pages/dashboard/settings/SettingsIndexPage.tsx
import { Link } from 'react-router-dom';
import './SettingsPages.css';
export function SettingsIndexPage() {
return (
<div className="settings-index">
<h2>Account Settings</h2>
<p>Manage your account preferences and settings.</p>
<div className="settings-cards">
<Link to="/dashboard/settings/profile" className="settings-card">
<div className="settings-icon">π€</div>
<h3>Profile Settings</h3>
<p>Update your name, bio, and profile picture</p>
</Link>
<Link to="/dashboard/settings/account" className="settings-card">
<div className="settings-icon">π</div>
<h3>Account Settings</h3>
<p>Change your email, password, and security options</p>
</Link>
<Link to="/dashboard/settings/preferences" className="settings-card">
<div className="settings-icon">π¨</div>
<h3>Preferences</h3>
<p>Customize your experience with theme and notifications</p>
</Link>
</div>
</div>
);
}
Profile Settings Page
Edit profile information. Create src/pages/dashboard/settings/ProfileSettingsPage.tsx:
// src/pages/dashboard/settings/ProfileSettingsPage.tsx
import { useState, FormEvent } from 'react';
import { getCurrentUser, updateUser } from '../../../utils/auth';
import './SettingsPages.css';
export function ProfileSettingsPage() {
const user = getCurrentUser();
const [name, setName] = useState(user?.name || '');
const [bio, setBio] = useState('Full-stack developer passionate about React and TypeScript');
const [website, setWebsite] = useState('https://example.com');
const [twitter, setTwitter] = useState('@username');
const [github, setGithub] = useState('username');
const [saved, setSaved] = useState(false);
const handleSubmit = (e: FormEvent) => {
e.preventDefault();
updateUser({ name });
setSaved(true);
setTimeout(() => setSaved(false), 3000);
};
return (
<div className="settings-page">
<h2>Profile Settings</h2>
<p>Update your public profile information</p>
{saved && (
<div className="success-banner">
β
Profile updated successfully!
</div>
)}
<form onSubmit={handleSubmit} className="settings-form">
<div className="form-section">
<h3>Basic Information</h3>
<div className="form-group">
<label htmlFor="name">Display Name</label>
<input
type="text"
id="name"
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="Your name"
/>
</div>
<div className="form-group">
<label htmlFor="bio">Bio</label>
<textarea
id="bio"
value={bio}
onChange={(e) => setBio(e.target.value)}
placeholder="Tell us about yourself"
rows={4}
/>
</div>
<div className="form-group">
<label htmlFor="website">Website</label>
<input
type="url"
id="website"
value={website}
onChange={(e) => setWebsite(e.target.value)}
placeholder="https://yourwebsite.com"
/>
</div>
</div>
<div className="form-section">
<h3>Social Links</h3>
<div className="form-group">
<label htmlFor="twitter">Twitter</label>
<input
type="text"
id="twitter"
value={twitter}
onChange={(e) => setTwitter(e.target.value)}
placeholder="@username"
/>
</div>
<div className="form-group">
<label htmlFor="github">GitHub</label>
<input
type="text"
id="github"
value={github}
onChange={(e) => setGithub(e.target.value)}
placeholder="username"
/>
</div>
</div>
<div className="form-actions">
<button type="submit" className="btn btn-primary">
Save Changes
</button>
</div>
</form>
</div>
);
}
Account Settings Page
Manage email and password. Create src/pages/dashboard/settings/AccountSettingsPage.tsx:
// src/pages/dashboard/settings/AccountSettingsPage.tsx
import { useState, FormEvent } from 'react';
import { getCurrentUser } from '../../../utils/auth';
import './SettingsPages.css';
export function AccountSettingsPage() {
const user = getCurrentUser();
const [email, setEmail] = useState(user?.email || '');
const [currentPassword, setCurrentPassword] = useState('');
const [newPassword, setNewPassword] = useState('');
const [confirmPassword, setConfirmPassword] = useState('');
const [saved, setSaved] = useState(false);
const [error, setError] = useState('');
const handleEmailSubmit = (e: FormEvent) => {
e.preventDefault();
setSaved(true);
setTimeout(() => setSaved(false), 3000);
};
const handlePasswordSubmit = (e: FormEvent) => {
e.preventDefault();
setError('');
if (newPassword !== confirmPassword) {
setError('New passwords do not match');
return;
}
if (newPassword.length < 6) {
setError('Password must be at least 6 characters');
return;
}
// Simulate password update
setCurrentPassword('');
setNewPassword('');
setConfirmPassword('');
setSaved(true);
setTimeout(() => setSaved(false), 3000);
};
return (
<div className="settings-page">
<h2>Account Settings</h2>
<p>Manage your email and password</p>
{saved && (
<div className="success-banner">
β
Settings updated successfully!
</div>
)}
{error && (
<div className="error-banner">
β οΈ {error}
</div>
)}
{/* Email section */}
<form onSubmit={handleEmailSubmit} className="settings-form">
<div className="form-section">
<h3>Email Address</h3>
<div className="form-group">
<label htmlFor="email">Email</label>
<input
type="email"
id="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="you@example.com"
/>
</div>
<div className="form-actions">
<button type="submit" className="btn btn-primary">
Update Email
</button>
</div>
</div>
</form>
{/* Password section */}
<form onSubmit={handlePasswordSubmit} className="settings-form">
<div className="form-section">
<h3>Change Password</h3>
<div className="form-group">
<label htmlFor="currentPassword">Current Password</label>
<input
type="password"
id="currentPassword"
value={currentPassword}
onChange={(e) => setCurrentPassword(e.target.value)}
placeholder="Enter current password"
/>
</div>
<div className="form-group">
<label htmlFor="newPassword">New Password</label>
<input
type="password"
id="newPassword"
value={newPassword}
onChange={(e) => setNewPassword(e.target.value)}
placeholder="Enter new password"
minLength={6}
/>
</div>
<div className="form-group">
<label htmlFor="confirmPassword">Confirm New Password</label>
<input
type="password"
id="confirmPassword"
value={confirmPassword}
onChange={(e) => setConfirmPassword(e.target.value)}
placeholder="Confirm new password"
minLength={6}
/>
</div>
<div className="form-actions">
<button type="submit" className="btn btn-primary">
Change Password
</button>
</div>
</div>
</form>
{/* Danger zone */}
<div className="form-section danger-zone">
<h3>Danger Zone</h3>
<p>These actions are permanent and cannot be undone.</p>
<button className="btn btn-danger">
Delete Account
</button>
</div>
</div>
);
}
Preferences Page
UI preferences and notifications. Create src/pages/dashboard/settings/PreferencesPage.tsx:
// src/pages/dashboard/settings/PreferencesPage.tsx
import { useState, FormEvent } from 'react';
import './SettingsPages.css';
export function PreferencesPage() {
const [theme, setTheme] = useState('light');
const [emailNotifications, setEmailNotifications] = useState(true);
const [commentNotifications, setCommentNotifications] = useState(true);
const [weeklyDigest, setWeeklyDigest] = useState(false);
const [saved, setSaved] = useState(false);
const handleSubmit = (e: FormEvent) => {
e.preventDefault();
setSaved(true);
setTimeout(() => setSaved(false), 3000);
};
return (
<div className="settings-page">
<h2>Preferences</h2>
<p>Customize your experience</p>
{saved && (
<div className="success-banner">
β
Preferences saved successfully!
</div>
)}
<form onSubmit={handleSubmit} className="settings-form">
<div className="form-section">
<h3>Appearance</h3>
<div className="form-group">
<label htmlFor="theme">Theme</label>
<select
id="theme"
value={theme}
onChange={(e) => setTheme(e.target.value)}
>
<option value="light">Light</option>
<option value="dark">Dark</option>
<option value="auto">Auto (system default)</option>
</select>
</div>
</div>
<div className="form-section">
<h3>Notifications</h3>
<div className="checkbox-group">
<label className="checkbox-label">
<input
type="checkbox"
checked={emailNotifications}
onChange={(e) => setEmailNotifications(e.target.checked)}
/>
<div>
<div className="checkbox-title">Email Notifications</div>
<div className="checkbox-description">
Receive email updates about your account activity
</div>
</div>
</label>
<label className="checkbox-label">
<input
type="checkbox"
checked={commentNotifications}
onChange={(e) => setCommentNotifications(e.target.checked)}
/>
<div>
<div className="checkbox-title">Comment Notifications</div>
<div className="checkbox-description">
Get notified when someone comments on your posts
</div>
</div>
</label>
<label className="checkbox-label">
<input
type="checkbox"
checked={weeklyDigest}
onChange={(e) => setWeeklyDigest(e.target.checked)}
/>
<div>
<div className="checkbox-title">Weekly Digest</div>
<div className="checkbox-description">
Receive a weekly summary of popular posts
</div>
</div>
</label>
</div>
</div>
<div className="form-actions">
<button type="submit" className="btn btn-primary">
Save Preferences
</button>
</div>
</form>
</div>
);
}
Settings Pages Styles
Create src/pages/dashboard/settings/SettingsPages.css:
/* src/pages/dashboard/settings/SettingsPages.css */
.settings-index {
padding: 1rem 0;
}
.settings-index h2 {
margin-bottom: 0.5rem;
}
.settings-index p {
color: var(--gray-600);
margin-bottom: 2rem;
}
/* Settings cards */
.settings-cards {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 1.5rem;
}
.settings-card {
background: white;
padding: 2rem;
border-radius: var(--radius-lg);
box-shadow: var(--shadow-sm);
text-decoration: none;
transition: transform 0.2s, box-shadow 0.2s;
display: block;
}
.settings-card:hover {
transform: translateY(-4px);
box-shadow: var(--shadow-md);
}
.settings-icon {
font-size: 3rem;
margin-bottom: 1rem;
}
.settings-card h3 {
color: var(--gray-900);
margin-bottom: 0.5rem;
}
.settings-card p {
color: var(--gray-600);
font-size: 0.875rem;
margin: 0;
}
/* Settings page */
.settings-page {
padding: 1rem 0;
}
.settings-page h2 {
margin-bottom: 0.5rem;
}
.settings-page > p {
color: var(--gray-600);
margin-bottom: 2rem;
}
/* Banners */
.success-banner {
background: #e8f5e9;
color: #2e7d32;
padding: 1rem;
border-radius: var(--radius-md);
margin-bottom: 1.5rem;
border-left: 4px solid #4caf50;
}
.error-banner {
background: #ffebee;
color: #c62828;
padding: 1rem;
border-radius: var(--radius-md);
margin-bottom: 1.5rem;
border-left: 4px solid #f44336;
}
/* Settings form */
.settings-form {
background: white;
padding: 2rem;
border-radius: var(--radius-lg);
box-shadow: var(--shadow-sm);
margin-bottom: 2rem;
}
.form-section {
margin-bottom: 2rem;
}
.form-section:last-child {
margin-bottom: 0;
}
.form-section h3 {
margin-bottom: 1.5rem;
padding-bottom: 0.75rem;
border-bottom: 2px solid var(--gray-200);
}
.form-group {
margin-bottom: 1.5rem;
}
.form-group label {
display: block;
margin-bottom: 0.5rem;
font-weight: 500;
color: var(--gray-700);
}
.form-group input,
.form-group textarea,
.form-group select {
width: 100%;
padding: 0.75rem;
border: 1px solid var(--gray-300);
border-radius: var(--radius-md);
font-family: inherit;
}
.form-group textarea {
resize: vertical;
}
/* Checkbox group */
.checkbox-group {
display: flex;
flex-direction: column;
gap: 1rem;
}
.checkbox-label {
display: flex;
align-items: flex-start;
gap: 1rem;
padding: 1rem;
background: var(--gray-50);
border-radius: var(--radius-md);
cursor: pointer;
transition: background 0.2s;
}
.checkbox-label:hover {
background: var(--gray-100);
}
.checkbox-label input[type="checkbox"] {
width: auto;
margin-top: 0.25rem;
cursor: pointer;
}
.checkbox-title {
font-weight: 500;
color: var(--gray-900);
margin-bottom: 0.25rem;
}
.checkbox-description {
font-size: 0.875rem;
color: var(--gray-600);
}
/* Form actions */
.form-actions {
display: flex;
gap: 1rem;
padding-top: 1rem;
}
/* Danger zone */
.danger-zone {
background: #fff5f5;
border: 2px solid #feb2b2;
padding: 1.5rem;
border-radius: var(--radius-lg);
}
.danger-zone h3 {
color: var(--danger);
border-bottom-color: #feb2b2;
}
.danger-zone p {
color: var(--gray-700);
margin-bottom: 1rem;
}
/* Responsive */
@media (max-width: 768px) {
.settings-cards {
grid-template-columns: 1fr;
}
.settings-form {
padding: 1.5rem;
}
.checkbox-label {
flex-direction: column;
align-items: flex-start;
}
}
β Settings Pages Features
- SettingsIndex - Overview cards linking to each settings section
- ProfileSettings - Edit name, bio, website, social links
- AccountSettings - Change email, password, delete account
- Preferences - Theme selection, notification settings
- Nested routing - All settings share the SettingsLayout navigation
- Success/error messages - Temporary banners for feedback
- Form validation - Password matching, length requirements
Testing Nested Routing
Verify the settings nested routing works correctly:
- Navigate to
/dashboard/settings- should show index with cards - Click "Profile Settings" - URL changes to
/dashboard/settings/profile - Notice the settings navigation tabs update (active state)
- Click "Account" tab - navigates to
/dashboard/settings/account - Click "Preferences" tab - navigates to
/dashboard/settings/preferences - Click dashboard sidebar "Settings" - goes back to index
- Notice SettingsLayout's
Outletswitches between pages
π ALL PAGES COMPLETE!
Congratulations! You've successfully built:
- β 3 layouts (Public, Auth, Dashboard)
- β 6 public pages
- β 3 authentication pages
- β 4 dashboard pages
- β 4 settings pages (nested routing)
- β Complete routing system with 20+ routes
Total: 20 pages across 3 layouts with sophisticated routing! π
π― Project Summary & Next Steps
Congratulations on completing this comprehensive React Router project! Let's review what you've built and explore ways to enhance it further.
What You've Accomplished
π Project Statistics
| Total Pages: | 20 unique pages |
| Routes: | 24+ defined routes |
| Layouts: | 4 layouts (Public, Auth, Dashboard, Settings) |
| Components: | 25+ components created |
| Lines of Code: | ~3,000+ lines |
| Features: | Search, filters, auth, CRUD, nested routing |
Key Concepts Mastered
β React Router Concepts
- Basic Routing - Routes, Route, BrowserRouter
- Navigation - Link, NavLink with active states
- Layout Routes - Outlet for persistent UI
- Dynamic Routes - URL parameters (postId, authorId, categoryName)
- Protected Routes - Authentication guards with redirects
- Query Parameters - Search and filter state in URL
- Nested Routes - Routes within routes (Settings)
- Programmatic Navigation - useNavigate for redirects
- Location State - Passing data between routes
- Index Routes - Default child routes
- 404 Handling - Catch-all route for not found
- useParams Hook - Extracting URL parameters
- useSearchParams Hook - Reading/writing query strings
- useLocation Hook - Accessing current location
Enhancement Ideas
π Take It Further
Here are ways to expand this project:
1. Real Backend Integration
- Replace mock data with real API calls
- Implement proper authentication with JWT
- Add actual database persistence
- Handle loading and error states
2. Advanced Features
- Add comments system on posts
- Implement post drafts vs published
- Add rich text editor (TipTap, Quill)
- Image upload functionality
- Post scheduling
- Analytics dashboard with charts
3. State Management
- Add Zustand or Redux for global state
- Implement React Query for data fetching
- Add optimistic updates
- Cache invalidation strategies
4. Performance Optimizations
- Code splitting with React.lazy()
- Image optimization and lazy loading
- Virtual scrolling for long lists
- Memoization with useMemo/useCallback
5. User Experience
- Add loading skeletons
- Implement toast notifications
- Add confirmation dialogs
- Keyboard shortcuts
- Dark mode toggle
- Animations and transitions
6. Testing
- Unit tests with React Testing Library
- Integration tests for user flows
- E2E tests with Playwright or Cypress
- Test routing logic and protected routes
Common Routing Patterns Reference
Quick Reference Guide
// 1. Basic Route
<Route path="/about" element={<AboutPage />} />
// 2. Dynamic Route
<Route path="/blog/:postId" element={<PostDetail />} />
// 3. Layout Route
<Route element={<PublicLayout />}>
<Route path="/" element={<HomePage />} />
</Route>
// 4. Protected Route
<Route element={<ProtectedRoute><DashboardLayout /></ProtectedRoute>}>
<Route path="/dashboard" element={<Dashboard />} />
</Route>
// 5. Nested Routes
<Route path="/dashboard/settings" element={<SettingsLayout />}>
<Route index element={<SettingsIndex />} />
<Route path="profile" element={<ProfileSettings />} />
</Route>
// 6. Index Route
<Route index element={<HomePage />} />
// 7. Catch-all (404)
<Route path="*" element={<NotFoundPage />} />
// 8. Programmatic Navigation
const navigate = useNavigate();
navigate('/dashboard');
// 9. Get URL Parameters
const { postId } = useParams();
// 10. Query Parameters
const [searchParams, setSearchParams] = useSearchParams();
const query = searchParams.get('q');
setSearchParams({ q: 'react' });
Deployment Checklist
π¦ Before Deploying
- β Replace mock data with real API
- β Implement proper authentication
- β Add environment variables
- β Set up error boundaries
- β Add analytics tracking
- β Optimize images and assets
- β Test on multiple browsers
- β Test on mobile devices
- β Configure 404 redirects on server
- β Set up CI/CD pipeline
- β Add meta tags for SEO
- β Configure CORS if needed
Resources for Continued Learning
π Further Reading
π Congratulations!
You've successfully completed the Multi-Page Blog Application project!
You've mastered React Router v6, built a production-quality application with 20+ pages, implemented nested routing, protected routes, and sophisticated navigation patterns. This project demonstrates real-world routing architecture that you can use as a foundation for any React application.
Keep building, keep learning, and keep pushing forward! π