├── 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 | 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/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 | {`${name}`} 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 |
67 | 75 | 76 | 84 | 85 | 86 | 87 | 88 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 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 |
74 | 82 | 83 | 91 | 92 | 100 | 101 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | {" "} 116 | 117 | 118 | 119 | 120 | 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 | {`${name}`} 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 --------------------------------------------------------------------------------