├── 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 |
15 | Go back home
16 |
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 |
7 |
8 |
9 |
Space Travelers Hub
10 |
11 |
12 | Rockets
13 | Dragons
14 | Missions
15 |
16 | My Profile
17 |
18 |
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 |
19 |
20 |
{name}
21 |
22 | reserved
23 | {description}
24 |
25 |
handleReservation(id, event)} type="button" className={reserved ? 'cancel-reservation-btn' : 'reserve-btn'}>{reserved ? 'Cancel Reservation' : 'Reserve Rocket'}
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 | 
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 |
20 |
21 |
22 |
{dragon.name}
23 |
24 | {dragon.reserved && (
25 |
26 | reserved
27 |
28 | )}
29 | {dragon.description}
30 |
31 |
handleReserve()}
35 | >
36 | {dragon.reserved ? 'Cancel Reservation' : 'Reserve Dragon'}
37 |
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 |
39 | {reserved ? 'Leave Mission' : 'Join Mission'}
40 |
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 | You need to enable JavaScript to run this app.
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
--------------------------------------------------------------------------------