Live Demo Github
簡介 用戶登入包括了 sign in 和 sign up 兩個頁面。都使用了 firebase 的 auth 功能處理。Firebase 會幫忙驗證 email 和 password 的合法性。最後也會處理只有登入的用戶才能瀏覽 browser 頁的功能。真的是 hen 方便。
Part 1 : Sign in Page + Sign up Page
Form 分為兩個部分:已經是用戶在 sign in page, 非用戶會導到 sign up page.
Sign in Page
components > form > index.js : 處理 Form
components > form > styles > form.js : 引入 styled ,在裡面處理 styled component
Signin.js : 表單會出現的地方
在 App.js 中加入這個 component
首先創造 Form container,需要form, title, input, submit button, error message, sign up.
index.js (components > form >index.js) >folded 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 import React from "react" import { Container, Base, Title, Text, TextSmall, Error , Input, Submit, Link} from "./styles/form" function Form ({ children, ...restProps } ) { return ( <Container {...restProps}>{children}</Container> ) } Form.Base = function FormBase ({children, ...restProps} ) { return <Base {...restProps }> {children}</Base > } Form.Title= function FormTitle ({children, ...restProps} ) { return <Title {...restProps }> {children}</Title > } Form.Text = function FormText ({children, ...restProps} ) { return <Text {...restProps }> {children}</Text > } Form.TextSmall = function FormTextSmall ({children, ...restProps} ) { return <TextSmall {...restProps }> {children}</TextSmall > } Form.Link = function FormLink ({children, ...restProps} ) { return <Link {...restProps }> {children}</Link > } Form.Error = function FormErrMessage ({children, ...restProps} ) { return <Error {...restProps }> {children}</Error > } Form.Input = function FormInput ({children, ...restProps} ) { return <Input {...restProps }> {children}</Input > } Form.Submit = function FormSubmit ({children, ...restProps} ) { return <Submit {...restProps }> {children}</Submit > } export default Form
整理 pages 路徑 index.js (pages) >folded 1 2 3 4 5 export { default as Home } from "./Home" export { default as Signin } from "./Signin"
在 signin.js 中處理了以下功能:
error 的出現依賴 state 來處理
點擊 submit button, email, password 的改變使用 state 處理
email/ password 其中一個沒有輸入 button 就會 disabled
加入 footer
Signin.js >folded 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 import React, { useState } from 'react' import { Form } from '../components' import { HeaderContainer } from '../containers/header' import { FooterContainer } from '../containers/footer' function Signin ( ) { const [ error, setError ] = useState("" ) const [ emailAddress, setEmailAddress ] = useState("" ) const [ password, setPassword ] = useState("" ) const isInvalid = password === '' | emailAddress === '' const handleSignin = (event ) => { event.preventDefault() } return ( <> <HeaderContainer> <Form> <Form.Title>Sign In</Form.Title> {error && <Form.Error > {error}</Form.Error > } <Form.Base onSubmit={handleSignin} method="POST" > <Form.Input placeholder="Email Address" value={emailAddress} onChange={({ target } ) => setEmailAddress(target.value)} /> <Form.Input type="password" placeholder="Password" value={password} autocomplete="off" onChange={({ target } ) => setPassword(target.value)} /> <Form.Submit disabled={isInvalid}type="submit" > Sign In </Form.Submit> <Form.Text > New to Netflix? <Form.Link to ="/signup" > Sign up now.</Form.Link > </Form.Text> <Form.TextSmall> This page is protected by Google reCAPTCHA. </Form.TextSmall> </Form.Base> </Form> </HeaderContainer> <FooterContainer/> </> ) } export default Signin
第 20 行 : 引入 header container,包著所有原件
第 26 行 : 需要接收 submit 的資料,method 為 POST
第 30, 38 行 : 更新 email value
第 13, 41 行 : 設定 button disabled 條件
第 46 行 : 添加 Link , 連接到 sign up page
第 56 行 : 添加 footer
Sign up Page Sign up page 的配置與 sign in 的差不多:FirstName, email, password, submit. 因為引用與 sign in 同一個 styled component,所以不需要再另外設置。
Signup.js (pages) >folded 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 import React, { useState } from "react" import { HeaderContainer } from '../containers/header' import { Form } from "../components" import * as ROUTES from "../constants/routes" import { FooterContainer } from "../containers/footer" function Signup ({ children, ...RestProps } ) { const [ firstName, setFirstName ] = useState("" ) const [ emailAddress, setEmailAddress ] = useState("" ) const [ password, setPassword ] = useState("" ) const [ error, setError ] = useState("" ) const isInvalid = firstName === "" || emailAddress === "" || password === "" const handleSignup = (event ) => { event.preventDefault() } return ( <> <HeaderContainer> <Form> <Form.Title>Sign Up</Form.Title> {error && <Form.error > {error}</Form.error > } <Form.Base onSubmit={handleSignup} method="POST" > <Form.Input placeholder="First Name" value={firstName} onChange = {({ target } )=> setFirstName(target.value)} /> <Form.Input placeholder="Email Address" value={emailAddress} onChange={({ target } ) => setEmailAddress(target.value)} /> <Form.Input type="password" placeholder="Password" value={password} autocomplete="off" onChange={({ target } ) => setPassword(target.value)} /> <Form.Submit disabled={isInvalid}type="submit" > Sign Up </Form.Submit> <Form.Text> Already a user? <Form.Link to ="/signin" > Sign in now.</Form.Link > </Form.Text> <Form.TextSmall> This page is protected by Google reCAPTCHA. </Form.TextSmall> </Form.Base> </Form> </HeaderContainer> <FooterContainer/> </> ) } export default Signup
Part 2 : 用戶選擇 Profile select
創建基本檔案
pages > browse.js
在 index.js (pages 的) 增加 export { default as Browse } from "./Browse"
components > profiles > index.js
components > profilesr > styles > profiles .js
containers > brwoser.js
containers > SelectProfileContainer.js
在 App.js 中引入 browse.js
App.js >folded 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 import React from 'react' import { Switch, Route } from 'react-router-dom' import * as ROUTES from './constants/routes' ;import { Home, Signin, Signup, Browse } from "./pages" function App ( ) { return ( <Switch> <Route path={ROUTES.SIGN_IN}> <Signin /> </Route> <Route path={ROUTES.SIGN_UP}> <Signup /> </Route> <Route path={ROUTES.BROWSE}> <Browse /> </Route> <Route path={ROUTES.HOME}> <Home /> </Route> </Switch> ); } export default App;
browse.js 中需要引入的東西:
header component (頁首)
route.js (跳轉頁面)
firebase context (film 資料)
select profile container (進入 film 列表前的用戶選擇)
footer container (頁腳)
用戶選擇 登入成功後,在進入 film 列表前有選擇用戶的區塊 (select profile container) 。在 browse.js 中創造用戶 profile,為一個 object,內有 display name 和 photo url. 選擇用戶區塊是否顯示是根據 display name 是否存在,如果存在則顯示設定好的 browser 畫面,不存在則顯示選擇用戶頁面. Profile 使用 state 來更新其狀態。
browse.js (containers) >folded 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 import React, {useState} from "react" import { Header } from "../components" import * as ROUTES from "../constants/routes" import { FirebaseContext } from "../context/firebase" import { SelectProfileContainer } from "./profiles" import { FooterContainer } from "./footer" function BrowseContainer ( ) { const [ profile, setProfile ] = useState({}) const user = { displayName: 'Karl' , photoURL:"1" } return profile.displayName ? ( <p> <p>Browse Container</p> <FooterContainer/> </p>): ( <SelectProfileContainer user={user} setProfile={ setProfile}/> ) } export { BrowseContainer }
接著設置 Profiles.js (containers), select profile container 會在這裡處理。需要引入:
header component (Logo, 撐開 logo 的 frame, 不要背景 )
ROUTES (Logo 跳轉頁面)
profile component
在 profiles component (index.js)裡創造需要的元件:Title, User, List, Picture, Name.
index.js (components > profiles ) >folded 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 import React from "react" import { Container, Title, List, User, Name, Picture } from "./styles/profiles" function Profiles ({ children, ...restProps } ) { return ( <Container {...restProps}>{children}</Container> ) } Profiles.Title = function ProfilesTitle ({ children, ...restProps } ) { return <Title {...restProps }> {children}</Title > } Profiles.List = function ProfilesList ({ children, ...restProps } ) { return <List {...restProps }> {children}</List > } Profiles.User = function ProfilesUser ({ children, ...restProps } ) { return <User {...restProps }> {children}</User > } Profiles.Name = function ProfilesName ({ children, ...restProps } ) { return <Name {...restProps }> {children}</Name > } Profiles.Picture= function ProfilesPicture ({src, ...restProps } ) { return <Picture {...restProps } src ={src? 照片鏈接: 照片鏈接} } //在瀏覽器還沒把照片load出來前顯示loading gif export default Profiles
在 profles container (Profiles.js) 中排版,設置頁面需要的東西。Profiles.users 在點擊之後會更新 profile 的 state,用來跳轉畫面。
profiles.js (containers) >folded 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 import React from "react" import { Header, Profiles } from "../components" import * as ROUTES from "../constants/routes" function SelectProfileContainer ({ user, setProfile } ) { return ( <> <Header bg={false }> <Header.Frame> <Header.Logo src="/images/misc/logo.png" to={ROUTES.HOME} alt="Netflix" /> </Header.Frame> </Header> <Profiles> <Profiles.Title>Who is Watching?</Profiles.Title> <Profiles.List> <Profiles.User onClick={() => setProfile({ displayName: user.displayName, photoURL: user.photoURL })} > <Profiles.Picture src={user.photoURL}/> <Profiles.Name>{user.displayName}</Profiles.Name> </Profiles.User> </Profiles.List> </Profiles> </> ) } export { SelectProfileContainer }
Part 3 : 連接 FireBase + 用戶 Auth
參考資料 用 Firebase Authentication 做一套簡易會員系統 – 電子郵件 密碼
連接 Firebase + firestore
在 context file 裡創建 firebase.js
firebase.js (context) >folded 1 2 import { createContext } from "react" export const FirebaseContext = createContext(null )
在 index.html 中加入以下程式碼
index.html >folded 1 2 <script src ="https://www.gstatic.com/firebasejs/8.4.3/firebase-app.js" > </script > <script src ="https://www.gstatic.com/firebasejs/8.4.3/firebase-firestore.js" > </script >
在 firebase 創建新的項目,跟著指示網下走
創建新的資料庫,點擊 firestore > 創建資料庫 >選擇生產者模式
回到專案頁面 > 點擊”網頁” > 添加名字 > 點擊註冊應用 > 跳出一個這個數據庫的資料
舊版本會有 database url,這個版本不需要
index.js 中加入 剛才創建的 firebase.js
index.js (src) >folded 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 import React from "react" import ReactDOM from "react-dom" import { BrowserRouter } from "react-router-dom" import { GlobalStyles } from "./global-style" import { FirebaseContext } from "./context/firebase" import App from "./App" const Config = { apiKey: "-----------------", authDomain: "-----------------", projectId: "-----------------", storageBucket: "-----------------", messagingSenderId: "-----------------", appId: "-----------------" } //firebase context 中以把 value 設為 {firebase: window.firebase}, 以 props 的方式傳進去 ReactDOM.render( <> <FirebaseContext.Provider value ={} > <GlobalStyles /> <BrowserRouter > <App /> </BrowserRouter > </FirebaseContext.Provider > </> , document.getElementById('root') );
Firebase Authendication 進入專案 > 點擊 Authendication > Sign in method (依照個人需求) > email/password 打開 > 在 html 中加入一段 auth 的程式碼
CustomPagination.js >folded 1 <script src ="https://www.gstatic.com/firebasejs/8.0.1/firebase-auth.js" > </script >
為 Sign up Page 加上 Authentication 在 index.js initializ firebase index.js >folded 1 2 const firebase = window .firebase.initializeApp(config)
在 signup.js 處理 authenation 讓用戶註冊賬號,如果註冊不成功便會出現錯誤信息,密碼少於6碼或是賬號已經被使用過等錯誤會被印出來。如果註冊成功會直接導到 profile select 那一頁,這個部分可以使用 useHistory 處理,不用重整畫面就可以做到跳轉頁面的效果。
這是一個 promise,所以使用 then() 以及 catch()來操作.
Signup.js (pages) >folded 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 import React, { useState, useContext } from "react" import { useHistory } from 'react-router-dom' ;import { HeaderContainer } from '../containers/header' import { FooterContainer } from "../containers/footer" import { FirebaseContext } from "../context/firebase" import { Form } from "../components" import * as ROUTES from "../constants/routes" function Signup ({ children, ...RestProps } ) { const history = useHistory() const { firebase } = useContext(FirebaseContext) const [ firstName, setFirstName ] = useState("" ) const [ emailAddress, setEmailAddress ] = useState("" ) const [ password, setPassword ] = useState("" ) const [ error, setError ] = useState("" ) const isInvalid = firstName === "" || emailAddress === "" || password === "" const handleSignup = (event ) => { event.preventDefault() firebase .auth() .createUserWithEmailAndPassword(emailAddress, password) .then((result ) => result.user .updateProfile({ displayName: firstName, photoURL: Math .floor(Math .random() * 4 ) + 1 }) .then(() => { setEmailAddress("" ) setPassword("" ) setError("" ) history.push(ROUTES.BROWSE) }) ).catch((error ) => setError(error.message)) } return ( <> <HeaderContainer> <Form> <Form.Title>Sign Up</Form.Title> {error && <Form.Error > {error}</Form.Error > } <Form.Base onSubmit={handleSignup} method="POST" > <Form.Input placeholder="First Name" value={firstName} onChange = {({ target } )=> setFirstName(target.value)} /> <Form.Input placeholder="Email Address" value={emailAddress} onChange={({ target } ) => setEmailAddress(target.value)} /> <Form.Input type="password" placeholder="Password" value={password} autocomplete="off" onChange={({ target } ) => setPassword(target.value)} /> <Form.Submit disabled={isInvalid}type="submit" > Sign Up </Form.Submit> <Form.Text> Already a user? <Form.Link to ="/signin" > Sign in now.</Form.Link > </Form.Text> <Form.TextSmall> This page is protected by Google reCAPTCHA. </Form.TextSmall> </Form.Base> </Form> </HeaderContainer> <FooterContainer/> </> ) } export default Signup
為 Sign in Page 加上 Authentication Sign in page 的操作步驟與 sign up 的差不多,差別只在點擊 sign in button 後的處理。
Signup.js (pages) >folded 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 import React, { useState, useContext } from 'react' import { useHistory } from "react-router-dom" import { Form } from '../components' import { HeaderContainer } from '../containers/header' import { FooterContainer } from '../containers/footer' import { FirebaseContext } from "../context/firebase" import * as ROUTES from "../constants/routes" function Signin ( ) { const history = useHistory() const { firebase } = useContext(FirebaseContext) const [ emailAddress, setEmailAddress ] = useState("" ) const [ password, setPassword ] = useState("" ) const [ error, setError ] = useState("" ) const isInvalid = password === '' | emailAddress === '' const handleSignin = (event ) => { event.preventDefault() firebase .auth() .signInWithEmailAndPassword ( emailAddress, password ) .then( setEmailAddress("" ), setPassword("" ), setError("" ), history.push(ROUTES.BROWSE) ) } return ( <> <HeaderContainer> <Form> <Form.Title>Sign In</Form.Title> {error && <Form.Error > {error}</Form.Error > } <Form.Base onSubmit={handleSignin} method="POST" > <Form.Input placeholder="Email Address" value={emailAddress} onChange={({ target } ) => setEmailAddress(target.value)} /> <Form.Input type="password" placeholder="Password" value={password} autocomplete="off" onChange={({ target } ) => setPassword(target.value)} /> <Form.Submit disabled={isInvalid}type="submit" > Sign In </Form.Submit> <Form.Text > New to Netflix? <Form.Link to ="/signup" > Sign up now.</Form.Link > </Form.Text> <Form.TextSmall> This page is protected by Google reCAPTCHA. </Form.TextSmall> </Form.Base> </Form> </HeaderContainer> <FooterContainer/> </> ) } export default Signin
寫入資料 在寫入資料前,要先把 rules 改成 true,但這個設定會讓所有人都有權限把資料寫進去。
導覽頁 Netflix Clone : 主頁 Netflix Clone : 首頁 Netflix Clone : 用戶登入頁 Netflix Clone : Browser 頁 Netflix Clone : 最後整理