├── src ├── images │ ├── food.jpg │ ├── hike.jpg │ └── lake.jpg ├── components │ ├── Events │ │ ├── Events.module.css │ │ ├── Events.jsx │ │ ├── EventItem.module.css │ │ └── EventItem.jsx │ ├── MainHeader │ │ ├── MainHeader.module.css │ │ └── MainHeader.jsx │ └── Cart │ │ ├── Cart.module.css │ │ └── Cart.jsx ├── main.jsx ├── index.css ├── App.jsx ├── data │ └── dummy-events.js └── contexts │ └── cart-context.jsx ├── vite.config.js ├── .gitignore ├── index.html ├── README.md ├── .eslintrc.cjs ├── package.json └── public └── vite.svg /src/images/food.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JohnMwendwa/context-excercise-1/HEAD/src/images/food.jpg -------------------------------------------------------------------------------- /src/images/hike.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JohnMwendwa/context-excercise-1/HEAD/src/images/hike.jpg -------------------------------------------------------------------------------- /src/images/lake.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JohnMwendwa/context-excercise-1/HEAD/src/images/lake.jpg -------------------------------------------------------------------------------- /src/components/Events/Events.module.css: -------------------------------------------------------------------------------- 1 | .events { 2 | list-style: none; 3 | margin: 3rem auto; 4 | width: 40rem; 5 | } 6 | 7 | -------------------------------------------------------------------------------- /vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import react from '@vitejs/plugin-react' 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | plugins: [react()], 7 | }) 8 | -------------------------------------------------------------------------------- /src/main.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom/client"; 3 | import App from "./App"; 4 | import "./index.css"; 5 | 6 | ReactDOM.createRoot(document.getElementById("root")).render( 7 | 8 | 9 | 10 | ); 11 | -------------------------------------------------------------------------------- /.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 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | * { 2 | box-sizing: border-box; 3 | } 4 | 5 | body { 6 | margin: 0; 7 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 8 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 9 | sans-serif; 10 | -webkit-font-smoothing: antialiased; 11 | -moz-osx-font-smoothing: grayscale; 12 | color: #2f2d2b; 13 | background-color: #eeeded; 14 | } 15 | -------------------------------------------------------------------------------- /src/App.jsx: -------------------------------------------------------------------------------- 1 | import { CartContextProvider } from "./contexts/cart-context"; 2 | import Events from "./components/Events/Events"; 3 | import MainHeader from "./components/MainHeader/MainHeader"; 4 | 5 | function App() { 6 | return ( 7 | 8 | 9 |
10 | 11 |
12 |
13 | ); 14 | } 15 | 16 | export default App; 17 | -------------------------------------------------------------------------------- /src/components/Events/Events.jsx: -------------------------------------------------------------------------------- 1 | import dummyEvents from "../../data/dummy-events"; 2 | import EventItem from "./EventItem"; 3 | import classes from "./Events.module.css"; 4 | 5 | function Events() { 6 | return ( 7 | 12 | ); 13 | } 14 | 15 | export default Events; 16 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vite + React 8 | 9 | 10 |
11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # React + Vite 2 | 3 | This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. 4 | 5 | Currently, two official plugins are available: 6 | 7 | - [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh 8 | - [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh 9 | -------------------------------------------------------------------------------- /src/components/MainHeader/MainHeader.module.css: -------------------------------------------------------------------------------- 1 | .header { 2 | width: 100%; 3 | height: 7rem; 4 | display: flex; 5 | justify-content: space-around; 6 | align-items: center; 7 | padding: 0 10%; 8 | border-bottom: 2px solid #e0e0e0; 9 | } 10 | 11 | .header button { 12 | cursor: pointer; 13 | font: inherit; 14 | background: #fdd4ab; 15 | border: none; 16 | color: #704815; 17 | border-radius: 4px; 18 | font-weight: bold; 19 | padding: 0.5rem 1.5rem; 20 | } -------------------------------------------------------------------------------- /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { browser: true, es2020: true }, 4 | extends: [ 5 | "eslint:recommended", 6 | "plugin:react/recommended", 7 | "plugin:react/jsx-runtime", 8 | "plugin:react-hooks/recommended", 9 | ], 10 | ignorePatterns: ["dist", ".eslintrc.cjs"], 11 | parserOptions: { ecmaVersion: "latest", sourceType: "module" }, 12 | settings: { react: { version: "18.2" } }, 13 | plugins: ["react-refresh"], 14 | rules: { 15 | "react-refresh/only-export-components": [ 16 | "warn", 17 | { allowConstantExport: true }, 18 | ], 19 | "react/prop-types": -1, 20 | }, 21 | }; 22 | -------------------------------------------------------------------------------- /src/data/dummy-events.js: -------------------------------------------------------------------------------- 1 | import lakeImage from '../images/lake.jpg'; 2 | import hikeImage from '../images/hike.jpg'; 3 | import foodImage from '../images/food.jpg'; 4 | 5 | const dummyEvents = [ 6 | { 7 | id: 'e1', 8 | title: 'Lake Festival', 9 | description: 'Join us for four days of fun and relaxed people.', 10 | image: lakeImage, 11 | price: 99 12 | }, 13 | { 14 | id: 'e2', 15 | title: 'Guided Hiking', 16 | description: 'Explore nature on one of the most beautiful hiking tracks.', 17 | image: hikeImage, 18 | price: 119 19 | }, 20 | { 21 | id: 'e3', 22 | title: 'Food & Nature', 23 | description: 'Enjoy the best of alpine cuisine.', 24 | image: foodImage, 25 | price: 219 26 | }, 27 | ]; 28 | 29 | export default dummyEvents; 30 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-context-excercise-1", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite --port 3000", 8 | "build": "vite build", 9 | "lint": "eslint . --ext js,jsx --report-unused-disable-directives --max-warnings 0", 10 | "preview": "vite preview" 11 | }, 12 | "dependencies": { 13 | "react": "^18.2.0", 14 | "react-dom": "^18.2.0" 15 | }, 16 | "devDependencies": { 17 | "@types/react": "^18.2.15", 18 | "@types/react-dom": "^18.2.7", 19 | "@vitejs/plugin-react": "^4.0.3", 20 | "eslint": "^8.45.0", 21 | "eslint-plugin-react": "^7.32.2", 22 | "eslint-plugin-react-hooks": "^4.6.0", 23 | "eslint-plugin-react-refresh": "^0.4.3", 24 | "vite": "^4.4.5" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/components/Events/EventItem.module.css: -------------------------------------------------------------------------------- 1 | .event { 2 | margin: 2rem 0; 3 | display: flex; 4 | align-items: flex-start; 5 | gap: 2rem; 6 | } 7 | 8 | .event img { 9 | object-fit: cover; 10 | border-radius: 14px; 11 | width: 7.5rem; 12 | height: 7.5rem; 13 | } 14 | 15 | .content { 16 | flex: 1; 17 | } 18 | 19 | .content h2 { 20 | margin: 0; 21 | } 22 | 23 | .price { 24 | font-weight: bold; 25 | background: #63615e; 26 | color: white; 27 | display: inline-block; 28 | padding: 0.25rem 0.5rem; 29 | border-radius: 4px; 30 | margin: 0.5rem 0; 31 | } 32 | 33 | .actions { 34 | display: flex; 35 | justify-content: flex-end; 36 | } 37 | 38 | .actions button { 39 | font: inherit; 40 | cursor: pointer; 41 | background-color: #fdd4ab; 42 | color: #2f2d2b; 43 | border: none; 44 | padding: 0.5rem 1.5rem; 45 | border-radius: 4px; 46 | } 47 | 48 | .actions button:hover { 49 | background-color: #fbbf82; 50 | } -------------------------------------------------------------------------------- /src/components/MainHeader/MainHeader.jsx: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | 3 | import Cart from "../Cart/Cart"; 4 | import classes from "./MainHeader.module.css"; 5 | import { useCartContext } from "../../contexts/cart-context"; 6 | 7 | function MainHeader() { 8 | const { cartItems } = useCartContext(); 9 | const [modalIsOpen, setModalIsOpen] = useState(); 10 | 11 | function openCartModalHandler() { 12 | setModalIsOpen(true); 13 | } 14 | 15 | function closeCartModalHandler() { 16 | setModalIsOpen(false); 17 | } 18 | 19 | const numCartItems = cartItems.length; 20 | 21 | return ( 22 | <> 23 |
24 |

