├── src ├── images │ ├── 3212580.png │ └── image1.svg ├── Pages │ ├── RocketsPage.jsx │ ├── MissionsPage.jsx │ ├── Dragons │ │ └── DragonsPage.jsx │ ├── ErrorPage │ │ ├── NotFound.jsx │ │ └── style.css │ └── Profile.jsx ├── components │ ├── Dragons │ │ ├── Profiles.jsx │ │ ├── DragonsList.jsx │ │ ├── Dragon.jsx │ │ └── style.css │ ├── Missions │ │ ├── MissionHeader.jsx │ │ ├── Missions.jsx │ │ ├── Mission.jsx │ │ └── Missions.css │ ├── Rockets.jsx │ ├── Navbar.jsx │ └── Rocket.jsx ├── Redux │ ├── configureStore.jsx │ ├── Missions │ │ └── missions.jsx │ ├── Rocket │ │ └── RocketsRedux.jsx │ └── Dragons │ │ └── dragonsSlice.jsx ├── index.js ├── index.css ├── App.js └── App.css ├── .babelrc ├── .gitignore ├── public ├── manifest.json └── index.html ├── .stylelintrc.json ├── .eslintrc.json ├── MIT.md ├── README.md ├── package.json └── .github └── workflows └── linters.yml /src/images/3212580.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pratap-panabaka/react-group-project/HEAD/src/images/3212580.png -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "@babel/preset-react" 4 | ], 5 | "plugins": ["@babel/plugin-syntax-jsx"] 6 | } -------------------------------------------------------------------------------- /src/Pages/RocketsPage.jsx: -------------------------------------------------------------------------------- 1 | import Rockets from '../components/Rockets'; 2 | 3 | const RocketsPage = () => ( 4 | 5 | ); 6 | 7 | export default RocketsPage; 8 | -------------------------------------------------------------------------------- /src/Pages/MissionsPage.jsx: -------------------------------------------------------------------------------- 1 | import Missions from '../components/Missions/Missions'; 2 | 3 | const MissionsPage = () => ( 4 | 5 | ); 6 | 7 | export default MissionsPage; 8 | -------------------------------------------------------------------------------- /src/components/Dragons/Profiles.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import './style.css'; 3 | 4 | const SpaceProfile = ({ className, name }) => ( 5 |
  • 6 | {name} 7 |
  • 8 | ); 9 | 10 | export default SpaceProfile; 11 | -------------------------------------------------------------------------------- /src/Pages/Dragons/DragonsPage.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import DragonsList from '../../components/Dragons/DragonsList'; 3 | 4 | const DragonsPage = () => ( 5 |
    6 | 7 |
    8 | ); 9 | 10 | export default DragonsPage; 11 | -------------------------------------------------------------------------------- /src/Redux/configureStore.jsx: -------------------------------------------------------------------------------- 1 | import { configureStore } from '@reduxjs/toolkit'; 2 | import dragonsReducer from './Dragons/dragonsSlice'; 3 | import { rocketReducer } from './Rocket/RocketsRedux'; 4 | import MissionsDataReducer from './Missions/missions'; 5 | 6 | const store = configureStore({ 7 | reducer: { 8 | rocketReducer, 9 | dragons: dragonsReducer, 10 | MissionsDataReducer, 11 | }, 12 | }); 13 | 14 | export default store; 15 | -------------------------------------------------------------------------------- /src/components/Missions/MissionHeader.jsx: -------------------------------------------------------------------------------- 1 | import './Missions.css'; 2 | 3 | const MissionHeader = () => ( 4 |
    5 |
    6 | Mission 7 |
    8 |
    9 | Description 10 |
    11 |
    12 | Status 13 |
    14 |
    15 |
    16 | ); 17 | 18 | export default MissionHeader; 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | 25 | # Reviewer Optional Comment - COMPLETED 26 | /.vscode 27 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom/client'; 3 | import { BrowserRouter } from 'react-router-dom'; 4 | import { Provider } from 'react-redux'; 5 | import './index.css'; 6 | import store from './Redux/configureStore'; 7 | import App from './App'; 8 | 9 | const root = ReactDOM.createRoot(document.getElementById('root')); 10 | root.render( 11 | 12 | 13 | 14 | 15 | , 16 | ); 17 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: 4 | -apple-system, 5 | BlinkMacSystemFont, 6 | "Segoe UI", 7 | "Roboto", 8 | "Oxygen", 9 | "Ubuntu", 10 | "Cantarell", 11 | "Fira Sans", 12 | "Droid Sans", 13 | "Helvetica Neue", 14 | sans-serif; 15 | -webkit-font-smoothing: antialiased; 16 | -moz-osx-font-smoothing: grayscale; 17 | } 18 | 19 | code { 20 | font-family: 21 | source-code-pro, 22 | Menlo, 23 | Monaco, 24 | Consolas, 25 | "Courier New", 26 | monospace; 27 | } 28 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /.stylelintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["stylelint-config-standard"], 3 | "plugins": ["stylelint-scss", "stylelint-csstree-validator"], 4 | "rules": { 5 | "at-rule-no-unknown": [ 6 | true, 7 | { 8 | "ignoreAtRules": ["tailwind", "apply", "variants", "responsive", "screen"] 9 | } 10 | ], 11 | "scss/at-rule-no-unknown": [ 12 | true, 13 | { 14 | "ignoreAtRules": ["tailwind", "apply", "variants", "responsive", "screen"] 15 | } 16 | ], 17 | "csstree/validator": true 18 | }, 19 | "ignoreFiles": ["build/**", "dist/**", "**/reset*.css", "**/bootstrap*.css", "**/*.js", "**/*.jsx"] 20 | } 21 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es6": true, 5 | "jest": true 6 | }, 7 | "parser": "@babel/eslint-parser", 8 | "parserOptions": { 9 | "ecmaFeatures": { 10 | "jsx": true 11 | }, 12 | "ecmaVersion": 2018, 13 | "sourceType": "module" 14 | }, 15 | "extends": ["airbnb", "plugin:react/recommended", "plugin:react-hooks/recommended"], 16 | "plugins": ["react"], 17 | "rules": { 18 | "react/jsx-filename-extension": ["warn", { "extensions": [".js", ".jsx"] }], 19 | "react/react-in-jsx-scope": "off", 20 | "import/no-unresolved": "off", 21 | "no-shadow": "off", 22 | "camelcase": "off", 23 | "react/prop-types": "off", 24 | "no-param-reassign": 0 25 | }, 26 | "ignorePatterns": [ 27 | "dist/", 28 | "build/" 29 | ] 30 | } 31 | -------------------------------------------------------------------------------- /src/App.js: -------------------------------------------------------------------------------- 1 | import './App.css'; 2 | import { Route, Routes } from 'react-router-dom'; 3 | import Navbar from './components/Navbar'; 4 | import MissionsPage from './Pages/MissionsPage'; 5 | import Profile from './Pages/Profile'; 6 | import DragonsPage from './Pages/Dragons/DragonsPage'; 7 | import NotFound from './Pages/ErrorPage/NotFound'; 8 | import Rockets from './components/Rockets'; 9 | 10 | function App() { 11 | return ( 12 | <> 13 | 14 | 15 | } /> 16 | } /> 17 | } /> 18 | } /> 19 | } /> 20 | 21 | 22 | ); 23 | } 24 | 25 | export default App; 26 | -------------------------------------------------------------------------------- /src/Pages/ErrorPage/NotFound.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Link } from 'react-router-dom'; 3 | import './style.css'; 4 | import { ReactComponent as Lost } from '../../images/image1.svg'; 5 | 6 | const NotFound = () => ( 7 |
    8 |
    9 |
    10 |

    11 | Sorry, this page isn't available 12 |

    13 | 14 | 17 | 18 |
    19 |
    20 | 21 |
    22 |
    23 |
    24 | ); 25 | 26 | export default NotFound; 27 | -------------------------------------------------------------------------------- /src/components/Rockets.jsx: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react'; 2 | import { useDispatch, useSelector } from 'react-redux'; 3 | import fetchRockets from '../Redux/Rocket/RocketsRedux'; 4 | import Rocket from './Rocket'; 5 | 6 | const Rockets = () => { 7 | const rockets = useSelector((state) => state.rocketReducer); 8 | const dispatch = useDispatch(); 9 | useEffect(() => { 10 | if (rockets.length === 0) { 11 | dispatch(fetchRockets()); 12 | } 13 | }, [dispatch, rockets.length]); 14 | return ( 15 |
    16 | {rockets.map((rocket) => ( 17 | 25 | ))} 26 |
    27 | ); 28 | }; 29 | 30 | export default Rockets; 31 | -------------------------------------------------------------------------------- /src/components/Navbar.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Link, useMatch, useResolvedPath } from 'react-router-dom'; 3 | import planetLogo from '../images/3212580.png'; 4 | 5 | const Navbar = () => ( 6 | 19 | ); 20 | 21 | const CustomLink = ({ to, children }) => { 22 | const isActive = useMatch({ path: useResolvedPath(to).pathname, end: true }); 23 | return ( 24 |
  • 25 | {children} 26 |
  • 27 | ); 28 | }; 29 | 30 | export default Navbar; 31 | -------------------------------------------------------------------------------- /src/components/Missions/Missions.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from 'react'; 2 | import { useSelector, useDispatch } from 'react-redux'; 3 | import { getMissionsFromAPIAction } from '../../Redux/Missions/missions'; 4 | import Mission from './Mission'; 5 | import MissionHeader from './MissionHeader'; 6 | 7 | const Missions = () => { 8 | const missionsData = useSelector((state) => state.MissionsDataReducer); 9 | const dispatch = useDispatch(); 10 | useEffect(() => { 11 | if (missionsData.length === 0) { 12 | dispatch(getMissionsFromAPIAction()); 13 | } 14 | }, [dispatch, missionsData.length]); 15 | 16 | return ( 17 | <> 18 | 19 |
    20 | {missionsData.map((mission) => ( 21 | 28 | ))} 29 |
    30 | 31 | ); 32 | }; 33 | export default Missions; 34 | -------------------------------------------------------------------------------- /MIT.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Abel, Sendy, PRATAP 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/components/Dragons/DragonsList.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from 'react'; 2 | import { useSelector, useDispatch } from 'react-redux'; 3 | import { 4 | getDragons, 5 | getStatus, 6 | getError, 7 | fetchDragons, 8 | } from '../../Redux/Dragons/dragonsSlice'; 9 | import Dragon from './Dragon'; 10 | 11 | const DragonsList = () => { 12 | const dispatch = useDispatch(); 13 | const dragons = useSelector(getDragons); 14 | const status = useSelector(getStatus); 15 | const error = useSelector(getError); 16 | 17 | useEffect(() => { 18 | if (status === 'idle') { 19 | dispatch(fetchDragons()); 20 | } 21 | }, [status, dispatch]); 22 | 23 | let content; 24 | 25 | if (status === 'Loading') { 26 | content =

    ",loading...",

    ; 27 | } else if (status === 'succeeded') { 28 | content = ( 29 |
      30 | {dragons.map((dragon) => ( 31 | 32 | ))} 33 |
    34 | ); 35 | } else if (status === 'failed') { 36 | content =

    {error}

    ; 37 | } 38 | 39 | return
    {content}
    ; 40 | }; 41 | export default DragonsList; 42 | -------------------------------------------------------------------------------- /src/components/Rocket.jsx: -------------------------------------------------------------------------------- 1 | import { useDispatch } from 'react-redux'; 2 | import { bookRocket, cancelReservation } from '../Redux/Rocket/RocketsRedux'; 3 | 4 | const Rocket = (props) => { 5 | const dispatch = useDispatch(); 6 | const { 7 | id, name, description, img, reserved, 8 | } = props; 9 | const handleReservation = (id, e) => { 10 | if (e.target.textContent === 'Reserve Rocket') { 11 | dispatch(bookRocket(id)); 12 | } else { 13 | dispatch(cancelReservation(id)); 14 | } 15 | }; 16 | return ( 17 |
    18 | Rocket 19 |
    20 |

    {name}

    21 |

    22 | reserved 23 | {description} 24 |

    25 | 26 |
    27 |
    28 | ); 29 | }; 30 | 31 | export default Rocket; 32 | -------------------------------------------------------------------------------- /src/Redux/Missions/missions.jsx: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | 3 | const GET_MISSIONS_DATA = 'GET_MISSIONS_DATA'; 4 | const JOIN_MISSION = 'JOIN_MISSION'; 5 | const LEAVE_MISSION = 'LEAVE_MISSION'; 6 | const API = 'https://api.spacexdata.com/v3/missions'; 7 | 8 | const initialMissionsData = []; 9 | 10 | const MissionsDataReducer = (state = initialMissionsData, action) => { 11 | switch (action.type) { 12 | case GET_MISSIONS_DATA: 13 | return [...action.payload]; 14 | case JOIN_MISSION: 15 | return [ 16 | ...state.map((mission) => { 17 | if (mission.mission_id !== action.id) return mission; 18 | return { ...mission, reserved: true }; 19 | }), 20 | ]; 21 | case LEAVE_MISSION: 22 | return [ 23 | ...state.map((mission) => { 24 | if (mission.mission_id !== action.id) return mission; 25 | return { ...mission, reserved: false }; 26 | }), 27 | ]; 28 | default: 29 | return state; 30 | } 31 | }; 32 | 33 | export const getMissionsFromAPIAction = () => (dispatch) => { 34 | axios.get(API).then((response) => { 35 | const missions = response.data; 36 | dispatch({ type: GET_MISSIONS_DATA, payload: missions }); 37 | }); 38 | }; 39 | 40 | export const joinMissionAction = (id) => (dispatch) => { 41 | dispatch({ type: JOIN_MISSION, id }); 42 | }; 43 | 44 | export const leaveMissionAction = (id) => (dispatch) => { 45 | dispatch({ type: LEAVE_MISSION, id }); 46 | }; 47 | 48 | export default MissionsDataReducer; 49 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![](https://img.shields.io/badge/Microverse-blueviolet) 2 | 3 | # Space Travelers' Hub 4 | 5 | The Space Travelers' Hub consists of Rockets, Missions, and the My Profile section. 6 | In this task, worked with the real live data from the SpaceX API. We built a web application for a company that provides commercial and scientific space travel services. The application will allow users to book rockets and join selected space missions. 7 | 8 | 9 | ## Built With 10 | 11 | - React & Redux 12 | 13 | ## Getting Started 14 | 15 | To get a local copy up and running follow these simple example steps. 16 | 17 | ```` 18 | git clone https://github.com/PRATAP-KUMAR/react-group-project 19 | cd react-group-project 20 | npm start 21 | ```` 22 | 23 | ## Authors 24 | 25 | **Abel Gebeyehu** 26 | GitHub: [@AbelG101](https://github.com/AbelG101) 27 | LinkedIn: Abel Gebeyehu 28 | 29 | **Sendy** 30 | Github: [@uisendy](https://github.com/uisendy) 31 | Twitter: @sinieke 32 | LinkedIn: LinkedIn 33 | 34 | **PRATAP PANABAKA** 35 | 36 | - GitHub: [@PRATAP-KUMAR](https://github.com/PRATAP-KUMAR) 37 | - Linkedin: [@LinkedIn](https://www.linkedin.com/in/pratap-kumar-panabaka) 38 | 39 | 40 | 41 | 42 | ## 🤝 Contributing 43 | 44 | Contributions, issues, and feature requests are welcome! 45 | 46 | Feel free to check the [issues page](../../issues/). 47 | 48 | ## Show your support 49 | 50 | Give a ⭐️ if you like this project! 51 | 52 | ## Acknowledgments 53 | 54 | - Microverse Curriculum 55 | 56 | ## 📝 License 57 | 58 | This project is [MIT](./MIT.md) licensed. 59 | -------------------------------------------------------------------------------- /src/components/Dragons/Dragon.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useDispatch } from 'react-redux'; 3 | import { dragonsReserved } from '../../Redux/Dragons/dragonsSlice'; 4 | import './style.css'; 5 | 6 | const Dragon = ({ dragon }) => { 7 | const dispatch = useDispatch(); 8 | 9 | const handleReserve = () => { 10 | dispatch( 11 | dragonsReserved({ reserveId: dragon.id, reserved: dragon.reserved }), 12 | ); 13 | }; 14 | return ( 15 | <> 16 |
  • 17 |
    18 |
    19 | dragon 20 |
    21 |
    22 |

    {dragon.name}

    23 |

    24 | {dragon.reserved && ( 25 | 26 | reserved 27 | 28 | )} 29 | {dragon.description} 30 |

    31 | 38 |
    39 |
    40 |
  • 41 | 42 | ); 43 | }; 44 | 45 | export default Dragon; 46 | -------------------------------------------------------------------------------- /src/Redux/Rocket/RocketsRedux.jsx: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | 3 | const API_URL = 'https://api.spacexdata.com/v3/rockets'; 4 | const GET_ROCKET = 'GET_ROCKET'; 5 | const RESERVE_ROCKET = 'RESERVE_ROCKET'; 6 | const CANCEL_RESERVATION = 'CANCEL_RESERVATION'; 7 | 8 | const fetchRockets = () => (dispatch) => { 9 | axios.get(API_URL).then((response) => { 10 | const rockets = response.data; 11 | dispatch({ type: GET_ROCKET, rockets }); 12 | }); 13 | }; 14 | 15 | const bookRocket = (id) => (dispatch) => { 16 | dispatch({ type: RESERVE_ROCKET, id }); 17 | }; 18 | 19 | const cancelReservation = (id) => (dispatch) => { 20 | dispatch({ type: CANCEL_RESERVATION, id }); 21 | }; 22 | 23 | const initialState = []; 24 | 25 | const rocketReducer = (state = initialState, action) => { 26 | switch (action.type) { 27 | case GET_ROCKET: 28 | return [ 29 | ...action.rockets, 30 | ]; 31 | case RESERVE_ROCKET: 32 | return [ 33 | ...state.map((rocket) => { 34 | if (rocket.rocket_id !== action.id) { 35 | return rocket; 36 | } 37 | return { ...rocket, reserved: true }; 38 | }), 39 | ]; 40 | case CANCEL_RESERVATION: 41 | return [ 42 | ...state.map((rocket) => { 43 | if (rocket.rocket_id !== action.id) { 44 | return rocket; 45 | } 46 | return { ...rocket, reserved: false }; 47 | }), 48 | ]; 49 | default: return state; 50 | } 51 | }; 52 | 53 | export default fetchRockets; 54 | export { rocketReducer, bookRocket, cancelReservation }; 55 | -------------------------------------------------------------------------------- /src/components/Missions/Mission.jsx: -------------------------------------------------------------------------------- 1 | import propTypes from 'prop-types'; 2 | import { useDispatch } from 'react-redux'; 3 | import { joinMissionAction, leaveMissionAction } from '../../Redux/Missions/missions'; 4 | import './Missions.css'; 5 | 6 | const Mission = (props) => { 7 | const { 8 | missionId, missionName, description, reserved, 9 | } = props; 10 | const dispatch = useDispatch(); 11 | 12 | const handleButtonClickAction = (e) => { 13 | e.preventDefault(); 14 | const { id } = e.target; 15 | if (e.target.textContent === 'Join Mission') dispatch(joinMissionAction(id)); 16 | if (e.target.textContent === 'Leave Mission') dispatch(leaveMissionAction(id)); 17 | }; 18 | 19 | return ( 20 | <> 21 |
    22 |
    23 | {missionName} 24 |
    25 |
    26 | {description} 27 |
    28 |
    29 | 30 | {reserved ? 'Active Member' : 'Not a Member'} 31 | 32 |
    33 |
    34 | 41 |
    42 |
    43 | 44 | ); 45 | }; 46 | 47 | Mission.propTypes = { 48 | missionId: propTypes.string.isRequired, 49 | }.isRequired; 50 | 51 | export default Mission; 52 | -------------------------------------------------------------------------------- /src/components/Missions/Missions.css: -------------------------------------------------------------------------------- 1 | .missions-data-header { 2 | display: flex; 3 | text-align: left; 4 | font-size: 1.25rem; 5 | font-weight: bold; 6 | background-color: #fafafa; 7 | margin: 0 2rem; 8 | } 9 | 10 | .missions-data { 11 | display: flex; 12 | text-align: left; 13 | font-size: 0.85rem; 14 | background-color: #fafafa; 15 | margin: 0 2rem; 16 | } 17 | 18 | .col-1 { 19 | flex: 0.15; 20 | border: 2px solid #f1f1f1; 21 | font-size: 1.2rem; 22 | font-weight: bold; 23 | padding: 0.4rem; 24 | } 25 | 26 | .col-2 { 27 | flex: 0.65; 28 | border: 2px solid #f1f1f1; 29 | text-align: left; 30 | padding: 0.8rem; 31 | line-height: 1.5; 32 | } 33 | 34 | .col-3, 35 | .col-4 { 36 | display: flex; 37 | justify-content: center; 38 | align-items: center; 39 | } 40 | 41 | .col-3 { 42 | flex: 0.1; 43 | border: 2px solid #f1f1f1; 44 | text-align: center; 45 | } 46 | 47 | .reserved span { 48 | background: #036565; 49 | padding: 0.2rem; 50 | border-radius: 0.5rem; 51 | color: #fff; 52 | font-size: 0.8rem; 53 | } 54 | 55 | .unreserved span { 56 | background: #5b5957; 57 | padding: 0.2rem; 58 | border-radius: 0.5rem; 59 | color: #fff; 60 | font-size: 0.8rem; 61 | } 62 | 63 | .col-4 { 64 | flex: 0.1; 65 | border: 2px solid #f1f1f1; 66 | text-align: center; 67 | } 68 | 69 | .join-mission > button { 70 | padding: 0.3rem; 71 | border-radius: 0.25rem; 72 | color: red; 73 | border: 2px solid red; 74 | font-size: 0.8rem; 75 | background-color: transparent; 76 | } 77 | 78 | .leave-mission > button { 79 | padding: 0.4rem; 80 | border-radius: 0.25rem; 81 | font-size: 0.8rem; 82 | background-color: transparent; 83 | border: 2px solid hsl(0, 0%, 40%); 84 | } 85 | 86 | .missions-data:nth-child(odd) { 87 | background-color: #f2f2ed; 88 | } 89 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-group-project", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@reduxjs/toolkit": "^1.8.5", 7 | "@testing-library/jest-dom": "^5.16.5", 8 | "@testing-library/react": "^13.4.0", 9 | "@testing-library/user-event": "^13.5.0", 10 | "axios": "^0.27.2", 11 | "prop-types": "^15.8.1", 12 | "react": "^18.2.0", 13 | "react-dom": "^18.2.0", 14 | "react-redux": "^8.0.2", 15 | "react-router-dom": "^6.4.0", 16 | "react-scripts": "5.0.1", 17 | "redux": "^4.2.0", 18 | "redux-thunk": "^2.4.1", 19 | "uuid": "^9.0.0", 20 | "web-vitals": "^2.1.4" 21 | }, 22 | "scripts": { 23 | "start": "react-scripts start", 24 | "build": "react-scripts build", 25 | "test": "react-scripts test", 26 | "eject": "react-scripts eject" 27 | }, 28 | "eslintConfig": { 29 | "extends": [ 30 | "react-app", 31 | "react-app/jest" 32 | ] 33 | }, 34 | "browserslist": { 35 | "production": [ 36 | ">0.2%", 37 | "not dead", 38 | "not op_mini all" 39 | ], 40 | "development": [ 41 | "last 1 chrome version", 42 | "last 1 firefox version", 43 | "last 1 safari version" 44 | ] 45 | }, 46 | "devDependencies": { 47 | "@babel/core": "^7.19.3", 48 | "@babel/eslint-parser": "^7.19.1", 49 | "@babel/plugin-syntax-jsx": "^7.18.6", 50 | "@babel/preset-env": "^7.19.1", 51 | "@babel/preset-react": "^7.18.6", 52 | "eslint": "^7.32.0", 53 | "eslint-config-airbnb": "^18.2.1", 54 | "eslint-plugin-import": "^2.26.0", 55 | "eslint-plugin-jsx-a11y": "^6.6.1", 56 | "eslint-plugin-react": "^7.31.8", 57 | "eslint-plugin-react-hooks": "^4.6.0", 58 | "stylelint": "^13.13.1", 59 | "stylelint-config-standard": "^21.0.0", 60 | "stylelint-csstree-validator": "^1.9.0", 61 | "stylelint-scss": "^3.21.0" 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/Pages/Profile.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useSelector } from 'react-redux'; 3 | import { getDragons } from '../Redux/Dragons/dragonsSlice'; 4 | import SpaceProfile from '../components/Dragons/Profiles'; 5 | 6 | const Profile = () => { 7 | const dragons = useSelector(getDragons); 8 | const rockets = useSelector((state) => state.rocketReducer); 9 | const missions = useSelector((state) => state.MissionsDataReducer); 10 | return ( 11 |
    12 |
    13 |

    My Rockets

    14 |
      15 | {rockets.filter((item) => item.reserved).map((item) => ( 16 | 17 | ))} 18 |
    19 |
    20 |
    21 |

    My Dragons

    22 |
      23 | {dragons.filter((item) => item.reserved).map((item) => ( 24 | 25 | ))} 26 |
    27 |
    28 |
    29 |

    My Missions

    30 |
      31 | {missions.filter((item) => item.reserved).map((item) => ( 32 | 33 | ))} 34 |
    35 |
    36 |
    37 | ); 38 | }; 39 | 40 | export default Profile; 41 | -------------------------------------------------------------------------------- /src/Pages/ErrorPage/style.css: -------------------------------------------------------------------------------- 1 | .error__page__container { 2 | overflow: hidden; 3 | position: relative; 4 | padding-inline: 6.5%; 5 | height: calc(100vh - 100px); 6 | display: flex; 7 | flex-direction: column; 8 | justify-content: center; 9 | } 10 | 11 | .error__page__wrapper { 12 | display: flex; 13 | flex-direction: column; 14 | justify-content: center; 15 | align-items: center; 16 | gap: 20px; 17 | } 18 | 19 | .error__page__message__container { 20 | text-align: center; 21 | width: 100%; 22 | } 23 | 24 | .error__page__message { 25 | margin-top: 3rem; 26 | color: #374151; 27 | font-size: 2.5rem; 28 | line-height: 1; 29 | font-weight: 300; 30 | text-align: center; 31 | } 32 | 33 | .back__home__btn { 34 | padding: 0.5rem; 35 | margin-top: 5rem; 36 | background-color: #00a396; 37 | transition-property: background-color, border-color, color, fill, stroke, opacity, box-shadow, transform; 38 | transition-duration: 200ms; 39 | transition-timing-function: cubic-bezier(0.4, 0, 1, 1); 40 | color: #fff; 41 | font-size: 1.125rem; 42 | font-weight: 300; 43 | border-width: 2px; 44 | border-color: #374151; 45 | } 46 | 47 | .back__home__btn:hover { 48 | background-color: #f66f1c; 49 | } 50 | 51 | .error__page__illustration { 52 | display: block; 53 | width: 100%; 54 | } 55 | 56 | @media (min-width: 768px) { 57 | .error__page__container { 58 | padding-top: 0; 59 | } 60 | 61 | .error__page__message__container { 62 | margin-bottom: 2rem; 63 | } 64 | 65 | .error__page__message { 66 | margin-top: 0; 67 | } 68 | 69 | .error__page__illustration { 70 | margin-top: 0; 71 | } 72 | } 73 | 74 | @media (min-width: 1024px) { 75 | .error__page__wrapper { 76 | flex-direction: row; 77 | } 78 | 79 | .error__page__message__container { 80 | text-align: left; 81 | } 82 | 83 | .error__page__message { 84 | font-size: 6rem; 85 | line-height: 1; 86 | text-align: left; 87 | } 88 | 89 | .error__page__illustration { 90 | max-width: 42rem; 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | 10 | 11 | 15 | 16 | 20 | 21 | 30 | Space Travelers Hub 31 | 32 | 33 | 34 |
    35 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /src/Redux/Dragons/dragonsSlice.jsx: -------------------------------------------------------------------------------- 1 | import { createSlice, createAsyncThunk } from '@reduxjs/toolkit'; 2 | import axios from 'axios'; 3 | 4 | const initialState = { 5 | dragons: [], 6 | status: 'idle', 7 | error: null, 8 | }; 9 | 10 | const URL = 'https://api.spacexdata.com/v3/dragons'; 11 | 12 | export const fetchDragons = createAsyncThunk( 13 | 'dragons/fetchDragons', 14 | async () => { 15 | try { 16 | const response = await axios.get(URL); 17 | return response.data; 18 | } catch (err) { 19 | throw new Error(err); 20 | } 21 | }, 22 | ); 23 | 24 | const dragonsSlice = createSlice({ 25 | name: 'dragons', 26 | initialState, 27 | reducers: { 28 | dragonsReserved: { 29 | reducer(state, action) { 30 | state.dragons = state.dragons.map((dragon) => ( 31 | dragon.id === action.payload.reserveId 32 | ? { ...dragon, reserved: !action.payload.reserved } : dragon 33 | )); 34 | }, 35 | }, 36 | }, 37 | extraReducers(builder) { 38 | builder 39 | .addCase(fetchDragons.pending, (state) => { 40 | state.status = 'loading'; 41 | }) 42 | .addCase(fetchDragons.fulfilled, (state, action) => { 43 | state.status = 'succeeded'; 44 | const loadedData = action.payload.map((payload) => { 45 | const { 46 | id, name, flickr_images, description, 47 | } = payload; 48 | return { 49 | id, name, flickr_images, description, reserved: false, 50 | }; 51 | }); 52 | state.dragons = [...loadedData]; 53 | }) 54 | .addCase(fetchDragons.rejected, (state, action) => { 55 | state.status = 'failed'; 56 | state.error = action.error.message; 57 | }); 58 | }, 59 | }); 60 | 61 | export const getDragons = (state) => state.dragons.dragons; 62 | export const getStatus = (state) => state.dragons.status; 63 | export const getError = (state) => state.dragons.error; 64 | 65 | export const { dragonsReserved } = dragonsSlice.actions; 66 | 67 | export default dragonsSlice.reducer; 68 | -------------------------------------------------------------------------------- /.github/workflows/linters.yml: -------------------------------------------------------------------------------- 1 | name: Linters 2 | 3 | on: pull_request 4 | 5 | env: 6 | FORCE_COLOR: 1 7 | 8 | jobs: 9 | eslint: 10 | name: ESLint 11 | runs-on: ubuntu-22.04 12 | steps: 13 | - uses: actions/checkout@v2 14 | - uses: actions/setup-node@v1 15 | with: 16 | node-version: "12.x" 17 | - name: Setup ESLint 18 | run: | 19 | npm install --save-dev eslint@7.x eslint-config-airbnb@18.x eslint-plugin-import@2.x eslint-plugin-jsx-a11y@6.x eslint-plugin-react@7.x eslint-plugin-react-hooks@4.x @babel/eslint-parser@7.x @babel/core@7.x @babel/plugin-syntax-jsx@7.x @babel/preset-env@7.x @babel/preset-react@7.x 20 | [ -f .eslintrc.json ] || wget https://raw.githubusercontent.com/microverseinc/linters-config/master/react-redux/.eslintrc.json 21 | [ -f .babelrc ] || wget https://raw.githubusercontent.com/microverseinc/linters-config/master/react-redux/.babelrc 22 | - name: ESLint Report 23 | run: npx eslint . 24 | stylelint: 25 | name: Stylelint 26 | runs-on: ubuntu-22.04 27 | steps: 28 | - uses: actions/checkout@v2 29 | - uses: actions/setup-node@v1 30 | with: 31 | node-version: "12.x" 32 | - name: Setup Stylelint 33 | run: | 34 | npm install --save-dev stylelint@13.x stylelint-scss@3.x stylelint-config-standard@21.x stylelint-csstree-validator@1.x 35 | [ -f .stylelintrc.json ] || wget https://raw.githubusercontent.com/microverseinc/linters-config/master/react-redux/.stylelintrc.json 36 | - name: Stylelint Report 37 | run: npx stylelint "**/*.{css,scss}" 38 | nodechecker: 39 | name: node_modules checker 40 | runs-on: ubuntu-22.04 41 | steps: 42 | - uses: actions/checkout@v2 43 | - name: Check node_modules existence 44 | run: | 45 | if [ -d "node_modules/" ]; then echo -e "\e[1;31mThe node_modules/ folder was pushed to the repo. Please remove it from the GitHub repository and try again."; echo -e "\e[1;32mYou can set up a .gitignore file with this folder included on it to prevent this from happening in the future." && exit 1; fi -------------------------------------------------------------------------------- /src/components/Dragons/style.css: -------------------------------------------------------------------------------- 1 | .dragon__container { 2 | margin-bottom: 2rem; 3 | } 4 | 5 | .dragon__wrapper { 6 | width: 87%; 7 | margin: 0 auto; 8 | border-bottom: rgba(128, 128, 128, 0.342) solid 1px; 9 | padding-block: 3rem; 10 | } 11 | 12 | .dragon__image__container { 13 | width: 100%; 14 | aspect-ratio: 1/1; 15 | } 16 | 17 | .dragon__image__container img { 18 | width: 100%; 19 | height: 100%; 20 | } 21 | 22 | .dragon_description_container { 23 | display: block; 24 | padding-top: 2rem; 25 | } 26 | 27 | .dragon__name { 28 | font-size: 1.5rem; 29 | font-weight: 600; 30 | padding-bottom: 0.5rem; 31 | } 32 | 33 | .dragon__description { 34 | font-size: 0.85rem; 35 | padding-bottom: 0.5rem; 36 | } 37 | 38 | .dragon__reserve__btn { 39 | padding: 12px 16px; 40 | background-color: #00b3a4; 41 | color: #fff; 42 | border-radius: 5px; 43 | border: none; 44 | margin: 10px 0; 45 | cursor: pointer; 46 | } 47 | 48 | .dragon__reserve__btn.is-active { 49 | border: 1px solid #6d757d; 50 | color: #6d757d; 51 | background-color: transparent; 52 | } 53 | 54 | .show__reserve__tag { 55 | font-size: 0.7rem; 56 | display: inline; 57 | padding: 2px 7px; 58 | border-radius: 7px; 59 | background-color: #ffd54f; 60 | color: #000; 61 | margin-right: 10px; 62 | } 63 | 64 | @media (min-width: 768px) { 65 | .dragon__wrapper { 66 | display: grid; 67 | grid-template-columns: 1fr 5fr; 68 | gap: 2rem; 69 | border: none; 70 | padding-block: 0; 71 | } 72 | 73 | .dragon__image__container { 74 | min-width: 368px; 75 | height: 368px; 76 | aspect-ratio: 1/1; 77 | } 78 | 79 | .dragon_description_container { 80 | padding-top: 0; 81 | } 82 | } 83 | 84 | /* ------- Dragon Profile Section ---------- */ 85 | 86 | .space__profile__container { 87 | width: 300px; 88 | margin: 0 auto; 89 | } 90 | 91 | .space__profile__heading { 92 | margin-bottom: 1rem; 93 | } 94 | 95 | .space__profile__list__wrapper { 96 | border: 1px solid #d3d3d3; 97 | overflow: hidden; 98 | } 99 | 100 | .space__profile__list { 101 | padding: 1rem 0.7rem; 102 | } 103 | 104 | .space__profile__list:not(:last-child) { 105 | border-bottom: 1px solid #d3d3d3; 106 | } 107 | -------------------------------------------------------------------------------- /src/App.css: -------------------------------------------------------------------------------- 1 | * { 2 | margin: 0; 3 | padding: 0; 4 | box-sizing: border-box; 5 | font-family: 'Poppins', sans-serif; 6 | } 7 | 8 | html { 9 | font-size: calc(14px + 0.390625vw); 10 | } 11 | 12 | body { 13 | background-color: #f5f6fa; 14 | } 15 | 16 | ul { 17 | list-style-type: none; 18 | } 19 | 20 | a { 21 | text-decoration: none; 22 | } 23 | 24 | .nav-container { 25 | width: 100%; 26 | display: flex; 27 | justify-content: space-between; 28 | align-items: center; 29 | box-shadow: 0 2px 2px -2px rgba(0, 0, 0, 0.2); 30 | padding: 20px 10px; 31 | margin-bottom: 20px; 32 | } 33 | 34 | .logo-container { 35 | display: flex; 36 | align-items: center; 37 | } 38 | 39 | .logo-img { 40 | width: 60px; 41 | height: 60px; 42 | margin-right: 25px; 43 | } 44 | 45 | .nav-links { 46 | display: flex; 47 | } 48 | 49 | .nav-links li { 50 | margin-right: 20px; 51 | } 52 | 53 | .nav-links li a { 54 | color: hsl(175, 100%, 32%); 55 | } 56 | 57 | .nav-links .vertical-divider { 58 | border: 1px solid #666; 59 | width: 1px; 60 | } 61 | 62 | .nav-links li .active-link { 63 | text-decoration: underline; 64 | color: #f66f1c; 65 | } 66 | 67 | .rockets-container { 68 | padding: 0 80px; 69 | } 70 | 71 | .rocket { 72 | display: flex; 73 | margin-bottom: 30px; 74 | 75 | /* border: 1px solid #000; */ 76 | } 77 | 78 | .rocket-img { 79 | min-width: 368px; 80 | height: 268px; 81 | margin-right: 35px; 82 | } 83 | 84 | .rocket-info { 85 | line-height: 1.5; 86 | padding: 10px 0; 87 | font-size: 0.85rem; 88 | } 89 | 90 | .reserve-btn { 91 | padding: 12px 16px; 92 | background-color: #00b3a4; 93 | color: #fff; 94 | border-radius: 5px; 95 | border: none; 96 | margin: 10px 0; 97 | cursor: pointer; 98 | } 99 | 100 | .cancel-reservation-btn { 101 | padding: 12px 16px; 102 | border: 1px solid #6d757d; 103 | color: #6d757d; 104 | background-color: transparent; 105 | border-radius: 5px; 106 | margin: 10px 0; 107 | cursor: pointer; 108 | } 109 | 110 | .reserve-btn:active { 111 | background-color: #00c7b6; 112 | } 113 | 114 | .reserved-badge { 115 | display: none; 116 | } 117 | 118 | .reserved-badge.active { 119 | font-size: 0.7rem; 120 | display: inline; 121 | padding: 2px 7px; 122 | border-radius: 7px; 123 | background-color: #ffd54f; 124 | color: #000; 125 | margin-right: 10px; 126 | } 127 | 128 | /* ------- Profile Page ---------------- */ 129 | 130 | .profile__container { 131 | display: grid; 132 | grid-template-columns: 1fr 1fr 1fr; 133 | column-gap: 2rem; 134 | row-gap: 4rem; 135 | } 136 | -------------------------------------------------------------------------------- /src/images/image1.svg: -------------------------------------------------------------------------------- 1 | warning --------------------------------------------------------------------------------