├── README.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
│ │ ├── shopping-cart.svg
│ │ └── user-profile-avatar.svg
│ ├── backend
│ │ └── firebase
│ │ │ ├── api
│ │ │ ├── auth.ts
│ │ │ ├── products-data.ts
│ │ │ └── utils.ts
│ │ │ ├── auth
│ │ │ ├── auth.ts
│ │ │ └── utils.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
└── chapter2
└── images
├── Figure2.10_Context.png
├── Figure2.1_JSXTranspilation.png
├── Figure2.3_Components.png
├── Figure2.4_State.png
├── Figure2.5_InitialVirtualDOM.png
├── Figure2.6_UpdatedVirtualDOM.png
├── Figure2.7_CompareVirtualDOMSnapshots.png
├── Figure2.8_UpdatedVirtualDOM.png
└── Figure2.9_DataFlow.png
/README.md:
--------------------------------------------------------------------------------
1 | ### The-Complete-React-Interview-Guide
2 | Repository for The Complete React Interview Guide book
3 |
4 | Written by [Sudheer Jonna](https://github.com/sudheerj) and [Andrew Baisden](https://github.com/andrewbaisden). Powered by Pack publishing, UK.
5 |
6 |
7 | __Table of Contents__
8 |
9 | * [Chapter 1, Brace yourself for interview preparation](https://github.com/sudheerj/the-complete-react-interview-guide/tree/master/chapter1)
10 | * Prepare your resume and cover letter
11 | * Build your Github profile or website portfolio
12 | * Where to find jobs and LinkedIn
13 | * Meetups and referrals
14 | * Interview tips
15 |
16 | * [Chapter 2, Understanding ReactJS fundamentals and its major features](https://github.com/sudheerj/the-complete-react-interview-guide/tree/master/chapter2)
17 | * Prerequisites to ReactJS
18 | * Introduce ReactJS and JSX
19 | * Building views with elements and components
20 | * Controlling the component data using props and state
21 | * Knowing the importance of key prop
22 | * Learning event handling
23 | * Understanding Virtual DOM
24 | * Difference between Unidirectional data flow and bidirectional data flow
25 | * Accessing DOM elements in React
26 | * Describing how to manage state globally using Context API
27 | * Understanding Server-side rendering technique
28 |
29 | * [Chapter 3, Hooks: Bring state, lifecycle and other features in functional components](https://github.com/sudheerj/the-complete-react-interview-guide/tree/master/chapter3)
30 | * Introduction to Hooks and their purpose
31 | * Local state management using hooks
32 | * Global state management using hooks
33 | * Performing side-effects in application
34 | * Accessing DOM nodes using Ref hooks
35 | * Optimizing the application performance
36 | * Learning about popular third-party hooks
37 | * Building your own hooks
38 | * Troubleshooting and debugging hooks
39 |
40 | * [Chapter 4, Handling routing and internationalisation](https://github.com/sudheerj/the-complete-react-interview-guide/tree/master/chapter4)
41 | * Navigating screens and introduction to React Router
42 | * Routes, types of routes, route and Link
43 | * Adding routes
44 | * Access URL parameters
45 | * Nesting routes
46 | * Introduce Internationalisation and localization
47 | * Adding translations and formatted messages
48 | * Passing arguments and placeholders
49 |
50 | * [Chapter 5, Advanced concepts of ReactJS](https://github.com/sudheerj/the-complete-react-interview-guide/tree/master/chapter5)
51 | * Exploring portals
52 | * Understanding error boundaries
53 | * Managing asynchronous actions with suspense API
54 | * Optimizing rendering performance using concurrent rendering
55 | * Debugging React applications with Profiler API
56 | * Strict mode
57 | * Static type checking
58 | * React in mobile environment and its features
59 |
60 | * [Chapter 6, Redux: The best state management solution](https://github.com/sudheerj/the-complete-react-interview-guide/tree/master/chapter6)
61 | * Understanding Flux pattern and Redux
62 | * Core principles of Redux, components and APIs
63 | * Redux middleware: Saga and Thunk
64 | * Debugging applications using Redux DevTools
65 |
66 | * [Chapter 7, Different approaches to apply CSS in ReactJS](https://github.com/sudheerj/the-complete-react-interview-guide/tree/master/chapter7)
67 | * Different solutions to apply CSS
68 | * Processors and CSS Modules
69 | * CSS-IN-JS approach and Styled components and it’s usage
70 | * How to use styled components in React application
71 |
72 | * [Chapter 8, Testing and debugging the React Application](https://github.com/sudheerj/the-complete-react-interview-guide/tree/master/chapter8)
73 | * Introduction of React testing helpers
74 | * Setup/teardown
75 | * Data fetching and mocking data
76 | * Events and timers
77 | * React DevTools for debugging and analysis
78 |
79 | * [Chapter 9, Rapid development with Next.js, Gatsby and Remix frameworks](https://github.com/sudheerj/the-complete-react-interview-guide/tree/master/chapter9)
80 | * React supercharged with full-stack frameworks
81 | * Static site generation
82 | * Server Side rendering
83 | * Adding page metadata
84 |
85 | * [Chapter 10, Cracking any real-world programming task ](https://github.com/sudheerj/the-complete-react-interview-guide/tree/master/chapter10)
86 | * Prepare your development environment
87 | * Choose right scaffolding tools or templates
88 | * Deciding the application architecture
89 | * Test your code
90 | * Create your git repository with REAMDE and share it
91 |
92 | * [Chapter 11, Ex #1: Build an App based on React Hooks/Redux, styled components and Firebase backend](https://github.com/sudheerj/the-complete-react-interview-guide/tree/master/chapter11)
93 | * Quick introduction to React concepts, Styled components and Firebase
94 | * Planning the application architecture
95 | * Build the business logic
96 | * Build the presentation layer
97 |
98 | * [Chapter 12, Ex #2: Build an App based on NextJS toolkit, authentication, SWR, GraphQL and deployment](https://github.com/sudheerj/the-complete-react-interview-guide/tree/master/chapter12)
99 | * Quick introduction to REST APIs
100 | * Planning the application architecture including authentication, SWR, GraphQL, and deployment
101 | * Build the business logic
102 | * Build the presentation layer
103 | * Implement testing
104 | * Deploy the application to access it for public
105 | * Create git repository with README documentation
106 | * Quiz: Test your coding skills
107 |
108 |
109 |
--------------------------------------------------------------------------------
/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 | ## Scripts
4 |
5 | - `dev`/`start` - start dev server and open browser
6 | - `build` - build for production
7 | - `preview` - locally preview production build
8 | - `test` - launch test runner
9 | - `format` - format the code based on prettier
10 | - `lint` - apply eslint for typescript files
11 |
12 |
--------------------------------------------------------------------------------
/chapter11/one-stop-electronics/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | One Stop Electronics
8 |
9 |
10 |
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, FormattedMessage } 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/utils"
11 | import { fetchProductsData } from "@/backend/firebase/api/utils"
12 | import { insertProductsData } from "@/backend/firebase/api/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 "@/App.css"
21 | import { selectCurrentLocale } from "@/app/store/user/user.selector"
22 | import { UserInfo } from "./backend/firebase/auth/utils"
23 |
24 | function App() {
25 | const dispatch = useAppDispatch()
26 | // let isDataLoaded = false;
27 |
28 | useEffect(() => {
29 | /* if(!isDataLoaded) {
30 | insertProductsData('products', PRODUCTS_DATA[0].items);
31 | }
32 | isDataLoaded = true; */
33 | const getProductsMap = async () => {
34 | const products = await fetchProductsData()
35 | dispatch(setProducts(products))
36 | }
37 |
38 | getProductsMap()
39 | const unsubscribe = onAuthStateChangedListener((user) => {
40 | if (user) {
41 | insertUserDataFromAuth(user)
42 | }
43 | const serializableUser =
44 | user && (({ displayName, email }) => ({ displayName, email }))(user)
45 | dispatch(setCurrentUser(serializableUser))
46 | })
47 |
48 | dispatch(setCurrentLocale(navigator.language))
49 | return unsubscribe
50 | }, [])
51 |
52 | const userLanguage = useAppSelector(selectCurrentLocale)
53 | return (
54 | <>
55 |
60 |
61 |
62 |
63 | } />
64 | } />
65 | } />
66 | } />
67 |
68 |
69 |
70 |
71 | >
72 | )
73 | }
74 |
75 | export default App
76 |
--------------------------------------------------------------------------------
/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 | const initialState: CartState = {
13 | cartProducts: [],
14 | }
15 |
16 | it("Should handle adding or incrementing products quantity inside cart", () => {
17 | const productToAdd: Cart = {
18 | id: 1,
19 | productImageUrl: "someurl.com",
20 | name: "Inspiron 15",
21 | price: 1200,
22 | quantity: 1,
23 | }
24 | const state = cartReducer(initialState, addProductToCart(productToAdd))
25 | expect(state.cartProducts.length).toEqual(1)
26 | })
27 |
28 | it("Should handle removing or decreasing products quantity inside cart", () => {
29 | const productToRemove: Cart = {
30 | id: 1,
31 | productImageUrl: "someurl.com",
32 | name: "Inspiron 15",
33 | price: 1200,
34 | quantity: 1,
35 | }
36 | const state = cartReducer(
37 | initialState,
38 | removeProductFromCart(productToRemove),
39 | )
40 | expect(state.cartProducts.length).toEqual(0)
41 | })
42 |
43 | it("Should handle clearing products from cart", () => {
44 | const initialState = {
45 | cartProducts: [
46 | {
47 | id: 2,
48 | productImageUrl: "someurl.com",
49 | name: "Apple Macbook",
50 | price: 1700,
51 | quantity: 2,
52 | },
53 | {
54 | id: 3,
55 | productImageUrl: "someurl.com",
56 | name: "Lenovo Thinkpad",
57 | price: 900,
58 | quantity: 2,
59 | },
60 | ],
61 | }
62 |
63 | const productToClear: Cart = {
64 | id: 2,
65 | productImageUrl: "someurl.com",
66 | name: "Apple Macbook",
67 | price: 1700,
68 | quantity: 1,
69 | }
70 |
71 | const state = cartReducer(initialState, clearProductFromCart(productToClear))
72 | expect(state.cartProducts.length).toEqual(1);
73 | })
74 |
75 | it("Should handle resetting cart products", () => {
76 | const state = cartReducer(initialState, resetCartProducts())
77 | expect(state.cartProducts.length).toEqual(0);
78 | })
79 | })
80 |
--------------------------------------------------------------------------------
/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 |
3 | import { userReducer } from "./user/user.slice"
4 | import { productsReducer } from "./product/product.slice"
5 | import { cartReducer } from "./cart/cart.slice"
6 |
7 | export const rootReducer = combineReducers({
8 | user: userReducer,
9 | products: productsReducer,
10 | cart: cartReducer,
11 | })
12 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/chapter11/one-stop-electronics/src/assets/minus.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/chapter11/one-stop-electronics/src/assets/one-stop-electronics-tab.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/chapter11/one-stop-electronics/src/assets/one-stop-electronics.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/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/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/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:
62 | "https://imgtr.ee/images/2023/08/06/8e6a61182c5f19021c33e23680a94c1d.png",
63 | price: 610,
64 | brand: "Samsung",
65 | category: "phone",
66 | },
67 | {
68 | id: 8,
69 | name: "Galaxy A54 5G",
70 | productImageUrl:
71 | "https://imgtr.ee/images/2023/08/06/f1e8315ee4eac3e11ab8e8ad86b71162.png",
72 | price: 999,
73 | brand: "Samsung",
74 | category: "phone",
75 | },
76 | {
77 | id: 9,
78 | name: "Huawei P30",
79 | productImageUrl:
80 | "https://imgtr.ee/images/2023/08/06/b59929133e78530b8463afc24a92d750.png",
81 | price: 300,
82 | brand: "Huawei",
83 | category: "phone",
84 | },
85 | {
86 | id: 10,
87 | name: "Apple14 Blue 128GB",
88 | productImageUrl:
89 | "https://imgtr.ee/images/2023/08/06/50ea5b3bf1aee95183968e9766ecadf4.png",
90 | price: 1599,
91 | brand: "Apple",
92 | category: "phone",
93 | },
94 | {
95 | id: 11,
96 | name: "Galaxy Tab S8",
97 | productImageUrl:
98 | "https://imgtr.ee/images/2023/08/06/5cf200c92dd65dbb1bec1c7efe75f2a3.png",
99 | price: 1099,
100 | brand: "Samsung",
101 | category: "tab",
102 | },
103 | {
104 | id: 12,
105 | name: "Galaxy Tab S9",
106 | productImageUrl:
107 | "https://imgtr.ee/images/2023/08/06/cfb83fde34d63e9d4595299b0674f4be.png",
108 | price: 2099,
109 | brand: "Samsung",
110 | category: "tab",
111 | },
112 | {
113 | id: 13,
114 | name: "Apple iPad 10.9",
115 | productImageUrl:
116 | "https://imgtr.ee/images/2023/08/06/8a8d8d9546eb0944376e558a116591bf.png",
117 | price: 899,
118 | brand: "Apple",
119 | category: "tab",
120 | },
121 | ],
122 | },
123 | ]
124 |
125 | export default PRODUCTS_DATA
126 |
--------------------------------------------------------------------------------
/chapter11/one-stop-electronics/src/backend/firebase/api/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 |
13 | import { User } from "firebase/auth"
14 |
15 | import { UserInfo } from "@/app/store/user/user.types.ts"
16 | import { Product } from "@/app/store/product/product.types"
17 |
18 | export const db = getFirestore()
19 |
20 | export type UserInfo = {
21 | displayName: string
22 | createdAt: Date
23 | email: string
24 | }
25 |
26 | export const insertProductsData = async (
27 | collectionKey: string,
28 | productItems: T[],
29 | ) => {
30 | const collectionRef = collection(db, collectionKey)
31 | const batch = writeBatch(db)
32 |
33 | productItems.forEach((product) => {
34 | const docRef = doc(collectionRef)
35 | batch.set(docRef, product)
36 | })
37 |
38 | await batch.commit()
39 | }
40 |
41 | export const fetchProductsData = async () => {
42 | const collectionRef = collection(db, "products")
43 | const queryRef = query(collectionRef)
44 |
45 | const querySnapshot = await getDocs(queryRef)
46 | return querySnapshot.docs.map((docSnapshot) => docSnapshot.data())
47 | }
48 |
49 | export const insertUserDataFromAuth = async (
50 | userAuth: User | null,
51 | ): Promise> => {
52 | if (!userAuth) return
53 |
54 | const userDocRef = doc(db, "users", userAuth.uid)
55 |
56 | const userSnapshot = await getDoc(userDocRef)
57 |
58 | if (!userSnapshot.exists()) {
59 | const { displayName, email } = userAuth
60 | const createdAt = new Date()
61 |
62 | try {
63 | await setDoc(userDocRef, {
64 | displayName,
65 | email,
66 | createdAt,
67 | })
68 | } catch (error) {
69 | console.log("error creating the user", error)
70 | }
71 | }
72 |
73 | return userSnapshot as QueryDocumentSnapshot
74 | }
75 |
--------------------------------------------------------------------------------
/chapter11/one-stop-electronics/src/backend/firebase/auth/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/auth/utils.ts:
--------------------------------------------------------------------------------
1 | export type UserInfo = {
2 | displayName: string
3 | createdAt: Date
4 | email: string
5 | }
6 |
--------------------------------------------------------------------------------
/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: "AIzaSyBldF8hTd2K51dbn_AGGTNkrfMcPV4VKjA",
6 | authDomain: "onestop-electronics.firebaseapp.com",
7 | projectId: "onestop-electronics",
8 | storageBucket: "onestop-electronics.appspot.com",
9 | messagingSenderId: "388971771085",
10 | appId: "1:388971771085:web:9dd330d39ce7a10d68a584",
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: 1rem;
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 |
72 |
73 |
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: 10rem;
16 | height: 8rem;
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: 1rem;
51 | line-height: 1.5rem;
52 | font-weight: 600;
53 | text-transform: capitalize;
54 | margin-bottom: 1rem;
55 | `
56 |
57 | export const Brand = styled.div`
58 | font-size: 0.75rem;
59 | line-height: 1rem;
60 | color: rgb(75 85 99);
61 | margin-bottom: 1rem;
62 | span {
63 | font-weight: 600;
64 | text-transform: capitalize;
65 | }
66 | `
67 |
68 | export const Price = styled.span`
69 | font-size: 0.75rem;
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 |
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/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(4, 1fr);
13 | column-gap: 1rem;
14 | row-gap: 1rem;
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/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 |
--------------------------------------------------------------------------------
/chapter2/images/Figure2.10_Context.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sudheerj/the-complete-react-interview-guide/73d724a10acc494100f4cc31070d966c870d820c/chapter2/images/Figure2.10_Context.png
--------------------------------------------------------------------------------
/chapter2/images/Figure2.1_JSXTranspilation.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sudheerj/the-complete-react-interview-guide/73d724a10acc494100f4cc31070d966c870d820c/chapter2/images/Figure2.1_JSXTranspilation.png
--------------------------------------------------------------------------------
/chapter2/images/Figure2.3_Components.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sudheerj/the-complete-react-interview-guide/73d724a10acc494100f4cc31070d966c870d820c/chapter2/images/Figure2.3_Components.png
--------------------------------------------------------------------------------
/chapter2/images/Figure2.4_State.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sudheerj/the-complete-react-interview-guide/73d724a10acc494100f4cc31070d966c870d820c/chapter2/images/Figure2.4_State.png
--------------------------------------------------------------------------------
/chapter2/images/Figure2.5_InitialVirtualDOM.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sudheerj/the-complete-react-interview-guide/73d724a10acc494100f4cc31070d966c870d820c/chapter2/images/Figure2.5_InitialVirtualDOM.png
--------------------------------------------------------------------------------
/chapter2/images/Figure2.6_UpdatedVirtualDOM.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sudheerj/the-complete-react-interview-guide/73d724a10acc494100f4cc31070d966c870d820c/chapter2/images/Figure2.6_UpdatedVirtualDOM.png
--------------------------------------------------------------------------------
/chapter2/images/Figure2.7_CompareVirtualDOMSnapshots.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sudheerj/the-complete-react-interview-guide/73d724a10acc494100f4cc31070d966c870d820c/chapter2/images/Figure2.7_CompareVirtualDOMSnapshots.png
--------------------------------------------------------------------------------
/chapter2/images/Figure2.8_UpdatedVirtualDOM.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sudheerj/the-complete-react-interview-guide/73d724a10acc494100f4cc31070d966c870d820c/chapter2/images/Figure2.8_UpdatedVirtualDOM.png
--------------------------------------------------------------------------------
/chapter2/images/Figure2.9_DataFlow.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sudheerj/the-complete-react-interview-guide/73d724a10acc494100f4cc31070d966c870d820c/chapter2/images/Figure2.9_DataFlow.png
--------------------------------------------------------------------------------