, September 29, 2022

0 kết quả được tìm thấy

Netflix Clone với ReactJS, Styled Components và Firebase (Firestore & Auth) - Phần 4


  •   10 min reads
Netflix Clone với ReactJS, Styled Components và Firebase (Firestore & Auth) - Phần 4

Trong phần 4 của series này, chúng ta sẽ tiếp tục hoàn thiện phần Guest Home Page . Lần này, chúng ta sẽ bắt tay vào code phần FAQs component, thực hiện config router để redirects qua lại các page nhé. Cùng bắt đầu thôi 😉!

I. Implement FAQ UI

Okie! Giờ chúng ta bắt đầu implement FAQ UI như hình bên dưới.

FAQ UI

Trong folder components ta tạo tiếp component FAQs  như sau:

FAQs component folder 

Bạn có thể lên trang Home của Netflix (Nhớ là Guest Home Page nha) thì bạn có thể thấy, khi ta click vô icon + thì nó sẽ dropdown phần content xuống như hình:

FAQ content

Như hình trên, mình cũng đã phân tích một chút về layout của đống này rồi, chúng ta bắt tay vào làm thôi 😁, đầu tiên trong file FAQs.jsx chúng ta sẽ style cho những thứ cần thiết như sau:

import styled from "styled-components/macro";

export const Container = styled.div`
	display: flex;
	border-bottom: 8px solid #222;
	position: relative;
	margin: 0;
	background: 0 0;

	@media only screen and (min-width: 550px) and (max-width: 949px),
		only screen and (min-width: 950px) and (max-width: 1449px),
		only screen and (min-width: 1450px) {
		padding: 70px 45px;
	}
`;

export const Header = styled.div`
	display: flex;
	justify-content: space-between;
	align-items: center;
	cursor: pointer;
	margin-bottom: 1px;
	font-size: 26px;
	font-weight: normal;
	background-color: #303030;
	padding: 0.8em 1.2em 0.8em 1.2em;
	user-select: none;
	transition: all 0.3s ease;

	img {
		filter: brightness(0) invert(1);
		width: 24px;
	}
`;

export const Body = styled.div`
	font-size: 26px;
	font-weight: normal;
	line-height: normal;
	background: #303030;
	white-space: pre-wrap;
	user-select: none;
	overflow: hidden;
	padding: 1.2em;
`;

export const Item = styled.div`
	color: #fff;
	margin: 0 0 8px 0;
`;

export const Frame = styled.div`
	margin-bottom: 40px;
`;

export const InnerWrapper = styled.div`
	max-width: 815px;
	margin: 1.25em auto;

	@media only screen and (min-width: 950px) and (max-width: 1449px),
		only screen and (min-width: 1450px) {
		margin-top: 20px;
		width: 75%;
		margin: 3.25em auto;
	}
`;

export const Inner = styled.div`
	display: flex;
	margin: auto;
	flex-direction: column;
	width: 100%;
`;

export const Title = styled.h1`
	font-size: 3.125rem;
	line-height: 1.1;
	margin-top: 0;
	margin-bottom: 0.5rem;
	color: #fff;
	text-align: center;

	@media screen and (max-width: 600px) {
		font-size: 35px;
	}
`;
FAQs.jsx

Tiếp đến là /FAQs/index.jsx:

import React, { useState, useContext, createContext } from "react";
import {
	Container,
	Inner,
	Header,
	Title,
	Frame,
	Item,
	Body,
	InnerWrapper
} from "./styles/FAQs";

const toggleContext = createContext();

export default function FAQs({ children, ...restProps }) {
	return (
		<Container {...restProps}>
			<Inner>{children}</Inner>
		</Container>
	);
}

FAQs.Title = function FAQsTitle({ children, ...restProps }) {
	return <Title {...restProps}>{children}</Title>;
};

FAQs.Frame = function FAQsFrame({ children, ...restProps }) {
	return <Frame {...restProps}>{children}</Frame>;
};

