React with SPA Blog


Posted by Christy on 2021-12-21

本文為實作 W22 作業一的過程筆記,接續把之前筆記九的 SPA 部落格完成

  1. 登入頁面:輸入帳號密碼後可以登入,參考 來學 React 吧之九_實作部落格

  2. 註冊頁面:可以開放使用者註冊

  3. About 頁面:隨意顯示一些關於這個部落格的話

  4. 文章列表頁面:可以看到所有文章,一頁只會顯示 5 筆,需要支援分頁功能,可以換頁

  5. 單篇文章頁面:點進去文章以後可以看到文章完整內容,參考 來學 React 吧之九_實作部落格

  6. 發表文章頁面:可以輸入標題跟內文發文

發表文章實作過程:

  1. Error logs:

Error log: Invalid request, "title" is required

應該是呼叫 api 的時候失敗,可是為什麼不能抓到標題呢?明明有輸入標題啊,開 dev Network tab 也有帶到標題,可是回傳錯誤訊息

原來呼叫 api 時,少寫 `'content-type': 'application/json',`

傳送成功以後,除了 title 跟 body 以外,還會自動帶上 id、userId、createdAt

不懂的地方:

  1. 不知道要帶什麼 header

  2. header 裡面的參數必須名稱是 title & body,但是 NewArticle 那一頁裡面的 useState 可以傳 title、articleContent 之類的

// WebAPI.js code

export const newArticle = (title, body) => {
  const token = getAuthToken();
  return fetch(`${BASE_URL}/posts`, {
    method: "POST",
    headers: {
      // 少了下面一行,所以不成功  
      "content-type": "application/json",
      authorization: `Bearer ${token}`,
    },
    body: JSON.stringify({
      title,
      body,
    }),
  }).then((res) => res.json());
};
// NewArticle.js
import { useState } from "react";
import { useNavigate } from "react-router-dom";
import styled from "styled-components";
import { newArticle } from "../../WebAPI";

const ArticleWrapper = styled.form`
  margin: 0 auto;
  width: 800px;
  padding: 20px;
`;

const ArticleTitle = styled.input`
  width: 360px;
  margin: 20px;
`;

const ArticleTextArea = styled.textarea`
  width: 600px;
  height: 300px;
  border-radius: 3px;
`;

const ArticleSubmit = styled.button`
  color: #666;
  display: block;
  margin-top: 20px;
`;

const ErrorMessage = styled.div`
  color: red;
  font-size: 20px;
  margin-bottom: 20px;
`;

export default function NewArticle() {
  const navigate = useNavigate();
  const [title, setTitle] = useState("");
  const [articleContent, setArticleContent] = useState("");
  const [errorMessage, setErrorMessage] = useState("");

  const handleNewArticle = () => {
    setErrorMessage(null);
    if (!title || !articleContent) {
      const errorMessage = "Please input missing field";
      setErrorMessage(errorMessage);
    } else {
      newArticle(title, articleContent).then((res) => {
        navigate("/");
      });
    }
  };

  return (
    <ArticleWrapper onSubmit={handleNewArticle}>
      Title:
      <ArticleTitle
        value={title}
        onChange={(e) => setTitle(e.target.value)}
        type="text"
      />
      <ArticleTextArea
        value={articleContent}
        onChange={(e) => setArticleContent(e.target.value)}
        type="text"
      />
      {errorMessage && <ErrorMessage>{errorMessage}</ErrorMessage>}
      <ArticleSubmit>New Post</ArticleSubmit>
    </ArticleWrapper>
  );
}

註冊頁面實作過程:

成功會在 Network tab 看到:

{ok: 1, data: {id: "0313fb238a9804", username: "Christy", nickname: "Christy", password: "Lidemy"}}
data: {id: "0313fb238a9804", username: "Christy", nickname: "Christy", password: "Lidemy"}
ok: 1

實作方式跟登入很像

