iT邦幫忙

2023 iThome 鐵人賽

DAY 23
0

https://ithelp.ithome.com.tw/upload/images/20231008/20136558qF6EckwBjM.jpg
先前我們已經將登入和註冊頁開發完,今天要來進行導覽列的開發。

大綱

  1. Wireframe
  2. 導覽列開發
    • 未登入的導覽列
    • 已登入的導覽列
    • 使用者功能列

1. Wireframe

未登入
https://ithelp.ithome.com.tw/upload/images/20231008/20136558U4w1a7vJOa.jpg

登入後
https://ithelp.ithome.com.tw/upload/images/20231008/20136558LtXpugCMv7.jpg

根據wireframe,我們將首頁和導覽列分別建立,因為導覽列在之後的頁面都會存在,是需要共用的元件。

2. 導覽列開發

我們先在layout資料夾底下新增Header.js,並進行切版和路由設定(點擊右上角的Sign In 時要能跳轉到Login頁面)

未登入的導覽列

import { Link } from "react-router-dom";

const Header = (props) => {
    return (
       <header className="w-screen h-14 bg-black flex justify-between items-center px-8">
        <Link to="/">
          <div className="flex justify-between items-center">
            {/* <img className="h-[24px]" src={logoImage} alt="logo" /> */}
            <h3 className="ml-4 text-white">BLOG DEV</h3>
          </div>
        </Link>
        <div className="text-gray-200">
        <Link to="/posts">
          <button className="px-8 py-2 hover:text-white">Posts</button>
        </Link>
        <Link to="/tags">
          <button className="px-8 py-2 hover:text-white">Tags</button>
        </Link> 
        <Link to="/login">
          <button className="px-8 py-2 hover:text-white">Contact</button>
        </Link>
        </div>

        <div className="flex items-center">
            <Link to="/login">
              <button className="text-gray-200 px-4 py-2 hover:text-white">
                Sign In
              </button>
            </Link>
        </div>
      </header>
  );
};

export default Header;

回到App.js加上Header.js的路由

//App.js
import Header from '../layout/Header;
import Login  from './pages/Login';
import Register from './pages/Register';
import './App.css';
import { createBrowserRouter, RouterProvider } from 'react-router-dom';

const router = createBrowserRouter([
  {
    path: '/', 
    element: <Header/>,
  },
  {
    path: '/login', 
    element: <Login/>,
  },
  {
    path: '/register', 
    element: <Register/>,
  }
])

const App = () => {
  return (
      <RouterProvider router={router}/>
  );
}

export default App;

已登入的導覽列

上面只製作了未登入時的導覽列,若使用者是有登入時,是需要顯示發布文章的按鈕和他的預設頭像,但我們要怎麼判斷使用者是否登入了呢?

這時就需要使用React的Context

React Context

React Context API 是 React.js 提供的一個用於元件間共享值的機制。它可以讓你在不必透過 props 鏈(props chain)將值手動傳遞的情況下,將值共享給元件樹中的任何一個元件。React Context

因為react context有以上特性,所以我們就能透過它來紀錄使用者是否登入的值
contexts資料夾底下建立AuthContext.js檔案:

//AuthContext.js

import React, { createContext, useContext, useState } from 'react';

const AuthContext = createContext();

//自訂hook useAuth,用來回傳當前的Context值,,方便在其他元件中使用
export const useAuth = () => {
  return useContext(AuthContext);
};

//透過AuthContext.Provider提供一個context的值給它的子元件。
//{ children }是它的子元件。透過value prop將認證狀態(isLoggedIn)和改變狀態的函數(setIsLoggedIn)傳遞給所有的子元件。
export const AuthProvider = ({ children }) => {

  const [isLoggedIn, setIsLoggedIn] = useState(!!localStorage.getItem('user'));
 
  
  return (
    <AuthContext.Provider value={{ isLoggedIn, setIsLoggedIn }}>
      {children}
    </AuthContext.Provider>
  );
};


修改App.js

先回到App.js,在原本的 <RouterProvider router={router}/>外加上<AuthProvider><AuthProvider/>,這樣我們才能存取到isLoggedInsetIsLoggedIn

import './App.css';
import { createBrowserRouter, RouterProvider } from 'react-router-dom';
import { AuthProvider } from './contexts/AuthContext';

import Login  from './pages/Login';
import Register from './pages/Register';
import HomePage from './pages/Home';

const router = createBrowserRouter([
  { 
      path: '/', 
      element: <HomePage />
  },
  {
    path: '/login', 
    element: <Login/>,
  },
  {
    path: '/register', 
    element: <Register/>,
  }
])

const App = () => {
  return (
    <AuthProvider>
      <RouterProvider router={router}/>
    </AuthProvider>
  );
}

export default App;

修改Login.js

Login.js,我們需要在登入成功後將isLoggedIn設為true

//Login.js

import { useState } from "react";
import { Link, useNavigate } from "react-router-dom";
import { useAuth } from "../contexts/AuthContext"; //引入useAuth
import api from "../api/api";
import Card from "../components/Card";
import useInput from "../hooks/useInput";

const LoginPage = (props) => {
  const navigate = useNavigate();
  
  //透過useAuth取得setIsLoggedIn的方法
  const { setIsLoggedIn } = useAuth();

  (略...)

  const handleSubmit = (event) => {
    if (!formIsValid) return;

    event.preventDefault();

    const userData = {
      email,
      password,
    };

    api
      .post("/auth/login", userData)
      .then((result) => {
        localStorage.setItem("user", JSON.stringify(result));
        //登入成功後要將isLoggedIn設為true
        setIsLoggedIn(true); 
        navigate("/");
      })
      .catch((error) => {
        setErrorMsg('Login failed. Your email or password is incorrect.')
        console.error(error);
      });
  };


  return (
    <Card>
         (略...)
    </Card>
  );
};

