├── .gitignore
├── Chapter02
├── addEventListener.html
├── appPassingProps.jsx
├── arrowFunctions.jsx
├── authorFragment.jsx
├── automaticBatching.jsx
├── bindingConstructor.jsx
├── citySearch.jsx
├── classComponent.jsx
├── consumeContext.jsx
├── createContext.jsx
├── employeeWithDestructuredProps.jsx
├── forwardRefDemo.jsx
├── functionComponent.jsx
├── higherOrderComponent.jsx
├── javascriptRendering.js
├── jsxRendering.jsx
├── keyProp.jsx
├── mutateNestedObject.js
├── mutateSeparateNestedObject.js
├── provideContext.jsx
├── publicClassFieldsSyntax.jsx
├── refsDemo.jsx
├── refsUsecases.jsx
├── restrictDefaultFormEvent.html
├── restrictDefaultReactEvent.jsx
├── scrollAnimation.js
├── signoutReactComponent.jsx
├── signoutReactElement.js
├── signoutReactElementObject.js
├── singoutReactElementJsx.jsx
├── syntheticEvent.jsx
├── updateStateHandler.jsx
├── updateStateObject.jsx
├── userMemoForm.jsx
├── userState.jsx
├── userStateClassComponent.jsx
└── userStateNestedObject.jsx
├── Chapter03
├── colorContextProvider.jsx
├── refContentRecreated.jsx
├── refContentSkipRecreated.jsx
├── updaterFunction.jsx
├── useCallbackSkipRerender.jsx
├── useDebugDemo.jsx
├── useEffectCorrectDependencies.jsx
├── useEffectDependencies.jsx
├── useEffectEmptyDependencies.jsx
├── useEffectFetchingData.jsx
├── useEffectSkipDependencies.jsx
├── useFetchCustomHook.jsx
├── useFetchHookConsumer.jsx
├── useImmerDemo.jsx
├── useImperativeHandleDemo.jsx
├── useMemoDemo.jsx
├── useMemoInsideLoop.jsx
├── useMemoInsideLoopFix.jsx
├── useMemoReturnObject.jsx
├── useMemoSkipRerender.jsx
├── useMemoWithDependencies.jsx
├── useMemoWithoutDependencies.jsx
├── useReducerDemo.jsx
└── useStateDemo.jsx
├── Chapter04
├── BrowserRouterAsRouter.js
├── BrowserRouterPofile.js
├── FormattedMessage.js
├── Link.js
├── aboutRouteComponent.js
├── aboutRouteElement.js
├── createBrowserRouterAbout.js
├── createBrowserRouterHome.js
├── createRoutesFromElements.js
├── notFoundErrorPages.js
├── pageRoutes.js
├── profilePageParamRoutes.js
├── useLocation.js
└── useParams.js
├── Chapter05
├── deepLayout.jsx
├── enableStrictMode.jsx
├── errorBoundaryDemo.jsx
├── errorBoundaryUsage.jsx
├── pageTransition.jsx
├── portalBubbling.html
├── portalModal.jsx
├── portalParent.jsx
├── profilerDemo.jsx
├── reactNativeNavMenu.jsx
├── strictModeInPage.jsx
├── suspenseDemo.jsx
├── tsconfig.json
└── useDeferredHookAlternative.jsx
├── Chapter06
├── ProviderWrapper.js
├── actionCreatorInsideLazyLoading.js
├── addTodoAction.js
├── applyMiddleware.js
├── citiesReducer.js
├── configureStoreWithRTK.js
├── customStoreEnhancer.js
├── dispatchAddCityAction.js
├── endpointsWithRTK.js
├── loggerMiddleware.js
├── mapDispatchToProps.js
├── mapDispatchToPropsSimplified.js
├── mapStateToProps.js
├── multipleMiddlewares.js
├── sagaFunction.js
├── storeUsageDemo.js
├── thunkActionCreator.js
├── thunkFunction.js
└── todoReducer.js
├── Chapter07
├── App.css
├── App.js
├── Home.css
├── Home.module.css
├── chapter7Snippet11.js
├── dynamicStyleColor.js
├── flexbox.js
├── homeImportCSS.js
├── homeInlineCSS.js
├── importCSSModules.js
└── importSCSS.js
├── Chapter08
├── counter.js
├── counter.test.js
├── counterApp.js
└── counterApp.test.js
├── Chapter09
├── metaAuthorTag.html
├── metaLanguageTag.html
├── metaNameTag.html
├── metaPropertyTag.html
├── metaRobotsTag.html
├── metaViewportTag.html
└── titleTag.html
├── Chapter10
└── gitRepoSetup.md
├── Chapter11
└── one-stop-electronics
│ ├── .gitignore
│ ├── .vscode
│ └── settings.json
│ ├── README.md
│ ├── index.html
│ ├── package-lock.json
│ ├── package.json
│ ├── src
│ ├── App.css
│ ├── App.tsx
│ ├── app
│ │ └── store
│ │ │ ├── cart
│ │ │ ├── cart.selector.ts
│ │ │ ├── cart.slice.spec.ts
│ │ │ ├── cart.slice.ts
│ │ │ └── cart.types.ts
│ │ │ ├── hooks.ts
│ │ │ ├── product
│ │ │ ├── product.selector.ts
│ │ │ ├── product.slice.ts
│ │ │ └── product.types.ts
│ │ │ ├── root-reducer.ts
│ │ │ ├── store.ts
│ │ │ └── user
│ │ │ ├── user.selector.ts
│ │ │ ├── user.slice.ts
│ │ │ └── user.types.ts.ts
│ ├── assets
│ │ ├── clear.svg
│ │ ├── minus.svg
│ │ ├── one-stop-electronics-tab.svg
│ │ ├── one-stop-electronics.svg
│ │ ├── plus.svg
│ │ ├── products
│ │ │ ├── apple14.png
│ │ │ ├── apple_ipad10.png
│ │ │ ├── galaxy_a34.png
│ │ │ ├── galaxy_a54.png
│ │ │ ├── galaxy_tab_s8.png
│ │ │ ├── galaxytab_s9.png
│ │ │ └── huawei_p30.png
│ │ ├── screens
│ │ │ ├── cart.png
│ │ │ ├── products.png
│ │ │ ├── signin.png
│ │ │ └── signup.png
│ │ ├── shopping-cart.svg
│ │ └── user-profile-avatar.svg
│ ├── backend
│ │ └── firebase
│ │ │ ├── api
│ │ │ ├── auth.ts
│ │ │ ├── db-utils.ts
│ │ │ └── products-data.ts
│ │ │ └── config.ts
│ ├── components
│ │ ├── button
│ │ │ ├── button.styles.tsx
│ │ │ └── button.tsx
│ │ ├── categories
│ │ │ ├── categories.styles.tsx
│ │ │ ├── categories.tsx
│ │ │ └── categories.types.tsx
│ │ ├── footer
│ │ │ ├── footer.styles.tsx
│ │ │ └── footer.tsx
│ │ ├── header
│ │ │ ├── header.styles.tsx
│ │ │ └── header.tsx
│ │ ├── input
│ │ │ ├── input.styles.tsx
│ │ │ └── input.tsx
│ │ ├── product
│ │ │ ├── product.styles.tsx
│ │ │ └── product.tsx
│ │ ├── select
│ │ │ ├── select.styles.tsx
│ │ │ └── select.tsx
│ │ └── spinner
│ │ │ ├── spinner.styles.tsx
│ │ │ └── spinner.tsx
│ ├── constants.tsx
│ ├── custom.d.ts
│ ├── features
│ │ ├── auth
│ │ │ ├── signin
│ │ │ │ ├── page.styles.tsx
│ │ │ │ └── page.tsx
│ │ │ └── signup
│ │ │ │ ├── page.styles.tsx
│ │ │ │ └── page.tsx
│ │ ├── cart
│ │ │ ├── cart.styles.tsx
│ │ │ └── cart.tsx
│ │ └── products
│ │ │ ├── products.styles.tsx
│ │ │ └── products.tsx
│ ├── global.styles.tsx
│ ├── i18n
│ │ ├── locale.ts
│ │ └── translations
│ │ │ ├── de-DE.json
│ │ │ ├── en-US.json
│ │ │ └── fr-FR.json
│ ├── index.css
│ ├── main.tsx
│ ├── setupTests.ts
│ └── vite-env.d.ts
│ ├── tsconfig.json
│ ├── tsconfig.node.json
│ └── vite.config.ts
├── Chapter12
├── HomePage.js
├── HomePage.test.js
├── HomePageApp.js
├── NotFoundPage.js
├── RootLayout.js
├── SessionProvider.js
├── authoBio.json
├── authoProfile.json
├── authorBlog.json
├── buildFrontendArchitecture.md
├── coffee-restaurant
│ ├── .gitignore
│ ├── README.md
│ ├── data
│ │ ├── menu.js
│ │ └── profile.js
│ ├── img
│ │ └── coffee-restaurant.jpg
│ ├── jest.config.mjs
│ ├── jsconfig.json
│ ├── next.config.js
│ ├── package-lock.json
│ ├── package.json
│ ├── public
│ │ ├── next.svg
│ │ └── vercel.svg
│ └── src
│ │ └── app
│ │ ├── account
│ │ ├── menu
│ │ │ └── page.js
│ │ └── profile
│ │ │ └── page.js
│ │ ├── api
│ │ └── auth
│ │ │ └── [...nextauth]
│ │ │ └── route.js
│ │ ├── components
│ │ ├── GlobalStyles.js
│ │ ├── MainMenu.js
│ │ └── Provider.js
│ │ ├── favicon.ico
│ │ ├── globals.css
│ │ ├── graphql
│ │ └── route.js
│ │ ├── layout.js
│ │ ├── lib
│ │ └── registry.js
│ │ ├── not-found.js
│ │ ├── nutrition
│ │ ├── page.js
│ │ └── page.test.js
│ │ ├── page.js
│ │ ├── page.module.css
│ │ ├── page.test.js
│ │ ├── queries
│ │ └── clientQueries.js
│ │ └── utils
│ │ ├── cors.js
│ │ └── withApollo.js
├── cors.js
├── createGlobalStyle.js
├── envSecrets.md
├── expressBackend.js
├── gitRepoPush.md
├── gitRepoSetup.md
├── graphQLMenuProfileQueries.js
├── graphQLQuery.graphql
├── graphQLQueryMenu.graphql
├── graphQLSchema.graphql
├── graphQLSchemaAndResolvers.js
├── initializeApollo.js
├── installFrontendPackages.md
├── installTestingLibrary.md
├── jestTestWatch.md
├── jordanBrewerprofile.js
├── mainNavigation.js
├── menuCoffee.js
├── menuPage.js
├── nextAuth.js
├── nextConfig.js
├── nextJest.js
├── nutritionPage.js
├── nutritionPage.test.js
└── styleSheet.js
├── LICENSE
└── README.md
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # next.js
12 | /.next/
13 | /out/
14 |
15 | # production
16 | /build
17 |
18 | # misc
19 | .DS_Store
20 | *.pem
21 |
22 | # debug
23 | npm-debug.log*
24 | yarn-debug.log*
25 | yarn-error.log*
26 |
27 | # local env files
28 | .env*.local
29 |
30 | # vercel
31 | .vercel
32 |
33 | # typescript
34 | *.tsbuildinfo
35 | next-env.d.ts
36 |
--------------------------------------------------------------------------------
/Chapter02/addEventListener.html:
--------------------------------------------------------------------------------
1 |
26 | >
27 | );
28 | }
29 |
--------------------------------------------------------------------------------
/Chapter02/userState.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from "react";
2 |
3 | function User() {
4 | const [message, setMessage] = useState("Welcome to React world");
5 | return (
6 | <>
7 | {message}
8 | >
9 | );
10 | }
11 |
--------------------------------------------------------------------------------
/Chapter02/userStateClassComponent.jsx:
--------------------------------------------------------------------------------
1 | class User extends React.Component {
2 | constructor(props) {
3 | super(props);
4 | this.state = {
5 | message: "Welcome to React world",
6 | };
7 | }
8 |
9 | render() {
10 | return (
11 | <>
12 | {this.state.message}
13 | >
14 | );
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/Chapter02/userStateNestedObject.jsx:
--------------------------------------------------------------------------------
1 | {
2 | name: 'Tom',
3 | age: 35,
4 | address: {
5 | country: 'United States',
6 | state: 'Texas',
7 | postalCode: 73301
8 | }
9 | }
--------------------------------------------------------------------------------
/Chapter03/colorContextProvider.jsx:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/Chapter03/refContentRecreated.jsx:
--------------------------------------------------------------------------------
1 | function CreateBlogArticle() {
2 | // This is an expensive object to create the article
3 | }
4 |
5 | function Blog() {
6 | const articleRef = useRef(new CreateBlogArticle());
7 | //...
8 | }
9 |
--------------------------------------------------------------------------------
/Chapter03/refContentSkipRecreated.jsx:
--------------------------------------------------------------------------------
1 | function Blog() {
2 | const articleRef = useRef(null);
3 |
4 | if (articleRef.current === null) {
5 | articleRef.current = new CreateBlogArticle();
6 | }
7 | //...
8 | }
9 |
--------------------------------------------------------------------------------
/Chapter03/updaterFunction.jsx:
--------------------------------------------------------------------------------
1 | function handleClick() {
2 | setCounter((a) => a + 1);
3 | setCounter((a) => a + 1);
4 | setCounter((a) => a + 1);
5 | }
6 |
--------------------------------------------------------------------------------
/Chapter03/useCallbackSkipRerender.jsx:
--------------------------------------------------------------------------------
1 | function TaxCalculation({ year, income }) {
2 | const handleSubmit = useCallback(
3 | (taxPayerDetails) => {
4 | post("/tax/" + year, {
5 | taxPayerDetails,
6 | income,
7 | });
8 | },
9 | [year, income]
10 | );
11 |
12 | return (
13 |
14 |
15 |
16 | );
17 | }
18 |
--------------------------------------------------------------------------------
/Chapter03/useDebugDemo.jsx:
--------------------------------------------------------------------------------
1 | const useFetchData = (url, initialData) => {
2 | useDebugValue(url);
3 | const [data, setData] = useState(initialData);
4 | const [loading, setLoading] = useState(false);
5 | const [error, setError] = useState(null);
6 |
7 | useDebugValue(error, (err) =>
8 | err ? `fetch is failed with ${err.message}` : "fetch is successful"
9 | );
10 |
11 | useEffect(() => {
12 | setLoading(true);
13 | fetch(url)
14 | .then((res) => res.json())
15 | .then((data) => setData(data))
16 | .catch((err) => setError(err))
17 | .finally(() => setLoading(false));
18 | }, [url]);
19 |
20 | useDebugValue(data, (items) =>
21 | items.length > 0 ? items.map((item) => item.title) : "No posts available"
22 | );
23 | return { data, loading };
24 | };
25 |
--------------------------------------------------------------------------------
/Chapter03/useEffectCorrectDependencies.jsx:
--------------------------------------------------------------------------------
1 | useEffect(() => {
2 | const userOptions = {
3 | url: userUrl,
4 | name,
5 | };
6 |
7 | const userUrl = buildUserURL(userOptions);
8 | fetch(userUrl)
9 | .then((res) => res.json())
10 | .then((users) => setUsers(users));
11 | }, [name]);
12 |
--------------------------------------------------------------------------------
/Chapter03/useEffectDependencies.jsx:
--------------------------------------------------------------------------------
1 | useEffect(() => {
2 | // Runs after first render and every re-render with dependency change
3 | }, [name, status]);
4 |
--------------------------------------------------------------------------------
/Chapter03/useEffectEmptyDependencies.jsx:
--------------------------------------------------------------------------------
1 | useEffect(() => {
2 | // Runs after initial render
3 | }, []);
4 |
--------------------------------------------------------------------------------
/Chapter03/useEffectFetchingData.jsx:
--------------------------------------------------------------------------------
1 | const userUrl = "https://jsonplaceholder.typicode.com/users";
2 |
3 | export default function Users() {
4 | const [users, setUsers] = useState([]);
5 | const [name, setName] = useState("John");
6 | const [message, setMessage] = useState("");
7 |
8 | const userQueryOptions = {
9 | url: userUrl,
10 | name,
11 | };
12 |
13 | useEffect(() => {
14 | const userUrl = buildUserURL(userQueryOptions); //buildUserURL is excluded from code snippet
15 | fetch(userUrl)
16 | .then((res) => res.json())
17 | .then((users) => setUsers(users));
18 | }, [userQueryOptions]);
19 |
20 | return (
21 | <>
22 | Users: {message}
23 | setMessage(e.target.value)} />
24 | setName(e.target.value)} />
25 | {users &&
26 | users.map((user) => (
27 |
28 | Name: {user.name}
29 | Email: {user.email}
30 |
31 | ))}
32 | >
33 | );
34 | }
35 |
--------------------------------------------------------------------------------
/Chapter03/useEffectSkipDependencies.jsx:
--------------------------------------------------------------------------------
1 | useEffect(() => {
2 |
3 | // Runs after every re-render
4 |
5 | });
--------------------------------------------------------------------------------
/Chapter03/useFetchCustomHook.jsx:
--------------------------------------------------------------------------------
1 | import { useState, useEffect } from "react";
2 |
3 | const useFetchData = (url, initialData) => {
4 | const [data, setData] = useState(initialData);
5 | const [loading, setLoading] = useState(false);
6 |
7 | useEffect(() => {
8 | setLoading(true);
9 | fetch(url)
10 | .then((res) => res.json())
11 | .then((data) => setData(data))
12 | .catch((err) => console.log(err))
13 | .finally(() => setLoading(false));
14 | }, [url]);
15 |
16 | return { data, loading };
17 | };
18 |
19 | export default useFetchData;
20 |
--------------------------------------------------------------------------------
/Chapter03/useFetchHookConsumer.jsx:
--------------------------------------------------------------------------------
1 | import useFetchData from "./useFetchData.js";
2 |
3 | export default function Posts() {
4 | const url = "https://jsonplaceholder.typicode.com/posts?userId=1";
5 | const { data, loading } = useFetchData(url, []);
6 |
7 | return (
8 | <>
9 | {loading && Loading posts...
}
10 | {data &&
11 | data.map((item) => (
12 |
13 |
14 | {item?.title}
15 |
16 | {item?.body}
17 |
18 |
19 | ))}
20 | >
21 | );
22 | }
23 |
--------------------------------------------------------------------------------
/Chapter03/useImmerDemo.jsx:
--------------------------------------------------------------------------------
1 | import { useImmer } from "use-immer";
2 |
3 | function UserProfile() {
4 | const [user, setUser] = useImmer({
5 | name: "Tom",
6 | address: {
7 | country: "United States",
8 | city: "Austin",
9 | postalCode: 73301,
10 | },
11 | });
12 |
13 | function updatePostalCode(code) {
14 | setUser((draft) => {
15 | draft.address.postalCode = code;
16 | });
17 | }
18 |
19 | return (
20 |
21 |
22 | Hello {user.name}, your latest postal code is ({user.address.postalCode}
23 | )
24 |
25 | {
27 | updatePostalCode(e.target.value);
28 | }}
29 | value={user.address.postalCode}
30 | />
31 |
32 | );
33 | }
34 |
--------------------------------------------------------------------------------
/Chapter03/useImperativeHandleDemo.jsx:
--------------------------------------------------------------------------------
1 | useImperativeHandle(ref, () => ({
2 | open: () => ref.current.invokeDialog(),
3 | close: () => ref.current.closeDilaog(),
4 | reset: () => ref.current.clearData(),
5 | }));
6 |
--------------------------------------------------------------------------------
/Chapter03/useMemoDemo.jsx:
--------------------------------------------------------------------------------
1 | import { useState, useMemo } from "react";
2 |
3 | function factorial(number) {
4 | if (number <= 0) {
5 | return "Number should be positive value.";
6 | } else if (number === 1) {
7 | return 1;
8 | } else {
9 | return number * factorial(number - 1);
10 | }
11 | }
12 |
13 | export default function CounterFactorial() {
14 | const [count, setCount] = useState(0);
15 | const [number, setNumber] = useState(1);
16 |
17 | const factorial = useMemo(() => factorial(number), [number]);
18 |
19 | return (
20 | <>
21 | Counter: {count}
22 | setCount(count + 1)}>Increment
23 | Factorial: {factorial}
24 | setNumber(number + 1)}
28 | />
29 | >
30 | );
31 | }
32 |
--------------------------------------------------------------------------------
/Chapter03/useMemoInsideLoop.jsx:
--------------------------------------------------------------------------------
1 | {
2 | products.map((product) => {
3 | const revenue = useMemo(() => calculateRevenue(product), [product]);
4 |
5 | return (
6 | <>
7 | Product: {product.name}
8 | Revenue: {revenue}
9 | >
10 | );
11 | });
12 | }
13 |
--------------------------------------------------------------------------------
/Chapter03/useMemoInsideLoopFix.jsx:
--------------------------------------------------------------------------------
1 | {
2 | products.map((product) => {
3 | return ;
4 | });
5 | }
6 |
--------------------------------------------------------------------------------
/Chapter03/useMemoReturnObject.jsx:
--------------------------------------------------------------------------------
1 | const findCity = useMemo(() => {
2 | return {
3 | country: "USA",
4 | name: name,
5 | };
6 | }, [name]);
7 |
--------------------------------------------------------------------------------
/Chapter03/useMemoSkipRerender.jsx:
--------------------------------------------------------------------------------
1 | import { memo } from 'react';
2 |
3 | const TaxPayer = memo(function TaxPayer({ onSubmit }) {
4 | // ...
5 | });
--------------------------------------------------------------------------------
/Chapter03/useMemoWithDependencies.jsx:
--------------------------------------------------------------------------------
1 | const filterCities = useMemo(
2 | () => filteredCities(city, country),
3 | [city, country]
4 | );
5 |
--------------------------------------------------------------------------------
/Chapter03/useMemoWithoutDependencies.jsx:
--------------------------------------------------------------------------------
1 | const filterCities = useMemo(() => filteredCities(city, country));
2 |
--------------------------------------------------------------------------------
/Chapter03/useReducerDemo.jsx:
--------------------------------------------------------------------------------
1 | function reducer(state, action) {
2 | switch (action.type) {
3 | case "increment":
4 | return { count: state.count + 1 };
5 |
6 | case "decrement":
7 | return { count: state.count - 1 };
8 |
9 | case "reset":
10 | return { count: action.payload };
11 |
12 | default:
13 | throw new Error();
14 | }
15 | }
16 |
17 | function init(initialCount) {
18 | return { count: initialCount };
19 | }
20 |
21 | function Counter() {
22 | const initialCount = 0;
23 |
24 | const [state, dispatch] = useReducer(reducer, initialCount, init);
25 |
26 | return (
27 | <>
28 | Count: {state.count}
29 | dispatch({ type: "reset", payload: initialCount })}
31 | >
32 | Reset
33 |
34 | dispatch({ type: "decrement" })}>decrement
35 | dispatch({ type: "increment" })}>increment
36 | >
37 | );
38 | }
39 |
--------------------------------------------------------------------------------
/Chapter03/useStateDemo.jsx:
--------------------------------------------------------------------------------
1 | import { useState } from "react";
2 |
3 | function Counter() {
4 | const [count, setCount] = useState(0);
5 |
6 | return (
7 | <>
8 | You clicked {count} times
9 | setCount(count + 1)}>Click me
10 | >
11 | );
12 | }
13 |
--------------------------------------------------------------------------------
/Chapter04/BrowserRouterAsRouter.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { BrowserRouter as Router, Route, Switch } from 'react-router-dom';
3 | import { NotFound, ErrorPage } from './ErrorComponents';
4 | import Home from './Home';
5 |
6 | function App() {
7 | return (
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 | );
16 | }
17 |
18 | export default App;
19 |
--------------------------------------------------------------------------------
/Chapter04/BrowserRouterPofile.js:
--------------------------------------------------------------------------------
1 | import { BrowserRouter, Route } from 'react-router-dom';
2 |
3 | function App() {
4 | return (
5 |
6 |
7 |
8 | );
9 | }
10 |
--------------------------------------------------------------------------------
/Chapter04/FormattedMessage.js:
--------------------------------------------------------------------------------
1 | import { IntlProvider, FormattedMessage } from 'react-intl';
2 | import English from './translations/en.json';
3 | import French from './translations/fr.json';
4 |
5 | const Home = () => (
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 | );
15 |
16 | // We can assume that we are able to get the user's preferred language from somewhere like in user or browser settings...
17 |
18 | const userLanguage = 'fr'; // This value can be dynamically created.
19 |
20 | const messages = {
21 | en: English,
22 |
23 | fr: French,
24 | };
25 |
26 | const App = () => (
27 |
28 |
29 |
30 | );
31 |
32 | export default App;
33 |
--------------------------------------------------------------------------------
/Chapter04/Link.js:
--------------------------------------------------------------------------------
1 | import { Link } from 'react-router-dom';
2 |
3 |
9 | My Link
10 | ;
11 |
--------------------------------------------------------------------------------
/Chapter04/aboutRouteComponent.js:
--------------------------------------------------------------------------------
1 | ;
2 |
--------------------------------------------------------------------------------
/Chapter04/aboutRouteElement.js:
--------------------------------------------------------------------------------
1 | } />;
2 |
--------------------------------------------------------------------------------
/Chapter04/createBrowserRouterAbout.js:
--------------------------------------------------------------------------------
1 | import { createRoot } from 'react-dom/client';
2 | import { createBrowserRouter, RouterProvider, Link } from 'react-router-dom';
3 |
4 | const router = createBrowserRouter([
5 | {
6 | path: '/',
7 | element: (
8 |
9 |
Hello World
10 | About Us
11 |
12 | ),
13 | },
14 |
15 | {
16 | path: 'about',
17 | element: About
,
18 | },
19 | ]);
20 |
21 | createRoot(document.getElementById('root')).render(
22 |
23 | );
24 |
--------------------------------------------------------------------------------
/Chapter04/createBrowserRouterHome.js:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import * as ReactDOM from 'react-dom/client';
3 | import { createBrowserRouter, RouterProvider } from 'react-router-dom';
4 | import './index.css';
5 |
6 | const router = createBrowserRouter([
7 | {
8 | path: '/',
9 |
10 | element: Hello world!
,
11 | },
12 | ]);
13 |
14 | ReactDOM.createRoot(document.getElementById('root')).render(
15 |
16 |
17 |
18 | );
19 |
--------------------------------------------------------------------------------
/Chapter04/createRoutesFromElements.js:
--------------------------------------------------------------------------------
1 | createBrowserRouter(
2 | createRoutesFromElements(
3 | }>
4 | } />
5 | }
8 | loader={({ request }) =>
9 | fetch('/data/api/admnin.json', {
10 | signal: request.signal,
11 | })
12 | }
13 | />
14 | }>
15 | } loader={redirectIfUser} />
16 |
17 |
18 |
19 | )
20 | );
21 |
--------------------------------------------------------------------------------
/Chapter04/notFoundErrorPages.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | const NotFound = () => {
4 | return 404 – The page was not found
;
5 | };
6 |
7 | const ErrorPage = () => {
8 | return An error occurred. :(
;
9 | };
10 |
11 | export { NotFound, ErrorPage };
12 |
--------------------------------------------------------------------------------
/Chapter04/pageRoutes.js:
--------------------------------------------------------------------------------
1 |
2 | }>
3 | } />
4 | } />
5 |
6 |
7 | } />
8 | ;
9 |
--------------------------------------------------------------------------------
/Chapter04/profilePageParamRoutes.js:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { Routes, Route, useParams } from 'react-router-dom';
3 |
4 | function ProfilePage() {
5 | // Get the userId param from the URL.
6 |
7 | let { userId } = useParams();
8 |
9 | // ...
10 | }
11 |
12 | function App() {
13 | return (
14 |
15 |
16 | } />
17 |
18 | {/* */}
19 |
20 |
21 | );
22 | }
23 |
--------------------------------------------------------------------------------
/Chapter04/useLocation.js:
--------------------------------------------------------------------------------
1 | import { useLocation } from 'react-router-dom';
2 |
3 | function MyComponent() {
4 | const location = useLocation();
5 |
6 | console.log(location.state.myData); // Outputs: "Hello, World!"
7 |
8 | // ...
9 | }
10 |
--------------------------------------------------------------------------------
/Chapter04/useParams.js:
--------------------------------------------------------------------------------
1 | import { Route, useParams } from 'react-router-dom';
2 |
3 | function MyComponent() {
4 | let { id } = useParams();
5 |
6 | return My id is: {id} ;
7 | }
8 |
9 | function App() {
10 | return (
11 |
12 |
13 |
14 | );
15 | }
16 |
--------------------------------------------------------------------------------
/Chapter05/deepLayout.jsx:
--------------------------------------------------------------------------------
1 | function MyLogoComponent() {
2 | return (
3 |
4 |
5 |
6 |
7 | This is a caption
8 |
9 |
10 |
11 | );
12 | }
--------------------------------------------------------------------------------
/Chapter05/enableStrictMode.jsx:
--------------------------------------------------------------------------------
1 | import { StrictMode } from "react";
2 | import { createRoot } from "react-dom/client";
3 |
4 | const root = createRoot(document.getElementById("root"));
5 | root.render(
6 |
7 |
8 |
9 | );
10 |
--------------------------------------------------------------------------------
/Chapter05/errorBoundaryDemo.jsx:
--------------------------------------------------------------------------------
1 | class MyErrorBoundary extends Component {
2 | constructor(props) {
3 | super(props);
4 | this.state = { isErrorThrown: false };
5 | }
6 |
7 | static getDerivedStateFromError(error) {
8 | return { isErrorThrown: true };
9 | }
10 |
11 | componentDidCatch(error, errorInfo) {
12 | logErrorToReportingService(error, errorInfo);
13 | }
14 |
15 | render() {
16 | if (this.state.isErrorThrown) {
17 | return Oops, the application is unavaialble. ;
18 | }
19 | return this.props.children;
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/Chapter05/errorBoundaryUsage.jsx:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/Chapter05/pageTransition.jsx:
--------------------------------------------------------------------------------
1 | function navigate(url) {
2 | startTransition(() => {
3 | setPage(url);
4 | });
5 | }
6 |
--------------------------------------------------------------------------------
/Chapter05/portalBubbling.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/Chapter05/portalModal.jsx:
--------------------------------------------------------------------------------
1 | import { createPortal } from "react-dom";
2 |
3 | const ModalWindow = ({ description, isOpen, onClose }) => {
4 | if (!isOpen) return null;
5 |
6 | return createPortal(
7 |
8 | {description}
9 | Close
10 |
,
11 | document.body
12 | );
13 | };
14 |
--------------------------------------------------------------------------------
/Chapter05/portalParent.jsx:
--------------------------------------------------------------------------------
1 | function ParentComponent() {
2 | const [open, setOpen] = useState(false);
3 |
4 | return (
5 |
6 | setOpen(true)}>Open Modal
7 | setOpen(false)}
11 | />
12 |
13 | );
14 | }
15 |
--------------------------------------------------------------------------------
/Chapter05/profilerDemo.jsx:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/Chapter05/reactNativeNavMenu.jsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import { View, Text } from "react-native";
3 | import { NavigationContainer } from "@react-navigation/native";
4 | import { createNativeStackNavigator } from "@react-navigation/native-stack";
5 | import HomeScreen from "components/HomeScreen";
6 | import ServicesScreen from "components/ServicesScreen";
7 |
8 | const Stack = createNativeStackNavigator();
9 |
10 | function App() {
11 | return (
12 |
13 |
14 |
15 |
16 |
17 |
18 | );
19 | }
20 |
21 | export default App;
22 |
--------------------------------------------------------------------------------
/Chapter05/strictModeInPage.jsx:
--------------------------------------------------------------------------------
1 | <>
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 | >;
12 |
--------------------------------------------------------------------------------
/Chapter05/suspenseDemo.jsx:
--------------------------------------------------------------------------------
1 | import { Suspense } from "react";
2 |
3 | import Posts from "./posts.js";
4 |
5 | export default function Author({ author }) {
6 | return (
7 | <>
8 | {author.name}
9 | {author.age}
10 | }>
11 |
12 |
13 | >
14 | );
15 | }
16 |
17 | function Loading() {
18 | return Loading... ;
19 | }
20 |
--------------------------------------------------------------------------------
/Chapter05/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 |
3 | "compilerOptions": {
4 | // ...
5 | "rootDir": "src",
6 | "outDir": "dist"
7 | // ...
8 | },
9 | }
--------------------------------------------------------------------------------
/Chapter05/useDeferredHookAlternative.jsx:
--------------------------------------------------------------------------------
1 | const deferredAuthorDetails = useDeferredValue(author);
--------------------------------------------------------------------------------
/Chapter06/ProviderWrapper.js:
--------------------------------------------------------------------------------
1 | const rootElement = document.getElementById("root");
2 |
3 | render(
4 |
5 |
6 | ,
7 | rootElement
8 | );
9 |
--------------------------------------------------------------------------------
/Chapter06/actionCreatorInsideLazyLoading.js:
--------------------------------------------------------------------------------
1 | function AuthorComponent({ authorId }) {
2 | //...
3 | const onLazyLoading = () => {
4 | dispatch(getPostsByAuthor(authorId))
5 | }
6 | }
--------------------------------------------------------------------------------
/Chapter06/addTodoAction.js:
--------------------------------------------------------------------------------
1 | function addTodo(todo) {
2 | return {
3 | type: 'ADD_TODO',
4 | payload: todo
5 | }
6 | }
--------------------------------------------------------------------------------
/Chapter06/applyMiddleware.js:
--------------------------------------------------------------------------------
1 | const middleware = applyMiddleware(loggerMiddleware);
2 |
--------------------------------------------------------------------------------
/Chapter06/citiesReducer.js:
--------------------------------------------------------------------------------
1 | function cities(cities = [], action) {
2 | switch (action.type) {
3 | case "ADD_CITY":
4 | return [
5 | ...cities,
6 | {
7 | name: action.payloadname,
8 | position: 1,
9 | },
10 | ];
11 | default:
12 | return cities;
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/Chapter06/configureStoreWithRTK.js:
--------------------------------------------------------------------------------
1 | import { configureStore } from "@reduxjs/toolkit";
2 | import { setupListeners } from "@reduxjs/toolkit/query";
3 | import { usersServerApi } from "./services/usersAPI";
4 |
5 | export const store = configureStore({
6 | reducer: {
7 | [usersServerApi.reducerPath]: usersServerApi.reducer,
8 | },
9 |
10 | middleware: (getDefaultMiddleware) =>
11 | getDefaultMiddleware().concat(usersServerApi.middleware),
12 | });
13 |
14 | setupListeners(store.dispatch);
15 |
--------------------------------------------------------------------------------
/Chapter06/customStoreEnhancer.js:
--------------------------------------------------------------------------------
1 | const ourCustomEnhancer =
2 | (createStore) => (reducer, initialState, enhancer) => {
3 | const customReducer = (state, action) => {
4 | // Logic to return new state
5 | };
6 | const store = createStore(customReducer, initialState, enhancer);
7 | //Add enhancer logic
8 | return {
9 | ...store,
10 | //Override the some store properties or add new properties
11 | };
12 | };
13 |
--------------------------------------------------------------------------------
/Chapter06/dispatchAddCityAction.js:
--------------------------------------------------------------------------------
1 | store.dispatch({
2 | type: "ADD_CITY",
3 | payload: "London",
4 | });
5 |
--------------------------------------------------------------------------------
/Chapter06/endpointsWithRTK.js:
--------------------------------------------------------------------------------
1 | import { createApi, fetchBaseQuery } from "@reduxjs/toolkit/query/react";
2 |
3 | export const usersServerApi = createApi({
4 | reducerPath: "api",
5 | baseQuery: fetchBaseQuery({
6 | baseUrl: "https://jsonplaceholder.typicode.com/",
7 | }),
8 |
9 | endpoints: (builder) => ({
10 | users: builder.query({
11 | query: (page = 1) => `users?page=${page}&limit=10`,
12 | }),
13 |
14 | createUser: builder.mutation({
15 | query: (name) => ({
16 | url: "users",
17 | method: "POST",
18 | body: { name },
19 | }),
20 | }),
21 | }),
22 | });
23 |
24 | export const { useUsersQuery, useCreateUserMutation } = usersServerApi;
25 |
--------------------------------------------------------------------------------
/Chapter06/loggerMiddleware.js:
--------------------------------------------------------------------------------
1 | const loggerMiddleware = (store) => (next) => (action) => {
2 | console.log("action", action);
3 | return next(action);
4 | };
5 |
--------------------------------------------------------------------------------
/Chapter06/mapDispatchToProps.js:
--------------------------------------------------------------------------------
1 | const mapDispatchToProps = (dispatch) => {
2 | return {
3 | toggleCity: (city) => {
4 | dispatch(changeCity(city));
5 | },
6 | };
7 | };
8 |
--------------------------------------------------------------------------------
/Chapter06/mapDispatchToPropsSimplified.js:
--------------------------------------------------------------------------------
1 | const mapDispatchToProps = {
2 | toggleCity
3 | };
--------------------------------------------------------------------------------
/Chapter06/mapStateToProps.js:
--------------------------------------------------------------------------------
1 | const mapStateToProps = (state) => {
2 | return {
3 | city: state.user.address.city,
4 | };
5 | };
6 | connect(mapStateToProps)(WeatherReport);
7 |
--------------------------------------------------------------------------------
/Chapter06/multipleMiddlewares.js:
--------------------------------------------------------------------------------
1 | const middleware = applyMiddleware(
2 | loggerMiddleware,
3 | firstMiddleware,
4 | secondMiddleware,
5 | thirdMiddleware
6 | );
7 |
--------------------------------------------------------------------------------
/Chapter06/sagaFunction.js:
--------------------------------------------------------------------------------
1 | import { takeLatest, put, call } from "redux-saga/effects";
2 | import { GET_POSTS } from "./actionTypes";
3 | import { getPostsSuccess, getPostsFail } from "./actions";
4 | import { getPosts } from "../backend/api/posts ";
5 |
6 | function* fetchAuthorPosts() {
7 | try {
8 | const response = yield call(getPosts);
9 | yield put(getPostsSuccess(response));
10 | } catch (error) {
11 | yield put(getPostsFail(error.response));
12 | }
13 | }
14 |
15 | function* postsSaga() {
16 | yield takeLatest(GET_POSTS, fetchAuthorPosts);
17 | }
18 |
19 | export default postsSaga;
20 |
--------------------------------------------------------------------------------
/Chapter06/storeUsageDemo.js:
--------------------------------------------------------------------------------
1 | import { createStore } from "redux";
2 | import todoReducer from "reducers/todoReducer";
3 |
4 | const store = createStore(todoReducer); // Create a store
5 | const firstTodo = addTodo({ name: "Running", priority: 2 });
6 |
7 | console.log(firstTodo);
8 | store.dispatch(firstTodo); // Dispatch a todo
9 |
10 | const secondTodo = addTodo({ name: "Eating", priority: 1 });
11 | console.log(secondTodo);
12 | store.dispatch(secondTodo);
13 | console.log(store.getState()); // Returns the todos list
14 |
--------------------------------------------------------------------------------
/Chapter06/thunkActionCreator.js:
--------------------------------------------------------------------------------
1 | export const getPostsByAuthor = (authorId) => async (dispatch) => {
2 | const response = await client.get(`/api/posts/${authorId}`);
3 | dispatch(postsLoaded(response.posts));
4 | };
5 |
--------------------------------------------------------------------------------
/Chapter06/thunkFunction.js:
--------------------------------------------------------------------------------
1 | const thunkFunction = (dispatch, getState) => {
2 | // The logic is usedThis is the place where you can write logic to dispatch actions or read state
3 | };
4 | store.dispatch(thunkFunction);
5 |
--------------------------------------------------------------------------------
/Chapter06/todoReducer.js:
--------------------------------------------------------------------------------
1 | const todoReducer = (state = initialState, action) => {
2 | switch (action.type) {
3 | case "ADD_TODO":
4 | const { name, priority } = action.payload;
5 | return [...state.todos, { name, priority }];
6 |
7 | default:
8 | return state;
9 | }
10 | };
11 |
--------------------------------------------------------------------------------
/Chapter07/App.css:
--------------------------------------------------------------------------------
1 | /* App.css */
2 |
3 | .container {
4 | text-align: center;
5 | margin: 0 auto;
6 | background-color: #bada55;
7 | padding: 1rem;
8 | }
9 |
10 | .title {
11 | font-size: 2rem;
12 | font-weight: bold;
13 | }
14 |
--------------------------------------------------------------------------------
/Chapter07/App.js:
--------------------------------------------------------------------------------
1 | // App.js
2 |
3 | import './App.css';
4 |
5 | export default function App() {
6 | return (
7 |
8 |
Hello, World!
9 |
10 | );
11 | }
12 |
--------------------------------------------------------------------------------
/Chapter07/Home.css:
--------------------------------------------------------------------------------
1 | /* Home.css */
2 |
3 | .container {
4 | margin: 0 auto;
5 | display: flex;
6 | flex-flow: column nowrap;
7 | background-color: #0384c8;
8 | padding: 2rem;
9 | }
10 |
11 | .main-content {
12 | display: flex;
13 | flex-flow: row nowrap;
14 | padding: 2rem 0;
15 | }
16 |
--------------------------------------------------------------------------------
/Chapter07/Home.module.css:
--------------------------------------------------------------------------------
1 | /* Home.module.css */
2 |
3 | .main {
4 | display: flex;
5 | padding: 2rem;
6 | color: #ffffff;
7 | }
8 |
9 | .box {
10 | background-color: rgb(241, 255, 240);
11 | color: #000;
12 | padding: 1rem;
13 | margin: 1rem;
14 | }
15 |
--------------------------------------------------------------------------------
/Chapter07/chapter7Snippet11.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 |
3 | const ContainerDiv = styled.div`
4 | color: blue;
5 |
6 | font-size: 30px;
7 | `;
8 |
9 | export default function Home() {
10 | return Hello World! ;
11 | }
12 |
--------------------------------------------------------------------------------
/Chapter07/dynamicStyleColor.js:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { useState } from 'react';
4 |
5 | export default function Home() {
6 | const [h1color, setH1Color] = useState('blue');
7 |
8 | return (
9 |
10 |
Hello World
11 |
12 | );
13 | }
14 |
--------------------------------------------------------------------------------
/Chapter07/flexbox.js:
--------------------------------------------------------------------------------
1 | export default function Home() {
2 | return (
3 | <>
4 |
9 | >
10 | );
11 | }
12 |
--------------------------------------------------------------------------------
/Chapter07/homeImportCSS.js:
--------------------------------------------------------------------------------
1 | import './Home.css';
2 |
3 | export default function Home() {
4 | return (
5 |
6 |
Heading 1
7 |
Heading 2
8 |
Heading 3
9 |
Heading 4
10 |
Heading 5
11 |
12 |
13 |
14 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam eu mi
15 | sit amet velit convallis tincidunt.
16 |
17 |
18 |
19 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam eu mi
20 | sit amet velit convallis tincidunt.
21 |
22 |
23 |
24 | );
25 | }
26 |
--------------------------------------------------------------------------------
/Chapter07/homeInlineCSS.js:
--------------------------------------------------------------------------------
1 | const container = {
2 | display: 'flex',
3 | flexFlow: 'column nowrap',
4 | backgroundColor: '#7e7dd6',
5 | padding: '2rem',
6 | };
7 |
8 | const mainContent = {
9 | display: 'flex',
10 | flexFlow: 'row nowrap',
11 | padding: '2rem 0',
12 | };
13 |
14 | export default function Home() {
15 | return (
16 | <>
17 |
18 |
Heading 1
19 |
20 |
Heading 2
21 |
22 |
Heading 3
23 |
24 |
Heading 4
25 |
26 |
Heading 5
27 |
28 |
29 |
30 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam eu
31 | mi sit amet velit convallis tincidunt.
32 |
33 |
34 |
35 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam eu
36 | mi sit amet velit convallis tincidunt.
37 |
38 |
39 |
40 | >
41 | );
42 | }
43 |
--------------------------------------------------------------------------------
/Chapter07/importCSSModules.js:
--------------------------------------------------------------------------------
1 | // page.js
2 | import styles from './Home.module.css';
3 |
4 | export default function Home() {
5 | return (
6 | <>
7 |
8 |
Hello World!
9 |
10 |
11 |
12 |
13 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. Etiam
14 | convallis, nulla non laoreet condimentum, turpis felis finibus metus,
15 | ut molestie risus enim id neque. Integer tristique purus non gravida
16 | sodales. Maecenas ultricies feugiat dolor lobortis commodo. Sed
17 | maximus vitae neque quis mollis.
18 |
19 |
20 |
21 |
22 |
23 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. Etiam
24 | convallis, nulla non laoreet condimentum, turpis felis finibus metus,
25 | ut molestie risus enim id neque. Integer tristique purus non gravida
26 | sodales. Maecenas ultricies feugiat dolor lobortis commodo. Sed
27 | maximus vitae neque quis mollis.
28 |
29 |
30 | >
31 | );
32 | }
33 |
--------------------------------------------------------------------------------
/Chapter07/importSCSS.js:
--------------------------------------------------------------------------------
1 | import './styles.scss';
2 |
3 | const MyComponent = () => {
4 | return Hello, World!
;
5 | };
6 |
7 | export default MyComponent;
8 |
--------------------------------------------------------------------------------
/Chapter08/counter.js:
--------------------------------------------------------------------------------
1 | import { useState } from 'react';
2 |
3 | const Counter = () => {
4 | const [count, setCount] = useState(0);
5 | const increment = () => setCount(count + 1);
6 | const decrement = () => setCount(count - 1);
7 |
8 | return (
9 |
10 |
Counter: {count}
11 |
12 | Increment
13 |
14 | Decrement
15 |
16 | );
17 | };
18 |
19 | export default Counter;
20 |
--------------------------------------------------------------------------------
/Chapter08/counter.test.js:
--------------------------------------------------------------------------------
1 | import { render, screen, fireEvent } from '@testing-library/react';
2 |
3 | import '@testing-library/jest-dom/extend-expect';
4 |
5 | import Counter from './Counter';
6 |
7 | describe('Counter component', () => {
8 | test('renders Counter component', () => {
9 | render( );
10 |
11 | expect(screen.getByText(/Counter:/i)).toBeInTheDocument();
12 | });
13 |
14 | test('increases the count when the Increment button is clicked', () => {
15 | render( );
16 |
17 | fireEvent.click(screen.getByText(/Increment/i));
18 |
19 | expect(screen.getByText(/Counter: 1/i)).toBeInTheDocument();
20 | });
21 |
22 | test('decreases the count when the Decrement button is clicked', () => {
23 | render( );
24 |
25 | fireEvent.click(screen.getByText(/Increment/i));
26 |
27 | fireEvent.click(screen.getByText(/Decrement/i));
28 |
29 | expect(screen.getByText(/Counter: 0/i)).toBeInTheDocument();
30 | });
31 | });
32 |
--------------------------------------------------------------------------------
/Chapter08/counterApp.js:
--------------------------------------------------------------------------------
1 | import { useState } from 'react';
2 |
3 | import './App.css';
4 |
5 | function App() {
6 | const [count, setCount] = useState(0);
7 |
8 | return (
9 |
10 |
Counter App
11 |
12 | Count: {count}
13 |
14 | setCount(count + 1)}
16 | data-testid="increment-button"
17 | >
18 | Increment
19 |
20 |
21 | setCount(count - 1)}
23 | data-testid="decrement-button"
24 | >
25 | Decrement
26 |
27 |
28 | );
29 | }
30 |
31 | export default App;
32 |
--------------------------------------------------------------------------------
/Chapter08/counterApp.test.js:
--------------------------------------------------------------------------------
1 | describe('Counter App', () => {
2 | beforeEach(() => {
3 | cy.visit('/');
4 | });
5 |
6 | it('increases the counter', () => {
7 | cy.get('[data-testid="increment-button"]').click();
8 |
9 | cy.get('[data-testid="counter-display"]').contains('Count: 1');
10 | });
11 |
12 | it('decreases the counter', () => {
13 | cy.get('[data-testid="decrement-button"]').click();
14 |
15 | cy.get('[data-testid="counter-display"]').contains('Count: -1');
16 | });
17 |
18 | it('increases and decreases the counter', () => {
19 | cy.get('[data-testid="increment-button"]').click().click();
20 |
21 | cy.get('[data-testid="decrement-button"]').click();
22 |
23 | cy.get('[data-testid="counter-display"]').contains('Count: 1');
24 | });
25 | });
26 |
--------------------------------------------------------------------------------
/Chapter09/metaAuthorTag.html:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/Chapter09/metaLanguageTag.html:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/Chapter09/metaNameTag.html:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/Chapter09/metaPropertyTag.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/Chapter09/metaRobotsTag.html:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/Chapter09/metaViewportTag.html:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/Chapter09/titleTag.html:
--------------------------------------------------------------------------------
1 | Home page – Programming content ;
2 |
--------------------------------------------------------------------------------
/Chapter10/gitRepoSetup.md:
--------------------------------------------------------------------------------
1 | echo "# myapp" >> README.md
2 |
3 | git init
4 | git add README.md
5 | git commit -m "first commit"
6 | git branch -M main
7 | git remote add origin https://github.com/yourusername/myapp.git
8 | git push -u origin main
9 |
--------------------------------------------------------------------------------
/Chapter11/one-stop-electronics/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | pnpm-debug.log*
8 | lerna-debug.log*
9 |
10 | # Dependencies
11 | node_modules
12 | .yarn/*
13 | !.yarn/patches
14 | !.yarn/plugins
15 | !.yarn/releases
16 | !.yarn/sdks
17 | !.yarn/versions
18 | # Swap the comments on the following lines if you don't wish to use zero-installs
19 | # Documentation here: https://yarnpkg.com/features/zero-installs
20 | !.yarn/cache
21 | #.pnp.*
22 |
23 | # Testing
24 | coverage
25 |
26 | # Production
27 | build
28 |
29 | # Miscellaneous
30 | *.local
31 |
--------------------------------------------------------------------------------
/Chapter11/one-stop-electronics/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "[typescriptreact]": {
3 | "editor.formatOnSave": true
4 | },
5 | "eslint.validate": [
6 | {
7 | "language": "typescript",
8 | "autoFix": true
9 | },
10 | {
11 | "language": "typescriptreact",
12 | "autoFix": true
13 | }
14 | ],
15 | "eslint.autoFixOnSave": true
16 | }
17 |
--------------------------------------------------------------------------------
/Chapter11/one-stop-electronics/README.md:
--------------------------------------------------------------------------------
1 | # one-stop-electronics
2 |
3 | The demo link is available here: https://onestop-electronics.netlify.app/
4 |
5 | ## Install and Setup
6 |
7 | ## Setup firebase project
8 | 1. Sign-in firebase console to configure the firebase project here: https://console.firebase.google.com/
9 | 2. Ignore google analytics for this project and select authentication and cloudstore firebase services
10 | 3. Copy the firebase configuration and store it for creating the firebase instance.
11 |
12 | ### Environment variable file setup
13 |
14 | Create a `.env.local` file and put it in the root folder with the firebase configuration from previous step.
15 |
16 | ```javascript
17 | VITE_FIREBASE_API_KEY = "yourfirebaseapikey"
18 | VITE_FIREBASE_AUTH_DOMAIN = "yourfirebaseauthdomain"
19 | VITE_FIREBASE_PROJECT_ID = "yourfirebaseprojectid"
20 | VITE_FIREBASE_STORAGE_BUCKET = "yourfirebasestoragebucket"
21 | VITE_FIREBASE_MESSAGING_SENDER_ID = "yourfirebasemessagingsenderid"
22 | VITE_FIREBASE_APP_ID = "yourfirebaseappid"
23 | ```
24 |
25 | Use the below scripts to run the code, tests and linting
26 |
27 | - `dev`/`start` - start dev server and open browser
28 | - `build` - build for production
29 | - `preview` - locally preview production build
30 | - `test` - launch test runner
31 | - `format` - format the code based on prettier
32 | - `lint` - apply eslint for typescript files
33 |
34 | ## Screens
35 | 1. SingUp:
36 | 
37 | 2. SingIn:
38 | 
39 | 3. Products:
40 | 
41 | 4. Cart:
42 | 
43 |
44 |
45 |
--------------------------------------------------------------------------------
/Chapter11/one-stop-electronics/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | One Stop Electronics
8 |
9 |
10 | You need to enable JavaScript to run this app.
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/Chapter11/one-stop-electronics/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "one-stop-electronics",
3 | "private": true,
4 | "version": "1.0.0",
5 | "type": "module",
6 | "scripts": {
7 | "dev": "vite",
8 | "start": "vite",
9 | "build": "tsc && vite build",
10 | "preview": "vite preview",
11 | "test": "vitest",
12 | "format": "prettier --write .",
13 | "lint": "eslint .",
14 | "lint:fix": "eslint src/* --fix",
15 | "type-check": "tsc"
16 | },
17 | "dependencies": {
18 | "@reduxjs/toolkit": "^1.8.1",
19 | "firebase": "^10.0.0",
20 | "react": "^18.2.0",
21 | "react-dom": "^18.2.0",
22 | "react-intl": "^6.4.4",
23 | "react-redux": "^8.0.1",
24 | "react-router-dom": "^6.14.1",
25 | "styled-components": "^6.0.4"
26 | },
27 | "devDependencies": {
28 | "@testing-library/dom": "^9.2.0",
29 | "@testing-library/jest-dom": "^5.11.4",
30 | "@testing-library/react": "^14.0.0",
31 | "@testing-library/user-event": "^14.2.5",
32 | "@types/react": "^18.0.15",
33 | "@types/react-dom": "^18.0.6",
34 | "@types/styled-components": "^5.1.26",
35 | "@types/testing-library__jest-dom": "^5.14.5",
36 | "@vitejs/plugin-react": "^4.0.0",
37 | "eslint": "^8.0.0",
38 | "eslint-config-react-app": "^7.0.1",
39 | "eslint-plugin-prettier": "^4.2.1",
40 | "jsdom": "^21.1.0",
41 | "prettier": "^2.7.1",
42 | "prettier-config-nick": "^1.0.2",
43 | "typescript": "^5.0.2",
44 | "vite": "^4.0.0",
45 | "vite-plugin-svgr": "^3.2.0",
46 | "vite-tsconfig-paths": "^4.2.0",
47 | "vitest": "^0.30.1"
48 | },
49 | "eslintConfig": {
50 | "extends": [
51 | "react-app",
52 | "react-app/jest"
53 | ],
54 | "plugins": [
55 | "prettier"
56 | ],
57 | "rules": {
58 | "prettier/prettier": "error",
59 | "react/jsx-no-target-blank": "off"
60 | }
61 | },
62 | "prettier": "prettier-config-nick"
63 | }
64 |
--------------------------------------------------------------------------------
/Chapter11/one-stop-electronics/src/App.css:
--------------------------------------------------------------------------------
1 | .app-content {
2 | padding-bottom: 3rem;
3 | }
4 |
--------------------------------------------------------------------------------
/Chapter11/one-stop-electronics/src/App.tsx:
--------------------------------------------------------------------------------
1 | import { useEffect } from "react"
2 | import { Routes, Route } from "react-router"
3 | import { IntlProvider } from "react-intl"
4 | import { useAppDispatch, useAppSelector } from "./app/store/hooks"
5 | import SignIn from "./features/auth/signin/page"
6 | import SignUp from "./features/auth/signup/page"
7 | import Header from "./components/header/header"
8 | import Products from "./features/products/products"
9 | import { onAuthStateChangedListener } from "./backend/firebase/api/auth"
10 | // import { insertUserDataFromAuth } from "./backend/firebase/api/db-utils"
11 | import { fetchProductsData } from "@/backend/firebase/api/db-utils"
12 | import { insertProductsData } from "@/backend/firebase/api/db-utils"
13 | import PRODUCTS_DATA from "@/backend/firebase/api/products-data"
14 | import { setProducts } from "@/app/store/product/product.slice"
15 | import { setCurrentUser, setCurrentLocale } from "@/app/store/user/user.slice"
16 | import Footer from "@/components/footer/footer"
17 | import CartProducts from "@/features/cart/cart"
18 | import { LOCALES } from "@/i18n/locale"
19 | import { DEFAULT_LOCALE } from "@/constants"
20 | import { selectCurrentLocale } from "@/app/store/user/user.selector"
21 | import "@/App.css"
22 |
23 | function App() {
24 | const dispatch = useAppDispatch()
25 | // let isDataLoaded = false;
26 |
27 | useEffect(() => {
28 | /* if(!isDataLoaded) {
29 | insertProductsData('products', PRODUCTS_DATA[0].items);
30 | }
31 | isDataLoaded = true; */
32 | const getProductsMap = async () => {
33 | const products = await fetchProductsData()
34 | dispatch(setProducts(products))
35 | }
36 |
37 | getProductsMap()
38 | const unsubscribe = onAuthStateChangedListener((user) => {
39 | /* if (user) {
40 | insertUserDataFromAuth(user)
41 | } */
42 | const serializableUser =
43 | user && (({ displayName, email }) => ({ displayName, email }))(user)
44 | dispatch(setCurrentUser(serializableUser))
45 | })
46 |
47 | dispatch(setCurrentLocale(navigator.language))
48 | return unsubscribe
49 | }, [])
50 |
51 | const userLanguage = useAppSelector(selectCurrentLocale)
52 | return (
53 |
58 |
59 |
60 |
61 | } />
62 | } />
63 | } />
64 | } />
65 |
66 |
67 |
68 |
69 | )
70 | }
71 |
72 | export default App
73 |
--------------------------------------------------------------------------------
/Chapter11/one-stop-electronics/src/app/store/cart/cart.selector.ts:
--------------------------------------------------------------------------------
1 | import { createSelector } from "reselect"
2 | import { RootState } from "@/app/store/store"
3 | import { CartState } from "./cart.types"
4 |
5 | const selectCartReducer = (state: RootState): CartState => state.cart
6 |
7 | export const selectCartProducts = createSelector(
8 | [selectCartReducer],
9 | (cartSlice) => cartSlice.cartProducts,
10 | )
11 |
12 | export const selectCartTotalPrice = createSelector(
13 | [selectCartProducts],
14 | (cartProducts): number =>
15 | cartProducts.reduce(
16 | (total, cartProduct) => total + cartProduct.quantity * cartProduct.price,
17 | 0,
18 | ),
19 | )
20 |
21 | export const selectCartProductsCount = createSelector(
22 | [selectCartProducts],
23 | (cartProducts): number =>
24 | cartProducts.reduce(
25 | (total, cartProduct) => total + cartProduct.quantity,
26 | 0,
27 | ),
28 | )
29 |
--------------------------------------------------------------------------------
/Chapter11/one-stop-electronics/src/app/store/cart/cart.slice.spec.ts:
--------------------------------------------------------------------------------
1 | import {
2 | cartReducer,
3 | clearProductFromCart,
4 | removeProductFromCart,
5 | resetCartProducts,
6 | } from "./cart.slice"
7 | import { CartState } from "./cart.types"
8 | import { Cart } from "./cart.types"
9 | import { addProductToCart } from "./cart.slice"
10 |
11 | describe("Cart Reducer", () => {
12 | let initialState: CartState = {
13 | cartProducts: [
14 | {
15 | id: 1,
16 | productImageUrl: "someurl.com",
17 | name: "Inspiron 15",
18 | price: 1200,
19 | quantity: 2,
20 | },
21 | ],
22 | }
23 |
24 | it("Should handle adding or incrementing products quantity inside cart", () => {
25 | const productToAdd: Cart = {
26 | id: 1,
27 | productImageUrl: "someurl.com",
28 | name: "Inspiron 15",
29 | price: 1200,
30 | quantity: 1,
31 | }
32 | const { cartProducts } = cartReducer(
33 | initialState,
34 | addProductToCart(productToAdd),
35 | )
36 | expect(cartProducts.length).toEqual(1)
37 | expect(cartProducts[0].quantity).toEqual(3)
38 | })
39 |
40 | it("Should handle removing or decreasing products quantity inside cart", () => {
41 | const productToRemove: Cart = {
42 | id: 1,
43 | productImageUrl: "someurl.com",
44 | name: "Inspiron 15",
45 | price: 1200,
46 | quantity: 1,
47 | }
48 | const state = cartReducer(
49 | initialState,
50 | removeProductFromCart(productToRemove),
51 | )
52 | expect(state.cartProducts.length).toEqual(1)
53 | expect(state.cartProducts[0].quantity).toEqual(1)
54 | })
55 |
56 | it("Should handle clearing products from cart", () => {
57 | initialState.cartProducts.concat([
58 | {
59 | id: 2,
60 | productImageUrl: "someurl.com",
61 | name: "Apple Macbook",
62 | price: 1700,
63 | quantity: 2,
64 | },
65 | ])
66 |
67 | const productToClear: Cart = {
68 | id: 2,
69 | productImageUrl: "someurl.com",
70 | name: "Apple Macbook",
71 | price: 1700,
72 | quantity: 2,
73 | }
74 |
75 | const state = cartReducer(
76 | initialState,
77 | clearProductFromCart(productToClear),
78 | )
79 | expect(state.cartProducts.length).toEqual(1)
80 | expect(state.cartProducts[0].quantity).toEqual(2)
81 | })
82 |
83 | it("Should handle resetting cart products", () => {
84 | const state = cartReducer(initialState, resetCartProducts())
85 | expect(state.cartProducts.length).toEqual(0)
86 | })
87 | })
88 |
--------------------------------------------------------------------------------
/Chapter11/one-stop-electronics/src/app/store/cart/cart.slice.ts:
--------------------------------------------------------------------------------
1 | import { createSlice } from "@reduxjs/toolkit"
2 | import { CartState, Cart } from "./cart.types"
3 | import { Product } from "@/app/store/product/product.types"
4 |
5 | const existingProduct = (cartProducts: Cart[], product: Product) =>
6 | cartProducts.find((cartProduct) => cartProduct.id === product.id)
7 |
8 | const addProduct = (cartProducts: Cart[], productToAdd: Product): Cart[] => {
9 | if (existingProduct(cartProducts, productToAdd)) {
10 | return cartProducts.map((product) =>
11 | product.id === productToAdd.id
12 | ? { ...product, quantity: product.quantity + 1 }
13 | : product,
14 | )
15 | }
16 |
17 | return [...cartProducts, { ...productToAdd, quantity: 1 }]
18 | }
19 |
20 | const removeProduct = (
21 | cartProducts: Cart[],
22 | productToRemove: Product,
23 | ): Cart[] => {
24 | const existingProductItem = existingProduct(cartProducts, productToRemove)
25 |
26 | if (existingProductItem && existingProductItem.quantity === 1) {
27 | return cartProducts.filter(
28 | (cartProduct) => cartProduct.id !== productToRemove.id,
29 | )
30 | }
31 |
32 | return cartProducts.map((cartProduct) =>
33 | cartProduct.id === productToRemove.id
34 | ? { ...cartProduct, quantity: cartProduct.quantity - 1 }
35 | : cartProduct,
36 | )
37 | }
38 |
39 | const clearProduct = (cartProducts: Cart[], productToClear: Product): Cart[] =>
40 | cartProducts.filter((cartProduct) => cartProduct.id !== productToClear.id)
41 |
42 | const INITIAL_STATE: CartState = {
43 | cartProducts: [],
44 | }
45 |
46 | export const cartSlice = createSlice({
47 | name: "cart",
48 | initialState: INITIAL_STATE,
49 | reducers: {
50 | addProductToCart(state, action) {
51 | state.cartProducts = addProduct(state.cartProducts, action.payload)
52 | },
53 | removeProductFromCart(state, action) {
54 | state.cartProducts = removeProduct(state.cartProducts, action.payload)
55 | },
56 | clearProductFromCart(state, action) {
57 | state.cartProducts = clearProduct(state.cartProducts, action.payload)
58 | },
59 | resetCartProducts(state) {
60 | state.cartProducts = []
61 | },
62 | },
63 | })
64 |
65 | export const {
66 | addProductToCart,
67 | removeProductFromCart,
68 | clearProductFromCart,
69 | resetCartProducts,
70 | } = cartSlice.actions
71 |
72 | export const cartReducer = cartSlice.reducer
73 |
--------------------------------------------------------------------------------
/Chapter11/one-stop-electronics/src/app/store/cart/cart.types.ts:
--------------------------------------------------------------------------------
1 | export type Cart = {
2 | id: number
3 | productImageUrl: string
4 | name: string
5 | price: number
6 | quantity: number
7 | }
8 |
9 | export type CartState = {
10 | cartProducts: Cart[]
11 | }
12 |
--------------------------------------------------------------------------------
/Chapter11/one-stop-electronics/src/app/store/hooks.ts:
--------------------------------------------------------------------------------
1 | import { TypedUseSelectorHook, useDispatch, useSelector } from "react-redux"
2 | import { useIntl } from "react-intl"
3 | import { useNavigate } from "react-router-dom"
4 | import type { RootState, AppDispatch } from "./store"
5 |
6 | // Use throughout your app instead of plain `useDispatch` and `useSelector`
7 | export const useAppDispatch: () => AppDispatch = useDispatch
8 | export const useAppSelector: TypedUseSelectorHook = useSelector
9 | export const useNavigator = useNavigate
10 | export const useAppIntl = useIntl
11 |
--------------------------------------------------------------------------------
/Chapter11/one-stop-electronics/src/app/store/product/product.selector.ts:
--------------------------------------------------------------------------------
1 | import { createSelector } from "reselect"
2 | import { RootState } from "@/app/store/store"
3 | import { Product, ProductMap, ProductsState } from "./product.types"
4 |
5 | const selectProductReducer = (state: RootState): ProductsState => state.products
6 |
7 | export const selectProducts = createSelector(
8 | [selectProductReducer],
9 | (productsSlice) => productsSlice.products,
10 | )
11 |
12 | export const selectCategory = createSelector(
13 | [selectProductReducer],
14 | (productsSlice) => productsSlice.category,
15 | )
16 |
17 | export const selectProductsIsLoading = createSelector(
18 | [selectProductReducer],
19 | (productsSlice) => productsSlice.isLoading,
20 | )
21 |
22 | export const selectProductsMap = createSelector(
23 | [selectProducts],
24 | (products): ProductMap =>
25 | products.reduce(
26 | (acc, product) => {
27 | const { category } = product
28 | acc[category]
29 | ? acc[category].push(product)
30 | : (acc[category] = [product])
31 | acc["all"].push(product)
32 | return acc
33 | },
34 | { all: [] } as ProductMap,
35 | ),
36 | )
37 |
--------------------------------------------------------------------------------
/Chapter11/one-stop-electronics/src/app/store/product/product.slice.ts:
--------------------------------------------------------------------------------
1 | import { createSlice } from "@reduxjs/toolkit"
2 | import { ProductsState } from "./product.types"
3 |
4 | const INITIAL_STATE: ProductsState = {
5 | products: [],
6 | category: "all",
7 | isLoading: true,
8 | }
9 |
10 | export const productsSlice = createSlice({
11 | name: "products",
12 | initialState: INITIAL_STATE,
13 | reducers: {
14 | setProducts(state, action) {
15 | state.products = action.payload
16 | state.isLoading = false
17 | },
18 | setCategory(state, action) {
19 | state.category = action.payload
20 | },
21 | },
22 | })
23 |
24 | export const { setProducts, setCategory } = productsSlice.actions
25 |
26 | export const productsReducer = productsSlice.reducer
27 |
--------------------------------------------------------------------------------
/Chapter11/one-stop-electronics/src/app/store/product/product.types.ts:
--------------------------------------------------------------------------------
1 | export type Product = {
2 | id: number
3 | productImageUrl: string
4 | name: string
5 | brand: string
6 | price: number
7 | category: string
8 | }
9 |
10 | export type ProductsState = {
11 | products: Product[]
12 | category: string
13 | isLoading: boolean
14 | }
15 |
16 | export type ProductMap = {
17 | [key: string]: Product[]
18 | }
19 |
--------------------------------------------------------------------------------
/Chapter11/one-stop-electronics/src/app/store/root-reducer.ts:
--------------------------------------------------------------------------------
1 | import { combineReducers } from "redux"
2 | import { userReducer } from "./user/user.slice"
3 | import { productsReducer } from "./product/product.slice"
4 | import { cartReducer } from "./cart/cart.slice"
5 |
6 | export const rootReducer = combineReducers({
7 | user: userReducer,
8 | products: productsReducer,
9 | cart: cartReducer,
10 | })
11 |
--------------------------------------------------------------------------------
/Chapter11/one-stop-electronics/src/app/store/store.ts:
--------------------------------------------------------------------------------
1 | import { configureStore, ThunkAction, Action } from "@reduxjs/toolkit"
2 | import { rootReducer } from "./root-reducer"
3 |
4 | export const store = configureStore({
5 | reducer: rootReducer,
6 | })
7 |
8 | export type AppDispatch = typeof store.dispatch
9 | export type RootState = ReturnType
10 | export type AppThunk = ThunkAction<
11 | ReturnType,
12 | RootState,
13 | unknown,
14 | Action
15 | >
16 |
--------------------------------------------------------------------------------
/Chapter11/one-stop-electronics/src/app/store/user/user.selector.ts:
--------------------------------------------------------------------------------
1 | import { createSelector } from "reselect"
2 | import { RootState } from "@/app/store/store.js"
3 | import { UserState } from "./user.types.ts"
4 |
5 | export const selectUserReducer = (state: RootState): UserState => state.user
6 |
7 | export const selectCurrentUser = createSelector(
8 | selectUserReducer,
9 | (userSlice) => userSlice.currentUser,
10 | )
11 |
12 | export const selectCurrentLocale = createSelector(
13 | selectUserReducer,
14 | (userSlice) => userSlice.locale,
15 | )
--------------------------------------------------------------------------------
/Chapter11/one-stop-electronics/src/app/store/user/user.slice.ts:
--------------------------------------------------------------------------------
1 | import { createSlice } from "@reduxjs/toolkit"
2 | import { UserState } from "./user.types.ts"
3 |
4 | const INITIAL_STATE: UserState = {
5 | currentUser: null,
6 | locale: 'en-US'
7 | }
8 |
9 | export const userSlice = createSlice({
10 | name: "user",
11 | initialState: INITIAL_STATE,
12 | reducers: {
13 | setCurrentUser(state, action) {
14 | state.currentUser = action.payload
15 | },
16 | setCurrentLocale(state, action) {
17 | state.locale = action.payload
18 | }
19 | },
20 | })
21 |
22 | export const { setCurrentUser, setCurrentLocale } = userSlice.actions
23 |
24 | export const userReducer = userSlice.reducer
25 |
--------------------------------------------------------------------------------
/Chapter11/one-stop-electronics/src/app/store/user/user.types.ts.ts:
--------------------------------------------------------------------------------
1 | export type UserInfo = {
2 | displayName: string
3 | createdAt: Date
4 | email: string
5 | }
6 |
7 | export type UserState = {
8 | currentUser: UserInfo | null,
9 | locale: string
10 | }
11 |
--------------------------------------------------------------------------------
/Chapter11/one-stop-electronics/src/assets/clear.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/Chapter11/one-stop-electronics/src/assets/minus.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/Chapter11/one-stop-electronics/src/assets/plus.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
plus-square Created with Sketch Beta.
7 |
--------------------------------------------------------------------------------
/Chapter11/one-stop-electronics/src/assets/products/apple14.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PacktPublishing/React-Interview-Guide/b0e2ba6102a81f820c07219e898318fe1f0c4ef4/Chapter11/one-stop-electronics/src/assets/products/apple14.png
--------------------------------------------------------------------------------
/Chapter11/one-stop-electronics/src/assets/products/apple_ipad10.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PacktPublishing/React-Interview-Guide/b0e2ba6102a81f820c07219e898318fe1f0c4ef4/Chapter11/one-stop-electronics/src/assets/products/apple_ipad10.png
--------------------------------------------------------------------------------
/Chapter11/one-stop-electronics/src/assets/products/galaxy_a34.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PacktPublishing/React-Interview-Guide/b0e2ba6102a81f820c07219e898318fe1f0c4ef4/Chapter11/one-stop-electronics/src/assets/products/galaxy_a34.png
--------------------------------------------------------------------------------
/Chapter11/one-stop-electronics/src/assets/products/galaxy_a54.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PacktPublishing/React-Interview-Guide/b0e2ba6102a81f820c07219e898318fe1f0c4ef4/Chapter11/one-stop-electronics/src/assets/products/galaxy_a54.png
--------------------------------------------------------------------------------
/Chapter11/one-stop-electronics/src/assets/products/galaxy_tab_s8.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PacktPublishing/React-Interview-Guide/b0e2ba6102a81f820c07219e898318fe1f0c4ef4/Chapter11/one-stop-electronics/src/assets/products/galaxy_tab_s8.png
--------------------------------------------------------------------------------
/Chapter11/one-stop-electronics/src/assets/products/galaxytab_s9.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PacktPublishing/React-Interview-Guide/b0e2ba6102a81f820c07219e898318fe1f0c4ef4/Chapter11/one-stop-electronics/src/assets/products/galaxytab_s9.png
--------------------------------------------------------------------------------
/Chapter11/one-stop-electronics/src/assets/products/huawei_p30.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PacktPublishing/React-Interview-Guide/b0e2ba6102a81f820c07219e898318fe1f0c4ef4/Chapter11/one-stop-electronics/src/assets/products/huawei_p30.png
--------------------------------------------------------------------------------
/Chapter11/one-stop-electronics/src/assets/screens/cart.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PacktPublishing/React-Interview-Guide/b0e2ba6102a81f820c07219e898318fe1f0c4ef4/Chapter11/one-stop-electronics/src/assets/screens/cart.png
--------------------------------------------------------------------------------
/Chapter11/one-stop-electronics/src/assets/screens/products.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PacktPublishing/React-Interview-Guide/b0e2ba6102a81f820c07219e898318fe1f0c4ef4/Chapter11/one-stop-electronics/src/assets/screens/products.png
--------------------------------------------------------------------------------
/Chapter11/one-stop-electronics/src/assets/screens/signin.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PacktPublishing/React-Interview-Guide/b0e2ba6102a81f820c07219e898318fe1f0c4ef4/Chapter11/one-stop-electronics/src/assets/screens/signin.png
--------------------------------------------------------------------------------
/Chapter11/one-stop-electronics/src/assets/screens/signup.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PacktPublishing/React-Interview-Guide/b0e2ba6102a81f820c07219e898318fe1f0c4ef4/Chapter11/one-stop-electronics/src/assets/screens/signup.png
--------------------------------------------------------------------------------
/Chapter11/one-stop-electronics/src/assets/shopping-cart.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
shopping-cart-filled
7 |
--------------------------------------------------------------------------------
/Chapter11/one-stop-electronics/src/assets/user-profile-avatar.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/Chapter11/one-stop-electronics/src/backend/firebase/api/auth.ts:
--------------------------------------------------------------------------------
1 | import { firebaseApp } from "@/backend/firebase/config"
2 | import {
3 | signInWithEmailAndPassword,
4 | signInWithPopup,
5 | signInWithRedirect,
6 | GoogleAuthProvider,
7 | createUserWithEmailAndPassword,
8 | updateProfile,
9 | signOut,
10 | getAuth,
11 | onAuthStateChanged,
12 | NextOrObserver,
13 | User,
14 | } from "firebase/auth"
15 |
16 | const auth = getAuth(firebaseApp)
17 |
18 | const googleProvider = new GoogleAuthProvider()
19 |
20 | googleProvider.setCustomParameters({
21 | prompt: "select_account",
22 | })
23 |
24 | export const signInEmailAndPassword = async (
25 | email: string,
26 | password: string,
27 | ) => {
28 | if (!email || !password) return
29 |
30 | return await signInWithEmailAndPassword(auth, email, password)
31 | }
32 |
33 | export const signInGooglePopup = () => signInWithPopup(auth, googleProvider)
34 | export const signInGoogleRedirect = () =>
35 | signInWithRedirect(auth, googleProvider)
36 |
37 | export const signUpEmailAndPassword = async (
38 | displayName: string,
39 | email: string,
40 | password: string,
41 | ): Promise => {
42 | const userInfo = await createUserWithEmailAndPassword(auth, email, password)
43 | await updateProfile(userInfo.user, { displayName })
44 | return userInfo.user
45 | }
46 |
47 | export const signOutUser = async () => await signOut(auth)
48 |
49 | export const onAuthStateChangedListener = (callback: NextOrObserver) =>
50 | onAuthStateChanged(auth, callback)
51 |
--------------------------------------------------------------------------------
/Chapter11/one-stop-electronics/src/backend/firebase/api/db-utils.ts:
--------------------------------------------------------------------------------
1 | import {
2 | getFirestore,
3 | doc,
4 | getDoc,
5 | setDoc,
6 | collection,
7 | writeBatch,
8 | query,
9 | getDocs,
10 | QueryDocumentSnapshot,
11 | } from "firebase/firestore"
12 | import { User } from "firebase/auth"
13 | import { UserInfo } from "@/app/store/user/user.types.ts"
14 | import { Product } from "@/app/store/product/product.types"
15 |
16 | export const db = getFirestore()
17 |
18 | export const insertProductsData = async (
19 | collectionKey: string,
20 | productItems: T[],
21 | ) => {
22 | const collectionRef = collection(db, collectionKey)
23 | const batch = writeBatch(db)
24 |
25 | productItems.forEach((product) => {
26 | const docRef = doc(collectionRef)
27 | batch.set(docRef, product)
28 | })
29 |
30 | await batch.commit()
31 | }
32 |
33 | export const fetchProductsData = async () => {
34 | const collectionRef = collection(db, "products")
35 | const queryRef = query(collectionRef)
36 | const querySnapshot = await getDocs(queryRef)
37 |
38 | return querySnapshot.docs.map((docSnapshot) => docSnapshot.data())
39 | }
40 |
41 | /* export const insertUserDataFromAuth = async (
42 | userAuth: User | null,
43 | ): Promise> => {
44 | if (!userAuth) return
45 |
46 | const userDocRef = doc(db, "users", userAuth.uid)
47 | const userSnapshot = await getDoc(userDocRef)
48 |
49 | if (!userSnapshot.exists()) {
50 | const { displayName, email } = userAuth
51 | const createdAt = new Date()
52 |
53 | try {
54 | await setDoc(userDocRef, {
55 | displayName,
56 | email,
57 | createdAt,
58 | })
59 | } catch (error) {
60 | console.log("error creating the user", error)
61 | }
62 | }
63 |
64 | return userSnapshot as QueryDocumentSnapshot
65 | }
66 | */
--------------------------------------------------------------------------------
/Chapter11/one-stop-electronics/src/backend/firebase/api/products-data.ts:
--------------------------------------------------------------------------------
1 | const PRODUCTS_DATA = [
2 | {
3 | items: [
4 | {
5 | id: 1,
6 | name: "Inspiron 15",
7 | productImageUrl:
8 | "https://i.dell.com/is/image/DellContent/content/dam/ss2/product-images/dell-client-products/notebooks/inspiron-notebooks/15-3520/media-gallery/in3520-cnb-00000ff090-sl.psd?fmt=png-alpha&pscan=auto&scl=1&hei=402&wid=402&qlt=100,1&resMode=sharp2&size=402,402&chrss=full",
9 | price: 1099,
10 | brand: "Dell",
11 | category: "laptop",
12 | },
13 | {
14 | id: 2,
15 | name: "Yoga Pro 7i",
16 | productImageUrl:
17 | "https://p3-ofp.static.pub/fes/cms/2023/03/17/1rip36y4d4hglboff4ukcwv38js8cf016072.png",
18 | price: 1319,
19 | brand: "Lenovo",
20 | category: "laptop",
21 | },
22 | {
23 | id: 3,
24 | name: "Inspiron 16",
25 | productImageUrl:
26 | "https://i.dell.com/is/image/DellContent/content/dam/ss2/product-images/dell-client-products/notebooks/inspiron-notebooks/16-5630-intel/media-gallery/non-touch/silver/in5630nt-cnb-00000ff090-sl.psd?fmt=png-alpha&pscan=auto&scl=1&wid=3346&hei=2067&qlt=100,1&resMode=sharp2&size=3346,2067&chrss=full&imwidth=5000",
27 | price: 1499,
28 | brand: "Dell",
29 | category: "laptop",
30 | },
31 | {
32 | id: 4,
33 | name: "Inspiron 14 2-in-1",
34 | productImageUrl:
35 | "https://i.dell.com/is/image/DellContent/content/dam/ss2/product-images/dell-client-products/notebooks/inspiron-notebooks/14-7430-2in1-intel/media-gallery/notebook-inspiron-14-7430-silver-fpr-gallery-2.psd?fmt=png-alpha&pscan=auto&scl=1&hei=402&wid=632&qlt=100,1&resMode=sharp2&size=632,402&chrss=full",
36 | price: 1449,
37 | brand: "Dell",
38 | category: "laptop",
39 | },
40 | {
41 | id: 5,
42 | name: "MacBook Air",
43 | productImageUrl:
44 | "https://hnsgsfp.imgix.net/4/images/detailed/65/MacBook_Air_-_Space_Grey_1.jpg?fit=fill&bg=0FFF&w=785&h=459&auto=format,compress",
45 | price: 1120,
46 | brand: "Apple",
47 | category: "laptop",
48 | },
49 | {
50 | id: 6,
51 | name: "MacBook Pro 13",
52 | productImageUrl:
53 | "https://hnsgsfp.imgix.net/4/images/detailed/90/Apple_13.3-inch_MacBook_Pro_-_Space_Grey_(IMG_1).jpg?fit=fill&bg=0FFF&w=785&h=459&auto=format,compress",
54 | price: 1899,
55 | brand: "Apple",
56 | category: "laptop",
57 | },
58 | {
59 | id: 7,
60 | name: "Galaxy A34 5G",
61 | productImageUrl: "https://i.ibb.co/2SvMqBf/galaxy-a34.png",
62 | price: 610,
63 | brand: "Samsung",
64 | category: "phone",
65 | },
66 | {
67 | id: 8,
68 | name: "Galaxy A54 5G",
69 | productImageUrl:
70 | "https://i.ibb.co/RPYnx5K/galaxy-a54.png",
71 | price: 999,
72 | brand: "Samsung",
73 | category: "phone",
74 | },
75 | {
76 | id: 9,
77 | name: "Huawei P30",
78 | productImageUrl:
79 | "https://i.ibb.co/yVcDnCR/huawei-p30.png",
80 | price: 300,
81 | brand: "Huawei",
82 | category: "phone",
83 | },
84 | {
85 | id: 10,
86 | name: "Apple14 Blue 128GB",
87 | productImageUrl:
88 | "https://i.ibb.co/kxLJxKZ/apple14.png",
89 | price: 1599,
90 | brand: "Apple",
91 | category: "phone",
92 | },
93 | {
94 | id: 11,
95 | name: "Galaxy Tab S8",
96 | productImageUrl:
97 | "https://i.ibb.co/dWZs1XF/galaxy-tab-s8.png",
98 | price: 1099,
99 | brand: "Samsung",
100 | category: "tab",
101 | },
102 | {
103 | id: 12,
104 | name: "Galaxy Tab S9",
105 | productImageUrl:
106 | "https://i.ibb.co/z81SX7F/galaxytab-s9.png",
107 | price: 2099,
108 | brand: "Samsung",
109 | category: "tab",
110 | },
111 | {
112 | id: 13,
113 | name: "Apple iPad 10.9",
114 | productImageUrl:
115 | "https://i.ibb.co/gZ4HzxV/apple-ipad10.png",
116 | price: 899,
117 | brand: "Apple",
118 | category: "tab",
119 | },
120 | ],
121 | },
122 | ]
123 |
124 | export default PRODUCTS_DATA
125 |
--------------------------------------------------------------------------------
/Chapter11/one-stop-electronics/src/backend/firebase/config.ts:
--------------------------------------------------------------------------------
1 | import { initializeApp } from "firebase/app"
2 | import { GoogleAuthProvider } from "firebase/auth"
3 |
4 | const firebaseConfig = {
5 | apiKey: import.meta.env.VITE_FIREBASE_API_KEY,
6 | authDomain: import.meta.env.VITE_FIREBASE_AUTH_DOMAIN,
7 | projectId: import.meta.env.VITE_FIREBASE_PROJECT_ID,
8 | storageBucket: import.meta.env.VITE_FIREBASE_STORAGE_BUCKET,
9 | messagingSenderId: import.meta.env.VITE_FIREBASE_SENDER_ID,
10 | appId: import.meta.env.VITE_FIREBASE_APP_ID,
11 | }
12 |
13 | export const firebaseApp = initializeApp(firebaseConfig)
14 | export const provider = new GoogleAuthProvider()
15 |
--------------------------------------------------------------------------------
/Chapter11/one-stop-electronics/src/components/button/button.styles.tsx:
--------------------------------------------------------------------------------
1 | import styled from "styled-components"
2 |
3 | export const BasicButton = styled.button`
4 | min-width: 10rem;
5 | width: auto;
6 | height: 2.5rem;
7 | line-height: 2.5rem;
8 | letter-spacing: 0.5px;
9 | padding: 0 2rem;
10 | background-color: rgb(112, 76, 182);
11 | color: white;
12 | font-size: 0.7rem;
13 | font-family: "Barlow Condensed";
14 | font-weight: bolder;
15 | text-transform: uppercase;
16 | border: none;
17 | border-radius: 0.2rem;
18 | cursor: pointer;
19 | display: flex;
20 | justify-content: center;
21 |
22 | &:hover {
23 | background-color: white;
24 | color: black;
25 | border: 1px solid black;
26 | }
27 | `
28 |
29 | export const InvertedButton = styled(BasicButton)`
30 | background-color: white;
31 | color: rgb(112, 76, 182);
32 | border: 1px solid black;
33 |
34 | &:hover {
35 | background-color: rgb(112, 76, 182);
36 | border: none;
37 | border: 1px solid white;
38 | color: white;
39 | }
40 | `
41 |
42 | export const SmallBasicButton = styled(BasicButton)`
43 | width: 4rem;
44 | height: 1.5rem;
45 | min-width: 0rem;
46 | padding: 0rem;
47 | letter-spacing: 0.1rem;
48 | line-height: 2rem;
49 | font-size: 0.4rem;
50 | align-items: center;
51 | letter-spacing: 0rem;
52 | `
--------------------------------------------------------------------------------
/Chapter11/one-stop-electronics/src/components/button/button.tsx:
--------------------------------------------------------------------------------
1 | import { FC, ButtonHTMLAttributes } from "react"
2 |
3 | import { BasicButton, InvertedButton, SmallBasicButton } from "./button.styles"
4 |
5 | export enum BUTTON_TYPE_CLASSES {
6 | basic = "basic",
7 | inverted = "inverted",
8 | small = "small",
9 | }
10 |
11 | const getButton = (buttonType = BUTTON_TYPE_CLASSES.basic) =>
12 | ({
13 | [BUTTON_TYPE_CLASSES.basic]: BasicButton,
14 | [BUTTON_TYPE_CLASSES.inverted]: InvertedButton,
15 | [BUTTON_TYPE_CLASSES.small]: SmallBasicButton,
16 | }[buttonType])
17 |
18 | export type ButtonProps = {
19 | buttonType?: BUTTON_TYPE_CLASSES
20 | } & ButtonHTMLAttributes
21 |
22 | const MyButton: FC = ({ children, buttonType, ...otherProps }) => {
23 | const CustomButton = getButton(buttonType)
24 | return {children}
25 | }
26 |
27 | export default MyButton
28 |
--------------------------------------------------------------------------------
/Chapter11/one-stop-electronics/src/components/categories/categories.styles.tsx:
--------------------------------------------------------------------------------
1 | import styled from "styled-components"
2 |
3 | export const CategoriesContainer = styled.div`
4 | width: 15%;
5 | padding: 0.5rem;
6 | user-select: none;
7 | cursor: pointer;
8 | text-transform: capitalize;
9 | font-weight: 500;
10 | background-color: #f1f1f1;
11 | font-size: 0.8rem;
12 | `
13 |
--------------------------------------------------------------------------------
/Chapter11/one-stop-electronics/src/components/categories/categories.tsx:
--------------------------------------------------------------------------------
1 | import { useIntl } from "react-intl"
2 | import { useAppDispatch } from "@/app/store/hooks"
3 | import { setCategory } from "@/app/store/product/product.slice"
4 | import { CategoriesContainer } from "./categories.styles"
5 | import { Category } from "./categories.types"
6 |
7 | export const Categories = () => {
8 | const intl = useIntl()
9 | const dispatch = useAppDispatch()
10 |
11 | const categories: Category[] = [
12 | { type: "all", name: intl.formatMessage({ id: "categories.all" }) },
13 | { type: "laptop", name: intl.formatMessage({ id: "categories.laptops" }) },
14 | { type: "phone", name: intl.formatMessage({ id: "categories.phones" }) },
15 | { type: "tab", name: intl.formatMessage({ id: "categories.tabs" }) },
16 | ]
17 |
18 | function handleChangeCategory(categoryType: string) {
19 | dispatch(setCategory(categoryType))
20 | }
21 | return (
22 |
23 | {categories.map((category: Category) => (
24 | handleChangeCategory(category.type)}
27 | >
28 | {category.name}
29 |
30 | ))}
31 |
32 | )
33 | }
34 |
--------------------------------------------------------------------------------
/Chapter11/one-stop-electronics/src/components/categories/categories.types.tsx:
--------------------------------------------------------------------------------
1 | export type Category = {
2 | type: string
3 | name: string
4 | }
--------------------------------------------------------------------------------
/Chapter11/one-stop-electronics/src/components/footer/footer.styles.tsx:
--------------------------------------------------------------------------------
1 | import styled from "styled-components"
2 |
3 | export const FooterContainer = styled.div`
4 | position: absolute;
5 | bottom: 0;
6 | width: 100%;
7 | height: 2rem;
8 | font-size: 0.5rem;
9 | padding: 0.5rem 0;
10 | margin-top: 1rem;
11 | margin-bottom: 0rem;
12 | background-color: rgb(112, 76, 182);
13 | color: white;
14 | text-align: center;
15 | `
16 |
--------------------------------------------------------------------------------
/Chapter11/one-stop-electronics/src/components/footer/footer.tsx:
--------------------------------------------------------------------------------
1 | import { FormattedMessage } from "react-intl"
2 | import { FooterContainer } from "./footer.styles"
3 |
4 | const Footer = () => {
5 | return (
6 |
7 |
8 |
9 |
10 |
11 | )
12 | }
13 |
14 | export default Footer
15 |
--------------------------------------------------------------------------------
/Chapter11/one-stop-electronics/src/components/header/header.styles.tsx:
--------------------------------------------------------------------------------
1 | import styled from "styled-components"
2 | import { Link } from "react-router-dom"
3 |
4 | export const NavContainer = styled.div`
5 | height: 4rem;
6 | width: 100%;
7 | display: flex;
8 | justify-content: space-between;
9 | margin-bottom: 0.2rem;
10 | background-color: rgb(112, 76, 182);
11 | color: white;
12 | `
13 |
14 | export const NavLogoContainer = styled(Link)`
15 | height: 100%;
16 | width: 5rem;
17 | padding-left: 4rem;
18 | padding-top: 0.5rem;
19 | `
20 |
21 | export const NavLinks = styled.div`
22 | width: 70%;
23 | height: 100%;
24 | display: flex;
25 | align-items: center;
26 | justify-content: flex-end;
27 | `
28 |
29 | export const NavLink = styled(Link)`
30 | display: flex;
31 | padding: 0.5rem 1rem;
32 | cursor: pointer;
33 | text-decoration: none;
34 | color: white;
35 | font-size: 0.8rem;
36 | align-items: center;
37 | `
38 |
39 | export const NavIconContainer = styled.div`
40 | width: 3rem;
41 | height: 100%;
42 | position: relative;
43 | display: flex;
44 | align-items: center;
45 | justify-content: center;
46 | cursor: pointer;
47 |
48 | svg {
49 | width: 24px;
50 | height: 24px;
51 | color: white;
52 | }
53 | `
54 |
55 | export const NavItemCount = styled.span`
56 | position: absolute;
57 | font-size: 10px;
58 | font-weight: bold;
59 | top: 0.6rem;
60 | right: 0.5rem;
61 |
62 | width: 20px;
63 | height: 20px;
64 | border-radius: 50%;
65 | background: rgba(247, 45, 45, 0.986);
66 | display: flex;
67 | align-items: center;
68 | justify-content: center;
69 | `
70 |
--------------------------------------------------------------------------------
/Chapter11/one-stop-electronics/src/components/header/header.tsx:
--------------------------------------------------------------------------------
1 | import { Fragment, ChangeEvent } from "react"
2 | import { FormattedMessage } from "react-intl"
3 | import MySelect from "@/components/select/select"
4 | import { useAppSelector } from "@/app/store/hooks"
5 | import { selectCurrentUser } from "@/app/store/user/user.selector"
6 | import { selectCartProductsCount } from "@/app/store/cart/cart.selector"
7 | import { resetCartProducts } from "@/app/store/cart/cart.slice"
8 | import { useAppDispatch, useNavigator } from "@/app/store/hooks"
9 | import { setCurrentLocale } from "@/app/store/user/user.slice"
10 | import { ReactComponent as Logo } from "@/assets/one-stop-electronics.svg"
11 | import { ReactComponent as ShoppingCartIcon } from "@/assets/shopping-cart.svg"
12 | import { ReactComponent as UserProfileIcon } from "@/assets/user-profile-avatar.svg"
13 | import { signOutUser } from "@/backend/firebase/api/auth"
14 |
15 | import {
16 | NavContainer,
17 | NavLinks,
18 | NavLink,
19 | NavLogoContainer,
20 | NavIconContainer,
21 | NavItemCount,
22 | } from "./header.styles"
23 |
24 | const Header = () => {
25 | const dispatch = useAppDispatch()
26 | const navigator = useNavigator()
27 | const currentUser = useAppSelector(selectCurrentUser)
28 | const cartProductsCount = useAppSelector(selectCartProductsCount)
29 |
30 | const signOut = () => {
31 | dispatch(signOutUser)
32 | dispatch(resetCartProducts())
33 | }
34 | const navigateToCart = () => navigator("/cart")
35 | const handleChangeLocale = (event: ChangeEvent) => {
36 | const locale = event.target.value
37 | dispatch(setCurrentLocale(locale))
38 | }
39 |
40 | return (
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 | {currentUser ? (
52 |
53 | {currentUser.displayName}
54 |
55 |
56 |
57 |
58 | ) : (
59 |
60 |
61 |
62 |
63 |
64 | )}
65 |
66 |
67 |
68 | {cartProductsCount}
69 |
70 |
71 | English
72 | French
73 | German
74 |
75 |
76 |
77 | )
78 | }
79 |
80 | export default Header
81 |
--------------------------------------------------------------------------------
/Chapter11/one-stop-electronics/src/components/input/input.styles.tsx:
--------------------------------------------------------------------------------
1 | import styled, { css } from "styled-components"
2 |
3 | const myInputColor = "grey"
4 |
5 | const MyShrinkLabelStyles = css`
6 | top: -0.9rem;
7 | font-size: 0.7rem;
8 | color: #000000;
9 | `
10 |
11 | type MyInputLabelProps = {
12 | shrink?: boolean
13 | }
14 |
15 | export const MyInputLabel = styled.label`
16 | color: ${myInputColor};
17 | font-size: 0.8rem;
18 | font-weight: normal;
19 | position: absolute;
20 | pointer-events: none;
21 | left: 0.2rem;
22 | top: 0.6rem;
23 | transition: 300ms ease all;
24 | ${({ shrink }) => shrink && MyShrinkLabelStyles};
25 |
26 | &:after {
27 | content: " *";
28 | color: red;
29 | }
30 | `
31 |
32 | export const MyInputText = styled.input`
33 | color: ${myInputColor};
34 | font-size: 1rem;
35 | padding: 0.5rem 0.9rem 0.6rem 0.5rem;
36 | display: block;
37 | border: none;
38 | border-bottom: 1px solid ${myInputColor};
39 | margin: 1.2rem 0;
40 | width: 16rem;
41 |
42 | &:focus {
43 | outline: none;
44 | }
45 |
46 | &:focus ~ ${MyInputLabel} {
47 | ${MyShrinkLabelStyles};
48 | }
49 | `
50 |
51 | export const MyInputGroup = styled.div`
52 | position: relative;
53 | margin: 2rem 1.2rem;
54 |
55 | input[type="password"] {
56 | letter-spacing: 0.3em;
57 | }
58 | `
59 |
--------------------------------------------------------------------------------
/Chapter11/one-stop-electronics/src/components/input/input.tsx:
--------------------------------------------------------------------------------
1 | import { FC, InputHTMLAttributes } from "react"
2 | import { MyInputLabel, MyInputText, MyInputGroup } from "./input.styles"
3 |
4 | export type MyInputProps = {
5 | label: string
6 | } & InputHTMLAttributes
7 |
8 | const MyInput: FC = ({ label, ...otherProps }) => {
9 | return (
10 |
11 |
12 | {label && (
13 |
20 | {label}
21 |
22 | )}
23 |
24 | )
25 | }
26 |
27 | export default MyInput
28 |
--------------------------------------------------------------------------------
/Chapter11/one-stop-electronics/src/components/product/product.styles.tsx:
--------------------------------------------------------------------------------
1 | import styled from "styled-components"
2 |
3 | type ImageBackgroundProps = {
4 | $hasWhiteBackgroundImage: boolean
5 | }
6 |
7 | export const ProductContainer = styled.div`
8 | display: flex;
9 | flex-direction: column;
10 | background-color: #f1f1f1;
11 | padding: 1rem;
12 | border-radius: 0.125rem;
13 |
14 | img {
15 | width: 7rem;
16 | height: 5rem;
17 | object-fit: fill;
18 | background-color: #f1f1f1;
19 | transition: 0.5s all ease-in-out;
20 | mix-blend-mode: ${(props) =>
21 | props.$hasWhiteBackgroundImage ? "multiply" : "normal"};
22 |
23 | &:hover {
24 | transform: scale(1.1);
25 | }
26 | }
27 |
28 | &:hover {
29 | img {
30 | opacity: 0.8;
31 | }
32 |
33 | button {
34 | opacity: 0.85;
35 | display: flex;
36 | }
37 | }
38 | `
39 |
40 | export const Footer = styled.div`
41 | width: 100%;
42 | display: flex;
43 | flex-direction: column;
44 | justify-content: space-between;
45 | font-size: 1rem;
46 | padding-left: 1rem;
47 | `
48 |
49 | export const Name = styled.h2`
50 | font-size: 0.8rem;
51 | line-height: 1rem;
52 | font-weight: 600;
53 | text-transform: capitalize;
54 | margin-bottom: 1rem;
55 | `
56 |
57 | export const Brand = styled.div`
58 | font-size: 0.6rem;
59 | line-height: 1rem;
60 | color: rgb(75 85 99);
61 | margin-bottom: 0.5rem;
62 | span {
63 | font-weight: 600;
64 | text-transform: capitalize;
65 | }
66 | `
67 |
68 | export const Price = styled.span`
69 | font-size: 0.6rem;
70 | line-height: 1rem;
71 | color: rgb(75 85 99);
72 | margin-bottom: 1rem;
73 | span {
74 | font-weight: 600;
75 | text-transform: capitalize;
76 | color: rgb(85, 118, 209);
77 | }
78 | `
79 |
--------------------------------------------------------------------------------
/Chapter11/one-stop-electronics/src/components/product/product.tsx:
--------------------------------------------------------------------------------
1 | import { FC } from "react"
2 | import { useDispatch } from "react-redux"
3 | import { FormattedMessage, FormattedNumber } from "react-intl"
4 | import { useAppSelector } from "@/app/store/hooks"
5 | import { selectCurrentUser } from "@/app/store/user/user.selector"
6 | import MyButton, { BUTTON_TYPE_CLASSES } from "@/components/button/button"
7 | import { Product } from "@/app/store/product/product.types"
8 | import { addProductToCart } from "@/app/store/cart/cart.slice"
9 | import { BRAND_NAMES } from "@/constants"
10 | import { ProductContainer, Footer, Name, Brand, Price } from "./product.styles"
11 |
12 | type ProductProps = {
13 | product: Product
14 | }
15 |
16 | const hasWhiteBackground = (brand: string) => BRAND_NAMES.includes(brand)
17 |
18 | const ProductItem: FC = ({ product }) => {
19 | const currentUser = useAppSelector(selectCurrentUser)
20 | const { name, price, productImageUrl, brand } = product
21 | const dispatch = useDispatch()
22 | const addCartProduct = () => dispatch(addProductToCart(product))
23 |
24 | return (
25 |
26 |
27 |
28 | {name}
29 |
30 | : {brand}
31 |
32 |
33 | :{" "}
34 |
35 |
40 |
41 |
42 | {currentUser && (
43 |
47 |
48 |
49 | )}
50 |
51 |
52 | )
53 | }
54 |
55 | export default ProductItem
56 |
--------------------------------------------------------------------------------
/Chapter11/one-stop-electronics/src/components/select/select.styles.tsx:
--------------------------------------------------------------------------------
1 | import { styled } from "styled-components"
2 |
3 | export const SelectContainer = styled.select`
4 | height: 1.1rem;
5 | background: white;
6 | color: gray;
7 | font-size: 0.7rem;
8 | border: none;
9 | margin: 1.7rem;
10 | border-radius: 0.125rem;
11 |
12 | option {
13 | color: black;
14 | background: white;
15 | font-weight: 500;
16 | display: flex;
17 | white-space: pre;
18 | min-height: 2rem;
19 | border-radius: 0rem !important;
20 | }
21 | `
22 |
--------------------------------------------------------------------------------
/Chapter11/one-stop-electronics/src/components/select/select.tsx:
--------------------------------------------------------------------------------
1 | import { SelectContainer } from "./select.styles"
2 | import { FC, SelectHTMLAttributes } from "react"
3 |
4 | const MySelect: FC> = ({...props}) => (
5 |
6 | )
7 |
8 | export default MySelect
--------------------------------------------------------------------------------
/Chapter11/one-stop-electronics/src/components/spinner/spinner.styles.tsx:
--------------------------------------------------------------------------------
1 | import styled from "styled-components"
2 |
3 | export const MySpinnerContainer = styled.div`
4 | display: inline-block;
5 | width: 2rem;
6 | height: 2rem;
7 | border: 0.5rem solid rgba(112, 76, 182, 0.5);
8 | border-radius: 50%;
9 | border-top-color: #3498db;
10 | animation: spin 1s ease-in-out infinite;
11 | -webkit-animation: spin 1s ease-in-out infinite;
12 | @keyframes spin {
13 | to {
14 | -webkit-transform: rotate(360deg);
15 | }
16 | }
17 | @-webkit-keyframes spin {
18 | to {
19 | -webkit-transform: rotate(360deg);
20 | }
21 | }
22 | `
23 |
--------------------------------------------------------------------------------
/Chapter11/one-stop-electronics/src/components/spinner/spinner.tsx:
--------------------------------------------------------------------------------
1 | import { MySpinnerContainer } from "./spinner.styles"
2 |
3 | const MySpinner = () =>
4 |
5 | export default MySpinner
6 |
--------------------------------------------------------------------------------
/Chapter11/one-stop-electronics/src/constants.tsx:
--------------------------------------------------------------------------------
1 | export const AUTH_USER_NOT_FOUND_MSG = "User is not found. Please try with correct user or proceed with signup"
2 | export const AUTH_EMAIL_ALREADY_IN_USE_MSG = "Cannot create user, email already in use"
3 | export const AUTH_WEAK_PASSWORD_MSG = "Password should be at least 6 characters"
4 | export const AUTH_INVALID_PASSWORD_MSG = "Password is wrong. Please try with correct password"
5 | export const DEFAULT_LOCALE = "en-US"
6 | export const BRAND_NAMES = ["Apple", "Samsung", "Huawei"]
7 |
--------------------------------------------------------------------------------
/Chapter11/one-stop-electronics/src/custom.d.ts:
--------------------------------------------------------------------------------
1 | declare module "*.svg" {
2 | import React = require("react")
3 | export const ReactComponent: React.FC>
4 | const src: string
5 | export default src
6 | }
7 |
--------------------------------------------------------------------------------
/Chapter11/one-stop-electronics/src/features/auth/signin/page.styles.tsx:
--------------------------------------------------------------------------------
1 | import styled from "styled-components"
2 | import { Link } from "react-router-dom"
3 |
4 | export const SignInContainer = styled.div`
5 | display: flex;
6 | flex-direction: column;
7 | position: relative;
8 | text-align: center;
9 | width: 20rem;
10 | margin: 1rem auto;
11 | padding: 1rem;
12 | border-radius: 0.2rem;
13 | box-shadow: rgba(0, 0, 0, 0.2) 0px 3px 3px -2px,
14 | rgba(0, 0, 0, 0.14) 0px 3px 4px 0px, rgba(0, 0, 0, 0.12) 0px 1px 8px 0px;
15 | `
16 |
17 | export const MyButtonsContainer = styled.div`
18 | display: flex;
19 | flex-direction: column;
20 | justify-content: space-between;
21 | gap: 1rem;
22 | padding: 1rem;
23 | `
24 |
25 | export const SignupLink = styled(Link)`
26 | padding-left: 0.2rem;
27 | color: blue;
28 | text-decoration: none;
29 | cursor: pointer;
30 | `
31 |
--------------------------------------------------------------------------------
/Chapter11/one-stop-electronics/src/features/auth/signin/page.tsx:
--------------------------------------------------------------------------------
1 | import { useState, FormEvent, ChangeEvent } from "react"
2 | import { FormattedMessage, useIntl } from "react-intl"
3 | import MyInputText from "@/components/input/input"
4 | import MyButton, { BUTTON_TYPE_CLASSES } from "@/components/button/button"
5 | import { AUTH_INVALID_PASSWORD_MSG, AUTH_USER_NOT_FOUND_MSG } from "@/constants"
6 | import {
7 | signInEmailAndPassword,
8 | signInGooglePopup,
9 | } from "@/backend/firebase/api/auth"
10 | import { useNavigator } from "@/app/store/hooks"
11 | import { SignInContainer, MyButtonsContainer, SignupLink } from "./page.styles"
12 | import { InfoContainer } from "@/global.styles"
13 | import { AuthError, AuthErrorCodes } from "firebase/auth"
14 |
15 | const defaultFormFields = {
16 | email: "",
17 | password: "",
18 | }
19 |
20 | const SignIn = () => {
21 | const intl = useIntl()
22 | const navigator = useNavigator()
23 | const [formFields, setFormFields] = useState(defaultFormFields)
24 | const { email, password } = formFields
25 |
26 | const resetFormFields = () => {
27 | setFormFields(defaultFormFields)
28 | }
29 |
30 | const signInWithGoogle = async () => {
31 | await signInGooglePopup()
32 | navigator("/")
33 | }
34 |
35 | const handleSubmit = async (event: FormEvent) => {
36 | event.preventDefault()
37 |
38 | try {
39 | await signInEmailAndPassword(email, password)
40 | resetFormFields()
41 | navigator("/")
42 | } catch (error) {
43 | if ((error as AuthError).code === AuthErrorCodes.USER_DELETED) {
44 | alert(AUTH_USER_NOT_FOUND_MSG)
45 | } else if (
46 | (error as AuthError).code === AuthErrorCodes.INVALID_PASSWORD
47 | ) {
48 | alert(AUTH_INVALID_PASSWORD_MSG)
49 | } else {
50 | alert(`User login encountered an error: ${error}`)
51 | }
52 | }
53 | }
54 |
55 | const handleChange = (event: ChangeEvent) => {
56 | const { name, value } = event.target
57 |
58 | setFormFields({ ...formFields, [name]: value })
59 | }
60 |
61 | return (
62 |
63 |
64 |
65 |
66 |
103 |
104 | )
105 | }
106 |
107 | export default SignIn
108 |
--------------------------------------------------------------------------------
/Chapter11/one-stop-electronics/src/features/auth/signup/page.styles.tsx:
--------------------------------------------------------------------------------
1 | import styled from "styled-components"
2 | import { Link } from "react-router-dom"
3 |
4 | export const SignUpContainer = styled.div`
5 | display: flex;
6 | flex-direction: column;
7 | position: relative;
8 | text-align: center;
9 | width: 20rem;
10 | margin: 1rem auto;
11 | padding: 1rem;
12 | border-radius: 4px;
13 | box-shadow: rgba(0, 0, 0, 0.2) 0px 3px 3px -2px,
14 | rgba(0, 0, 0, 0.14) 0px 3px 4px 0px, rgba(0, 0, 0, 0.12) 0px 1px 8px 0px;
15 | `
16 |
17 | export const MyButtonsContainer = styled.div`
18 | display: flex;
19 | flex-direction: column;
20 | justify-content: space-between;
21 | gap: 1rem;
22 | padding: 1rem;
23 | `
24 |
25 | export const LoginLink = styled(Link)`
26 | color: blue;
27 | text-decoration: none;
28 | cursor: pointer;
29 | `
30 |
--------------------------------------------------------------------------------
/Chapter11/one-stop-electronics/src/features/auth/signup/page.tsx:
--------------------------------------------------------------------------------
1 | import { useState, FormEvent, ChangeEvent } from "react"
2 | import { FormattedMessage, useIntl } from "react-intl"
3 | import { AuthError, AuthErrorCodes } from "firebase/auth"
4 | import { useDispatch } from "react-redux"
5 |
6 | import MyInputText from "@/components/input/input"
7 | import MyButton from "@/components/button/button"
8 |
9 | import { SignUpContainer, LoginLink, MyButtonsContainer } from "./page.styles"
10 | import { signUpEmailAndPassword } from "@/backend/firebase/api/auth"
11 | // import { insertUserDataFromAuth } from "@/backend/firebase/api/db-utils"
12 | import { useNavigator } from "@/app/store/hooks"
13 | import { InfoContainer } from "@/global.styles"
14 | import {
15 | AUTH_EMAIL_ALREADY_IN_USE_MSG,
16 | AUTH_WEAK_PASSWORD_MSG,
17 | } from "@/constants"
18 |
19 | const defaultFormFields = {
20 | displayName: "",
21 | email: "",
22 | password: "",
23 | confirmPassword: "",
24 | }
25 |
26 | const SignUp = () => {
27 | const intl = useIntl()
28 | const navigator = useNavigator()
29 | const [formFields, setFormFields] = useState(defaultFormFields)
30 | const { displayName, email, password, confirmPassword } = formFields
31 | const dispatch = useDispatch()
32 |
33 | const resetFormFields = () => {
34 | setFormFields(defaultFormFields)
35 | }
36 |
37 | const handleSubmit = async (event: FormEvent) => {
38 | event.preventDefault()
39 |
40 | if (password !== confirmPassword) {
41 | alert("passwords do not match")
42 | return
43 | }
44 |
45 | try {
46 | const user = await signUpEmailAndPassword(displayName, email, password)
47 |
48 | // await insertUserDataFromAuth(user)
49 | resetFormFields()
50 | navigator("/")
51 | } catch (error) {
52 | if ((error as AuthError).code === AuthErrorCodes.EMAIL_EXISTS) {
53 | alert(AUTH_EMAIL_ALREADY_IN_USE_MSG)
54 | } else if ((error as AuthError).code === AuthErrorCodes.WEAK_PASSWORD) {
55 | alert(AUTH_WEAK_PASSWORD_MSG)
56 | } else {
57 | alert(`User creation encountered an error: ${error}`)
58 | }
59 | }
60 | }
61 |
62 | const handleChange = (event: ChangeEvent) => {
63 | const { name, value } = event.target
64 |
65 | setFormFields({ ...formFields, [name]: value })
66 | }
67 |
68 | return (
69 |
70 |
71 |
72 |
73 |
121 |
122 | )
123 | }
124 |
125 | export default SignUp
126 |
--------------------------------------------------------------------------------
/Chapter11/one-stop-electronics/src/features/cart/cart.styles.tsx:
--------------------------------------------------------------------------------
1 | import { styled } from "styled-components"
2 |
3 | export const CartContainer = styled.div`
4 | width: 75%;
5 | min-height: 5rem;
6 | font-size: 1rem;
7 | align-items: center;
8 | font-weight: 600;
9 | `
10 |
11 | export const CartItemContainer = styled.div`
12 | display: flex;
13 | flex-direction: row;
14 | margin: 0.5rem 2rem;
15 | padding: 1rem;
16 | gap: 2rem;
17 | text-align: center;
18 | border: 1px solid #e7eaf0;
19 | border-radius: 0.4rem;
20 | `
21 |
22 | export const ProductImageContainer = styled.div`
23 | width: 20%;
24 | display: flex;
25 | justify-content: center;
26 | align-items: center;
27 |
28 | img {
29 | width: 5rem;
30 | height: 5rem;
31 | }
32 | `
33 |
34 | export const FieldContainer = styled.div`
35 | display: flex;
36 | width: 18%;
37 | justify-content: center;
38 | align-items: center;
39 | `
40 |
41 | export const QuantityContainer = styled.div`
42 | width: 25%;
43 | display: flex;
44 | flex-direction: row;
45 | justify-content: center;
46 | align-items: center;
47 | `
48 |
49 | export const IconContainer = styled.div`
50 | width: 3rem;
51 | display: flex;
52 | align-items: center;
53 | justify-content: center;
54 | cursor: pointer;
55 |
56 | svg {
57 | width: 24px;
58 | height: 24px;
59 | color: white;
60 | }
61 | `
62 |
63 | export const CartFooterContainer = styled.div`
64 | padding: 2rem 0;
65 | margin: 4rem 2rem 2rem 2rem;
66 | display: flex;
67 | align-items: center;
68 | justify-content: space-between;
69 | border-top: 1px solid #e7eaf0;
70 | `
71 |
72 | export const EmptyCartContainer = styled.div`
73 | position: absolute;
74 | top: 50%;
75 | left: 50%;
76 | transform: translateX(-50%) translateY(-50%);
77 | `
78 |
--------------------------------------------------------------------------------
/Chapter11/one-stop-electronics/src/features/cart/cart.tsx:
--------------------------------------------------------------------------------
1 | import { FormattedMessage, FormattedNumber } from "react-intl"
2 | import { useAppSelector } from "@/app/store/hooks"
3 | import {
4 | selectCartProducts,
5 | selectCartTotalPrice,
6 | } from "@/app/store/cart/cart.selector"
7 | import {
8 | clearProductFromCart,
9 | addProductToCart,
10 | removeProductFromCart,
11 | } from "@/app/store/cart/cart.slice"
12 | import { Cart } from "@/app/store/cart/cart.types"
13 | import { useAppDispatch } from "@/app/store/hooks"
14 | import MyButton from "@/components/button/button"
15 | import { ReactComponent as PlusCartProduct } from "@/assets/plus.svg"
16 | import { ReactComponent as MinusCartProduct } from "@/assets/minus.svg"
17 | import { ReactComponent as ClearCartProduct } from "@/assets/clear.svg"
18 | import {
19 | CartContainer,
20 | CartItemContainer,
21 | ProductImageContainer,
22 | QuantityContainer,
23 | IconContainer,
24 | FieldContainer,
25 | EmptyCartContainer,
26 | CartFooterContainer,
27 | } from "./cart.styles"
28 |
29 | const CartProducts = () => {
30 | const cartProducts = useAppSelector(selectCartProducts)
31 | const cartProductsTotalCost = useAppSelector(selectCartTotalPrice)
32 | const dispatch = useAppDispatch()
33 |
34 | const clearCartProduct = (cartProduct: Cart) =>
35 | dispatch(clearProductFromCart(cartProduct))
36 | const addCartProduct = (cartProduct: Cart) =>
37 | dispatch(addProductToCart(cartProduct))
38 | const removeCartProduct = (cartProduct: Cart) =>
39 | dispatch(removeProductFromCart(cartProduct))
40 |
41 | return (
42 |
43 | {cartProducts &&
44 | cartProducts.map((cartProduct) => {
45 | const { id, productImageUrl, name, quantity, price } = cartProduct
46 | return (
47 |
48 |
49 |
50 |
51 |
52 | {name}
53 |
54 |
55 | addCartProduct(cartProduct)}>
56 |
57 |
58 | {quantity}
59 | removeCartProduct(cartProduct)}>
60 |
61 |
62 |
63 |
64 |
69 |
70 | clearCartProduct(cartProduct)}>
71 |
72 |
73 |
74 | )
75 | })}
76 | {cartProducts.length > 0 && (
77 |
78 |
79 | :
80 |
85 |
86 |
87 |
88 |
89 |
90 | )}
91 |
92 | {cartProducts.length === 0 && (
93 |
94 |
95 |
96 |
97 |
98 | )}
99 |
100 | )
101 | }
102 |
103 | export default CartProducts
104 |
--------------------------------------------------------------------------------
/Chapter11/one-stop-electronics/src/features/products/products.styles.tsx:
--------------------------------------------------------------------------------
1 | import styled from "styled-components"
2 |
3 | export const LayoutContainer = styled.div`
4 | display: flex;
5 | flex-direction: row;
6 | padding: 1rem;
7 | gap: 1rem;
8 | `
9 |
10 | export const ProductsContainer = styled.div`
11 | display: grid;
12 | grid-template-columns: repeat(5, 1fr);
13 | column-gap: 0.5rem;
14 | row-gap: 0.5rem;
15 | `
16 |
17 | export const LoaderContainer = styled.div`
18 | position: absolute;
19 | top: 50%;
20 | left: 50%;
21 | transform: translateX(-50%) translateY(-50%);
22 | `
23 |
24 | export const Title = styled.h2`
25 | font-size: 38px;
26 | margin-bottom: 25px;
27 | text-align: center;
28 | `
29 |
--------------------------------------------------------------------------------
/Chapter11/one-stop-electronics/src/features/products/products.tsx:
--------------------------------------------------------------------------------
1 | import { useState, useEffect, Fragment } from "react"
2 | import { useParams } from "react-router-dom"
3 | import { useAppSelector } from "@/app/store/hooks"
4 | import ProductItem from "@/components/product/product"
5 | import MySpinner from "@/components/spinner/spinner"
6 | import { insertProductsData } from "@/backend/firebase/api/db-utils"
7 | import { Product } from "@/app/store/product/product.types"
8 | import {
9 | selectProductsMap,
10 | selectCategory,
11 | selectProductsIsLoading,
12 | } from "@/app/store/product/product.selector"
13 | import { Categories } from "@/components/categories/categories"
14 | import {
15 | ProductsContainer,
16 | Title,
17 | LayoutContainer,
18 | LoaderContainer,
19 | } from "./products.styles"
20 |
21 | const Products = () => {
22 | const productsMap = useAppSelector(selectProductsMap)
23 | const category = useAppSelector(selectCategory)
24 | const isLoading = useAppSelector(selectProductsIsLoading)
25 | const [products, setProducts] = useState(productsMap[category])
26 |
27 | useEffect(() => {
28 | setProducts(productsMap[category])
29 | }, [category, productsMap])
30 |
31 | return (
32 |
33 |
34 |
35 |
36 | {isLoading ? (
37 |
38 |
39 |
40 | ) : (
41 | products &&
42 | products.map((product: Product) => (
43 |
44 | ))
45 | )}
46 |
47 |
48 |
49 | )
50 | }
51 |
52 | export default Products
53 |
--------------------------------------------------------------------------------
/Chapter11/one-stop-electronics/src/global.styles.tsx:
--------------------------------------------------------------------------------
1 | import styled from "styled-components"
2 |
3 | export const InfoContainer = styled.span`
4 | font-size: 0.7rem;
5 | `
6 |
--------------------------------------------------------------------------------
/Chapter11/one-stop-electronics/src/i18n/locale.ts:
--------------------------------------------------------------------------------
1 | import ENGLISH from "./translations/en-US.json"
2 | import FRENCH from "./translations/fr-FR.json"
3 | import GERMAN from "./translations/de-DE.json"
4 |
5 | export const LOCALES: any = {
6 | "en-US": ENGLISH,
7 | "fr-FR": FRENCH,
8 | "de-DE": GERMAN
9 | }
10 |
--------------------------------------------------------------------------------
/Chapter11/one-stop-electronics/src/i18n/translations/de-DE.json:
--------------------------------------------------------------------------------
1 | {
2 | "header.navlink.products": "PRODUKTE",
3 | "header.navlink.signin": "ANMELDEN",
4 | "footer.copyright.message": "Copyright © 2023 OneStore Electronics. Powered by OneStore Electronics.",
5 | "categories.all": "Alle Kategorien",
6 | "categories.laptops": "Laptops",
7 | "categories.phones": "Smartphones",
8 | "categories.tabs": "Registerkarten",
9 | "product.brand": "Marke",
10 | "product.price": "Preis",
11 | "product.add_to_cart": "In den Warenkorb",
12 | "signin.title": "Anmelden",
13 | "signin.email_signin": "Anmelden",
14 | "signin.google_signin": "Mit Google anmelden",
15 | "signin.account_signup_info": "Du hast noch kein Konto?",
16 | "signin.account_signup": "Anmelden",
17 | "signin.email.label": "E-Mail",
18 | "signin.password.label": "Passwort",
19 | "signup.title": "Anmelden",
20 | "signup.email_signup": "Anmelden",
21 | "signup.account_signin_info": "Hast du bereits ein Konto?",
22 | "signup.displayname.label": "Benutzername",
23 | "signup.email.label": "E-Mail",
24 | "signup.password.label": "Passwort",
25 | "signup.confirm_password.label": "Passwort bestätigen",
26 | "cart.total": "Gesamt",
27 | "cart.checkout": "Zur Kasse",
28 | "cart.empty.basket.description": "Dein Warenkorb ist leer"
29 | }
--------------------------------------------------------------------------------
/Chapter11/one-stop-electronics/src/i18n/translations/en-US.json:
--------------------------------------------------------------------------------
1 | {
2 | "header.navlink.products": "PRODUCTS",
3 | "header.navlink.signin": "SIGN IN",
4 | "footer.copyright.message": "Copyright © 2023 OneStore Electronics. Powered by OneStore Electronics.",
5 | "categories.all": "All categories",
6 | "categories.laptops": "Laptops",
7 | "categories.phones": "Smart Phones",
8 | "categories.tabs": "Tabs",
9 | "product.brand": "Brand",
10 | "product.price": "Price",
11 | "product.add_to_cart": "Add to cart",
12 | "signin.title": "Login",
13 | "signin.email_signin": "Sign In",
14 | "signin.google_signin": "SignIn With Google",
15 | "signin.account_signup_info": "Don't have an account?",
16 | "signin.account_signup": "Sign Up",
17 | "signin.email.label": "Email",
18 | "signin.password.label": "Password",
19 | "signup.title": "Sign Up",
20 | "signup.email_signup": "Sign Up",
21 | "signup.account_signin_info": "Already have an account?",
22 | "signup.displayname.label": "User Name",
23 | "signup.email.label": "Email",
24 | "signup.password.label": "Password",
25 | "signup.confirm_password.label": "Confirm Password",
26 | "cart.total": "Total",
27 | "cart.checkout": "Checkout",
28 | "cart.empty.basket.description": "Your basket is empty"
29 | }
30 |
--------------------------------------------------------------------------------
/Chapter11/one-stop-electronics/src/i18n/translations/fr-FR.json:
--------------------------------------------------------------------------------
1 | {
2 | "header.navlink.products": "PRODUITS",
3 | "header.navlink.signin": "CONNEXION",
4 | "footer.copyright.message": "Copyright © 2023 OneStore Electronics. Propulsé par OneStore Electronics.",
5 | "categories.all": "Toutes les catégories",
6 | "categories.laptops": "Ordinateurs portables",
7 | "categories.phones": "Téléphones intelligents",
8 | "categories.tabs": "Onglets",
9 | "product.brand": "Marque",
10 | "product.price": "Prix",
11 | "product.add_to_cart": "Ajouter au panier",
12 | "signin.title": "Connexion",
13 | "signin.email_signin": "Connexion",
14 | "signin.google_signin": "Se connecter avec Google",
15 | "signin.account_signup_info": "Vous n'avez pas de compte ?",
16 | "signin.account_signup": "S'inscrire",
17 | "signin.email.label": "E-mail",
18 | "signin.password.label": "Mot de passe",
19 | "signup.title": "S'inscrire",
20 | "signup.email_signup": "S'inscrire",
21 | "signup.account_signin_info": "Vous avez déjà un compte ?",
22 | "signup.displayname.label": "Nom d'utilisateur",
23 | "signup.email.label": "E-mail",
24 | "signup.password.label": "Mot de passe",
25 | "signup.confirm_password.label": "Confirmer le mot de passe",
26 | "cart.total": "Total",
27 | "cart.checkout": "Commander",
28 | "cart.empty.basket.description": "Votre panier est vide"
29 | }
30 |
--------------------------------------------------------------------------------
/Chapter11/one-stop-electronics/src/index.css:
--------------------------------------------------------------------------------
1 | body {
2 | margin: 0;
3 | font-family: Barlow Condensed,Roboto,Helvetica,Arial,sans-serif;
4 | -webkit-font-smoothing: antialiased;
5 | -moz-osx-font-smoothing: grayscale;
6 | min-height: 100vh;
7 | position: relative;
8 | }
9 |
10 | code {
11 | font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New",
12 | monospace;
13 | }
14 |
--------------------------------------------------------------------------------
/Chapter11/one-stop-electronics/src/main.tsx:
--------------------------------------------------------------------------------
1 | import ReactDOM from "react-dom/client"
2 | import { BrowserRouter } from "react-router-dom"
3 | import { Provider } from "react-redux"
4 | import { store } from "./app/store/store"
5 | import App from "./App"
6 | import "./index.css"
7 |
8 | ReactDOM.createRoot(document.getElementById("root")!).render(
9 |
10 |
11 |
12 |
13 |
14 | )
15 |
--------------------------------------------------------------------------------
/Chapter11/one-stop-electronics/src/setupTests.ts:
--------------------------------------------------------------------------------
1 | ///
2 | import "@testing-library/jest-dom"
3 |
--------------------------------------------------------------------------------
/Chapter11/one-stop-electronics/src/vite-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/Chapter11/one-stop-electronics/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ESNext",
4 | "useDefineForClassFields": true,
5 | "lib": ["DOM", "DOM.Iterable", "ESNext"],
6 | "allowJs": false,
7 | "skipLibCheck": true,
8 | "esModuleInterop": false,
9 | "allowSyntheticDefaultImports": true,
10 | "strict": true,
11 | "module": "ESNext",
12 | "moduleResolution": "bundler",
13 | "resolveJsonModule": true,
14 | "isolatedModules": true,
15 | "noEmit": true,
16 | "jsx": "react-jsx",
17 | "types": ["testing-library__jest-dom"],
18 | "baseUrl": "src",
19 | "paths": {
20 | "@/*": ["./*"]
21 | }
22 | },
23 | "include": ["src", "src/custom.d.ts"],
24 | "references": [{ "path": "./tsconfig.node.json" }]
25 | }
26 |
--------------------------------------------------------------------------------
/Chapter11/one-stop-electronics/tsconfig.node.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "composite": true,
4 | "module": "ESNext",
5 | "moduleResolution": "bundler",
6 | "allowSyntheticDefaultImports": true
7 | },
8 | "include": ["vite.config.ts"]
9 | }
10 |
--------------------------------------------------------------------------------
/Chapter11/one-stop-electronics/vite.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from "vitest/config"
2 | import react from "@vitejs/plugin-react"
3 | import svgr from "vite-plugin-svgr"
4 | import tsconfigPaths from "vite-tsconfig-paths"
5 |
6 | // https://vitejs.dev/config/
7 | export default defineConfig({
8 | plugins: [
9 | react(),
10 | svgr({
11 | svgrOptions: {
12 | // svgr options
13 | },
14 | }),
15 | tsconfigPaths(),
16 | ],
17 | server: {
18 | open: true,
19 | },
20 | build: {
21 | outDir: "build",
22 | sourcemap: true,
23 | },
24 | test: {
25 | globals: true,
26 | environment: "jsdom",
27 | setupFiles: "src/setupTests",
28 | mockReset: true,
29 | },
30 | })
31 |
--------------------------------------------------------------------------------
/Chapter12/HomePage.js:
--------------------------------------------------------------------------------
1 | 'use client';
2 | import { styled, ThemeProvider } from 'styled-components';
3 | import GlobalStyle from '../../src/app/components/GlobalStyles';
4 | import MainMenu from './components/MainMenu';
5 |
6 | const theme = {
7 | colors: {
8 | primary: 'rgb(15 23 42)',
9 | },
10 | };
11 |
12 | const MainContainer = styled.div`
13 | margin: 0 auto;
14 | width: 100%;
15 | `;
16 |
17 | const CoverHeadingBG = styled.div`
18 | margin: 2rem auto;
19 | display: flex;
20 | flex-flow: column;
21 | align-items: center;
22 | background-color: rgb(6 95 70);
23 | color: rgb(255 255 255);
24 | border-radius: 2rem;
25 | padding: 2rem;
26 | `;
27 |
28 | const CoverHeading = styled.h1`
29 | text-transform: uppercase;
30 | `;
31 |
32 | const CoverIntro = styled.p`
33 | font-size: 1.4rem;
34 | margin: 2rem 2rem;
35 | `;
36 |
37 | const Hero = styled.div`
38 | margin: 2rem auto;
39 | background-image: url('https://res.cloudinary.com/d74fh3kw/image/upload/v1692557430/coffee-restaurant/coffee-shop_zlkf7u.jpg');
40 | background-repeat: no-repeat;
41 | background-size: cover;
42 | background-position: center;
43 | background-color: rgb(4 120 87);
44 | height: 67.5rem;
45 | width: 100%;
46 | `;
47 |
48 | export default function Home() {
49 | return (
50 | <>
51 |
52 |
53 |
54 |
55 |
56 | Summer time is here!
57 |
58 | Our summer menu has arrived. Freshen up your day with our creamy
59 | and delicious coffee range, iced teas and mouth watering snacks.
60 |
61 |
62 |
63 |
64 |
65 | >
66 | );
67 | }
68 |
--------------------------------------------------------------------------------
/Chapter12/HomePage.test.js:
--------------------------------------------------------------------------------
1 | import { render, screen } from '@testing-library/react';
2 | import '@testing-library/jest-dom';
3 | import Home from './page';
4 | describe('Home', () => {
5 | it('renders without crashing', () => {
6 | render( );
7 | });
8 | it('displays the correct heading and intro', () => {
9 | render( );
10 | expect(screen.getByText('Summer time is here!')).toBeInTheDocument();
11 | expect(
12 | screen.getByText(
13 | /Our summer menu has arrived. Freshen up your day with our creamy and delicious coffee range, iced teas and mouth watering snacks./
14 | )
15 | ).toBeInTheDocument();
16 | });
17 | });
18 |
--------------------------------------------------------------------------------
/Chapter12/HomePageApp.js:
--------------------------------------------------------------------------------
1 | 'use client';
2 | import { styled, ThemeProvider } from 'styled-components';
3 | import GlobalStyle from '../../src/app/components/GlobalStyles';
4 | import MainMenu from './components/MainMenu';
5 |
6 | const theme = {
7 | colors: {
8 | primary: 'rgb(15 23 42)',
9 | },
10 | };
11 |
12 | const MainContainer = styled.div`
13 | margin: 0 auto;
14 | width: 100%;
15 | `;
16 |
17 | const CoverHeadingBG = styled.div`
18 | margin: 2rem auto;
19 | display: flex;
20 | flex-flow: column;
21 | align-items: center;
22 | background-color: rgb(6 95 70);
23 | color: rgb(255 255 255);
24 | border-radius: 2rem;
25 | padding: 2rem;
26 | `;
27 |
28 | const CoverHeading = styled.h1`
29 | text-transform: uppercase;
30 | `;
31 |
32 | const CoverIntro = styled.p`
33 | font-size: 1.4rem;
34 | margin: 2rem 2rem;
35 | `;
36 |
37 | const Hero = styled.div`
38 | margin: 2rem auto;
39 | background-image: url('https://res.cloudinary.com/d74fh3kw/image/upload/v1692557430/coffee-restaurant/coffee-shop_zlkf7u.jpg');
40 | background-repeat: no-repeat;
41 | background-size: cover;
42 | background-position: center;
43 | background-color: rgb(4 120 87);
44 | height: 67.5rem;
45 | width: 100%;
46 | `;
47 |
48 | export default function Home() {
49 | return (
50 | <>
51 |
52 |
53 |
54 |
55 |
56 | Summer time is here!
57 |
58 | Our summer menu has arrived. Freshen up your day with our creamy
59 | and delicious coffee range, iced teas and mouth watering snacks.
60 |
61 |
62 |
63 |
64 |
65 | >
66 | );
67 | }
68 |
--------------------------------------------------------------------------------
/Chapter12/NotFoundPage.js:
--------------------------------------------------------------------------------
1 | 'use client';
2 | import { styled, ThemeProvider } from 'styled-components';
3 | import GlobalStyle from '../../src/app/components/GlobalStyles';
4 | import MainMenu from './components/MainMenu';
5 |
6 | const theme = {
7 | colors: {
8 | primary: 'rgb(15 23 42)',
9 | },
10 | };
11 |
12 | const MainContainer = styled.div`
13 | margin: 2rem auto;
14 | max-width: 120rem;
15 | padding: 2rem;
16 | width: 100%;
17 | `;
18 |
19 | const PageTitle = styled.h1`
20 | color: #ffffff;
21 | `;
22 |
23 | const PageIntro = styled.p`
24 | color: #ffffff;
25 | margin-top: 2rem;
26 | font-size: 1.4rem;
27 | `;
28 |
29 | export default function NotFound() {
30 | return (
31 | <>
32 |
33 |
34 |
35 |
36 | Page Not Found
37 | Could not find requested page :(
38 |
39 |
40 | >
41 | );
42 | }
43 |
--------------------------------------------------------------------------------
/Chapter12/RootLayout.js:
--------------------------------------------------------------------------------
1 | import './globals.css';
2 | import { Dosis } from 'next/font/google';
3 | import StyledComponentsRegistry from './lib/registry';
4 |
5 | const dosis = Dosis({ subsets: ['latin'] });
6 |
7 | export const metadata = {
8 | title: 'Resturant App',
9 | description: 'Generated by create next app',
10 | };
11 |
12 | export default function RootLayout({ children }) {
13 | return (
14 |
15 |
16 | {children}
17 |
18 |
19 | );
20 | }
21 |
--------------------------------------------------------------------------------
/Chapter12/SessionProvider.js:
--------------------------------------------------------------------------------
1 | 'use client';
2 | import { SessionProvider } from 'next-auth/react';
3 |
4 | const Provider = ({ children }) => {
5 | return {children} ;
6 | };
7 |
8 | export default Provider;
9 |
--------------------------------------------------------------------------------
/Chapter12/authoBio.json:
--------------------------------------------------------------------------------
1 | {
2 | "id": 64,
3 | "name": "Jack Thomas",
4 | "email": "jackthomas@gamil.com"
5 | }
6 |
--------------------------------------------------------------------------------
/Chapter12/authoProfile.json:
--------------------------------------------------------------------------------
1 | {
2 | "data": {
3 | "post": {
4 | "id": "1",
5 | "title": "Hello World",
6 | "content": "Welcome to my first blog.",
7 | "author": {
8 | "id": "64",
9 | "name": "Jack Thomas",
10 | "email": "jackthomas@gamil.com"
11 | }
12 | }
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/Chapter12/authorBlog.json:
--------------------------------------------------------------------------------
1 | {
2 | "id": 1,
3 | "title": "Hello World",
4 | "content": "Welcome to my first blog.",
5 | "authorId": 64
6 | }
7 |
--------------------------------------------------------------------------------
/Chapter12/buildFrontendArchitecture.md:
--------------------------------------------------------------------------------
1 | mkdir data
2 | touch data/menu.js data/profile.js
3 | touch .env.local jest.config.mjs
4 | cd src/app
5 | mkdir account account/menu account/profile
6 | touch account/menu/page.js account/profile/page.js
7 | mkdir api api/auth api/auth/"[...nextauth]"
8 | touch api/auth/"[...nextauth]"/route.js
9 | mkdir components graphql lib nutrition queries rewards utils
10 | touch components/GlobalStyles.js components/MainMenu.js components/Provider.js
11 | touch graphql/route.js
12 | touch lib/registry.js
13 | touch nutrition/page.js nutrition/page.test.js
14 | touch queries/clientQueries.js
15 | touch utils/withApollo.js utils/cors.js
16 | touch not-found.js page.test.js
17 |
--------------------------------------------------------------------------------
/Chapter12/coffee-restaurant/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # next.js
12 | /.next/
13 | /out/
14 |
15 | # production
16 | /build
17 |
18 | # misc
19 | .DS_Store
20 | *.pem
21 |
22 | # debug
23 | npm-debug.log*
24 | yarn-debug.log*
25 | yarn-error.log*
26 |
27 | # local env files
28 | .env*.local
29 |
30 | # vercel
31 | .vercel
32 |
33 | # typescript
34 | *.tsbuildinfo
35 | next-env.d.ts
36 |
--------------------------------------------------------------------------------
/Chapter12/coffee-restaurant/README.md:
--------------------------------------------------------------------------------
1 | # Coffee Restaurant App
2 |
3 | 
4 |
5 | ## Install and Setup
6 |
7 | ### Environment variable file setup
8 |
9 | First, create a `.env.local` file and put it in the root folder. Then create a GitHub and Google client Id and secret for your `.env.local` file. Follow this guide here [https://next-auth.js.org/configuration/providers/oauth](https://next-auth.js.org/configuration/providers/oauth).
10 |
11 | Your `.env.local` file should look like this with key-value pairs:
12 |
13 | The `NEXTAUTH_SECRET` can be anything just create a random string as if you were generating a secure password there are many tools that can do this for you.
14 |
15 | ```shell
16 |
17 | NEXTAUTH_SECRET="yoursecret"
18 |
19 | GITHUB_ID="yourgithubid"
20 |
21 | GITHUB_SECRET="yourgithubsecret"
22 |
23 | GOOGLE_ID="yourgoogleid"
24 |
25 | GOOGLE_SECRET="yourgooglesecret"
26 |
27 | ```
28 |
29 | ### GraphQL API Usage
30 |
31 | The GraphQL API server is at [http://localhost:3000/graphql](http://localhost:3000/graphql)
32 |
33 | Update the function in `utils/withApollo.js` with your GraphQL endpoint.
34 |
35 | ```javascript
36 | export function initializeApollo(initialState = null) {
37 | const _apolloClient = new ApolloClient({
38 | // Local GraphQL endpoint
39 |
40 | uri: 'http://localhost:3000/graphql',
41 |
42 | // Your online GraphQL endpoint
43 |
44 | // uri: 'https://coffee-restaurant.vercel.app/graphql',
45 |
46 | cache: new InMemoryCache().restore(initialState || {}),
47 | });
48 |
49 | return _apolloClient;
50 | }
51 | ```
52 |
53 | Next, run the development server:
54 |
55 | ```bash
56 |
57 | npm run dev
58 |
59 | # or
60 |
61 | yarn dev
62 |
63 | # or
64 |
65 | pnpm dev
66 |
67 | ```
68 |
69 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the app.
70 |
--------------------------------------------------------------------------------
/Chapter12/coffee-restaurant/data/menu.js:
--------------------------------------------------------------------------------
1 | export const menu = [
2 | {
3 | id: '1',
4 | foodType: 'Drinks',
5 | name: 'Latte',
6 | description: 'Steamed milk',
7 | },
8 | {
9 | id: '2',
10 | foodType: 'Drinks',
11 | name: 'Cappuccino',
12 | description: 'Espresso',
13 | },
14 | ];
15 |
--------------------------------------------------------------------------------
/Chapter12/coffee-restaurant/data/profile.js:
--------------------------------------------------------------------------------
1 | export const profile = [
2 | {
3 | id: '1',
4 | bio: `Born and raised in London, my name is Jordan Brewer and I am a passionate coffee aficionado with a heart as warm as a freshly brewed cup of java.`,
5 | },
6 | ];
7 |
--------------------------------------------------------------------------------
/Chapter12/coffee-restaurant/img/coffee-restaurant.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PacktPublishing/React-Interview-Guide/b0e2ba6102a81f820c07219e898318fe1f0c4ef4/Chapter12/coffee-restaurant/img/coffee-restaurant.jpg
--------------------------------------------------------------------------------
/Chapter12/coffee-restaurant/jest.config.mjs:
--------------------------------------------------------------------------------
1 | import nextJest from 'next/jest.js';
2 |
3 | const createJestConfig = nextJest({
4 | // Provide the path to your Next.js app to load next.config.js and .env files in your test environment
5 | dir: './',
6 | });
7 |
8 | // Add any custom config to be passed to Jest
9 | /** @type {import('jest').Config} */
10 | const config = {
11 | // Add more setup options before each test is run
12 | // setupFilesAfterEnv: ['/jest.setup.js'],
13 |
14 | testEnvironment: 'jest-environment-jsdom',
15 | };
16 |
17 | // createJestConfig is exported this way to ensure that next/jest can load the Next.js config which is async
18 | export default createJestConfig(config);
19 |
--------------------------------------------------------------------------------
/Chapter12/coffee-restaurant/jsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "paths": {
4 | "@/*": ["./src/*"]
5 | }
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/Chapter12/coffee-restaurant/next.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('next').NextConfig} */
2 | const nextConfig = {
3 | compiler: {
4 | styledComponents: true,
5 | },
6 | };
7 |
8 | module.exports = nextConfig;
9 |
--------------------------------------------------------------------------------
/Chapter12/coffee-restaurant/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "my-app-restaurant",
3 | "version": "0.1.0",
4 | "private": true,
5 | "scripts": {
6 | "dev": "next dev",
7 | "build": "next build",
8 | "start": "next start",
9 | "lint": "next lint",
10 | "test": "jest --watch"
11 | },
12 | "dependencies": {
13 | "@apollo/client": "^3.7.14",
14 | "@apollo/server": "^4.7.1",
15 | "@as-integrations/next": "^1.3.0",
16 | "@testing-library/user-event": "^14.4.3",
17 | "graphql": "^16.6.0",
18 | "graphql-tag": "^2.12.6",
19 | "next": "13.4.2",
20 | "next-auth": "^4.22.1",
21 | "react": "18.2.0",
22 | "react-dom": "18.2.0",
23 | "styled-components": "^6.0.0-rc.1"
24 | },
25 | "devDependencies": {
26 | "@testing-library/jest-dom": "^5.16.5",
27 | "@testing-library/react": "^14.0.0",
28 | "jest": "^29.5.0",
29 | "jest-environment-jsdom": "^29.5.0"
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/Chapter12/coffee-restaurant/public/next.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/Chapter12/coffee-restaurant/public/vercel.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/Chapter12/coffee-restaurant/src/app/account/menu/page.js:
--------------------------------------------------------------------------------
1 | 'use client';
2 | import { useSession, signIn, signOut } from 'next-auth/react';
3 | import { useQuery } from '@apollo/client';
4 | import { GET_MENU } from '@/app/queries/clientQueries';
5 | import withApollo from '../../utils/withApollo';
6 | import { styled, ThemeProvider } from 'styled-components';
7 | import GlobalStyle from '../../components/GlobalStyles';
8 | import MainMenu from '../../components/MainMenu';
9 |
10 | const theme = {
11 | colors: {
12 | primary: 'rgb(15 23 42)',
13 | },
14 | };
15 |
16 | const MainContainer = styled.div`
17 | margin: 2rem auto;
18 | max-width: 120rem;
19 | padding: 2rem;
20 | width: 100%;
21 | `;
22 |
23 | const PageTitle = styled.h1`
24 | color: #ffffff;
25 | `;
26 |
27 | const LoginStatus = styled.p`
28 | color: #ffffff;
29 | `;
30 |
31 | const SignInOutButton = styled.button`
32 | color: #ffffff;
33 | padding: 0.5rem;
34 | cursor: pointer;
35 | margin: 2rem 0 2rem 0;
36 | `;
37 |
38 | const ContentContainer = styled.div`
39 | display: flex;
40 | flex-flow: column wrap;
41 | `;
42 |
43 | const Content = styled.p`
44 | color: #ffffff;
45 | font-size: 1.4rem;
46 | `;
47 |
48 | const ItemContainer = styled.div`
49 | display: flex;
50 | flex-flow: row nowrap;
51 | margin: 2rem 0 2rem 0;
52 | border: 0.1rem solid black;
53 | `;
54 |
55 | const ItemDescription = styled.div`
56 | margin-left: 1rem;
57 | `;
58 |
59 | const Menu = () => {
60 | const { loading, error, data } = useQuery(GET_MENU);
61 | const { data: session, status } = useSession();
62 | const userEmail = session?.user?.email;
63 |
64 | if (loading) return Loading... ;
65 | if (error) return Something went wrong ;
66 |
67 | if (status === 'loading') {
68 | return Hang on there... ;
69 | }
70 |
71 | if (status === 'authenticated') {
72 | return (
73 | <>
74 |
75 |
76 |
77 |
78 | Menu
79 | Signed in as {userEmail}
80 | signOut()}>
81 | Sign out
82 |
83 | {!loading && !error && (
84 |
85 | {data.menu.map((items) => (
86 |
87 |
88 |
89 | {items.name}
90 | {items.foodType}
91 | {items.description}
92 |
93 |
94 |
95 | ))}
96 |
97 | )}
98 |
99 |
100 | >
101 | );
102 | }
103 |
104 | return (
105 | <>
106 |
107 |
108 |
109 |
110 | Menu
111 | signIn('')}>Sign in
112 | Not signed in. Sign in to view the menu.
113 |
114 |
115 | >
116 | );
117 | };
118 |
119 | export default withApollo(Menu);
120 |
--------------------------------------------------------------------------------
/Chapter12/coffee-restaurant/src/app/account/profile/page.js:
--------------------------------------------------------------------------------
1 | 'use client';
2 | import { useSession, signIn, signOut } from 'next-auth/react';
3 | import { useQuery } from '@apollo/client';
4 | import { GET_PROFILE } from '@/app/queries/clientQueries';
5 | import withApollo from '../../utils/withApollo';
6 | import { styled, ThemeProvider } from 'styled-components';
7 | import GlobalStyle from '../../components/GlobalStyles';
8 | import MainMenu from '../../components/MainMenu';
9 |
10 | const theme = {
11 | colors: {
12 | primary: 'rgb(15 23 42)',
13 | },
14 | };
15 |
16 | const MainContainer = styled.div`
17 | margin: 2rem auto;
18 | max-width: 120rem;
19 | padding: 2rem;
20 | width: 100%;
21 | `;
22 |
23 | const PageTitle = styled.h1`
24 | color: #ffffff;
25 | `;
26 |
27 | const LoginStatus = styled.p`
28 | color: #ffffff;
29 | `;
30 |
31 | const SignInOutButton = styled.button`
32 | color: #ffffff;
33 | padding: 0.5rem;
34 | cursor: pointer;
35 | margin: 2rem 0 2rem 0;
36 | `;
37 |
38 | const ContentContainer = styled.div`
39 | display: flex;
40 | flex-flow: row wrap;
41 | `;
42 |
43 | const Content = styled.p`
44 | color: #ffffff;
45 | font-size: 1.4rem;
46 | margin-top: 2rem;
47 | `;
48 |
49 | const ClientProtectPage = () => {
50 | const { loading, error, data } = useQuery(GET_PROFILE);
51 | const { data: session, status } = useSession();
52 | const userEmail = session?.user?.email;
53 |
54 | if (loading) return Loading... ;
55 | if (error) return Something went wrong ;
56 |
57 | if (status === 'loading') {
58 | return Hang on there... ;
59 | }
60 |
61 | if (status === 'authenticated') {
62 | return (
63 | <>
64 |
65 |
66 |
67 |
68 | Profile
69 | Signed in as {userEmail}
70 | signOut()}>
71 | Sign out
72 |
73 | {!loading && !error && (
74 |
75 | {data.profile.map((account) => (
76 |
77 | {account.bio}
78 |
79 | ))}
80 |
81 | )}
82 |
83 |
84 | >
85 | );
86 | }
87 |
88 | return (
89 | <>
90 |
91 |
92 |
93 |
94 | Profile
95 | signIn('')}>Sign in
96 |
97 | Not signed in. Sign in to view your profile.
98 |
99 |
100 |
101 | >
102 | );
103 | };
104 |
105 | export default withApollo(ClientProtectPage);
106 |
--------------------------------------------------------------------------------
/Chapter12/coffee-restaurant/src/app/api/auth/[...nextauth]/route.js:
--------------------------------------------------------------------------------
1 | import NextAuth from 'next-auth';
2 | import GithubProvider from 'next-auth/providers/github';
3 | import GoogleProvider from 'next-auth/providers/google';
4 |
5 | export const handler = NextAuth({
6 | providers: [
7 | GithubProvider({
8 | clientId: process.env.GITHUB_ID,
9 | clientSecret: process.env.GITHUB_SECRET,
10 | }),
11 | GoogleProvider({
12 | clientId: process.env.GOOGLE_ID,
13 | clientSecret: process.env.GOOGLE_SECRET,
14 | }),
15 | ],
16 | });
17 |
18 | export { handler as GET, handler as POST };
19 |
--------------------------------------------------------------------------------
/Chapter12/coffee-restaurant/src/app/components/GlobalStyles.js:
--------------------------------------------------------------------------------
1 | import { createGlobalStyle } from 'styled-components';
2 |
3 | const GlobalStyle = createGlobalStyle`
4 | html,
5 | body {
6 | color: ${({ theme }) => theme.colors.primary};
7 | padding: 0;
8 | margin: 0;
9 | font-size: 1rem;
10 | background: rgb(6 78 59);
11 | }
12 |
13 | * {
14 | box-sizing: border-box;
15 | }
16 | `;
17 |
18 | export default GlobalStyle;
19 |
--------------------------------------------------------------------------------
/Chapter12/coffee-restaurant/src/app/components/MainMenu.js:
--------------------------------------------------------------------------------
1 | import Link from 'next/link';
2 | import { styled } from 'styled-components';
3 |
4 | const MainNavigation = styled.nav`
5 | position: relative;
6 | z-index: 1;
7 | display: flex;
8 | flex-flow: wrap;
9 | justify-content: space-around;
10 | font-size: 2rem;
11 | padding: 1rem;
12 | background: rgb(250 250 250);
13 | `;
14 |
15 | export default function MainMenu() {
16 | return (
17 |
18 | Home
19 | Nutrition
20 | Menu
21 | Profile
22 |
23 | );
24 | }
25 |
--------------------------------------------------------------------------------
/Chapter12/coffee-restaurant/src/app/components/Provider.js:
--------------------------------------------------------------------------------
1 | 'use client';
2 | import { SessionProvider } from 'next-auth/react';
3 |
4 | const Provider = ({ children }) => {
5 | return {children} ;
6 | };
7 |
8 | export default Provider;
9 |
--------------------------------------------------------------------------------
/Chapter12/coffee-restaurant/src/app/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PacktPublishing/React-Interview-Guide/b0e2ba6102a81f820c07219e898318fe1f0c4ef4/Chapter12/coffee-restaurant/src/app/favicon.ico
--------------------------------------------------------------------------------
/Chapter12/coffee-restaurant/src/app/globals.css:
--------------------------------------------------------------------------------
1 | :root {
2 | --max-width: 1100px;
3 | --border-radius: 12px;
4 | --font-mono: ui-monospace, Menlo, Monaco, 'Cascadia Mono', 'Segoe UI Mono',
5 | 'Roboto Mono', 'Oxygen Mono', 'Ubuntu Monospace', 'Source Code Pro',
6 | 'Fira Mono', 'Droid Sans Mono', 'Courier New', monospace;
7 |
8 | --foreground-rgb: 0, 0, 0;
9 | --background-start-rgb: 214, 219, 220;
10 | --background-end-rgb: 255, 255, 255;
11 |
12 | --primary-glow: conic-gradient(
13 | from 180deg at 50% 50%,
14 | #16abff33 0deg,
15 | #0885ff33 55deg,
16 | #54d6ff33 120deg,
17 | #0071ff33 160deg,
18 | transparent 360deg
19 | );
20 | --secondary-glow: radial-gradient(
21 | rgba(255, 255, 255, 1),
22 | rgba(255, 255, 255, 0)
23 | );
24 |
25 | --tile-start-rgb: 239, 245, 249;
26 | --tile-end-rgb: 228, 232, 233;
27 | --tile-border: conic-gradient(
28 | #00000080,
29 | #00000040,
30 | #00000030,
31 | #00000020,
32 | #00000010,
33 | #00000010,
34 | #00000080
35 | );
36 |
37 | --callout-rgb: 238, 240, 241;
38 | --callout-border-rgb: 172, 175, 176;
39 | --card-rgb: 180, 185, 188;
40 | --card-border-rgb: 131, 134, 135;
41 | }
42 |
43 | @media (prefers-color-scheme: dark) {
44 | :root {
45 | --foreground-rgb: 255, 255, 255;
46 | --background-start-rgb: 0, 0, 0;
47 | --background-end-rgb: 0, 0, 0;
48 |
49 | --primary-glow: radial-gradient(rgba(1, 65, 255, 0.4), rgba(1, 65, 255, 0));
50 | --secondary-glow: linear-gradient(
51 | to bottom right,
52 | rgba(1, 65, 255, 0),
53 | rgba(1, 65, 255, 0),
54 | rgba(1, 65, 255, 0.3)
55 | );
56 |
57 | --tile-start-rgb: 2, 13, 46;
58 | --tile-end-rgb: 2, 5, 19;
59 | --tile-border: conic-gradient(
60 | #ffffff80,
61 | #ffffff40,
62 | #ffffff30,
63 | #ffffff20,
64 | #ffffff10,
65 | #ffffff10,
66 | #ffffff80
67 | );
68 |
69 | --callout-rgb: 20, 20, 20;
70 | --callout-border-rgb: 108, 108, 108;
71 | --card-rgb: 100, 100, 100;
72 | --card-border-rgb: 200, 200, 200;
73 | }
74 | }
75 |
76 | * {
77 | box-sizing: border-box;
78 | padding: 0;
79 | margin: 0;
80 | }
81 |
82 | html,
83 | body {
84 | max-width: 100vw;
85 | overflow-x: hidden;
86 | }
87 |
88 | body {
89 | color: rgb(var(--foreground-rgb));
90 | background: linear-gradient(
91 | to bottom,
92 | transparent,
93 | rgb(var(--background-end-rgb))
94 | )
95 | rgb(var(--background-start-rgb));
96 | }
97 |
98 | a {
99 | color: inherit;
100 | text-decoration: none;
101 | }
102 |
103 | @media (prefers-color-scheme: dark) {
104 | html {
105 | color-scheme: dark;
106 | }
107 | }
108 |
--------------------------------------------------------------------------------
/Chapter12/coffee-restaurant/src/app/graphql/route.js:
--------------------------------------------------------------------------------
1 | import { ApolloServer } from '@apollo/server';
2 | import { startServerAndCreateNextHandler } from '@as-integrations/next';
3 | import { gql } from 'graphql-tag';
4 | import { menu } from '../../../data/menu';
5 | import { profile } from '../../../data/profile';
6 | import allowCors from '../utils/cors';
7 |
8 | // Define the GraphQL schema and resolvers
9 | const typeDefs = gql`
10 | type Menu {
11 | id: String
12 | foodType: String
13 | name: String
14 | description: String
15 | }
16 |
17 | type Profile {
18 | id: String
19 | bio: String
20 | }
21 |
22 | type Query {
23 | menu: [Menu]
24 | profile: [Profile]
25 | }
26 | `;
27 |
28 | const resolvers = {
29 | Query: {
30 | menu: () => menu,
31 | profile: () => profile,
32 | },
33 | };
34 |
35 | // Create the Apollo Server
36 | const server = new ApolloServer({
37 | typeDefs,
38 | resolvers,
39 | });
40 |
41 | const handler = startServerAndCreateNextHandler(server, {
42 | context: async (req, res) => ({ req, res }),
43 | });
44 |
45 | export async function GET(request) {
46 | return handler(request);
47 | }
48 |
49 | export async function POST(request) {
50 | return handler(request);
51 | }
52 |
53 | export default allowCors(handler);
54 |
--------------------------------------------------------------------------------
/Chapter12/coffee-restaurant/src/app/layout.js:
--------------------------------------------------------------------------------
1 | import './globals.css';
2 | import { Dosis } from 'next/font/google';
3 | import StyledComponentsRegistry from './lib/registry';
4 |
5 | const dosis = Dosis({ subsets: ['latin'] });
6 |
7 | export const metadata = {
8 | title: 'Resturant App',
9 | description: 'Generated by create next app',
10 | };
11 |
12 | export default function RootLayout({ children }) {
13 | return (
14 |
15 |
16 | {children}
17 |
18 |
19 | );
20 | }
21 |
--------------------------------------------------------------------------------
/Chapter12/coffee-restaurant/src/app/lib/registry.js:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import React, { useState } from 'react';
4 | import { useServerInsertedHTML } from 'next/navigation';
5 | import { ServerStyleSheet, StyleSheetManager } from 'styled-components';
6 |
7 | export default function StyledComponentsRegistry({ children }) {
8 | // Only create stylesheet once with lazy initial state
9 | // x-ref: https://reactjs.org/docs/hooks-reference.html#lazy-initial-state
10 | const [styledComponentsStyleSheet] = useState(() => new ServerStyleSheet());
11 |
12 | useServerInsertedHTML(() => {
13 | const styles = styledComponentsStyleSheet.getStyleElement();
14 | styledComponentsStyleSheet.instance.clearTag();
15 | return <>{styles}>;
16 | });
17 |
18 | if (typeof window !== 'undefined') return <>{children}>;
19 |
20 | return (
21 |
22 | {children}
23 |
24 | );
25 | }
26 |
--------------------------------------------------------------------------------
/Chapter12/coffee-restaurant/src/app/not-found.js:
--------------------------------------------------------------------------------
1 | 'use client';
2 | import { styled, ThemeProvider } from 'styled-components';
3 | import GlobalStyle from '../../src/app/components/GlobalStyles';
4 | import MainMenu from './components/MainMenu';
5 |
6 | const theme = {
7 | colors: {
8 | primary: 'rgb(15 23 42)',
9 | },
10 | };
11 |
12 | const MainContainer = styled.div`
13 | margin: 2rem auto;
14 | max-width: 120rem;
15 | padding: 2rem;
16 | width: 100%;
17 | `;
18 |
19 | const PageTitle = styled.h1`
20 | color: #ffffff;
21 | `;
22 |
23 | const PageIntro = styled.p`
24 | color: #ffffff;
25 | margin-top: 2rem;
26 | font-size: 1.4rem;
27 | `;
28 |
29 | export default function NotFound() {
30 | return (
31 | <>
32 |
33 |
34 |
35 |
36 | Page Not Found
37 | Could not find requested page :(
38 |
39 |
40 | >
41 | );
42 | }
43 |
--------------------------------------------------------------------------------
/Chapter12/coffee-restaurant/src/app/nutrition/page.js:
--------------------------------------------------------------------------------
1 | 'use client';
2 | import { styled, ThemeProvider } from 'styled-components';
3 | import GlobalStyle from '../components/GlobalStyles';
4 | import MainMenu from '../components/MainMenu';
5 |
6 | const theme = {
7 | colors: {
8 | primary: 'rgb(15 23 42)',
9 | },
10 | };
11 |
12 | const MainContainer = styled.div`
13 | margin: 2rem auto;
14 | max-width: 120rem;
15 | padding: 2rem;
16 | width: 100%;
17 | `;
18 |
19 | const PageTitle = styled.h1`
20 | color: #ffffff;
21 | `;
22 |
23 | const PageIntro = styled.p`
24 | color: #ffffff;
25 | margin-top: 2rem;
26 | font-size: 1.4rem;
27 | `;
28 |
29 | export default function Nutrition() {
30 | return (
31 | <>
32 |
33 |
34 |
35 |
36 | Nutrition
37 | Nutrition is good for health and diet!
38 |
39 |
40 | >
41 | );
42 | }
43 |
--------------------------------------------------------------------------------
/Chapter12/coffee-restaurant/src/app/nutrition/page.test.js:
--------------------------------------------------------------------------------
1 | import { render, screen } from '@testing-library/react';
2 | import '@testing-library/jest-dom';
3 | import Nutrition from './page';
4 |
5 | describe('Nutrition', () => {
6 | it('renders without crashing', () => {
7 | render( );
8 | });
9 |
10 | it('displays the correct title and intro', () => {
11 | render( );
12 | expect(
13 | screen.getByText('Nutrition is good for health and diet!')
14 | ).toBeInTheDocument();
15 | });
16 | });
17 |
--------------------------------------------------------------------------------
/Chapter12/coffee-restaurant/src/app/page.js:
--------------------------------------------------------------------------------
1 | 'use client';
2 | import { styled, ThemeProvider } from 'styled-components';
3 | import GlobalStyle from '../../src/app/components/GlobalStyles';
4 | import MainMenu from './components/MainMenu';
5 |
6 | const theme = {
7 | colors: {
8 | primary: 'rgb(15 23 42)',
9 | },
10 | };
11 |
12 | const MainContainer = styled.div`
13 | margin: 0 auto;
14 | width: 100%;
15 | `;
16 |
17 | const CoverHeadingBG = styled.div`
18 | margin: 2rem auto;
19 | display: flex;
20 | flex-flow: column;
21 | align-items: center;
22 | background-color: rgb(6 95 70);
23 | color: rgb(255 255 255);
24 | border-radius: 2rem;
25 | padding: 2rem;
26 | `;
27 |
28 | const CoverHeading = styled.h1`
29 | text-transform: uppercase;
30 | `;
31 |
32 | const CoverIntro = styled.p`
33 | font-size: 1.4rem;
34 | margin: 2rem 2rem;
35 | `;
36 |
37 | const Hero = styled.div`
38 | margin: 2rem auto;
39 | background-image: url('https://res.cloudinary.com/d74fh3kw/image/upload/v1692557430/coffee-restaurant/coffee-shop_zlkf7u.jpg');
40 | background-repeat: no-repeat;
41 | background-size: cover;
42 | background-position: center;
43 | background-color: rgb(4 120 87);
44 | height: 67.5rem;
45 | width: 100%;
46 | `;
47 |
48 | export default function Home() {
49 | return (
50 | <>
51 |
52 |
53 |
54 |
55 |
56 | Summer time is here!
57 |
58 | Our summer menu has arrived. Freshen up your day with our creamy
59 | and delicious coffee range, iced teas and mouth watering snacks.
60 |
61 |
62 |
63 |
64 |
65 | >
66 | );
67 | }
68 |
--------------------------------------------------------------------------------
/Chapter12/coffee-restaurant/src/app/page.module.css:
--------------------------------------------------------------------------------
1 | .main {
2 | display: flex;
3 | flex-direction: column;
4 | justify-content: space-between;
5 | align-items: center;
6 | padding: 6rem;
7 | min-height: 100vh;
8 | }
9 |
10 | .description {
11 | display: inherit;
12 | justify-content: inherit;
13 | align-items: inherit;
14 | font-size: 0.85rem;
15 | max-width: var(--max-width);
16 | width: 100%;
17 | z-index: 2;
18 | font-family: var(--font-mono);
19 | }
20 |
21 | .description a {
22 | display: flex;
23 | justify-content: center;
24 | align-items: center;
25 | gap: 0.5rem;
26 | }
27 |
28 | .description p {
29 | position: relative;
30 | margin: 0;
31 | padding: 1rem;
32 | background-color: rgba(var(--callout-rgb), 0.5);
33 | border: 1px solid rgba(var(--callout-border-rgb), 0.3);
34 | border-radius: var(--border-radius);
35 | }
36 |
37 | .code {
38 | font-weight: 700;
39 | font-family: var(--font-mono);
40 | }
41 |
42 | .grid {
43 | display: grid;
44 | grid-template-columns: repeat(4, minmax(25%, auto));
45 | width: var(--max-width);
46 | max-width: 100%;
47 | }
48 |
49 | .card {
50 | padding: 1rem 1.2rem;
51 | border-radius: var(--border-radius);
52 | background: rgba(var(--card-rgb), 0);
53 | border: 1px solid rgba(var(--card-border-rgb), 0);
54 | transition: background 200ms, border 200ms;
55 | }
56 |
57 | .card span {
58 | display: inline-block;
59 | transition: transform 200ms;
60 | }
61 |
62 | .card h2 {
63 | font-weight: 600;
64 | margin-bottom: 0.7rem;
65 | }
66 |
67 | .card p {
68 | margin: 0;
69 | opacity: 0.6;
70 | font-size: 0.9rem;
71 | line-height: 1.5;
72 | max-width: 30ch;
73 | }
74 |
75 | .center {
76 | display: flex;
77 | justify-content: center;
78 | align-items: center;
79 | position: relative;
80 | padding: 4rem 0;
81 | }
82 |
83 | .center::before {
84 | background: var(--secondary-glow);
85 | border-radius: 50%;
86 | width: 480px;
87 | height: 360px;
88 | margin-left: -400px;
89 | }
90 |
91 | .center::after {
92 | background: var(--primary-glow);
93 | width: 240px;
94 | height: 180px;
95 | z-index: -1;
96 | }
97 |
98 | .center::before,
99 | .center::after {
100 | content: '';
101 | left: 50%;
102 | position: absolute;
103 | filter: blur(45px);
104 | transform: translateZ(0);
105 | }
106 |
107 | .logo {
108 | position: relative;
109 | }
110 | /* Enable hover only on non-touch devices */
111 | @media (hover: hover) and (pointer: fine) {
112 | .card:hover {
113 | background: rgba(var(--card-rgb), 0.1);
114 | border: 1px solid rgba(var(--card-border-rgb), 0.15);
115 | }
116 |
117 | .card:hover span {
118 | transform: translateX(4px);
119 | }
120 | }
121 |
122 | @media (prefers-reduced-motion) {
123 | .card:hover span {
124 | transform: none;
125 | }
126 | }
127 |
128 | /* Mobile */
129 | @media (max-width: 700px) {
130 | .content {
131 | padding: 4rem;
132 | }
133 |
134 | .grid {
135 | grid-template-columns: 1fr;
136 | margin-bottom: 120px;
137 | max-width: 320px;
138 | text-align: center;
139 | }
140 |
141 | .card {
142 | padding: 1rem 2.5rem;
143 | }
144 |
145 | .card h2 {
146 | margin-bottom: 0.5rem;
147 | }
148 |
149 | .center {
150 | padding: 8rem 0 6rem;
151 | }
152 |
153 | .center::before {
154 | transform: none;
155 | height: 300px;
156 | }
157 |
158 | .description {
159 | font-size: 0.8rem;
160 | }
161 |
162 | .description a {
163 | padding: 1rem;
164 | }
165 |
166 | .description p,
167 | .description div {
168 | display: flex;
169 | justify-content: center;
170 | position: fixed;
171 | width: 100%;
172 | }
173 |
174 | .description p {
175 | align-items: center;
176 | inset: 0 0 auto;
177 | padding: 2rem 1rem 1.4rem;
178 | border-radius: 0;
179 | border: none;
180 | border-bottom: 1px solid rgba(var(--callout-border-rgb), 0.25);
181 | background: linear-gradient(
182 | to bottom,
183 | rgba(var(--background-start-rgb), 1),
184 | rgba(var(--callout-rgb), 0.5)
185 | );
186 | background-clip: padding-box;
187 | backdrop-filter: blur(24px);
188 | }
189 |
190 | .description div {
191 | align-items: flex-end;
192 | pointer-events: none;
193 | inset: auto 0 0;
194 | padding: 2rem;
195 | height: 200px;
196 | background: linear-gradient(
197 | to bottom,
198 | transparent 0%,
199 | rgb(var(--background-end-rgb)) 40%
200 | );
201 | z-index: 1;
202 | }
203 | }
204 |
205 | /* Tablet and Smaller Desktop */
206 | @media (min-width: 701px) and (max-width: 1120px) {
207 | .grid {
208 | grid-template-columns: repeat(2, 50%);
209 | }
210 | }
211 |
212 | @media (prefers-color-scheme: dark) {
213 | .vercelLogo {
214 | filter: invert(1);
215 | }
216 |
217 | .logo {
218 | filter: invert(1) drop-shadow(0 0 0.3rem #ffffff70);
219 | }
220 | }
221 |
222 | @keyframes rotate {
223 | from {
224 | transform: rotate(360deg);
225 | }
226 | to {
227 | transform: rotate(0deg);
228 | }
229 | }
230 |
--------------------------------------------------------------------------------
/Chapter12/coffee-restaurant/src/app/page.test.js:
--------------------------------------------------------------------------------
1 | import { render, screen } from '@testing-library/react';
2 | import '@testing-library/jest-dom';
3 | import Home from './page';
4 |
5 | describe('Home', () => {
6 | it('renders without crashing', () => {
7 | render( );
8 | });
9 |
10 | it('displays the correct heading and intro', () => {
11 | render( );
12 | expect(screen.getByText('Summer time is here!')).toBeInTheDocument();
13 | expect(
14 | screen.getByText(
15 | /Our summer menu has arrived. Freshen up your day with our creamy and delicious coffee range, iced teas and mouth watering snacks./
16 | )
17 | ).toBeInTheDocument();
18 | });
19 | });
20 |
--------------------------------------------------------------------------------
/Chapter12/coffee-restaurant/src/app/queries/clientQueries.js:
--------------------------------------------------------------------------------
1 | import { gql } from '@apollo/client';
2 |
3 | const GET_MENU = gql`
4 | query {
5 | menu {
6 | id
7 | name
8 | foodType
9 | description
10 | }
11 | }
12 | `;
13 |
14 | const GET_PROFILE = gql`
15 | query {
16 | profile {
17 | id
18 | bio
19 | }
20 | }
21 | `;
22 | export { GET_MENU, GET_PROFILE };
23 |
--------------------------------------------------------------------------------
/Chapter12/coffee-restaurant/src/app/utils/cors.js:
--------------------------------------------------------------------------------
1 | const allowCors = (fn) => async (req, res) => {
2 | res.setHeader('Access-Control-Allow-Credentials', true);
3 | res.setHeader('Access-Control-Allow-Origin', '*');
4 |
5 | res.setHeader('Access-Control-Allow-Origin', req.headers.origin);
6 | res.setHeader(
7 | 'Access-Control-Allow-Methods',
8 | 'GET,OPTIONS,PATCH,DELETE,POST,PUT'
9 | );
10 | res.setHeader(
11 | 'Access-Control-Allow-Headers',
12 | 'X-CSRF-Token, X-Requested-With, Accept, Accept-Version, Content-Length, Content-MD5, Content-Type, Date, X-Api-Version'
13 | );
14 | if (req.method === 'OPTIONS') {
15 | res.status(200).end();
16 | return;
17 | }
18 | await fn(req, res);
19 | };
20 |
21 | export default allowCors;
22 |
--------------------------------------------------------------------------------
/Chapter12/coffee-restaurant/src/app/utils/withApollo.js:
--------------------------------------------------------------------------------
1 | import { ApolloClient, InMemoryCache, ApolloProvider } from '@apollo/client';
2 | import { useMemo } from 'react';
3 | import { SessionProvider } from 'next-auth/react';
4 |
5 | export function initializeApollo(initialState = null) {
6 | const _apolloClient = new ApolloClient({
7 | // Local GraphQL endpoint
8 | // uri: 'http://localhost:3000/graphql',
9 | // Your online GraphQL endpoint
10 | uri: 'https://coffee-restaurant.vercel.app/graphql',
11 | cache: new InMemoryCache().restore(initialState || {}),
12 | });
13 |
14 | return _apolloClient;
15 | }
16 |
17 | export function useApollo(initialState) {
18 | const store = useMemo(() => initializeApollo(initialState), [initialState]);
19 | return store;
20 | }
21 |
22 | export default function withApollo(PageComponent) {
23 | const WithApollo = ({ apolloClient, apolloState, session, ...pageProps }) => {
24 | const client = useApollo(apolloState);
25 | return (
26 |
27 |
28 |
29 |
30 |
31 | );
32 | };
33 |
34 | // On the server
35 | if (typeof window === 'undefined') {
36 | WithApollo.getInitialProps = async (ctx) => {
37 | const apolloClient = initializeApollo();
38 |
39 | let pageProps = {};
40 | if (PageComponent.getInitialProps) {
41 | pageProps = await PageComponent.getInitialProps(ctx);
42 | }
43 |
44 | if (ctx.res && ctx.res.finished) {
45 | // When redirecting, the response is finished.
46 | // No point in continuing to render
47 | return pageProps;
48 | }
49 |
50 | const apolloState = apolloClient.cache.extract();
51 | return {
52 | ...pageProps,
53 | apolloState,
54 | };
55 | };
56 | }
57 |
58 | return WithApollo;
59 | }
60 |
--------------------------------------------------------------------------------
/Chapter12/cors.js:
--------------------------------------------------------------------------------
1 | const allowCors = (fn) => async (req, res) => {
2 | res.setHeader('Access-Control-Allow-Credentials', true);
3 | res.setHeader('Access-Control-Allow-Origin', '*');
4 |
5 | res.setHeader('Access-Control-Allow-Origin', req.headers.origin);
6 | res.setHeader(
7 | 'Access-Control-Allow-Methods',
8 | 'GET,OPTIONS,PATCH,DELETE,POST,PUT'
9 | );
10 | res.setHeader(
11 | 'Access-Control-Allow-Headers',
12 | 'X-CSRF-Token, X-Requested-With, Accept, Accept-Version, Content-Length, Content-MD5, Content-Type, Date, X-Api-Version'
13 | );
14 | if (req.method === 'OPTIONS') {
15 | res.status(200).end();
16 | return;
17 | }
18 | await fn(req, res);
19 | };
20 |
21 | export default allowCors;
22 |
--------------------------------------------------------------------------------
/Chapter12/createGlobalStyle.js:
--------------------------------------------------------------------------------
1 | import { createGlobalStyle } from 'styled-components';
2 |
3 | const GlobalStyle = createGlobalStyle`
4 | html,
5 | body {
6 | color: ${({ theme }) => theme.colors.primary};
7 | padding: 0;
8 | margin: 0;
9 | font-size: 1rem;
10 | background: rgb(6 78 59);
11 | }
12 |
13 | * {
14 | box-sizing: border-box;
15 | }
16 | `;
17 |
18 | export default GlobalStyle;
19 |
--------------------------------------------------------------------------------
/Chapter12/envSecrets.md:
--------------------------------------------------------------------------------
1 | NEXTAUTH_SECRET="yournextsecret"
2 | GITHUB_ID="yourgithubid"
3 | GITHUB_SECRET="yourgithubsecret"
4 | GOOGLE_ID="yourgoogleid"
5 | GOOGLE_SECRET="yourgooglesecret"
6 |
--------------------------------------------------------------------------------
/Chapter12/expressBackend.js:
--------------------------------------------------------------------------------
1 | const express = require('express');
2 |
3 | const cors = require('cors');
4 |
5 | const path = require('path');
6 |
7 | require('dotenv').config();
8 |
9 | const app = express();
10 |
11 | app.use(cors());
12 |
13 | app.use(express.urlencoded({ extended: false }));
14 |
15 | app.use(express.json());
16 |
17 | app.use('/static', express.static(path.join(__dirname + '/public')));
18 |
19 | app.get('/api', (req, res) => {
20 | res.json({ msg: 'API Route' });
21 | });
22 |
23 | app.post('/post/:data', (req, res) => {
24 | const data = req.params.data;
25 |
26 | console.log(data);
27 |
28 | res.json({ msg: `Data logged ${data}` });
29 | });
30 |
31 | const port = process.env.PORT || 8080;
32 |
33 | app.listen(port, () =>
34 | console.log(`Server running on port ${port}, http://localhost:${port}`)
35 | );
36 |
--------------------------------------------------------------------------------
/Chapter12/gitRepoPush.md:
--------------------------------------------------------------------------------
1 | git status
2 | git add .
3 | git commit -m "vercel graphql endpoint for uri"
4 | git push
5 |
--------------------------------------------------------------------------------
/Chapter12/gitRepoSetup.md:
--------------------------------------------------------------------------------
1 | git init
2 | git add .
3 | git commit -m "first commit"
4 | git branch -M main
5 | git remote add origin https://github.com/yourname/yourprojectname.git
6 | git push -u origin main
7 |
--------------------------------------------------------------------------------
/Chapter12/graphQLMenuProfileQueries.js:
--------------------------------------------------------------------------------
1 | import { gql } from '@apollo/client';
2 |
3 | const GET_MENU = gql`
4 | query {
5 | menu {
6 | id
7 | name
8 | foodType
9 | description
10 | }
11 | }
12 | `;
13 |
14 | const GET_PROFILE = gql`
15 | query {
16 | profile {
17 | id
18 | bio
19 | }
20 | }
21 | `;
22 | export { GET_MENU, GET_PROFILE };
23 |
--------------------------------------------------------------------------------
/Chapter12/graphQLQuery.graphql:
--------------------------------------------------------------------------------
1 | query {
2 | post(id: 1) {
3 | id
4 | title
5 | content
6 | author {
7 | id
8 | name
9 | email
10 | }
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/Chapter12/graphQLQueryMenu.graphql:
--------------------------------------------------------------------------------
1 | query {
2 | menu {
3 | id
4 | name
5 | foodType
6 | description
7 | img
8 | nutrition {
9 | carbohydrates
10 | energy
11 | fat
12 | fibre
13 | protien
14 | salt
15 | saturatedFat
16 | sugar
17 | }
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/Chapter12/graphQLSchema.graphql:
--------------------------------------------------------------------------------
1 | type Post {
2 | id: ID!
3 | title: String!
4 | content: String!
5 | author: Author!
6 | }
7 |
8 | type Author {
9 | id: ID!
10 | name: String!
11 | email: String!
12 | }
13 |
14 | type Query {
15 | post(id: ID!): Post
16 | }
17 |
--------------------------------------------------------------------------------
/Chapter12/graphQLSchemaAndResolvers.js:
--------------------------------------------------------------------------------
1 | import { ApolloServer } from '@apollo/server';
2 | import { startServerAndCreateNextHandler } from '@as-integrations/next';
3 | import { gql } from 'graphql-tag';
4 | import { menu } from '../../../data/menu';
5 | import { profile } from '../../../data/profile';
6 | import allowCors from '../utils/cors';
7 |
8 | // Define the GraphQL schema and resolvers
9 | const typeDefs = gql`
10 | type Menu {
11 | id: String
12 | foodType: String
13 | name: String
14 | description: String
15 | }
16 |
17 | type Profile {
18 | id: String
19 | bio: String
20 | }
21 |
22 | type Query {
23 | menu: [Menu]
24 | profile: [Profile]
25 | }
26 | `;
27 |
28 | const resolvers = {
29 | Query: {
30 | menu: () => menu,
31 | profile: () => profile,
32 | },
33 | };
34 |
35 | // Create the Apollo Server
36 | const server = new ApolloServer({
37 | typeDefs,
38 | resolvers,
39 | });
40 |
41 | const handler = startServerAndCreateNextHandler(server, {
42 | context: async (req, res) => ({ req, res }),
43 | });
44 |
45 | export async function GET(request) {
46 | return handler(request);
47 | }
48 |
49 | export async function POST(request) {
50 | return handler(request);
51 | }
52 |
53 | export default allowCors(handler);
54 |
--------------------------------------------------------------------------------
/Chapter12/initializeApollo.js:
--------------------------------------------------------------------------------
1 | import { ApolloClient, InMemoryCache, ApolloProvider } from '@apollo/client';
2 | import { useMemo } from 'react';
3 | import { SessionProvider } from 'next-auth/react';
4 |
5 | export function initializeApollo(initialState = null) {
6 | const _apolloClient = new ApolloClient({
7 | // Local GraphQL endpoint
8 | // uri: 'http://localhost:3000/graphql',
9 | // Your online GraphQL endpoint
10 | uri: 'https://coffee-restaurant.vercel.app/graphql',
11 | cache: new InMemoryCache().restore(initialState || {}),
12 | });
13 |
14 | return _apolloClient;
15 | }
16 |
17 | export function useApollo(initialState) {
18 | const store = useMemo(() => initializeApollo(initialState), [initialState]);
19 | return store;
20 | }
21 |
22 | export default function withApollo(PageComponent) {
23 | const WithApollo = ({ apolloClient, apolloState, session, ...pageProps }) => {
24 | const client = useApollo(apolloState);
25 | return (
26 |
27 |
28 |
29 |
30 |
31 | );
32 | };
33 |
34 | // On the server
35 | if (typeof window === 'undefined') {
36 | WithApollo.getInitialProps = async (ctx) => {
37 | const apolloClient = initializeApollo();
38 |
39 | let pageProps = {};
40 | if (PageComponent.getInitialProps) {
41 | pageProps = await PageComponent.getInitialProps(ctx);
42 | }
43 |
44 | if (ctx.res && ctx.res.finished) {
45 | // When redirecting, the response is finished.
46 | // No point in continuing to render
47 | return pageProps;
48 | }
49 |
50 | const apolloState = apolloClient.cache.extract();
51 | return {
52 | ...pageProps,
53 | apolloState,
54 | };
55 | };
56 | }
57 |
58 | return WithApollo;
59 | }
60 |
--------------------------------------------------------------------------------
/Chapter12/installFrontendPackages.md:
--------------------------------------------------------------------------------
1 | npm i @apollo/client @apollo/server @as-integrations/next @testing-library/user-event graphql graphql-tag next-auth styled-components@latest
2 |
--------------------------------------------------------------------------------
/Chapter12/installTestingLibrary.md:
--------------------------------------------------------------------------------
1 | npm i --save-dev @testing-library/jest-dom @testing-library/react jest jest-environment-jsdom
2 |
--------------------------------------------------------------------------------
/Chapter12/jestTestWatch.md:
--------------------------------------------------------------------------------
1 | "test": "jest --watch",
2 |
--------------------------------------------------------------------------------
/Chapter12/jordanBrewerprofile.js:
--------------------------------------------------------------------------------
1 | export const profile = [
2 | {
3 | id: '1',
4 | bio: `Born and raised in London, my name is Jordan Brewer and I am a passionate coffee aficionado with a heart as warm as a freshly brewed cup of java.`,
5 | },
6 | ];
7 |
--------------------------------------------------------------------------------
/Chapter12/mainNavigation.js:
--------------------------------------------------------------------------------
1 | import Link from 'next/link';
2 | import { styled } from 'styled-components';
3 |
4 | const MainNavigation = styled.nav`
5 | position: relative;
6 | z-index: 1;
7 | display: flex;
8 | flex-flow: wrap;
9 | justify-content: space-around;
10 | font-size: 2rem;
11 | padding: 1rem;
12 | background: rgb(250 250 250);
13 | `;
14 |
15 | export default function MainMenu() {
16 | return (
17 |
18 | Home
19 | Nutrition
20 | Menu
21 | Profile
22 |
23 | );
24 | }
25 |
--------------------------------------------------------------------------------
/Chapter12/menuCoffee.js:
--------------------------------------------------------------------------------
1 | export const menu = [
2 | {
3 | id: '1',
4 | foodType: 'Drinks',
5 | name: 'Latte',
6 | description: 'Steamed milk',
7 | },
8 | {
9 | id: '2',
10 | foodType: 'Drinks',
11 | name: 'Cappuccino',
12 | description: 'Espresso',
13 | },
14 | ];
15 |
--------------------------------------------------------------------------------
/Chapter12/menuPage.js:
--------------------------------------------------------------------------------
1 | 'use client';
2 | import { useSession, signIn, signOut } from 'next-auth/react';
3 | import { useQuery } from '@apollo/client';
4 | import { GET_MENU } from '@/app/queries/clientQueries';
5 | import withApollo from '../../utils/withApollo';
6 | import { styled, ThemeProvider } from 'styled-components';
7 | import GlobalStyle from '../../components/GlobalStyles';
8 | import MainMenu from '../../components/MainMenu';
9 |
10 | const theme = {
11 | colors: {
12 | primary: 'rgb(15 23 42)',
13 | },
14 | };
15 |
16 | const MainContainer = styled.div`
17 | margin: 2rem auto;
18 | max-width: 120rem;
19 | padding: 2rem;
20 | width: 100%;
21 | `;
22 |
23 | const PageTitle = styled.h1`
24 | color: #ffffff;
25 | `;
26 |
27 | const LoginStatus = styled.p`
28 | color: #ffffff;
29 | `;
30 |
31 | const SignInOutButton = styled.button`
32 | color: #ffffff;
33 | padding: 0.5rem;
34 | cursor: pointer;
35 | margin: 2rem 0 2rem 0;
36 | `;
37 |
38 | const ContentContainer = styled.div`
39 | display: flex;
40 | flex-flow: column wrap;
41 | `;
42 |
43 | const Content = styled.p`
44 | color: #ffffff;
45 | font-size: 1.4rem;
46 | `;
47 |
48 | const ItemContainer = styled.div`
49 | display: flex;
50 | flex-flow: row nowrap;
51 | margin: 2rem 0 2rem 0;
52 | border: 0.1rem solid black;
53 | `;
54 |
55 | const ItemDescription = styled.div`
56 | margin-left: 1rem;
57 | `;
58 |
59 | const Menu = () => {
60 | const { loading, error, data } = useQuery(GET_MENU);
61 | const { data: session, status } = useSession();
62 | const userEmail = session?.user?.email;
63 |
64 | if (loading) return Loading... ;
65 | if (error) return Something went wrong ;
66 |
67 | if (status === 'loading') {
68 | return Hang on there... ;
69 | }
70 |
71 | if (status === 'authenticated') {
72 | return (
73 | <>
74 |
75 |
76 |
77 |
78 | Menu
79 | Signed in as {userEmail}
80 | signOut()}>
81 | Sign out
82 |
83 | {!loading && !error && (
84 |
85 | {data.menu.map((items) => (
86 |
87 |
88 |
89 | {items.name}
90 | {items.foodType}
91 | {items.description}
92 |
93 |
94 |
95 | ))}
96 |
97 | )}
98 |
99 |
100 | >
101 | );
102 | }
103 |
104 | return (
105 | <>
106 |
107 |
108 |
109 |
110 | Menu
111 | signIn('')}>Sign in
112 | Not signed in. Sign in to view the menu.
113 |
114 |
115 | >
116 | );
117 | };
118 |
119 | export default withApollo(Menu);
120 |
--------------------------------------------------------------------------------
/Chapter12/nextAuth.js:
--------------------------------------------------------------------------------
1 | import NextAuth from 'next-auth';
2 | import GithubProvider from 'next-auth/providers/github';
3 | import GoogleProvider from 'next-auth/providers/google';
4 |
5 | export const handler = NextAuth({
6 | providers: [
7 | GithubProvider({
8 | clientId: process.env.GITHUB_ID,
9 | clientSecret: process.env.GITHUB_SECRET,
10 | }),
11 |
12 | GoogleProvider({
13 | clientId: process.env.GOOGLE_ID,
14 | clientSecret: process.env.GOOGLE_SECRET,
15 | }),
16 | ],
17 | });
18 |
19 | export { handler as GET, handler as POST };
20 |
--------------------------------------------------------------------------------
/Chapter12/nextConfig.js:
--------------------------------------------------------------------------------
1 | /** @type {import('next').NextConfig} */
2 |
3 | const nextConfig = {
4 | compiler: {
5 | styledComponents: true,
6 | },
7 | };
8 |
9 | module.exports = nextConfig;
10 |
--------------------------------------------------------------------------------
/Chapter12/nextJest.js:
--------------------------------------------------------------------------------
1 | import nextJest from 'next/jest.js';
2 |
3 | const createJestConfig = nextJest({
4 | // Provide the path to your Next.js app to load next.config.js and .env files in your test environment
5 |
6 | dir: './',
7 | });
8 |
9 | // Add any custom config to be passed to Jest
10 |
11 | /** @type {import('jest').Config} */
12 |
13 | const config = {
14 | // Add more setup options before each test is run
15 |
16 | // setupFilesAfterEnv: ['/jest.setup.js'],
17 |
18 | testEnvironment: 'jest-environment-jsdom',
19 | };
20 |
21 | // createJestConfig is exported this way to ensure that next/jest can load the Next.js config which is async
22 |
23 | export default createJestConfig(config);
24 |
--------------------------------------------------------------------------------
/Chapter12/nutritionPage.js:
--------------------------------------------------------------------------------
1 | 'use client';
2 | import { styled, ThemeProvider } from 'styled-components';
3 | import GlobalStyle from '../components/GlobalStyles';
4 | import MainMenu from '../components/MainMenu';
5 |
6 | const theme = {
7 | colors: {
8 | primary: 'rgb(15 23 42)',
9 | },
10 | };
11 |
12 | const MainContainer = styled.div`
13 | margin: 2rem auto;
14 | max-width: 120rem;
15 | padding: 2rem;
16 | width: 100%;
17 | `;
18 |
19 | const PageTitle = styled.h1`
20 | color: #ffffff;
21 | `;
22 |
23 | const PageIntro = styled.p`
24 | color: #ffffff;
25 | margin-top: 2rem;
26 | font-size: 1.4rem;
27 | `;
28 |
29 | export default function Nutrition() {
30 | return (
31 | <>
32 |
33 |
34 |
35 |
36 | Nutrition
37 | Nutrition is good for health and diet!
38 |
39 |
40 | >
41 | );
42 | }
43 |
--------------------------------------------------------------------------------
/Chapter12/nutritionPage.test.js:
--------------------------------------------------------------------------------
1 | import { render, screen } from '@testing-library/react';
2 | import '@testing-library/jest-dom';
3 | import Nutrition from './page';
4 |
5 | describe('Nutrition', () => {
6 | it('renders without crashing', () => {
7 | render( );
8 | });
9 |
10 | it('displays the correct title and intro', () => {
11 | render( );
12 | expect(
13 | screen.getByText('Nutrition is good for health and diet!')
14 | ).toBeInTheDocument();
15 | });
16 | });
17 |
--------------------------------------------------------------------------------
/Chapter12/styleSheet.js:
--------------------------------------------------------------------------------
1 | 'use client';
2 | import React, { useState } from 'react';
3 | import { useServerInsertedHTML } from 'next/navigation';
4 | import { ServerStyleSheet, StyleSheetManager } from 'styled-components';
5 |
6 | export default function StyledComponentsRegistry({ children }) {
7 | // Only create stylesheet once with lazy initial state
8 |
9 | // x-ref: https://reactjs.org/docs/hooks-reference.html#lazy-initial-state
10 |
11 | const [styledComponentsStyleSheet] = useState(() => new ServerStyleSheet());
12 |
13 | useServerInsertedHTML(() => {
14 | const styles = styledComponentsStyleSheet.getStyleElement();
15 |
16 | styledComponentsStyleSheet.instance.clearTag();
17 |
18 | return <>{styles}>;
19 | });
20 |
21 | if (typeof window !== 'undefined') return <>{children}>;
22 |
23 | return (
24 |
25 | {children}
26 |
27 | );
28 | }
29 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023 Packt
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # React Interview Guide
2 | React Interview Guide, Published by Packt
3 |
4 |
5 |
6 | This is the code repository for [React Interview Guide](https://www.packtpub.com/product/react-interview-guide/9781803241517), published by Packt.
7 |
8 | **Learn all you need to know to ace any React interview and land your dream job**
9 |
10 | ## What is this book about?
11 |
12 | This book will aid individuals preparing for React job interviews as well as seasoned developers looking to expand their React knowledge. A basic understanding of fundamental web development concepts and technologies, such as HTML, JavaScript, and CSS is necessary to get started.
13 |
14 | This book covers the following exciting features:
15 |
16 | * Discover contemporary strategies to make your profile shine in a competitive marketplace
17 | * Access diverse interview questions, from basic to advanced, to land a React developer job
18 | * Master React’s latest features, performance optimization, and debugging techniques step by step
19 | * Grasp techniques and tips to cover crucial skills for the React developer role
20 | * Understand various frameworks and libraries to prepare to answer any React interview question
21 | * Build fully fledged React-based applications to ace coding assignments
22 |
23 | If you feel this book is for you, get your [copy](https://www.amazon.com/React-Interview-Guide-Learn-interview/dp/1803241519) today!
24 |
25 |
27 |
28 | ## Instructions and Navigations
29 | All of the files is organized into folders. For example, Chapter2
30 |
31 |
32 | **Following is what you need for this book:**
33 |
34 | Are you struggling with React job interviews, feeling hindered by the lack of knowledge or confidence? Look no further than the React Interview Guide. Complete with the latest features of the React library, this comprehensive solution will assist you in taking a definitive step forward in your career as a React developer.
35 |
36 |
37 | With the following software and hardware list you can run all code files present in the book (Chapter 2-12).
38 |
39 | ### Software and Hardware List
40 | | Software required | OS required |
41 | | ------------------------------------ | ----------------------------------- |
42 | | React 18 Windows | macOS, or Linux |
43 | | TypeScript 3.7 | |
44 | | TECMAScript 11 | |
45 |
46 |
47 | ### Related products
48 | * React and React Native [[Packt]](https://www.packtpub.com/product/react-and-react-native-fourth-edition/9781803231280) [[Amazon]](https://www.amazon.com/React-Native-cross-platform-JavaScript-applications/dp/1803231289)
49 |
50 | * Learn React with TypeScript [[Packt]](https://www.packtpub.com/product/unreal-engine-5-character-creation-animation-and-cinematics/9781801812443) [[Amazon]](https://www.amazon.com/Learn-React-TypeScript-Beginners-development/dp/1789610257/ref=tmm_pap_swatch_0?_encoding=UTF8&qid=&sr=)
51 |
52 | ## Get to Know the Author
53 | **Sudheer Jonna**, hailing from Nellore, India, is a seasoned lead software developer currently based in Singapore. With expertise in Java and JavaScript, he specializes in architecting and developing large-scale applications, prioritizing robust architectures and high performance. Sudheer's comprehensive knowledge spans Single Page Applications (ReactJS/VueJS/Angular), backend API development, SQL, and containerization, enabling him to assist developers in advancing their careers. Besides his professional role, Sudheer is a published author, speaker, trainer, blogger, and an active member of public forums.
54 |
55 | **Andrew Baisden**, born and raised in London, England, is an accomplished Software Developer, Content Creator, and Technical Writer. Proficient in JavaScript and React, Andrew excels in developing user-friendly and captivating applications. His versatility extends to Python and C#, enhancing his reputation as a well-rounded programmer. Andrew's talent for simplifying complex concepts has made him a sought-after technical writer. Alongside his professional achievements, Andrew has a significant online presence, amassing a combined following of over 20,000 on various social media platforms, accessible through his Linktree profile.
56 |
57 |
--------------------------------------------------------------------------------