Skip to content

nepikn/blog-mindly

Repository files navigation

Blog Mindly

使用 React Router 管理路由、Material UI 作為元件庫

預期功能

用戶可以

  • 登入並瀏覽不同文章類別
  • 透過上/下一頁在不同的文章類別之間切換
  • 對文章表示「🙂」、「👍」

展示

部署於個人網域

首頁 登入 菜單 儀表板
root signin menu dashboard

API

路徑 方法 用途
/auth POST 核對密碼、簽署 token
/auth GET 驗證、解碼 token
/post GET 查詢所有貼文
/post/reactions GET 查詢所有貼文的回應
/post/:category GET 查詢特定類別的貼文

主要技術

  • 前端
    • react v18
    • react-router-dom v6
    • @mui/material v6
  • 後端
    • express v4
  • 測試
    • vitest v2
    • @testing-library/react v16

指令

# 安裝
pnpm install
# 開發
pnpm dev
# 測試
pnpm test

學習內容

  • 調整 Apache 使其
    • 回應對於三級網域的請求
    • 反向代理 /api 到後端伺服器的 port
    • 對於無效的請求路徑一律回應 index.html 以便前端管理路由
# /etc/apache2/sites-available/subdomain-ssl.conf
<VirtualHost *:443>
    ServerName blog.unconscious.cc
    VirtualDocumentRoot /var/www/subdomain/%1

    ProxyRequests Off
    ProxyPass /api http://localhost:3001
    ProxyPassReverse /api http://localhost:3001

    <Directory ".">
        RewriteEngine On

        RewriteCond %{REQUEST_FILENAME} !-f
        RewriteCond %{REQUEST_FILENAME} !-d
        RewriteRule . index.html [L]
    </Directory>
    # ...
</VirtualHost>
// packages/server/src/routers/auth.js
export const auth = express
  .Router()
  .post("", (req, res, next) => {
    const { name, password } = req.body;
    // ...
    const token = jwt.sign({ name }, process.env.JWT_SECRET);

    res.json(token);
  })
  .get("", (req, res, next) => {
    try {
      // ...
      const secret = process.env.JWT_SECRET;
      const decoded = jwt.verify(token, secret);

      res.json(decoded);
    } catch (err) {
      // ...
    }
  });
// packages/client/src/routes/index.jsx
export default [
  // ...
  {
    path: ":category",
    lazy: async () => {
      const { Category } = await import("./dashboard");
      return {
        element: <Category />,
      };
    },
    // ...
  },
];
// packages/client/src/components/post.jsx
function Reactions({ reactions, title }) {
  const user = useContext(Auth);
  const fetcher = useFetcher();

  return [
    // ...
  ].map(({ icons, isIconButton, value, ...props }) => {
    // ...
    return (
      <fetcher.Form method="put">
        <input hidden name="title" defaultValue={title} />
        <input hidden name="username" defaultValue={user.name} />
        <input hidden name="reaction" defaultValue={value} />
        {/* ... */}
      </fetcher.Form>
    );
  });
}
// packages/client/test/routes/dashboard/children/category.test.jsx
describe("Post", () => {
  it("changes icons and numbers if reacted", async () => {
    vi.mock("localforage");

    await setup(
      // ...
      async (user) =>
        user.click(
          await screen.findByRole("button", { name: /117/i }),
        ),
    );

    expect(
      await screen.findByRole("button", { name: /118/i }),
    ).toContainElement(screen.getByTestId("EmojiEmotionsIcon"));
  });
});

展望

  • msw 模擬後端回應

相關資料

素材

About

使用 React Router 管理路由、Material UI 作為元件庫

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages