【實作記錄】Netflix Clone:用戶登入頁

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
Form.Base = function FormBase({children, ...restProps}) {
return <Base {...restProps}>{children}</Base>
}

//標題
Form.Title= function FormTitle({children, ...restProps}) {
return <Title {...restProps}>{children}</Title>
}

//sign up guide
Form.Text = function FormText({children, ...restProps}) {
return <Text {...restProps}>{children}</Text>
}

//note
Form.TextSmall = function FormTextSmall({children, ...restProps}) {
return <TextSmall {...restProps}>{children}</TextSmall>
}

//gsign up link
Form.Link = function FormLink({children, ...restProps}) {
return <Link {...restProps}>{children}</Link>
}

Form.Error = function FormErrMessage({children, ...restProps}) {
return <Error {...restProps}>{children}</Error>
}

//email, password
Form.Input = function FormInput({children, ...restProps}) {
return <Input {...restProps}>{children}</Input>
}

//Submit Button
Form.Submit = function FormSubmit({children, ...restProps}) {
return <Submit {...restProps}>{children}</Submit>
}

export default Form

整理 pages 路徑

index.js (pages) >folded
1
2
3
4
5
// filename: index.js (pages)

export { default as Home } from "./Home"
export { default as Signin } from "./Signin"


在 Signin.js 中設置 form

在 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
// filename : Signin.js

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>}

//包著 email 和 password
<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}/> //參數要往下傳, selectProfileContainer 才能使用 user 等
)
}

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>
}

//注意照片傳進來的參數 : 沒有children,傳進 src
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

  1. 在 context file 裡創建 firebase.js
firebase.js (context) >folded
1
2
import { createContext } from "react"
export const FirebaseContext = createContext(null)
  1. 在 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>
  1. 在 firebase 創建新的項目,跟著指示網下走
  2. 創建新的資料庫,點擊 firestore > 創建資料庫 >選擇生產者模式
    • 選擇資料存放地區
  3. 回到專案頁面 > 點擊”網頁” > 添加名字 > 點擊註冊應用 > 跳出一個這個數據庫的資料
    • 舊版本會有 database url,這個版本不需要

  1. 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
//filename :index.js 
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
  • 由於需要用到 firebase context 來操作 firebase

    • 第 6 行 : 引入 { firebaseContext }
    • 第 12 行 : 使用 useContext 調用
    • 第 24 行 : 使用 firebase 的 method, 本身為一個 promise
  • 處理點擊 sign up 後的動作

    • 第 25, 26 行 : 使用 firebase 的 method
    • 第 27 行 : 成功註冊後更新 profile
    • 第 31 行 : photoURL 使用隨機號碼當做 url (image文件夾裡有 4 張照片,檔名為1, 2, 3, 4)
    • 第 33 行 : 成功註冊後清除輸入欄
    • 第 37 行 : 使用 useHistory 把網頁導到 browser 那頁
    • 第 39 行 : 註冊失敗會出現錯誤信息

為 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 : 最後整理

Comments