文章列表頁面:

我嘗試到 `${BASE_URL}/posts?_sort=createdAt&_order=desc&_page=2&_limit=5 這裡

我的問題還是一樣,就是 header 要怎麼帶

居然有這種東西 React js Pagination With API Call Using React-paginate

react-paginate

原來老師有提示文章總數是在 posts header 裡面的 Response Headers 的 total: x-total-count,,要切到 Network tab 看比較準,但我從所有文章裡面計算出來的好像也可以

我要做的功能有:

  1. 點擊上一頁,出現上五筆文章數

  2. 點擊下一頁,出現下五筆文章數

  3. 顯示目前頁數

API 的部分:

export const getAllArticles = () => {
  return fetch(`${BASE_URL}/posts?_sort=createdAt&_order=desc`).then((res) =>
    res.json()
  );
};

export const getLimitArticles = (page) => {
  return fetch(
    `${BASE_URL}/posts?_page=${page}&_limit=5&_sort=createdAt&_order=desc`
  ).then((res) => res.json());
};

文章列表的部分:

// LimitArticles.js code

import React, { useState, useEffect } from "react";
import { Link } from "react-router-dom";
import styled from "styled-components";
import { getAllArticles, getLimitArticles } from "../../WebAPI";

const Root = styled.div`
  width: 80%;
  margin: 30px auto;
`;

const PostContainer = styled.div`
  display: flex;
  align-items: flex-end;
  justify-content: space-between;
  padding: 16px;
`;

const PostTitle = styled(Link)`
  font-size: 16px;
  color: #666;
  text-decoration: none;
`;

const PostDate = styled.div`
  color: #666;
`;

const Pagination = styled.div`
  margin: 0 auto;
  text-align: center;
`;

const Page = styled.div`
  display: inline-block;
  padding: 10px;
  background: #zzz;
  cursor: pointer;
`;

const CurrentPage = styled.div`
  display: inline-block;
  padding: 10px;
  background: #zzz;
`;

const PageInfo = styled.div`
  text-align: center;
  margin-top: 20px;
  font-size: 18px;
`;

function Post({ post }) {
  return (
    <PostContainer>
      <PostTitle to={`/posts/${post.id}`}>{post.title}</PostTitle>
      <PostDate>{new Date(post.createdAt).toLocaleString()}</PostDate>
    </PostContainer>
  );
}

export default function LimitArticles() {
  const [posts, setPosts] = useState([]);
  const [currentPage, setCurrentPage] = useState(1);
  const [totalPages, setTotalPages] = useState(0);

  useEffect(() => {
    getLimitArticles(currentPage).then((posts) => {
      setPosts(posts);
    });

    getAllArticles().then((data) => {
      setTotalPages((totalPages) => Math.ceil((data.length - 1) / 5));
    });
  }, [currentPage, totalPages]);

  const handlePrevPageClick = () => {
    if (currentPage > 1) {
      setCurrentPage((currentPage) => currentPage - 1);
    }
  };

  const handleNextPageClick = () => {
    if (currentPage < totalPages) {
      setCurrentPage(currentPage + 1);
    }
  };

  return (
    <Root>
      {posts.map((post) => (
        <Post key={post.id} post={post} />
      ))}
      <Pagination>
        {!(currentPage === 1) && (
          <Page onClick={handlePrevPageClick}>Prev</Page>
        )}
        <CurrentPage>{currentPage}</CurrentPage>
        {!(currentPage === totalPages) && (
          <Page onClick={handleNextPageClick}>Next</Page>
        )}
      </Pagination>
      <PageInfo>
        {currentPage} / {totalPages}
      </PageInfo>
    </Root>
  );
}









Related Posts

第 19 期 Python 程式設計入門-作業任務8

第 19 期 Python 程式設計入門-作業任務8

介紹 Git

介紹 Git

Array Cardio Day 2

Array Cardio Day 2


Comments