FAQs.Item = function FAQsItem({ children, ...restProps }) {
	const [toggleShow, setToggleShow] = useState(false);
	return (
		<toggleContext.Provider value={{ toggleShow, setToggleShow }}>
			<Item {...restProps}>{children}</Item>
		</toggleContext.Provider>
	);
};

FAQs.Header = function FAQsHeader({ children, ...restProps }) {
	const { toggleShow, setToggleShow } = useContext(toggleContext);
	return (
		<Header onClick={() => setToggleShow(!toggleShow)} {...restProps}>
			{children}
			{toggleShow ? (
				<img src="/images/icons/close-slim.png" alt="close" />
			) : (
				<img src="/images/icons/add.png" alt="open" />
			)}
		</Header>
	);
};

FAQs.Body = function FAQsBody({ children, ...restProps }) {
	const { toggleShow } = useContext(toggleContext);
	return toggleShow ? <Body {...restProps}>{children}</Body> : null;
};

FAQs.InnerWrapper = function FAQsInnerWrapper({ children, ...restProps }) {
	return <InnerWrapper {...restProps}>{children}</InnerWrapper>;
};
index.jsx

Trong folder containers ta thêm file FAQsContainer.jsx và thêm đoạn code sau:

import React from "react";
import { FAQs } from "../components/index";
import faqsData from "../fixtures/faqs.json";

export default function FAQsContainer() {
	return (
		<FAQs>
			<FAQs.Title>Frequently Asked Questions</FAQs.Title>
			<FAQs.InnerWrapper>
				{faqsData.map((item) => (
					<FAQs.Item key={item.id}>
						<FAQs.Header>{item.header}</FAQs.Header>
						<FAQs.Body>{item.body}</FAQs.Body>
					</FAQs.Item>
				))}
			</FAQs.InnerWrapper>
		</FAQs>
	);
}
FAQsContainer.jsx

Trong case này (trong file /FAQs/index.jsx) mình dùng context để truyền đi những data cần thiết để chúng có thể giao tiếp với nhau. Như ở phần 1 mình đã trình bày, trong bài này chúng ta sẽ không đề cập sâu vào thằng context của react do đó mình chỉ giải thích sơ bộ về cách hoạt động của component trên với context nhé 😉.

Đầu tiên,  Item là phần sẽ wrap bên ngoài của phần HeaderBody, do đó khi click vào Header thì phần Body sẽ được show ra bên dưới Header và đồng thời icon của Header cũng thay đổi tương ứng.

Mình đã tạo một context có tên là toggleContext để lưu trữ nữa data phục vụ cho việc này, trong method Item mình tạo một statetoggleShow sau đó dùng <toggleContext.Provider> wrap bên ngoài Item và truyền giá trị của state vào thông qua value.

Lúc này trong phần Header ta sẽ lấy giá trị từ context và sử dụng chúng để set value true/flase (open or close). Trong phần Body ta chỉ cần lấy giá trị context ra để handle là xong.

Demo:

  • close.
FAQ Close
  • Open.
FAQ Opened

Tiếp theo, chúng ta sẽ làm phần Form đăng ký member trong phần của FAQs luôn nhé 😉.

  • Tạo các file và thư mục như sau:
OptionForm component folder
  • Trong file OptionForm.jsx, ta code như sau:
import styled from "styled-components/macro";

export const Container = styled.div`
	display: flex;
	justify-content: center;
	flex-direction: column;
	height: 100%;
	padding-top: 0.85rem;
	flex-wrap: wrap;
	@media (max-width: 1000px) {
		flex-direction: column;
		align-items: center;
	}

	@media only screen and (min-width: 550px) and (max-width: 949px),
		only screen and (min-width: 950px) and (max-width: 1449px),
		only screen and (min-width: 1450px) {
		padding: 70px 45px;
	}
`;

