Open In App

Build a News Portal Using Next.js

Last Updated : 14 Aug, 2024
Comments
Improve
Suggest changes
Like Article
Like
Report

In this article, we will explore how to build a news portal using Next.js, a popular React framework for server-side rendering and static site generation. The news portal will allow users to publish and categorize news articles, providing a seamless user experience with fast page loads and dynamic content.

Output Preview: Let us have a look at how the final output will look like.

preview
News Portal Using Next.js

Prerequisites

Approach to Build a News Portal Using Next.js

  • Set up new Next.js Project
  • For state management we will utilize the built-in React hooks such as useState and useEffect.
  • Create Navbar component for navigation of application.
  • Create Home page to display the list of news which has functionality to search and filter the news.
  • Create ArticleCard component which displays the indivisual news.
  • Create new-article page which will have Form to add a new news article.
  • Create a page for dynamic route which will display details about specific news article.
  • We will use Tailwind CSS classes for responsive and modern design.
  • We will utilize localStorage to save and retrieve news data.

Steps to Create a Recipe Manager Using Next.js

Step 1: Initialized the Nextjs app

npx create-next-app@latest news-portal

Step 2: It will ask you some questions, so choose as the following bolded option.

Screenshot-2024-08-13-121456
Setup

Project Structure

s
Folder Structure

Updated Dependencies

"dependencies": {
"react": "^18",
"react-dom": "^18",
"next": "14.2.5"
},
"devDependencies": {
"postcss": "^8",
"tailwindcss": "^3.4.1"
}

Create the required files and write the following code.

Below mentioned is the CSS code for setting up global styles in the project. This file will include base styles, components, and utilities from Tailwind CSS:

CSS
/* globals.css */
@tailwind base;
@tailwind components;
@tailwind utilities;

Below mentioned is the JavaScript code for the news portal website. This component will handle the display and filtering of news articles, using React hooks and components:

JavaScript
// page.js
'use client'

import { useEffect, useState } from 'react';
import ArticleCard from './components/ArticleCard';
import Navbar from './components/Navbar';

export default function Home() {
    const [articles, setArticles] = useState([]);
    const [filteredArticles, setFilteredArticles] = useState([]);
    const [searchQuery, setSearchQuery] = useState('');
    const [selectedCategory, setSelectedCategory] = useState('All');
    const [categories, setCategories] = 
   useState(['All', 'Technology', 'Sports', 'Health', 'Business', 'Entertainment']); 
// Example categories

    useEffect(() => {
        // Retrieve articles from localStorage
        const storedArticles = localStorage.getItem('articles');
        if (storedArticles) {
            setArticles(JSON.parse(storedArticles));
        }
    }, []);

    useEffect(() => {
        // Filter articles based on search query and selected category
        setFilteredArticles(
            articles.filter(article => {
                const matchesSearch = 
     article.title.toLowerCase().includes(searchQuery.toLowerCase());
                const matchesCategory = selectedCategory === 'All' 
   || article.category === selectedCategory;
                return matchesSearch && matchesCategory;
            })
        );
    }, [searchQuery, selectedCategory, articles]);

    return (
        <>
            <Navbar />
            <div className="container mx-auto px-4 sm:px-6 lg:px-8">
                <div className="flex flex-col sm:flex-row justify-center items-center my-4 gap-2">
                    <input
                        type="text"
                        placeholder="Search by title"
                        value={searchQuery}
                        onChange={(e) => setSearchQuery(e.target.value)}
                        className="w-full sm:w-64 p-2 border border-gray-300 rounded"
                    />
                    <select
                        value={selectedCategory}
                        onChange={(e) => setSelectedCategory(e.target.value)}
                        className="w-full sm:w-64 p-2 border border-gray-300 rounded mt-2 sm:mt-0"
                    >
                        {categories.map((cat, index) => (
                            <option key={index} value={cat}>
                                {cat}
                            </option>
                        ))}
                    </select>
                </div>
                <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
                    {filteredArticles.length > 0 ? (
                        filteredArticles.map((article, index) => (
                            <ArticleCard key={index} article={article} />
                        ))
                    ) : (
                        <p className="col-span-full text-center 
            text-gray-500">No articles found.</p>
                    )}
                </div>
            </div>
        </>
    );
}

