توسعه فرانت سایت خبری با API و جاوا اسکریپت

Please login to bookmark Close

در پروژه‌های تجاری امروز، front-end و back-end جدا از هم توسعه داده می‌شوند تا هر بخش تخصصی‌تر و حرفه‌ای‌تر پیش برود. ارتباط این دو هم از طریق API انجام می‌شود. در این مطلب می‌خواهم نشان بدهم چطور می‌شود یک پروژۀ واقعی را با همین روش ساخت. برای این کار همه‌چیز را بسیار ساده نگه داشته‌ام و APIهایی ابتدایی طراحی کرده‌ام تا بدون درگیر شدن با پیچیدگی‌ها، مسیر اصلی را دنبال کنیم.

راه اندازی پروژه

راه اندازی بکند

برای بکند این پروژه، از همان پروژه ای که در بخش «توسعه فرانت سایت خبری به روش سنتی با جنگو» راه اندازی کردیم استفاده خواهیم کرد.

بدین منظور، اگر آن پروژه را قبلا راه اندازی کرده‌اید فقط کافی است آن را اجرا کنید. در غیر اینصورت، می‌توانید مراحل راه اندازی آن را از اینجا دنبال کنید.

راه اندازی فرانت‌اند

برای فرانت‌اند این پروژه یک قالب از قبل آماده کرده‌ام. برای دانلود، روی دکمه زیر کلیک کنید.

آشنایی با پروژه

پس از راه اندازی و اجرای پروژه، با باز کردن آدرس زیر، می‌توانید لیست همه API های مورد نیاز را مشاهده کنید.

http://localhost:8000/swagger/

همه توسعه‌هایی که در این بخش انجام خواهیم داد، در کدبیس فرانت انجام خواهد شد.

تسک اول – توسعه صفحه اصلی

شرح تسک اول

صفحه اصلی را طوری توسعه دهید که خروجی نهایی شبیه تصویر زیر باشد.

انجام تسک اول – توسعه صفحه اصلی

تسک اول نسبت به تسک های دیگر ساده‌تر ولی بزرگ‌تر است. برای سهولت خودمان، بهتر است توسعه این تسک را در چند بخش به شرح زیر تقسیم بندی کنیم.

  • بخش اول: ابتدا نمایش لیست اخبار که مهم‌ترین بخش صفحه است.
  • بخش دوم: سپس توسعه دکمه های صفحه‌بندی که بدون آنها نمایش سایر اخبار امکان پذیر نیست.
  • بخش سوم: در نهایت فرم جستجو و فیلتر

تقسیم‌بندی و اولویت بندی فوق را بر اساس میزان سادگی و اهمیت هر بخش از این صفحه انجام داده‌ام.

تسک اول، بخش اول – توسعه لیست اخبار

فایل index.html را باز کرده و کد زیر را در آن قرار دهید.

<!doctype html>
<html lang="en" dir="ltr">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>codebaz — News List</title>
    <link rel="stylesheet" href="style.css" />
  </head>
  <body>
    <header class="navbar">
      <a href="index.html" class="brand">📰 codebaz</a>
      <a href="add-news.html" class="btn btn-primary">➕ Add News</a>
    </header>

    <main class="container">
      <div class="page-header">
        <h2>📋 Latest News</h2>
      </div>
      <h2>Add your content here</h2>
    </main>

    <script>
      fetch("http://localhost:8000/api/news")
        .then((response) => response.json())
        .then((data) => {
          console.log(data);
        })
        .catch((err) => console.error(err));
    </script>
  </body>
</html>

در کد بالا:

  • خط ۲۲ تا ۲۹: یک تگ script تعریف شده تا بتوانیم کد های js را در آن قرار دهیم. مرورگر کد های این فایل را خط به خط می‌خواند و اجرا می‌کند و در آخرین بخش جاوا اسکریپت هایی که در این تگ نوشته شده را اجرا می‌کند.
  • خط ۲۳: با استفاده از تابع fetch لیست اخبار را از بکند درخواست می‌کنیم.
  • خط ۲۴: منتظر می‌مانیم تا نتیجه از بکند برسد.
  • خط ۲۵: منتظر می‌مانیم تا نتیجه ای که از بکند رسیده، به فرمت json تبدیل شود.
  • خط ۲۶: نتیجه ای که از بکند آماده و در قالب json در آمده را در کنسول مرورگر چاپ می‌کنیم.
  • خط ۲۸: در صورتی که هر یک از مراحل فوق با مشکل مواجه شود، پیغام خطا را در کنسول چاپ می‌کنیم.

اکنون اگر این صفحه را در مرورگر را باز کرده و به تب console بروید، خروجی زیر را خواهید دید.

در تصویر بالا:

  • بخش اول پاسخی که از بکند دریافت شده را نمایش می‌دهد.
  • بخش دوم آدرس کدی که این خروجی از آنجا آمده را نمایش می‌دهد. در اینجا می‌گوید که این خروجی مربوط به خط ۲۶ فایل index.html است.

اکنون کد بالا را به شکل زیر توسعه دهید.

<!doctype html>
<html lang="en" dir="ltr">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>codebaz — News List</title>
    <link rel="stylesheet" href="style.css" />
  </head>
  <body>
    <header class="navbar">
      <a href="index.html" class="brand">📰 codebaz</a>
      <a href="add-news.html" class="btn btn-primary">➕ Add News</a>
    </header>

    <main class="container">
      <div class="page-header">
        <h2>📋 Latest News</h2>
      </div>
      <div id="result"></div>
    </main>

    <script>
      fetch("http://localhost:8000/api/news")
        .then((response) => response.json())
        .then((data) => {
          const container = document.getElementById("result");
          data.results.forEach((news) => {
            const imgSrc = news.image || `https://placehold.co/150x150/0f5858/white?text=${news.category}`;
            var newsCard = `
              <div class="news-row">
                <img class="news-thumb" src="${imgSrc}">
                <div class="news-info">
                    <a href="news-detail.html?id=${news.id}" class="title-link">${news.title}</a>
                    <div class="meta">
                      <span>📅 ${news.publish_date}</span>
                      <span class="category-badge cat-${news.category.toLowerCase()}">${news.category}</span>
                    </div>
                </div>
                <a href="news-detail.html?id=${news.id}" class="detail-btn">Details →</a>
              </div>
            `;
            container.insertAdjacentHTML("beforeend", newsCard);
          });
        })
        .catch((err) => console.error(err));
    </script>
  </body>
</html>

در کد بالا:

  • خط ۱۹: یک div برای نمایش خروجی api در نظر گرفته‌ایم.
  • خط ۲۶: div ای که برای نمایش خروجی در نظر گرفته‌ایم را انتخاب می‌کنیم.
  • خط ۲۷: یک حلقه روی اخباری که از بکند گرفته‌ایم ایجاد کرده‌ایم.
  • خط ۲۸: در صورتی که خبر عکس نداشته باشد، یک عکس پیش‌فرض برای آن در نظر می‌گیریم.
  • خط ۲۹ تا ۴۱: کد مربوط به کارت خبر را به عنوان یک قالب با اطلاعات اخبار مقدار دهی کرده و در یک متغیر به نام newsCard ذخیره می‌کنیم.
  • خط ۴۲: newsCard را به div ای که برای نمایش نتیجه در نظر گرفته بودیم اضافه می‌کنیم.

خروجی کد بالا مشابه زیر خواهد بود.

تبریک! اولین بخش از تسک اول به پایان رسید.

تسک اول، بخش دوم – دکمه‌های صفحه بندی

اگر page=2? را در آدرس api قرار دهیم، دومین صفحه از لیست اخبار نمایش داده خواهد شد.