export default LoginPage;


修改Header.js

回到Header.js,引入useAuth並加上是否登入的判斷,若已登入則顯示頭像和建立文章的按鈕

import { Fragment } from "react";
import { Link, useNavigate } from "react-router-dom";
//引入useAuth
import { useAuth } from "../../contexts/AuthContext"; 
import avatar from "../../assets/avatar.jpg";

const Header = (props) => {
 return (
      <header className="w-screen h-14 bg-black flex justify-between items-center px-8">
        <Link to="/">
          <div className="flex justify-between items-center">
            {/* <img className="h-[24px]" src={logoImage} alt="logo" /> */}
            <h3 className="ml-4 text-white">BLOG DEV</h3>
          </div>
        </Link>
        <div className="text-gray-200">
        <Link to="/posts">
          <button className="px-8 py-2 hover:text-white">Posts</button>
        </Link>
        <Link to="/tags">
          <button className="px-8 py-2 hover:text-white">Tag</button>
        </Link> 
        </div>

        <div className="flex items-center">\
           {/* 判斷是否登入再顯示對應樣式 */}
           {isLoggedIn ? (
            <LoggedInComponents />
          ) : (
            <Link to="/login">
              <button className="text-gray-200 px-4 py-2 hover:text-white">
                Sign In
              </button>
            </Link>
          )}
        </div>
      </header>
   );
};
export default Header;

//登入後的樣式
const LoggedInComponents = () => {
  const navigate = useNavigate();

  const handleSignOut = () => {
    localStorage.removeItem("user");
    navigate("/login");
  };

  return (
    <Fragment>
      <Link to="/edit-post/new">
        <button className="flex items-center bg-violet-600 px-4 py-1 text-gray-100 hover:text-white hover:bg-violet-700 duration-150 mr-4 rounded">
          <AiOutlinePlus />
          <p className="ml-2">New Post</p>
        </button>
      </Link>

      <div className="w-[32px] h-[32px] rounded-full overflow-hidden bg-gray-500 text-white">
          User
      </div>
    </Fragment>
  );
};

使用者功能列

https://ithelp.ithome.com.tw/upload/images/20231008/20136558Nd8h5Yd9Yb.jpg

當我們在點擊頭像時,可以叫出功能列,我們使用headless UI來製作Menu。
功能列裡面有

  • 使用者資訊頁(後續會開發)
  • 登出
//Header.js
import { Fragment } from "react";
import { Link, useNavigate } from "react-router-dom";
import { useAuth } from "../../contexts/AuthContext"; 
import { AiOutlinePlus } from "react-icons/ai";
//引入headlessui
import { Menu, Transition } from "@headlessui/react";
import avatar from "../../assets/avatar.jpg";

const Header = (props) => {
   (略...)
}
export default Header;

//登入後的樣式
const LoggedInComponents = () => {
  const navigate = useNavigate();

  //登出
  const handleSignOut = () => {
    //清除localstorage的使用者資訊,並導回登入頁
    localStorage.removeItem("user");
    navigate("/login");
  };

  return (
    <Fragment>
      <Link to="/edit-post/new">
        <button className="flex items-center bg-violet-600 px-4 py-1 text-gray-100 hover:text-white hover:bg-violet-700 duration-150 mr-4 rounded">
          <AiOutlinePlus />
          <p className="ml-2">New Post</p>
        </button>
      </Link>

      <Menu as="div" className="relative inline-block text-left">
        <Menu.Button>
          <div className="w-[32px] h-[32px] rounded-full overflow-hidden bg-gray-500 text-white">
           User
          </div>
        </Menu.Button>
        <Transition
          as={Fragment}
          enter="transition ease-out duration-100"
          enterFrom="transform opacity-0 scale-95"
          enterTo="transform opacity-100 scale-100"
          leave="transition ease-in duration-75"
          leaveFrom="transform opacity-100 scale-100"
          leaveTo="transform opacity-0 scale-95"
        >
          <Menu.Items className="absolute right-0 mt-2 w-56 origin-top-right bg-white shadow-lg focus:outline-none flex flex-col rounded">
            <Menu.Item as={Fragment}>
              <a
                href="/account-settings"
                className="p-2 text-gray-800 bg-white hover:bg-neutral-100 text-left"
              >
                Account settings
              </a>
            </Menu.Item>
            <Menu.Item as={Fragment}>
              <button
                className="p-2 text-gray-800 bg-white hover:bg-neutral-100 text-left"
                onClick={handleSignOut}
              >
                Sign out
              </button>
            </Menu.Item>
          </Menu.Items>
        </Transition>
      </Menu>
    </Fragment>
  );
};

參考資料


上一篇
[Day22] 前端Authentication處理
下一篇
[Day24] 框架頁面和首頁切版
系列文
初探全端之旅: 以MERN技術建立個人部落格31
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

1 則留言

0
LaNLaN
iT邦新手 5 級 ‧ 2024-02-27 16:18:39

Yihsuan 大你好,Header.js 裡的 <header> 的部分沒有 return,小提醒,感謝這系列文章,我繼續學習

Yihsuan iT邦新手 5 級 ‧ 2024-08-24 15:18:54 檢舉

感謝你的提醒!! 已修正

我要留言

立即登入留言