本文為實作 W22 作業一的過程筆記,接續把之前筆記九的 SPA 部落格完成
登入頁面:輸入帳號密碼後可以登入,參考 來學 React 吧之九_實作部落格
註冊頁面:可以開放使用者註冊
About 頁面:隨意顯示一些關於這個部落格的話
文章列表頁面:可以看到所有文章,一頁只會顯示 5 筆,需要支援分頁功能,可以換頁
單篇文章頁面:點進去文章以後可以看到文章完整內容,參考 來學 React 吧之九_實作部落格
發表文章頁面:可以輸入標題跟內文發文
發表文章實作過程:
- Error logs:
Error log: Invalid request, "title" is required
應該是呼叫 api 的時候失敗,可是為什麼不能抓到標題呢?明明有輸入標題啊,開 dev Network tab 也有帶到標題,可是回傳錯誤訊息
原來呼叫 api 時,少寫 `'content-type': 'application/json',`
傳送成功以後,除了 title 跟 body 以外,還會自動帶上 id、userId、createdAt
不懂的地方:
不知道要帶什麼 header
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
原來老師有提示文章總數是在 posts header 裡面的 Response Headers 的 total: x-total-count,,要切到 Network tab 看比較準,但我從所有文章裡面計算出來的好像也可以
我要做的功能有:
點擊上一頁,出現上五筆文章數
點擊下一頁,出現下五筆文章數
顯示目前頁數
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>
);
}