<script>
  fetch("http://localhost:8000/api/news?page=2")
    .then((response) => response.json())
    .then((data) => {
      const container = document.getElementById("result");
      data.results.forEach((news) => {
        const imgSrc = news.image || `https://placehold.co/150x150/0f5858/white?text=${news.category}`;
        var newsCard = `
          <div class="news-row">
            <img class="news-thumb" src="${imgSrc}">
            <div class="news-info">
                <a href="news-detail.html?id=${news.id}" class="title-link">${news.title}</a>
                <div class="meta">
                  <span>📅 ${news.publish_date}</span>
                  <span class="category-badge cat-${news.category.toLowerCase()}">${news.category}</span>
                </div>
            </div>
            <a href="news-detail.html?id=${news.id}" class="detail-btn">Details →</a>
          </div>
        `;
        container.insertAdjacentHTML("beforeend", newsCard);
      });
    })
    .catch((err) => console.error(err));
</script>

برای اینکه بتوانیم صفحات دیگر را نمایش دهیم، کافیست کاری کنیم که بتوان این عدد را به صورت داینامیک با یک دکمه تغییر داد. بدین منظور کد را به شکل زیر اصلاح کنید.

<!doctype html>
<html lang="en" dir="ltr">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>codebaz - News List</title>
    <link rel="stylesheet" href="style.css" />
  </head>
  <body>
    <header class="navbar">
      <a href="index.html" class="brand">📰 codebaz</a>
      <a href="add-news.html" class="btn btn-primary">➕ Add News</a>
    </header>

    <main class="container">
      <div class="page-header">
        <h2>📋 Latest News</h2>
      </div>
      <div id="result"></div>
      <div class="pagination">
        <a onclick="handlePage(1)" href="#!">1</a>
        <a onclick="handlePage(2)" href="#!">2</a>
        <a onclick="handlePage(3)" href="#!">3</a>
      </div>
    </main>

    <script>
      function handlePage (pageNumber=1) {
        fetch("http://localhost:8000/api/news?page=" + pageNumber)
          .then((response) => response.json())
          .then((data) => {
            const container = document.getElementById("result");
            data.results.forEach((news) => {
              const imgSrc = news.image || `https://placehold.co/150x150/0f5858/white?text=${news.category}`;
              var newsCard = `
                <div class="news-row">
                  <img class="news-thumb" src="${imgSrc}">
                  <div class="news-info">
                      <a href="news-detail.html?id=${news.id}" class="title-link">${news.title}</a>
                      <div class="meta">
                        <span>📅 ${news.publish_date}</span>
                        <span class="category-badge cat-${news.category.toLowerCase()}">${news.category}</span>
                      </div>
                  </div>
                  <a href="news-detail.html?id=${news.id}" class="detail-btn">Details →</a>
                </div>
              `;
              container.insertAdjacentHTML("beforeend", newsCard);
            });
          })
          .catch((err) => console.error(err));
      }
      handlePage()
    </script>
  </body>
</html>

در کد بالا یک تابع به نام handlePage تعریف کردیم که شماره صفحه را گرفته و اخبار آن صفحه را نمایش می‌دهد.

  • خط ۲۰ تا ۲۴: چند دکمه برای جا به جایی بین صفحات طراحی کردیم. هر کدام از این دکمه‌ها، handlePage را با ورودی متناسبی فراخوانی می‌کنند.
  • خط ۲۸: تابع handlePage در اینجا تعریف شده و عدد 1 به عنوان مقدار پیش‌فرض ورودی آن در نظر گرفته شده است.
  • خط ۲۹: ورودی تابع که pageNumber نام دارد را به آدرس api میدهیم تا صفحه مورد نظر از بکند فراخوانی شود.
  • خط ۵۳: تابعی که تعریف کردیم را یک بار خودمان صرف نظر از اینکه روی دکمه ای کلیک شده باشد فراخوانی می‌کنیم تا هنگامی که صفحه برای اولین بار لود می‌شود خالی نباشد.

اکنون مشکل این است که با هر بار کلیک روی دکمه‌ها، لیست اخبار بلند تر می‌شود. این حالت برای زمانی که بخواهیم دکمه «نمایش بیشتر» را پیاده سازی کنیم خوب است اما در اینجا ما میخواهیم که صفحه بندی داشته باشیم پس این در حال حاضر به درد ما نمی‌خورد.

برای جلوگیری از بلند شدن لیست اخبار و نمایش اخبار صفحه جدید به جای اخبار صفحه قبلی، کد را به شکل زیر اصلاح کنید.

<script>
  function handlePage (pageNumber=1) {
    fetch("http://localhost:8000/api/news?page=" + pageNumber)
      .then((response) => response.json())
      .then((data) => {
        const container = document.getElementById("result");
        var cards = ''
        data.results.forEach((news) => {
          const imgSrc = news.image || `https://placehold.co/150x150/0f5858/white?text=${news.category}`;
          cards += `
            <div class="news-row">
              <img class="news-thumb" src="${imgSrc}">
              <div class="news-info">
                  <a href="news-detail.html?id=${news.id}" class="title-link">${news.title}</a>
                  <div class="meta">
                    <span>📅 ${news.publish_date}</span>
                    <span class="category-badge cat-${news.category.toLowerCase()}">${news.category}</span>
                  </div>
              </div>
              <a href="news-detail.html?id=${news.id}" class="detail-btn">Details →</a>
            </div>
          `;
        });
        container.innerHTML = cards
      })
      .catch((err) => console.error(err));
  }
  handlePage()
</script>

در کد بالا، همه کارت هایی که باید در یک صفحه نمایش داده شود را در یک متغیر به نام cards ذخیره کرده و در نهایت خارج از حلقه در خط ۵۰ به عنوان محتوای HTML در container قرار می‌دهیم.

اکنون مشکل این است که فقط می‌توان بین ۳ صفحه اول جا به جا شد. یعنی راهی برای رفتن به صفحات ۴ به بعد وجود ندارد. برای حل این مشکل، کد را به شکل زیر اصلاح کنید.

<!doctype html>
<html lang="en" dir="ltr">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>codebaz - News List</title>
    <link rel="stylesheet" href="style.css" />
  </head>
  <body>
    <header class="navbar">
      <a href="index.html" class="brand">📰 codebaz</a>
      <a href="add-news.html" class="btn btn-primary">➕ Add News</a>
    </header>

    <main class="container">
      <div class="page-header">
        <h2>📋 Latest News</h2>
      </div>
      <div id="result"></div>
      <div id="pagination" class="pagination"></div>
    </main>

    <script>
      const PAGE_SIZE = 4;
      var paginationContainer = document.getElementById("pagination");

      function handlePage(pageNumber = 1) {
        fetch("http://localhost:8000/api/news?page=" + pageNumber)
          .then((response) => response.json())
          .then((data) => {
            var pageCount = Math.floor(data.count / PAGE_SIZE) + 1;
            var paginationButtons = ''
            if (pageCount === 2) {
              paginationButtons = `
                <a onclick="handlePage(1)" class="${pageNumber === 1 && 'active'}" href="#!">1</a>
                <a onclick="handlePage(2)" class="${pageNumber === 2 && 'active'}" href="#!">2</a>`;
            } else if (pageCount > 2 && pageNumber === 1) {
                paginationButtons = `
                <a onclick="handlePage(1)" class="active" href="#!">1</a>
                <a onclick="handlePage(2)" href="#!">2</a>
                <a onclick="handlePage(3)" href="#!">3</a>`;
            } else {
              paginationButtons = `
                <a onclick="handlePage(${pageNumber - 1})" href="#!">${pageNumber - 1}</a>
                <a onclick="handlePage(${pageNumber})" class="active" href="#!">${pageNumber}</a>
                <a onclick="handlePage(${pageNumber + 1})" href="#!">${pageNumber + 1}</a>`;
            }
            paginationContainer.innerHTML = paginationButtons;

            const container = document.getElementById("result");
            var cards = "";
            data.results.forEach((news) => {
              const imgSrc = news.image || `https://placehold.co/150x150/0f5858/white?text=${news.category}`;
              cards += `
                <div class="news-row">
                  <img class="news-thumb" src="${imgSrc}">
                  <div class="news-info">
                      <a href="news-detail.html?id=${news.id}" class="title-link">${news.title}</a>
                      <div class="meta">
                        <span>📅 ${news.publish_date}</span>
                        <span class="category-badge cat-${news.category.toLowerCase()}">${news.category}</span>
                      </div>
                  </div>
                  <a href="news-detail.html?id=${news.id}" class="detail-btn">Details →</a>
                </div>
              `;
            });
            container.innerHTML = cards;
          })
          .catch((err) => console.error(err));
      }
      handlePage();
    </script>
  </body>