StateEvents Shop

25 | 26 |
27 | {modalIsOpen && } 28 | 29 | ); 30 | } 31 | 32 | export default MainHeader; 33 | -------------------------------------------------------------------------------- /src/contexts/cart-context.jsx: -------------------------------------------------------------------------------- 1 | import { createContext, useContext, useState } from "react"; 2 | 3 | //create cart context 4 | const CartContext = createContext({ 5 | cartItems: [], 6 | addItemHandler: () => {}, 7 | removeItemHandler: () => {}, 8 | }); 9 | 10 | // function to consume the cart context object 11 | export const useCartContext = () => { 12 | return useContext(CartContext); 13 | }; 14 | 15 | // cart context provider 16 | export const CartContextProvider = ({ children }) => { 17 | const [cartItems, setCartItems] = useState([]); 18 | 19 | const addItemHandler = (item) => { 20 | setCartItems((prev) => [...prev, item]); 21 | }; 22 | 23 | const removeItemHandler = (itemId) => { 24 | setCartItems((prev) => prev.filter((item) => item.id !== itemId)); 25 | }; 26 | 27 | const cartItemsCtx = { 28 | cartItems, 29 | addItemHandler, 30 | removeItemHandler, 31 | }; 32 | return ( 33 | {children} 34 | ); 35 | }; 36 | -------------------------------------------------------------------------------- /src/components/Cart/Cart.module.css: -------------------------------------------------------------------------------- 1 | .backdrop { 2 | position: fixed; 3 | top: 0; 4 | left: 0; 5 | width: 100%; 6 | height: 100vh; 7 | background-color: rgba(0, 0, 0, 0.7); 8 | } 9 | 10 | .cart { 11 | position: fixed; 12 | top: 20vh; 13 | left: calc(50% - 20rem); 14 | width: 40rem; 15 | padding: 1.5rem; 16 | background-color: #fdf1e6; 17 | border-radius: 8px; 18 | } 19 | 20 | .cart h2 { 21 | margin: 0; 22 | } 23 | 24 | .total { 25 | font-weight: bold; 26 | font-size: 1.25rem; 27 | text-align: right; 28 | } 29 | 30 | .actions { 31 | display: flex; 32 | justify-content: flex-end; 33 | gap: 1rem; 34 | } 35 | 36 | .actions button { 37 | font: inherit; 38 | cursor: pointer; 39 | background-color: #fdd4ab; 40 | color: #2f2d2b; 41 | border: none; 42 | padding: 0.5rem 1.5rem; 43 | border-radius: 4px; 44 | } 45 | 46 | .actions button:hover { 47 | font: inherit; 48 | cursor: pointer; 49 | background-color: #f8c897; 50 | color: #2f2d2b; 51 | border: none; 52 | padding: 0.5rem 1.5rem; 53 | border-radius: 4px; 54 | } -------------------------------------------------------------------------------- /src/components/Cart/Cart.jsx: -------------------------------------------------------------------------------- 1 | import ReactDOM from "react-dom"; 2 | 3 | import classes from "./Cart.module.css"; 4 | import { useCartContext } from "../../contexts/cart-context"; 5 | 6 | function Cart({ onClose }) { 7 | const { cartItems: items } = useCartContext(); 8 | const total = items.reduce((prevVal, item) => prevVal + item.price, 0); 9 | 10 | return ReactDOM.createPortal( 11 | <> 12 |
13 | 28 | , 29 | document.getElementById("modal") 30 | ); 31 | } 32 | 33 | export default Cart; 34 | -------------------------------------------------------------------------------- /src/components/Events/EventItem.jsx: -------------------------------------------------------------------------------- 1 | import { useCartContext } from "../../contexts/cart-context"; 2 | import classes from "./EventItem.module.css"; 3 | 4 | function EventItem({ event }) { 5 | const { cartItems, addItemHandler, removeItemHandler } = useCartContext(); 6 | 7 | const isInCart = cartItems.some((item) => item.id === event.id); 8 | 9 | let buttonCaption = "Add to Cart"; 10 | let buttonAction = () => addItemHandler(event); 11 | 12 | if (isInCart) { 13 | buttonCaption = "Remove from Cart"; 14 | buttonAction = () => removeItemHandler(event.id); 15 | } 16 | 17 | return ( 18 |
  • 19 | {event.title} 20 |
    21 |

    {event.title}

    22 |

    ${event.price}

    23 |

    {event.description}

    24 |
    25 | 26 |
    27 |
    28 |
  • 29 | ); 30 | } 31 | 32 | export default EventItem; 33 | -------------------------------------------------------------------------------- /public/vite.svg: -------------------------------------------------------------------------------- 1 | --------------------------------------------------------------------------------