Below mentioned is the JavaScript code for the Navbar component of the news portal website. This component is responsible for rendering the navigation bar, which includes links to the Home page and the page for creating a new article. It also includes a responsive menu toggle for smaller screens:

JavaScript
// Navbar.js

import Link from 'next/link';
import { useState } from 'react';

const Navbar = () => {
    const [isOpen, setIsOpen] = useState(false);

    const toggleMenu = () => setIsOpen(!isOpen);

    return (
        <nav className="bg-gradient-to-r from-blue-600 to-blue-800 p-4">
            <div className="container mx-auto flex flex-wrap items-center justify-between">
                <div className="flex items-center">
                    <Link href="/">
                        <h1 className="text-white text-2xl font-bold cursor-pointer">News Hub</h1>
                    </Link>
                </div>
                <button
                    className="text-white lg:hidden"
                    onClick={toggleMenu}
                    aria-label="Toggle menu"
                >
                    <svg className="w-6 h-6" fill="none" stroke="currentColor" 
               viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
                        <path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" 
                      d="M4 6h16M4 12h16m-7 6h7"></path>
                    </svg>
                </button>
                <div className={`w-full lg:flex lg:items-center lg:w-auto ${isOpen ? 
                "block" : "hidden"}`}>
                    <Link href="/">
                        <span className="block lg:inline-block text-white hover:text-gray-300 
                  mr-4 transition duration-300">Home</span>
                    </Link>
                    <Link href="/new-article">
                        <span className="block lg:inline-block text-white hover:text-gray-300 
                     mr-4 transition duration-300">New Article</span>
                    </Link>

                </div>
            </div>
        </nav>
    );
};

export default Navbar;

Below mentioned is the JavaScript code for the ArticleCard component of the news portal website. This component is responsible for displaying a single article in a card format, including its image, title, author, publication date, and a "Read More" link that redirects to the full article page:

JavaScript
// ArticleCard.js

import Link from 'next/link';

const ArticleCard = ({ article }) => {
    const articleUrl = `/article/${article.id}`;

    return (
        <div className="max-w-xs md:max-w-sm lg:max-w-md xl:max-w-lg rounded 
	overflow-hidden bg-white shadow-lg m-2 
	transition-transform transform hover:scale-105 
	hover:shadow-lg hover:bg-gray-50">
            <img className="w-full h-48 object-cover" src={article.image} alt={article.title} />
            <div className="px-4 py-2">
                <div className="font-bold text-lg mb-1 truncate">{article.title}</div>
                <p className="text-gray-700 text-sm mb-1">By {article.author}</p>
                <p className="text-gray-500 text-xs mb-2">Published
          on {new Date(article.publicationDate).toLocaleDateString()}</p>
                <Link href={articleUrl}>
                    <span className="mt-2 py-1 px-3 bg-blue-600 text-white 
           font-semibold rounded-lg shadow-md hover:bg-blue-700 focus:outline-none 
             focus:ring-2 focus:ring-blue-500 transition">
                        Read More
                    </span>
                </Link>
            </div>
        </div>
    );
};

export default ArticleCard;

Below is the JavaScript code for the NewArticle component. This code creates a form for adding a new article to the news portal.

JavaScript
// pages/new-article.js

import { useState } from 'react';
import { useRouter } from 'next/navigation';
import "../app/globals.css";
import Navbar from '../app/components/Navbar';