</html>

در کد بالا

  • خط ۲۰: یک id به div داده‌ایم و محتوای درونش را پاک کرده‌ایم تا بعدا با جاوا اسکریپت آن را بگیریم و دکمه ها را درونش اضافه کنیم.
  • خط ۲۴: تعداد آیتم در هر صفحه را برای محاسبات بعدی ذخیره می‌کنیم.
  • خط ۳۱: تعداد کل صفحات را محاسبه می‌کنیم.
  • خط ۳۲: یک متغیر ساخته‌ایم تا کد دکمه‌های صفحه بندی را در آن قرار دهیم.
  • خط ۳۳ تا ۳۶: اگر فقط دو صفحه داشتیم فقط دو دکمه، یکی برای رفتن به صفحه اول و دیگری برای رفتن به صفحه دوم در آن می‌سازیم.
  • خط ۳۷ تا ۴۱: اگر بیش از دو صفحه داشتیم و در حال نمایش اولین صفحه بودیم، ۳ دکمه برای رفتن به صفحات اول تا سوم طراحی می‌کنیم.
  • خط ۴۲ تا ۴۷: در غیر اینصورت (اگر بیش از دو صفحه داشتیم و در حال نمایش صفحه ای به جز صفحه اول بودیم)، ۳ دکمه به ترتیب زیر می‌سازیم.
    • دکمه اول برای رفتن به یک صفحه قبل (pageNumber - 1)
    • دکمه دوم برای نمایش صفحه فعلی (pageNumber)
    • دکمه سوم برای نمایش صفحه بعد (pageNumber + 1)

خدارو شکر بالاخره توانستیم صفحه بندی را توسعه دهیم. فقط مشکلی که در حال حاضر داریم این است که این صفحه بندی کاملا شبیه به چیزی که خواسته شده نیست. چیزی که خواسته شده دکمه‌های Next و Previous دارد. اکنون وقت آن است که این دکمه ها را هم پیاده سازی کنیم.

برای افزودن دکمه Next و Previous کد را به شکل زیر اصلاح کنید.

<!doctype html>
<html lang="en" dir="ltr">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>codebaz - News List</title>
    <link rel="stylesheet" href="style.css" />
  </head>
  <body>
    <header class="navbar">
      <a href="index.html" class="brand">📰 codebaz</a>
      <a href="add-news.html" class="btn btn-primary">➕ Add News</a>
    </header>

    <main class="container">
      <div class="page-header">
        <h2>📋 Latest News</h2>
      </div>
      <div id="result"></div>
      <div id="pagination" class="pagination"></div>
    </main>

    <script>
      const PAGE_SIZE = 4;
      var paginationContainer = document.getElementById("pagination");

      function handlePage(pageNumber = 1) {
        fetch("http://localhost:8000/api/news?page=" + pageNumber)
          .then((response) => response.json())
          .then((data) => {
            var pageCount = Math.floor(data.count / PAGE_SIZE) + 1;
            var paginationButtons = ''
            if (pageCount === 2) {
              paginationButtons = `
                <a onclick="handlePage(1)" class="${pageNumber === 1 && 'active'}" href="#!">1</a>
                <a onclick="handlePage(2)" class="${pageNumber === 2 && 'active'}" href="#!">2</a>`;
            } else if (pageCount > 2 && pageNumber === 1) {
                paginationButtons = `
                <span class="disabled">← Previous</span>
                <a onclick="handlePage(1)" class="active" href="#!">1</a>
                <a onclick="handlePage(2)" href="#!">2</a>
                <a onclick="handlePage(3)" href="#!">3</a>
                <a onclick="handlePage(2)" href="#!">Next →</a>`;
            } else {
              paginationButtons = `
                <a onclick="handlePage(${pageNumber - 1})" href="#!">← Previous</a>
                <a onclick="handlePage(${pageNumber - 1})" href="#!">${pageNumber - 1}</a>
                <a onclick="handlePage(${pageNumber})" class="active" href="#!">${pageNumber}</a>
                <a onclick="handlePage(${pageNumber + 1})" href="#!">${pageNumber + 1}</a>
                <a onclick="handlePage(${pageNumber + 1})" href="#!">Next →</a>`;
            }
            paginationContainer.innerHTML = paginationButtons;

            const container = document.getElementById("result");
            var cards = "";
            data.results.forEach((news) => {
              const imgSrc = news.image || `https://placehold.co/150x150/0f5858/white?text=${news.category}`;
              cards += `
                <div class="news-row">
                  <img class="news-thumb" src="${imgSrc}">
                  <div class="news-info">
                      <a href="news-detail.html?id=${news.id}" class="title-link">${news.title}</a>
                      <div class="meta">
                        <span>📅 ${news.publish_date}</span>
                        <span class="category-badge cat-${news.category.toLowerCase()}">${news.category}</span>
                      </div>
                  </div>
                  <a href="news-detail.html?id=${news.id}" class="detail-btn">Details →</a>
                </div>
              `;
            });
            container.innerHTML = cards;
          })
          .catch((err) => console.error(err));
      }
      handlePage();
    </script>
  </body>
</html>

تبریک! بالاخره این بخش به پایان رسید.

تسک اول، بخش سوم – فرم جستجو

اگر کد صفحه را به شکل زیر اصلاح کنید، همه اخباری که در عنوان یا توضیحاتشان عبارت National وجود دارد لیست می‌شوند.

<script>
  const PAGE_SIZE = 4;
  var paginationContainer = document.getElementById("pagination");

  function handlePage(pageNumber = 1) {
    fetch("http://localhost:8000/api/news?page=" + pageNumber + '&search=National')
      .then((response) => response.json())
      .then((data) => {
        var pageCount = Math.floor(data.count / PAGE_SIZE) + 1;
        var paginationButtons = ''
        if (pageCount === 2) {
          paginationButtons = `
            <a onclick="handlePage(1)" class="${pageNumber === 1 && 'active'}" href="#!">1</a>
            <a onclick="handlePage(2)" class="${pageNumber === 2 && 'active'}" href="#!">2</a>`;
        } else if (pageCount > 2 && pageNumber === 1) {
            paginationButtons = `
            <span class="disabled">← Previous</span>
            <a onclick="handlePage(1)" class="active" href="#!">1</a>
            <a onclick="handlePage(2)" href="#!">2</a>
            <a onclick="handlePage(3)" href="#!">3</a>
            <a onclick="handlePage(2)" href="#!">Next →</a>`;
        } else {
          paginationButtons = `
            <a onclick="handlePage(${pageNumber - 1})" href="#!">← Previous</a>
            <a onclick="handlePage(${pageNumber - 1})" href="#!">${pageNumber - 1}</a>
            <a onclick="handlePage(${pageNumber})" class="active" href="#!">${pageNumber}</a>
            <a onclick="handlePage(${pageNumber + 1})" href="#!">${pageNumber + 1}</a>
            <a onclick="handlePage(${pageNumber + 1})" href="#!">Next →</a>`;
        }
        paginationContainer.innerHTML = paginationButtons;

        const container = document.getElementById("result");
        var cards = "";
        data.results.forEach((news) => {
          const imgSrc = news.image || `https://placehold.co/150x150/0f5858/white?text=${news.category}`;
          cards += `
            <div class="news-row">
              <img class="news-thumb" src="${imgSrc}">
              <div class="news-info">
                  <a href="news-detail.html?id=${news.id}" class="title-link">${news.title}</a>
                  <div class="meta">
                    <span>📅 ${news.publish_date}</span>
                    <span class="category-badge cat-${news.category.toLowerCase()}">${news.category}</span>
                  </div>
              </div>
              <a href="news-detail.html?id=${news.id}" class="detail-btn">Details →</a>
            </div>
          `;
        });
        container.innerHTML = cards;
      })
      .catch((err) => console.error(err));
  }
  handlePage();