export const Button = styled.button`
	display: flex;
	align-items: center;
	height: 60px;
	background-color: #e50914;
	color: white;
	text-transform: capitalize;
	padding: 0 1em;
	font-size: 1.625rem;
	border: 0;
	border-radius: 2px;
	border-top-left-radius: 0;
	border-bottom-left-radius: 0;
	border-left: 1px solid #333;
	cursor: pointer;
	img {
		margin-left: 10px;
		filter: brightness(0) invert(1);
		width: 24px;
		@media (max-width: 1000px) {
			width: 16px;
		}
	}
	&:hover {
		background: #f40612;
	}
`;

export const Input = styled.input`
	max-width: 450px;
	width: 100%;
	border: 0;
	padding: 10px;
	height: 60px;
	box-sizing: border-box;
	font-size: 16px;
	border-radius: 2px;
	border-top-right-radius: 0;
	border-bottom-right-radius: 0;
`;

export const Text = styled.p`
	font-size: 19.2px;
	color: white;
	text-align: center;
	padding-top: 0.85rem;
	@media (max-width: 600px) {
		font-size: 16px;
		line-height: 22px;
	}
`;

export const Break = styled.div`
	flex-basis: 100%;
	height: 0;
`;

export const WrapContent = styled.div`
	position: relative;
	width: 100%;
	padding: 75px 0;
	max-width: 950px;
	margin: 0 auto;
	text-align: center;
	z-index: 1;
	font-size: 1.625rem;
	font-weight: 400;
	color: #fff;
`;

export const WrapForm = styled.form`
	text-align: center;

	@media only screen and (min-width: 950px) and (max-width: 1449px),
		only screen and (min-width: 1450px) {
		display: flex;
		flex-direction: row;
		justify-content: center;
		margin-bottom: 1rem;
	}
`;

export const WrapFormFAQ = styled.div`
	display: flex;
	flex-direction: column;
	margin: 0 auto;
	width: 100%;
	max-width: 950px;
`;

export const Title = styled.h1`
	max-width: 640px;
	margin: 0 auto;
	line-height: 1.1;
	font-size: 1.75rem;

	@media only screen and (min-width: 950px) and (max-width: 1449px),
		only screen and (min-width: 1450px) {
		font-size: 4rem;
	}

	@media only screen and (min-width: 950px) and (max-width: 1449px) {
		font-size: 3.125rem;
	}

	@media only screen and (min-width: 1450px) {
		max-width: 800px;
	}
`;

export const SubTitle = styled.h2`
	font-size: 1.125rem;
	margin: 1rem auto;
	max-width: 640px;
	font-weight: 400;

	@media only screen and (min-width: 950px) and (max-width: 1449px),
		only screen and (min-width: 1450px) {
		font-size: 1.625rem;
	}
`;
OptionForm.jsx
  • Trong /OptionForm/index.jsx, ta thêm đoạn code như sau:
import React from "react";
import {
	Container,
	Button,
	Input,
	Text,
	Break,
	Title,
	SubTitle,
	WrapForm,
	WrapContent,
	WrapFormFAQ
} from "./styles/OptionForm";

export default function OptionForm({ children, ...restProps }) {
	return <Container {...restProps}>{children}</Container>;
}

OptionForm.Input = function OptionFormInput({ ...restProps }) {
	return <Input {...restProps} />;
};

OptionForm.Text = function OptionFormText({ children, ...restProps }) {
	return <Text {...restProps}>{children}</Text>;
};

OptionForm.Button = function OptionFormButton({ children, ...restProps }) {
	return (
		<Button {...restProps}>
			{children}{" "}
			<img src="/images/icons/chevron-right.png" alt="Finish Sign Up" />
		</Button>
	);
};

OptionForm.Break = function OptionFormBreak({ ...restProps }) {
	return <Break {...restProps} />;
};

OptionForm.WrapForm = function OptionFormWrapForm({ children, ...restProps }) {
	return <WrapForm {...restProps}>{children}</WrapForm>;
};

OptionForm.WrapFormFAQ = function OptionFormWrapFormFAQ({
	children,
	...restProps
}) {
	return <WrapFormFAQ {...restProps}>{children}</WrapFormFAQ>;
};

OptionForm.WrapContent = function OptionFormWrapContent({
	children,
	...restProps
}) {
	return <WrapContent {...restProps}>{children}</WrapContent>;
};