export default function NewArticle() {
    const [title, setTitle] = useState('');
    const [content, setContent] = useState('');
    const [author, setAuthor] = useState('');
    const [publicationDate, setPublicationDate] = useState('');
    const [image, setImage] = useState('');
    const [category, setCategory] = useState('');
    const [errors, setErrors] = useState({});
    const router = useRouter();

    const categories = ['Technology', 'Sports', 'Health', 'Business', 'Entertainment'];

    const validateForm = () => {
        const errors = {};
        if (!title.trim()) errors.title = "Title is required.";
        if (!content.trim()) errors.content = "Content is required.";
        if (!author.trim()) errors.author = "Author is required.";
        if (!publicationDate.trim()) errors.publicationDate = "Publication date is required.";
        if (!image.trim()) errors.image = "Image URL is required.";
        if (!category.trim()) errors.category = "Category is required.";

        setErrors(errors);
        return Object.keys(errors).length === 0;
    };

    const generateUniqueId = () => {
        return Date.now().toString(); // Generate a unique ID based on timestamp
    };

    const handleSubmit = (e) => {
        e.preventDefault();

        if (!validateForm()) return;

        const newArticle = {
            id: generateUniqueId(), // Add unique ID here
            title,
            content,
            author,
            publicationDate,
            image,
            category
        };

        const storedArticles = localStorage.getItem('articles');
        const articles = storedArticles ? JSON.parse(storedArticles) : [];
        articles.push(newArticle);
        localStorage.setItem('articles', JSON.stringify(articles));

        router.push('/');
    };

    return (
        <>
            <Navbar />
            <div className="flex items-center justify-center min-h-screen bg-gray-100 px-4">
                <div className="w-full max-w-4xl bg-white shadow-md rounded-lg p-8 mx-4 md:mx-0">
                    <h2 className="text-2xl md:text-3xl font-semibold mb-6 text-gray-800">
                    Add a New Article</h2>
                    <form onSubmit={handleSubmit} className="space-y-4">
                        <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
                            <div>
                                <label htmlFor="title" className="block 
                              text-sm font-medium text-gray-700">Title</label>
                                <input
                                    id="title"
                                    type="text"
                                    placeholder="Enter the article title"
                                    value={title}
                                    onChange={(e) => setTitle(e.target.value)}
                                    className={`block w-full p-3 border rounded-md 
                         shadow-sm focus:outline-none focus:ring-2 ${errors.title ? 'border-red-500 
                     focus:ring-red-500' : 'border-gray-300 focus:ring-blue-500'}`}
                                />
                                {errors.title && <p className="text-red-500 
                     text-sm mt-1">{errors.title}</p>}
                            </div>
                            <div>
                                <label htmlFor="author" className="block text-sm 
                          font-medium text-gray-700">Author</label>
                                <input
                                    id="author"
                                    type="text"
                                    placeholder="Enter the author's name"
                                    value={author}
                                    onChange={(e) => setAuthor(e.target.value)}
                                    className={`block w-full p-3 border rounded-md shadow-sm 
                 focus:outline-none focus:ring-2 ${errors.author ? 'border-red-500 focus:ring-red-500' : 
              'border-gray-300 focus:ring-blue-500'}`}
                                />
                                {errors.author && <p className="text-red-500 
              text-sm mt-1">{errors.author}</p>}
                            </div>
                        </div>
                        <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
                            <div>
                                <label htmlFor="publicationDate" className="block 
                     text-sm font-medium text-gray-700">Publication Date</label>
                                <input
                                    id="publicationDate"
                                    type="date"
                                    value={publicationDate}
                                    onChange={(e) => setPublicationDate(e.target.value)}
                                    className={`block w-full p-3 border rounded-md 
           shadow-sm focus:outline-none focus:ring-2 ${errors.publicationDate ? 'border-red-500 
            focus:ring-red-500' : 'border-gray-300 focus:ring-blue-500'}`}
                                />
                                {errors.publicationDate && <p className="text-red-500 
                text-sm mt-1">{errors.publicationDate}</p>}
                            </div>
                            <div>
                                <label htmlFor="image" className="block text-sm font-medium 
                     text-gray-700">Image URL</label>
                                <input
                                    id="image"
                                    type="text"
                                    placeholder="Enter the image URL"
                                    value={image}
                                    onChange={(e) => setImage(e.target.value)}
                                    className={`block w-full p-3 border rounded-md 
          shadow-sm focus:outline-none focus:ring-2 ${errors.image ? 'border-red-500 focus:ring-red-500' 
            : 'border-gray-300 focus:ring-blue-500'}`}
                                />
                                {errors.image && <p className="text-red-500 text-sm mt-1">
                    {errors.image}</p>}
                            </div>
                        </div>
                        <div>
                            <label htmlFor="category" className="block text-sm font-medium 
              text-gray-700">Category</label>
                            <select
                                id="category"
                                value={category}
                                onChange={(e) => setCategory(e.target.value)}
                                className={`block w-full p-3 border rounded-md shadow-sm 
                                focus:outline-none focus:ring-2 ${errors.category ? 
             'border-red-500 focus:ring-red-500' : 'border-gray-300 focus:ring-blue-500'}`}
                            >
                                <option value="">Select a category</option>
                                {categories.map((cat, index) => (
                                    <option key={index} value={cat}>{cat}</option>
                                ))}
                            </select>
                            {errors.category && <p className="text-red-500 
                     text-sm mt-1">{errors.category}</p>}
                        </div>
                        <div>
                            <label htmlFor="content" className="block text-sm 
              font-medium text-gray-700">Content</label>
                            <textarea
                                id="content"
                                placeholder="Enter the article content"
                                value={content}
                                onChange={(e) => setContent(e.target.value)}
                                className={`block w-full p-3 border rounded-md 
            shadow-sm focus:outline-none focus:ring-2 ${errors.content ? 'border-red-500 
                         focus:ring-red-500' : 'border-gray-300 focus:ring-blue-500'}`}
                            ></textarea>
                            {errors.content && <p className="text-red-500 
                                     text-sm mt-1">{errors.content}</p>}
                        </div>
                        <button
                            type="submit"
                            className="w-full py-3 bg-blue-600 text-white 
                  font-semibold rounded-lg shadow-md hover:bg-blue-700 focus:outline-none focus:ring-2 
                  focus:ring-blue-500"
                        >
                            Add Article
                        </button>
                    </form>
                </div>
            </div>
        </>
    );
}

This component displays detailed information about a specific article based on its ID from the URL.

JavaScript
// pages/article/[id].js

import { useRouter } from 'next/router';
import { useEffect, useState } from 'react';
import '@/app/globals.css';
import Link from 'next/link';
import Navbar from '@/app/components/Navbar';

const ArticleDetail = () => {
    const router = useRouter();
    const { id } = router.query; // Get the article ID from the URL
    const [article, setArticle] = useState(null);

    useEffect(() => {
        if (id) {
            const storedArticles = localStorage.getItem('articles');
            const articles = storedArticles ? JSON.parse(storedArticles) : [];
            const selectedArticle = articles.find(article => article.id === id);
            setArticle(selectedArticle);
        }
    }, [id]);

    if (!article) return <p>Loading...</p>;

    return (
        <>
            <Navbar />
            <div className="container mx-auto px-4 sm:px-6 lg:px-8 py-8">
                <div className="bg-white shadow-md rounded-lg p-6">
                    <h1 className="text-3xl font-semibold mb-4">{article.title}</h1>
                    <p className="text-gray-700 text-sm mb-2">By {article.author}</p>
                    <p className="text-gray-500 text-xs mb-4">Published on 
                    {new Date(article.publicationDate).toLocaleDateString()}</p>
                    <div className="mb-4">
                        <img className="w-full h-auto max-h-64 object-contain" 
                     src={article.image} alt={article.title} />
                    </div>
                    <div className="text-gray-700 text-sm mb-4">
                        <p>{article.content}</p>
                    </div>
                    <Link href="/">
                        <span className="py-2 px-4 bg-blue-600 text-white 
         font-semibold rounded-lg hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500">
                            Back to Home
                        </span>
                    </Link>
                </div>
            </div>
        </>
    );
};

export default ArticleDetail;

Step 3: Start your application using the following command.

npm run dev

Output:


Next Article

Similar Reads