</script>

در تکه کد بالا، با قرار دادن &search=National در آدرس درخواستی، فقط اخباری لیست می‌شوند که عبارت National در عنوان یا توضیحاتشان وجود داشته باشد.

اگر بتوانیم این عملیات را با استفاده از جاوا اسکریپت به صورت داینامیک انجام دهیم، سرچ پیاده سازی خواهد شد.

برای پیاده سازی جستجو، کد را به شکل زیر اصلاح کنید.

<!doctype html>
<html lang="en" dir="ltr">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>codebaz - News List</title>
    <link rel="stylesheet" href="style.css" />
  </head>
  <body>
    <header class="navbar">
      <a href="index.html" class="brand">📰 codebaz</a>
      <a href="add-news.html" class="btn btn-primary">➕ Add News</a>
    </header>

    <main class="container">
      <div class="page-header">
        <h2>📋 Latest News</h2>
      </div>
      <div class="toolbar">
        <input type="text" name="search" class="search-input" placeholder="Search by title..." />
        <button onclick="handleSearch()" class="apply-btn">🔍 Apply Filters</button>
      </div>
      <div id="result"></div>
      <div id="pagination" class="pagination"></div>
    </main>

    <script>
      var SEARCH_TERM = "";
      function handleSearch() {
        var searchInput = document.querySelector('[name="search"]');
        handlePage(1, searchInput.value);
      }

      const PAGE_SIZE = 4;
      var paginationContainer = document.getElementById("pagination");

      function handlePage(pageNumber = 1, searchTerm = "") {
        fetch("http://localhost:8000/api/news?page=" + pageNumber + "&search=" + searchTerm)
          .then((response) => response.json())
          .then((data) => {
            var pageCount = Math.floor(data.count / PAGE_SIZE) + 1;
            var paginationButtons = "";
            if (pageCount === 2) {
              paginationButtons = `
                <a onclick="handlePage(1)" class="${pageNumber === 1 && "active"}" href="#!">1</a>
                <a onclick="handlePage(2)" class="${pageNumber === 2 && "active"}" href="#!">2</a>`;
            } else if (pageCount > 2 && pageNumber === 1) {
              paginationButtons = `
                <span class="disabled">← Previous</span>
                <a onclick="handlePage(1)" class="active" href="#!">1</a>
                <a onclick="handlePage(2)" href="#!">2</a>
                <a onclick="handlePage(3)" href="#!">3</a>
                <a onclick="handlePage(2)" href="#!">Next →</a>`;
            } else {
              paginationButtons = `
                <a onclick="handlePage(${pageNumber - 1})" href="#!">← Previous</a>
                <a onclick="handlePage(${pageNumber - 1})" href="#!">${pageNumber - 1}</a>
                <a onclick="handlePage(${pageNumber})" class="active" href="#!">${pageNumber}</a>
                <a onclick="handlePage(${pageNumber + 1})" href="#!">${pageNumber + 1}</a>
                <a onclick="handlePage(${pageNumber + 1})" href="#!">Next →</a>`;
            }
            paginationContainer.innerHTML = paginationButtons;

            const container = document.getElementById("result");
            var cards = "";
            data.results.forEach((news) => {
              const imgSrc = news.image || `https://placehold.co/150x150/0f5858/white?text=${news.category}`;
              cards += `
                <div class="news-row">
                  <img class="news-thumb" src="${imgSrc}">
                  <div class="news-info">
                      <a href="news-detail.html?id=${news.id}" class="title-link">${news.title}</a>
                      <div class="meta">
                        <span>📅 ${news.publish_date}</span>
                        <span class="category-badge cat-${news.category.toLowerCase()}">${news.category}</span>
                      </div>
                  </div>
                  <a href="news-detail.html?id=${news.id}" class="detail-btn">Details →</a>
                </div>
              `;
            });
            container.innerHTML = cards;
          })
          .catch((err) => console.error(err));
      }
      handlePage();
    </script>
  </body>
</html>

در کد بالا

  • خط ۱۹ تا ۲۲: یک فرم (بدون تگ form) شامل یک اینپوت برای وارد کردن متن مورد جستجو و یک دکمه برای ثبت آن در نظر گرفته‌ایم.
  • خط ۲۸: یک متغیر برای نگه داشتن مقداری که جستجو می‌شود در نظر گرفته‌ایم.
  • خط ۲۹: یک تابع به نام handleSearch توسعه داده‌ایم که هر وقت روی دکمه کلیک شود فراخوانی می‌شود.
  • خط ۳۰: اینپوتی که اتریبیوت name آن برابر search است را میگیریم.
  • خط ۳۱: تابع handlePage را به نحوی فراخوانی می‌کنیم که اولین صفحه ای که جستجوی ما را در بر می‌گیرد نمایش دهد.
  • خط ۳۷: تابع handlePage را به نحوی اصلاح کرده‌ایم که مقدار جستجو را بتوان به عنوان ورودی به آن ارسال کرد.
  • خط ۳۸: مقداری که جستجو شده است را نیز به درخواست خود از api اضافه می‌کنیم.

اکنون جستجو کار می‌کند اما اگر پس از جستجو برای دیدن اخبار صفحات بعد، روی دکمه های صفحه بندی کلیک کنید، متوجه خواهید شد که جستجو در صفحات دیگر اعمال نمی‌شود. دلیل آن این است که این دکمه ها، صرفا شماره صفحه را به تابع handlePage ارسال می‌کنند و عبارت جستجو شده را ارسال نمی‌کنند.

برای حل این مشکل، کد را به شکل زیر اصلاح کنید.