OptionForm.Title = function OptionFormTitle({ children, ...restProps }) {
	return <Title {...restProps}>{children}</Title>;
};

OptionForm.SubTitle = function OptionFormSubTitle({ children, ...restProps }) {
	return <SubTitle {...restProps}>{children}</SubTitle>;
};
index.jsx

Vì Button này thuộc phần FAQs luôn nên ta sẽ thêm nó vào trong file FAQsContainer.jsx luôn nhé.

import React from "react";
import { FAQs, OptionForm } from "../components/index";
import faqsData from "../fixtures/faqs.json";

export default function FAQsContainer() {
	return (
		<FAQs>
			<FAQs.Title>Frequently Asked Questions</FAQs.Title>
			<FAQs.InnerWrapper>
				{faqsData.map((item) => (
					<FAQs.Item key={item.id}>
						<FAQs.Header>{item.header}</FAQs.Header>
						<FAQs.Body>{item.body}</FAQs.Body>
					</FAQs.Item>
				))}
			</FAQs.InnerWrapper>
			<OptionForm.WrapFormFAQ>
				<OptionForm.Text>
					Ready to watch? Enter your email to create or restart your
					membership.
				</OptionForm.Text>
				<OptionForm.WrapForm>
					<OptionForm.Input placeholder="Email address" />
					<OptionForm.Button>Get Started</OptionForm.Button>
				</OptionForm.WrapForm>
			</OptionForm.WrapFormFAQ>
		</FAQs>
	);
}
FAQsContainer.jsx

Okie! xong, chúng ta run thử xem thế nào.

FAQs UI demo

Okie đẹp rồi nhé 😁.

II. Config router và phân chia các trang.

1. Config router.

Trước tiên ta cần cài thêm package react-router-dom để có thể config router nhé npm install react-router-dom

Sau khi install xong, trong folder src ta tạo folder và file như sau /constants/routes.js và trong file routes.js ta config như sau:

export const HOME = "/";
export const SIGN_UP = "/signup";
export const SIGN_IN = "/signin";
export const BROWSE = "/browse";
routes.js

Chúng ta chỉnh sửa một chút trong containers nhé, trong folder containers ta tạo thêm index.jsx và config như sau:

import StoryContainer from "./storyContainer";
import FAQsContainer from "./FAQsContainer";
import FooterContainer from "./footerContainer";

export { StoryContainer, FAQsContainer, FooterContainer };
index.jsx

Trong file App.js ta edit và thêm đoạn code như sau:

import React from "react";
import {
	StoryContainer,
	FAQsContainer,
	FooterContainer
} from "./containers/index";
import { BrowserRouter, Route } from "react-router-dom";
import * as ROUTES from "./constants/routes";

export default function App() {
	return (
		<BrowserRouter>
			<Route exact path={ROUTES.HOME}>
				<StoryContainer />
				<FAQsContainer />
				<FooterContainer />
			</Route>
			<Route exact path="/users">
				<h1>User Page</h1>
			</Route>
		</BrowserRouter>
	);
}
App.js

2. Phân chia các trang.

Một website thông thường sẽ có rất nhiều page, để website có thể redirects sang các page khác thì sẽ tuân theo một vài điều kiện nhất định. Đối với Netflix-clone hiện tại của chúng ta, mình sẽ chia trước các page như sau: Guest Home, SignUp, SignInBrowse (Browse là gì thì mình sẽ nói sau nhé).

Trong folder src ta tạo thêm file như sau:

  • /src/pages/home.jsx(Nếu có rồi thì thôi nhé):
import React from "react";
import {
	StoryContainer,
	FAQsContainer,
	FooterContainer
} from "../containers/index";

export default function Home() {
	return (
		<>
			<StoryContainer />
			<FAQsContainer />
			<FooterContainer />
		</>
	);
}
home.jsx
  • /src/pages/signup.jsx:
import React from "react";

export default function SignUp() {
	return <h1>Sign Up</h1>;
}
signup.jsx
  • /src/pages/signin.jsx:
import React from "react";

export default function SignIn() {
	return <h1>Sign In</h1>;
}
signin.jsx
  • /src/pages/browse.jsx:
import React from "react";

export default function Browse() {
	return <h1>Browse</h1>;
}
browse.jsx
  • /src/pages/index.jsx:
import Home from "./home";
import SignIn from "./signin";
import SignUp from "./signup";
import Browse from "./browse";

export { Home, SignIn, SignUp, Browse };
index.jsx
  • Trong file App.js ta edit lại như sau:
import React from "react";
import { BrowserRouter, Route } from "react-router-dom";
import * as ROUTES from "./constants/routes";
import { Home, SignIn, SignUp, Browse } from "./pages/index";

export default function App() {
	return (
		<BrowserRouter>
			<Route exact path={ROUTES.SIGN_IN}>
				<SignIn />
			</Route>
			<Route exact path={ROUTES.SIGN_UP}>
				<SignUp />
			</Route>
			<Route exact path={ROUTES.BROWSE}>
				<Browse />
			</Route>
			<Route exact path={ROUTES.HOME}>
				<Home />
			</Route>
		</BrowserRouter>
	);
}

Okie! Cơ bản như vậy đã xong, thử xem work không nhé 😁.

Redirect to another page (Sign In)

III. Tổng kết.

Okie như vậy là đã xong, trong bài này, chúng ta đã code và hoàn thiện được phần FAQs của trang, apply context vào để quản lý các state, config router và phân chia các page cho app của chúng ta. Trong phần tiếp theo chúng ta sẽ hoàn thiện Guest Home page này luôn nhé 😉. Cảm hơn các bạn đã đọc! See u again!

Bài viết cùng seri

Bài viết liên quan

ReactJS Tutorial for Beginners Phần 2

Trong bài viết này chúng ta sẽ đi qua năm nội dung đầu tiên của tutorial...

ReactJS Tutorial for Beginners Phần 2
ReactJS vs React Native - Gà cùng một mẹ liệu có giống nhau?

Bạn có đang rối giữa ReactJS vs React Native? Nên chọn cái nào thì tốt hơn? Có thể tái sử dụng code của ReactJS cho React Native hay không? Hãy khám phá câu trả lời thông qua bài viết so sánh ReactJS vs React Native này bạn nhé!...

ReactJS vs React Native - Gà cùng một mẹ liệu có giống nhau?
Netflix Clone với ReactJS, Styled Components và Firebase (Firestore & Auth) - Phần 7

Nối tiếp phần 6 của series, trong phần này, chúng ta sẽ bắt tay vào code Sign Up page,config lại phần router để đáp ứng được vấn đề Authentication kèm theo tạo custom hook để listening for Authentication. Không dài dòng nữa, Chúng ta bắt đầu thôi 😁....

Netflix Clone với ReactJS, Styled Components và Firebase (Firestore & Auth) - Phần 7
Netflix Clone với ReactJS, Styled Components và Firebase (Firestore & Auth) - Phần 6

Trong phần này, chúng ta sẽ config React Context firebase cho project của chúng ta, sau đó tạo Sign In Page và apply Authentication màfirebase cung cấp cho project của chúng ta nhé. Bắt đầu thôi 😉!...

Netflix Clone với ReactJS, Styled Components và Firebase (Firestore & Auth) - Phần 6
Netflix Clone với ReactJS, Styled Components và Firebase (Firestore & Auth) - Phần 5

Trong phần 5 này, chúng ta sẽ hoàn thiện Guest Home Page và config để connect với firebase, chuẩn bị cho các phần tiếp theo nhé 😉....

Netflix Clone với ReactJS, Styled Components và Firebase (Firestore & Auth) - Phần 5
You've successfully subscribed to 200Lab Blog
Great! Next, complete checkout for full access to 200Lab Blog
Xin chào mừng bạn đã quay trở lại
OK! Tài khoản của bạn đã kích hoạt thành công.
Success! Your billing info is updated.
Billing info update failed.
Your link has expired.