function handlePage(pageNumber = 1, searchTerm = "") {
    fetch("http://localhost:8000/api/news?page=" + pageNumber + "&search=" + searchTerm)
      .then((response) => response.json())
      .then((data) => {
        var pageCount = Math.floor(data.count / PAGE_SIZE) + 1;
        var paginationButtons = "";
        if (pageCount === 2) {
          paginationButtons = `
            <a onclick="handlePage(1, '${searchTerm}')" class="${pageNumber === 1 && "active"}" href="#!">1</a>
            <a onclick="handlePage(2, '${searchTerm}')" class="${pageNumber === 2 && "active"}" href="#!">2</a>`;
        } else if (pageCount > 2 && pageNumber === 1) {
          paginationButtons = `
            <span class="disabled">← Previous</span>
            <a onclick="handlePage(1, '${searchTerm}')" class="active" href="#!">1</a>
            <a onclick="handlePage(2, '${searchTerm}')" href="#!">2</a>
            <a onclick="handlePage(3, '${searchTerm}')" href="#!">3</a>
            <a onclick="handlePage(2, '${searchTerm}')" href="#!">Next →</a>`;
        } else {
          paginationButtons = `
            <a onclick="handlePage(${pageNumber - 1}, '${searchTerm}')" href="#!">← Previous</a>
            <a onclick="handlePage(${pageNumber - 1}, '${searchTerm}')" href="#!">${pageNumber - 1}</a>
            <a onclick="handlePage(${pageNumber}, '${searchTerm}')" class="active" href="#!">${pageNumber}</a>
            <a onclick="handlePage(${pageNumber + 1}, '${searchTerm}')" href="#!">${pageNumber + 1}</a>
            <a onclick="handlePage(${pageNumber + 1}, '${searchTerm}')" href="#!">Next →</a>`;
        }

هنوز یک مشکل دیگر باقی است. اگر چیزی را جستجو کنیم که کمتر از ۵ نتیجه داشته باشد، نمایش دکمه‌های صفحه بندی با مشکل مواجه خواهد شد زیرا در این حالت نتایج تنها در یک صفحه خلاصه می‌شود ولی ما اصلا حالتی که یک صفحه را به درستی نمایش دهد در نظر نگرفته ایم.

برای حل این مشکل، کد را به شکل زیر اصلاح کنید.

function handlePage(pageNumber = 1, searchTerm = "") {
  fetch("http://localhost:8000/api/news?page=" + pageNumber + "&search=" + searchTerm)
    .then((response) => response.json())
    .then((data) => {
      var pageCount = Math.floor(data.count / PAGE_SIZE) + 1;
      var paginationButtons = "";
      if (pageCount === 1) {
        paginationButtons = ''
      } else if (pageCount === 2) {
        paginationButtons = `
          <a onclick="handlePage(1, '${searchTerm}')" class="${pageNumber === 1 && "active"}" href="#!">1</a>
          <a onclick="handlePage(2, '${searchTerm}')" class="${pageNumber === 2 && "active"}" href="#!">2</a>`;
      } else if (pageCount > 2 && pageNumber === 1) {
        paginationButtons = `
          <span class="disabled">← Previous</span>
          <a onclick="handlePage(1, '${searchTerm}')" class="active" href="#!">1</a>
          <a onclick="handlePage(2, '${searchTerm}')" href="#!">2</a>
          <a onclick="handlePage(3, '${searchTerm}')" href="#!">3</a>
          <a onclick="handlePage(2, '${searchTerm}')" href="#!">Next →</a>`;
      } else {
        paginationButtons = `
          <a onclick="handlePage(${pageNumber - 1}, '${searchTerm}')" href="#!">← Previous</a>
          <a onclick="handlePage(${pageNumber - 1}, '${searchTerm}')" href="#!">${pageNumber - 1}</a>
          <a onclick="handlePage(${pageNumber}, '${searchTerm}')" class="active" href="#!">${pageNumber}</a>
          <a onclick="handlePage(${pageNumber + 1}, '${searchTerm}')" href="#!">${pageNumber + 1}</a>
          <a onclick="handlePage(${pageNumber + 1}, '${searchTerm}')" href="#!">Next →</a>`;
      }
      paginationContainer.innerHTML = paginationButtons;

در تکه کد بالا کاری کرده‌ایم که اگر تعداد نتایج کمتر از یک صفحه بود، دکمه‌های صفحه بندی نمایش داده نشوند.

اکنون نوبت فیلترینگ است. برای افزودن امکان فیلتر کردن اخبار بر اساس دسته آن کد را به شکل زیر اصلاح کنید.

<!doctype html>
<html lang="en" dir="ltr">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>codebaz - News List</title>
    <link rel="stylesheet" href="style.css" />
  </head>
  <body>
    <header class="navbar">
      <a href="index.html" class="brand">📰 codebaz</a>
      <a href="add-news.html" class="btn btn-primary">➕ Add News</a>
    </header>

    <main class="container">
      <div class="page-header">
        <h2>📋 Latest News</h2>
      </div>
      <div class="toolbar">
        <input type="text" name="search" class="search-input" placeholder="Search by title..." />
        <select name="category" class="filter-select">
          <option value="">All Categories</option>
          <option value="cultural">Cultural</option>
          <option value="political">Political</option>
          <option value="sports">Sports</option>
          <option value="scientific">Scientific</option>
        </select>
        <button onclick="handleSearch()" class="apply-btn">🔍 Apply Filters</button>
      </div>
      <div id="result"></div>
      <div id="pagination" class="pagination"></div>
    </main>

    <script>
      var SEARCH_TERM = "";
      function handleSearch() {
        var searchInput = document.querySelector('[name="search"]');
        var category = document.querySelector('[name="category"]');
        handlePage(1, searchInput.value, category.value);
      }

      const PAGE_SIZE = 4;
      var paginationContainer = document.getElementById("pagination");

      function handlePage(pageNumber = 1, searchTerm = "", category = "") {
        fetch("http://localhost:8000/api/news?page=" + pageNumber + "&search=" + searchTerm + '&category=' + category)
          .then((response) => response.json())
          .then((data) => {
            var pageCount = Math.floor(data.count / PAGE_SIZE) + 1;
            var paginationButtons = "";
            if (pageCount === 1) {
              paginationButtons = "";
            } else if (pageCount === 2) {
              paginationButtons = `
                <a onclick="handlePage(1, '${searchTerm}')" class="${pageNumber === 1 && "active"}" href="#!">1</a>
                <a onclick="handlePage(2, '${searchTerm}')" class="${pageNumber === 2 && "active"}" href="#!">2</a>`;
            } else if (pageCount > 2 && pageNumber === 1) {
              paginationButtons = `
                <span class="disabled">← Previous</span>
                <a onclick="handlePage(1, '${searchTerm}')" class="active" href="#!">1</a>
                <a onclick="handlePage(2, '${searchTerm}')" href="#!">2</a>
                <a onclick="handlePage(3, '${searchTerm}')" href="#!">3</a>
                <a onclick="handlePage(2, '${searchTerm}')" href="#!">Next →</a>`;
            } else {
              paginationButtons = `
                <a onclick="handlePage(${pageNumber - 1}, '${searchTerm}')" href="#!">← Previous</a>
                <a onclick="handlePage(${pageNumber - 1}, '${searchTerm}')" href="#!">${pageNumber - 1}</a>
                <a onclick="handlePage(${pageNumber}, '${searchTerm}')" class="active" href="#!">${pageNumber}</a>
                <a onclick="handlePage(${pageNumber + 1}, '${searchTerm}')" href="#!">${pageNumber + 1}</a>
                <a onclick="handlePage(${pageNumber + 1}, '${searchTerm}')" href="#!">Next →</a>`;
            }
            paginationContainer.innerHTML = paginationButtons;

            const container = document.getElementById("result");
            var cards = "";
            data.results.forEach((news) => {
              const imgSrc = news.image || `https://placehold.co/150x150/0f5858/white?text=${news.category}`;
              cards += `
                <div class="news-row">
                  <img class="news-thumb" src="${imgSrc}">
                  <div class="news-info">
                      <a href="news-detail.html?id=${news.id}" class="title-link">${news.title}</a>
                      <div class="meta">
                        <span>📅 ${news.publish_date}</span>
                        <span class="category-badge cat-${news.category.toLowerCase()}">${news.category}</span>
                      </div>
                  </div>
                  <a href="news-detail.html?id=${news.id}" class="detail-btn">Details →</a>
                </div>
              `;
            });
            container.innerHTML = cards;
          })
          .catch((err) => console.error(err));
      }
      handlePage();
    </script>
  </body>
</html>

در کد بالا:

  • خط ۲۱ تا ۲۷: با استفاده از select-option یک منو برای انتخاب دسته‌ها ایجاد کرده‌ایم.
  • خط ۳۸: مقدار انتخاب شده را بر اساس اتریبیوت name آن می‌گیریم.
  • خط ۳۹: مقدار انتخاب شده را به عنوان سومین ورودی به تابع handlePage ارسال می‌کنیم.
  • خط ۴۵: امکان دریافت category را به عنوان سومین ورودی به تابع handlePage اضافه کرده‌ایم.
  • خط ۴۶: دسته انتخابی را به بکند ارسال می‌کنیم.

تبریک! اکنون دسته بندی نیز اضافه شد و توسعه این صفحه به پایان رسید.

تسک دوم – توسعه صفحه جزئیات

شرح تسک دوم

صفحه جزئیات خبر را طوری توسعه دهید که خروجی نهایی شبیه تصویر زیر باشد.

انجام تسک دوم

ابتدا ظاهر صفحه را با اطلاعات فیک درست می‌کنیم. برای این منظور، کد زیر را در صفحه news-detail.html وارد کنید.

<!doctype html>
<html lang="en" dir="ltr">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>codebaz — News Detail</title>
    <link rel="stylesheet" href="style.css" />
  </head>
  <body>
    <header class="navbar">
      <a href="index.html" class="brand">📰 codebaz</a>
      <a href="index.html" class="btn btn-outline">← Back to List</a>
    </header>

    <main class="container">
      <div class="detail-card">
        <a href="index.html" class="back-link">← Back to all news</a>
        <div class="detail-header">
          <img src="https://placehold.co/400x250/0f5858/white?text=Parliament" alt="News image" />
          <div class="detail-title">
            <h1>Parliament Budget Session Highlights</h1>
            <div class="meta">
              <span>📅 2026-05-15</span>
              <span class="category-badge cat-political">Political</span>
            </div>
          </div>
        </div>
        <div class="description">
          Lawmakers convened today to debate the annual budget proposal, focusing on healthcare and infrastructure
          spending. The session extended late into the evening with heated discussions over tax reforms. The final vote
          is expected next week.
        </div>
      </div>
    </main>
  </body>
</html>

با اضافه کردن کد بالا، ظاهر صفحه درست می‌شود ولی مشکل این است که فرقی ندارد شما کدام خبر را باز کنید. این محتوای ثابت برای همه خبر ها نمایش داده می‌شود. برای اینکه ظاهر این صفحه متناسب با محتوای خبر بارگذاری شود، کد را به شکل زیر اصلاح کنید.

<!doctype html>
<html lang="en" dir="ltr">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>codebaz — News Detail</title>
    <link rel="stylesheet" href="style.css" />
  </head>
  <body>
    <header class="navbar">
      <a href="index.html" class="brand">📰 codebaz</a>
      <a href="index.html" class="btn btn-outline">← Back to List</a>
    </header>

    <main class="container">
      <div class="detail-card">
        <a href="index.html" class="back-link">← Back to all news</a>
        <div id="news"></div>
      </div>
    </main>
    <script>
      const params = new URLSearchParams(window.location.search);
      const newsId = params.get('id');
      fetch("http://localhost:8000/api/news/" + newsId)
        .then((response) => response.json())
        .then((news) => {
          const imgSrc = news.image || `https://placehold.co/150x150/0f5858/white?text=${news.category}`;
          var newsHTML = `
          <div class="detail-header">
            <img src="${imgSrc}" />
            <div class="detail-title">
              <h1>${news.title}</h1>
              <div class="meta">
                <span>📅 ${news.publish_date}</span>
                <span class="category-badge cat-${news.category.toLowerCase()}">${news.category}</span>
              </div>
            </div>
          </div>
          <div class="description">
          ${news.description}
          </div>`

          var newsContainer = document.getElementById('news')
          newsContainer.innerHTML = newsHTML
        })
    </script>
  </body>
</html>

در کد بالا:

  • خط ۱۸: یک div در نظر گرفته ایم که کد های نمایش خبر را بعدا با جاوا اسکریپت به آن اضافه کنیم.
  • خط ۲۲ و ۲۳: فرض کنید id یک خبر برابر ۳۰۰ باشد، در این صورت اگر کاربر با کلیک روی دکمه detail از صفحه لیست اخبار به صفحه جزئیات خبر هدایت شده باشد، ?id=300 در آدرس صفحه قرار دارد که مشخص می‌کند قصد نمایش کدام خبر را داریم. این خط id خبر را می‌گیرد.
  • خط ۲۴ تا ۴۵: با استفاده از id ای که قبل تر گرفتیم، خبر مورد نظر را از بکند می‌گیریم، سپس کد های html آن را ساخته و در نهایت در آن div ای که خط ۱۸ گرفتیم وارد می‌کنیم.

اکنون برای افزودن دکمه ویرایش و حذف، کد را به شکل زیر اصلاح کنید.

<!doctype html>
<html lang="en" dir="ltr">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>codebaz — News Detail</title>
    <link rel="stylesheet" href="style.css" />
  </head>
  <body>
    <header class="navbar">
      <a href="index.html" class="brand">📰 codebaz</a>
      <a href="index.html" class="btn btn-outline">← Back to List</a>
    </header>

    <main class="container">
      <div class="detail-card">
        <a href="index.html" class="back-link">← Back to all news</a>
        <div id="news"></div>
      </div>
    </main>
    <script>
      const params = new URLSearchParams(window.location.search);
      const newsId = params.get("id");
      fetch("http://localhost:8000/api/news/" + newsId)
        .then((response) => response.json())
        .then((news) => {
          const imgSrc = news.image || `https://placehold.co/150x150/0f5858/white?text=${news.category}`;
          var newsHTML = `
          <div class="detail-header">
            <img src="${imgSrc}" />
            <div class="detail-title">
              <h1>${news.title}</h1>
              <div class="meta">
                <span>📅 ${news.publish_date}</span>
                <span class="category-badge cat-${news.category.toLowerCase()}">${news.category}</span>
              </div>
            </div>
          </div>
          <div class="description">
          ${news.description}
          </div>

          <br />
          <div class="btn btn-danger" onclick="handleDelete(${newsId})">
            🗑 Delete This Article
          </div>
          <a href="add-news.html?id=${newsId}" class="btn detail-btn">
            Edit
          </a>`;

          var newsContainer = document.getElementById("news");
          newsContainer.innerHTML = newsHTML;
        });

      function handleDelete(newsId) {
        fetch("http://localhost:8000/api/news/" + newsId + "/", {
          method: "DELETE",
        })
          .then((response) => {
            if (response.status === 204) {
              window.location.href = "index.html";
            } else {
              alert("There is a problem with deleting item!");
            }
          })
          .then((data) => console.log(data));
      }
    </script>
  </body>
</html>

در تکه کد بالا:

  • خط ۴۳: با تگ <br /> کمی فاصله بالای دکمه ها ایجاد کرده ایم.
  • خط ۴۴ تا ۴۶: یک دکمه که با کلیک روی آن تابع handleDelete اجرا شده و خبر را حذف می‌کند.
  • خط ۴۷ تا ۴۹: یک دکمه که صفحه ویرایش خبر را همراه با ارسال id خبر یه آن صفحه باز می‌کند.
  • خط ۵۵ تا ۶۷: یک تابع که آدرس حذف خبر را با متد DELETE فراخوانی کرده و اگر خبر با موفقیت پاک شود، کاربر را به صفحه لیست اخبار هدایت می‌کند.
    • خط ۵۷: در اینجا method را مشخص کرده ایم. اگر این خط نباشد، بکند متوجه نمی‌شود که ما میخواهیم خبر را پاک کنیم.
    • خط ۶۰: اگر خبر با موفقیت پاک شده باشد، بکند عدد 204 را به عنوان status خروجی می‌دهد. پس در اینجا، با استفاده از یک شرط، می‌توانیم بفهمیم که خبر به درستی پاک شده یا نه. اگر به درستی پاک شده بود، کاربر را با استفاده از کد خط ۶۱ به صفحه لیست اخبار هدایت می‌کنیم، در غیر اینصورت، با نمایش یک پیغام خطا، به کاربر اطلاع می‌دهیم که عملیات ناموفق بوده است.

تسک سوم – صفحه ویرایش خبر

شرح تسک سوم

صفحه ویرایش خبر را طوری توسعه دهید که خروجی نهایی شبیه تصویر زیر باشد.

انجام تسک سوم

صفحه add-news.html را باز کرده و کد را به شکل زیر اصلاح کنید.

<!doctype html>
<html lang="en" dir="ltr">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>codebaz — Add News</title>
    <link rel="stylesheet" href="style.css" />
  </head>
  <body>
    <header class="navbar">
      <a href="index.html" class="brand">📰 codebaz</a>
      <a href="index.html" class="btn btn-outline">← Back to List</a>
    </header>

    <main class="container">
      <div class="form-card">
        <h2>📝 Add New Article</h2>
        <form action="#" method="post" enctype="multipart/form-data">
          <div class="form-grid">
            <div class="form-group full-width">
              <label for="title">Title *</label>
              <input type="text" id="title" name="title" placeholder="Enter news title" required="" />
            </div>
            <div class="form-group full-width">
              <label for="desc">Description *</label>
              <textarea id="desc" name="description" placeholder="Full article text..." required=""></textarea>
            </div>
            <div class="form-group full-width">
              <label for="img">Image</label>
              <input type="file" id="img" name="image" accept="image/*" />
            </div>
            <div class="form-group">
              <label for="date">Publish Date *</label>
              <input type="date" id="date" name="publish_date" required="" />
            </div>
            <div class="form-group">
              <label for="category">Category *</label>
              <select id="category" name="category" required="">
                <option value="">-- Select --</option>
                <option value="cultural">Cultural</option>
                <option value="political">Political</option>
                <option value="sports">Sports</option>
                <option value="scientific">Scientific</option>
              </select>
            </div>
            <div class="form-group full-width">
              <label class="checkbox-wrap">
                <input type="checkbox" id="draft" name="saveAsDraft" />
                Save as Draft
              </label>
            </div>
          </div>
          <div class="form-actions">
            <button type="submit" class="btn btn-primary">💾 Save Article</button>
            <a href="index.html" class="btn btn-outline">Cancel</a>
          </div>
        </form>
      </div>
    </main>
  </body>
</html>

تکه کد بالا، ظاهر صفحه را مطابق چیزی که خواسته شده آماده کرده است اما هنوز هیچ عملکردی برای این صفحه پیاده سازی نشده. برای اینکه به این صفحه روج ببخشیم، از جاوااسکریپت کمک می‌گیریم و کد ها را به شکل زیر اصلاح می‌کنیم.

<!doctype html>
<html lang="en" dir="ltr">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>codebaz — Add News</title>
    <link rel="stylesheet" href="style.css" />
  </head>
  <body>
    <header class="navbar">
      <a href="index.html" class="brand">📰 codebaz</a>
      <a href="index.html" class="btn btn-outline">← Back to List</a>
    </header>

    <main class="container">
      <div class="form-card">
        <h2>📝 Add New Article</h2>
        <form action="#" method="post" enctype="multipart/form-data" onsubmit="handleSubmit(event)">
          <div class="form-grid">
            <div class="form-group full-width">
              <label for="title">Title *</label>
              <input type="text" id="title" name="title" placeholder="Enter news title" required="" />
            </div>
            <div class="form-group full-width">
              <label for="desc">Description *</label>
              <textarea id="desc" name="description" placeholder="Full article text..." required=""></textarea>
            </div>
            <div class="form-group full-width">
              <label for="img">Image</label>
              <input type="file" id="img" name="image" accept="image/*" />
            </div>
            <div class="form-group">
              <label for="date">Publish Date *</label>
              <input type="date" id="date" name="publish_date" required="" />
            </div>
            <div class="form-group">
              <label for="category">Category *</label>
              <select id="category" name="category" required="">
                <option value="">-- Select --</option>
                <option value="cultural">Cultural</option>
                <option value="political">Political</option>
                <option value="sports">Sports</option>
                <option value="scientific">Scientific</option>
              </select>
            </div>
            <div class="form-group full-width">
              <label class="checkbox-wrap">
                <input type="checkbox" id="draft" name="saveAsDraft" />
                Save as Draft
              </label>
            </div>
          </div>
          <div class="form-actions">
            <button type="submit" class="btn btn-primary">💾 Save Article</button>
            <a href="index.html" class="btn btn-outline">Cancel</a>
          </div>
        </form>
      </div>
    </main>
    <script>
      const params = new URLSearchParams(window.location.search);
      const newsId = params.get("id");

      function handleSubmit(e) {
        e.preventDefault();

        const form = e.target;
        const formData = new FormData(form);

        fetch(`http://localhost:8000/api/news/${newsId}/`, {
          method: "PATCH",
          body: formData,
        })
          .then((response) => response.json())
          .then((result) => {
            window.location.href = "index.html";
          })
          .catch((error) => {
            console.error(error);
            alert("❌ خطا در ثبت خبر: " + error.message);
          });
      }
    </script>
  </body>
</html>

در کد بالا:

  • خط ۱۸: در این خط به فرم می‌گوییم که هر وقت خواست ثبت شود، تابع handleSubmit را اجرا کند.
  • خط ۶۱ و ۶۲: در این بخش id خبر را از آدرس صفحه استخراج می‌کنیم تا بعدا بتوانیم در خط ۷۰ از آن استفاده کنیم.
  • خط ۶۴ تا ۸۲: در این بخش تابع handleSubmit پیاده سازی شده است.
    • خط ۶۴: e در واقع event ای است که با ثبت فرم به عنوان ورودی به این تابع ارسال شده است. از آنجایی که این event با کلیک روی دکمه submit اجرا شده است، e به این دکمه اشاره دارد. از آنجایی که فشردن این دکمه باعث ارسال اطلاعات فرم به روش پیش‌فررض همراه با رفرش صفحه می‌شود، ما با e.preventDefault() عملکرد پیش‌فرض را لغو می‌کنیم تا خودمان با استفاده از api این فرم را ثبت کنیم تا از رفرش شدن صفحه جلوگیری کنیم.
    • خط ۶۷ و ۶۸: اطلاعاتی که در فرم وارد شده را استخراج می‌کنیم و در formData می‌ریزیم.
    • خط ۷۰ تا ۷۳: formData را به عنوان body (اطلاعات فرم) با متد PATCH به بکند ارسال می‌کنیم.
    • خط ۷۶: پس از ثبت فرم، کاربر را به صفحه لیست اخبار هدایت می‌کنیم.
    • خط ۷۹ و ۸۰: در صورتی که ثبت خبر با خطایی مواجه شود، آن را نمایش می‌دهیم.

اکنون می‌توان یک خبر را با موفقیت ویرایش کرد اما یک مشکلی وجود دارد. زمانی که فرم باز می‌شود، هیچ مقدار پیش‌فرضی در آن نیست این در صورتی است که فرم باید با مقادیر فعلی خبر پر شده باشد. بدین منظور تکه کد زیر را به انتهای تگ script اضافه کنید.

<!doctype html>
<html lang="en" dir="ltr">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>codebaz — Add News</title>
    <link rel="stylesheet" href="style.css" />
  </head>
  <body>
    <header class="navbar">
      <a href="index.html" class="brand">📰 codebaz</a>
      <a href="index.html" class="btn btn-outline">← Back to List</a>
    </header>

    <main class="container">
      <div class="form-card">
        <h2>📝 Add New Article</h2>
        <form action="#" method="post" enctype="multipart/form-data" onsubmit="handleSubmit(event)">
          <div class="form-grid">
            <div class="form-group full-width">
              <label for="title">Title *</label>
              <input type="text" id="title" name="title" placeholder="Enter news title" required="" />
            </div>
            <div class="form-group full-width">
              <label for="desc">Description *</label>
              <textarea id="desc" name="description" placeholder="Full article text..." required=""></textarea>
            </div>
            <div id="image-container" class="form-group full-width">
              <label for="img">Image</label>
              <input type="file" id="img" name="image" accept="image/*" />
            </div>
            <div class="form-group">
              <label for="date">Publish Date *</label>
              <input type="date" id="date" name="publish_date" required="" />
            </div>
            <div class="form-group">
              <label for="category">Category *</label>
              <select id="category" name="category" required="">
                <option value="">-- Select --</option>
                <option value="cultural">Cultural</option>
                <option value="political">Political</option>
                <option value="sports">Sports</option>
                <option value="scientific">Scientific</option>
              </select>
            </div>
            <div class="form-group full-width">
              <label class="checkbox-wrap">
                <input type="checkbox" id="draft" name="saveAsDraft" />
                Save as Draft
              </label>
            </div>
          </div>
          <div class="form-actions">
            <button type="submit" class="btn btn-primary">💾 Save Article</button>
            <a href="index.html" class="btn btn-outline">Cancel</a>
          </div>
        </form>
      </div>
    </main>
    <script>
      const params = new URLSearchParams(window.location.search);
      const newsId = params.get("id");

      function handleSubmit(e) {
        e.preventDefault();

        const form = e.target;
        const formData = new FormData(form);

        fetch(`http://localhost:8000/api/news/${newsId}/`, {
          method: "PATCH",
          body: formData,
        })
          .then((response) => response.json())
          .then((result) => {
            window.location.href = "index.html";
          })
          .catch((error) => {
            console.error(error);
            alert("❌ خطا در ثبت خبر: " + error.message);
          });
      }

      fetch("http://localhost:8000/api/news/" + newsId)
        .then((response) => response.json())
        .then((data) => {
          document.querySelector('h2').innerText = '📝 ' + data.title;
          document.querySelector('[name="title"]').value = data.title;
          document.querySelector('[name="description"]').value = data.description;
          document.querySelector('[name="publish_date"]').value = data.publish_date;
          document.querySelector('[name="category"]').value = data.category;
          document.querySelector('[name="saveAsDraft"]').checked = data.is_draft;
          document
          .getElementById("image-container")
          .insertAdjacentHTML("beforeend", `<a href="${data.image}">View Image</a>`);
        });
    </script>
  </body>
</html>

در تکه کد بالا:

  • خط ۲۸: در این خط یک id به این تگ اختصاص می‌دهیم تا بتوانیم بعدا با جاوا اسکریپت آن را بگیریم و مقدار پیش‌فرض فیلد عکس را در آن نمایش دهیم.
  • ۸۴ تا ۹۶: جزئیات این خبر را از بکند گرفته و در فرم جاگذاری می‌کنیم.
    • خط ۸۷: اینجا عنوان خبر را علاوه بر input در بالای صفحه جاگذاری می‌کنیم.
    • خط ۸۸ تا ۹۲: مقدار فیلد ها از بکند پرسیده و در فرانت جاگذاری می‌شود.
    • خط ۹۳ تا ۹۵: آز آنجایی که File Upload Field به دلایل امنیتی نمی‌تواندمقدار پیش‌فرض داشته باشد، با استفاده از جاوا اسکریپت، یک لینک برای نمایش مقدار پیش‌فرض فایل ایجاد می‌کنیم.

تسک چهارم – صفحه افزودن خبر

شرح تسک چهارم

صفحه افزودن خبر را طوری توسعه دهید که خروجی آن شبیه تصویر زیر شود.

انجام تسک چهارم

کد های صفحه add-news.html را به شکل زیر اصلاح کنید.

<!doctype html>
<html lang="en" dir="ltr">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>codebaz - Add News</title>
    <link rel="stylesheet" href="style.css" />
  </head>
  <body>
    <header class="navbar">
      <a href="index.html" class="brand">📰 codebaz</a>
      <a href="index.html" class="btn btn-outline">← Back to List</a>
    </header>

    <main class="container">
      <div class="form-card">
        <h2>📝 Add New Article</h2>
        <form action="#" method="post" enctype="multipart/form-data" onsubmit="handleSubmit(event)">
          <div class="form-grid">
            <div class="form-group full-width">
              <label for="title">Title *</label>
              <input type="text" id="title" name="title" placeholder="Enter news title" required="" />
            </div>
            <div class="form-group full-width">
              <label for="desc">Description *</label>
              <textarea id="desc" name="description" placeholder="Full article text..." required=""></textarea>
            </div>
            <div id="image-container" class="form-group full-width">
              <label for="img">Image</label>
              <input type="file" id="img" name="image" accept="image/*" />
            </div>
            <div class="form-group">
              <label for="date">Publish Date *</label>
              <input type="date" id="date" name="publish_date" required="" />
            </div>
            <div class="form-group">
              <label for="category">Category *</label>
              <select id="category" name="category" required="">
                <option value="">-- Select --</option>
                <option value="cultural">Cultural</option>
                <option value="political">Political</option>
                <option value="sports">Sports</option>
                <option value="scientific">Scientific</option>
              </select>
            </div>
            <div class="form-group full-width">
              <label class="checkbox-wrap">
                <input type="checkbox" id="draft" name="saveAsDraft" />
                Save as Draft
              </label>
            </div>
          </div>
          <div class="form-actions">
            <button type="submit" class="btn btn-primary">💾 Save Article</button>
            <a href="index.html" class="btn btn-outline">Cancel</a>
          </div>
        </form>
      </div>
    </main>
    <script>
      const params = new URLSearchParams(window.location.search);
      const newsId = params.get("id");

      function handleSubmit(e) {
        e.preventDefault();

        const form = e.target;
        const formData = new FormData(form);

        var url;
        if (newsId) {
          url = `http://localhost:8000/api/news/${newsId}/`
        } else {
          url = `http://localhost:8000/api/news/`
        }

        var method = newsId ? 'PATCH' : 'POST'

        fetch(url, {
          method: method,
          body: formData,
        })
          .then((response) => response.json())
          .then((result) => {
            window.location.href = "index.html";
          })
          .catch((error) => {
            console.error(error);
            alert("❌ خطا در ثبت خبر: " + error.message);
          });
      }

      if (newsId) {
        fetch("http://localhost:8000/api/news/" + newsId)
        .then((response) => response.json())
        .then((data) => {
          document.querySelector('h2').innerText = '📝 ' + data.title;
          document.querySelector('[name="title"]').value = data.title;
          document.querySelector('[name="description"]').value = data.description;
          document.querySelector('[name="publish_date"]').value = data.publish_date;
          document.querySelector('[name="category"]').value = data.category;
          document.querySelector('[name="saveAsDraft"]').checked = data.is_draft;
          document
          .getElementById("image-container")
          .insertAdjacentHTML("beforeend", `<a href="${data.image}">View Image</a>`);
        });
      }
    </script>
  </body>
</html>

در کد بالا:

  • خط ۷۰ تا ۷۵: url را بر اساس newsId مقدار دهی می‌کنیم تا بتوانیم جدا بودن آدرس مربوط به ایجاد خبر جدید و ویرایش خبر را مدیریت کنیم.
  • خط ۷۷: اگر newsId مقدار داشته باشد، متد PATCH و در غیر اینصورت، POST می‌شود.
  • خط ۹۳: اگر newsId مقدار داشته باشد، مقدار دهی فرم انجام می‌شود در غیر اینصورت نیازی به مقدار دهی فرم نیست.

تبریک! این تسک نیز به پایان رسید.

جمع بندی

هدف اصلی این بخش، آشنایی شما به‌عنوان یک توسعه‌دهنده بک‌اند با فرآیند توسعه فرانت‌اند بود. در این مسیر، با استفاده از JavaScript و یک REST API، یک وب‌سایت خبری ساده پیاده‌سازی کردیم و مفاهیمی مانند دریافت داده از سرور، مدیریت state در سمت کلاینت و ایجاد رابط کاربری پویا را به صورت عملی بررسی نمودیم.

لازم به ذکر است که پیاده‌سازی ارائه‌شده بیش از آنکه برای استفاده در پروژه‌های واقعی و تولیدی مناسب باشد، با هدف درک گردش کار، چالش‌ها و الگوهای رایج در سمت فرانت طراحی شده است. توصیه می‌شود برای پروژه‌های جدی، از فریم‌ورک‌ها و ابزارهای استاندارد این حوزه بهره ببرید.

Please login to bookmark Close
نظرات

دیدگاهتان را بنویسید

فهرست مطالب

سرفصل دوره

تمرین

این قسمت تمرین ندارد!

پاسخ تمرین ها

هنوز برای تمرین‌های این قسمت پاسخی ثبت نشده است!

اشتراک گذاری

چرا بهتره از فیلترشکن استفاده کنید؟

من همه ویدئو ها و پادکست های کُدباز رو توی یوتیوب و ساندکلود و پلتفرم هایی آپلود می‌کنم که اغلب فیلتر هستند.

اغلب آموزش‌ها ویدئو و پادکست دارند. پس اگر می‌خواهید از محتوای سایت بیشترین استفاده رو ببرید نیاز به فیلتر شکن دارید.

توجه داشته باشید که برای خرید از فروشگاه بهتره فیلتر شکن رو خاموش کنید.

تنظیمات

انتخاب زبان
تغییر تم