├── public
├── robots.txt
├── favicon.ico
├── logo192.png
├── logo512.png
├── manifest.json
└── index.html
├── src
├── images
│ └── planet.png
├── http-common.js
├── __tests__
│ └── component-testing
│ │ ├── __snapshots__
│ │ ├── Rockets.test.js.snap
│ │ ├── Missions.test.js.snap
│ │ ├── profile.test.js.snap
│ │ └── Nav.test.js.snap
│ │ ├── Rockets.test.js
│ │ ├── profile.test.js
│ │ ├── Missions.test.js
│ │ └── Nav.test.js
├── setupTests.js
├── redux
│ ├── configureStore.js
│ ├── missions
│ │ └── missions.js
│ └── rockets
│ │ └── rockets.js
├── reportWebVitals.js
├── index.css
├── App.css
├── index.js
├── App.js
├── styles
│ ├── Nav.css
│ ├── Profile.css
│ ├── Rocket.css
│ └── Missions.css
├── pages
│ ├── Nav.js
│ ├── Rockets.js
│ ├── Missions.js
│ └── Profile.js
└── logo.svg
├── .babelrc
├── .gitignore
├── .stylelintrc.json
├── .eslintrc.json
├── package.json
├── .github
└── workflows
│ └── linters.yml
└── README.md
/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 | Disallow:
4 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Oklukeok/space-travellers-hub/HEAD/public/favicon.ico
--------------------------------------------------------------------------------
/public/logo192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Oklukeok/space-travellers-hub/HEAD/public/logo192.png
--------------------------------------------------------------------------------
/public/logo512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Oklukeok/space-travellers-hub/HEAD/public/logo512.png
--------------------------------------------------------------------------------
/src/images/planet.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Oklukeok/space-travellers-hub/HEAD/src/images/planet.png
--------------------------------------------------------------------------------
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": [
3 | "@babel/preset-react"
4 | ],
5 | "plugins": ["@babel/plugin-syntax-jsx"]
6 | }
--------------------------------------------------------------------------------
/src/http-common.js:
--------------------------------------------------------------------------------
1 | export const urlRockets = 'https://api.spacexdata.com/v3/rockets';
2 | export const urlMissions = 'https://api.spacexdata.com/v3/missions';
3 |
--------------------------------------------------------------------------------
/src/__tests__/component-testing/__snapshots__/Rockets.test.js.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`Testing if Rockets component renders correctly 1`] = `
4 |
7 | `;
8 |
--------------------------------------------------------------------------------
/src/setupTests.js:
--------------------------------------------------------------------------------
1 | // jest-dom adds custom jest matchers for asserting on DOM nodes.
2 | // allows you to do things like:
3 | // expect(element).toHaveTextContent(/react/i)
4 | // learn more: https://github.com/testing-library/jest-dom
5 | import '@testing-library/jest-dom';
6 |
--------------------------------------------------------------------------------
/src/redux/configureStore.js:
--------------------------------------------------------------------------------
1 | import { configureStore } from '@reduxjs/toolkit';
2 | import rocketsReducer from './rockets/rockets';
3 | import missionsReducer from './missions/missions';
4 |
5 | const store = configureStore({
6 | reducer: {
7 | rockets: rocketsReducer,
8 | missions: missionsReducer,
9 | },
10 | });
11 |
12 | export default store;
13 |
--------------------------------------------------------------------------------
/src/reportWebVitals.js:
--------------------------------------------------------------------------------
1 | const reportWebVitals = (onPerfEntry) => {
2 | if (onPerfEntry && onPerfEntry instanceof Function) {
3 | import('web-vitals').then(({
4 | getCLS, getFID, getFCP, getLCP, getTTFB,
5 | }) => {
6 | getCLS(onPerfEntry);
7 | getFID(onPerfEntry);
8 | getFCP(onPerfEntry);
9 | getLCP(onPerfEntry);
10 | getTTFB(onPerfEntry);
11 | });
12 | }
13 | };
14 |
15 | export default reportWebVitals;
16 |
--------------------------------------------------------------------------------
/.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 | # Local Netlify folder
26 | .netlify
27 |
--------------------------------------------------------------------------------
/src/__tests__/component-testing/Rockets.test.js:
--------------------------------------------------------------------------------
1 | import renderer from 'react-test-renderer';
2 | import { Provider } from 'react-redux';
3 | import Rockets from '../../pages/Rockets';
4 | import store from '../../redux/configureStore';
5 |
6 | test('Testing if Rockets component renders correctly', () => {
7 | const rocketsSnapshot = renderer.create(
8 |
9 |
10 | ,
11 | ).toJSON();
12 | expect(rocketsSnapshot).toMatchSnapshot();
13 | });
14 |
--------------------------------------------------------------------------------
/src/__tests__/component-testing/profile.test.js:
--------------------------------------------------------------------------------
1 | import { Provider } from 'react-redux';
2 | import renderer from 'react-test-renderer';
3 | import Profile from '../../pages/Profile';
4 | import store from '../../redux/configureStore';
5 |
6 | test('Testing if Profile component renders correctly', () => {
7 | const profileSnapshot = renderer.create(
8 |
9 |
10 | ,
11 | ).toJSON();
12 | expect(profileSnapshot).toMatchSnapshot();
13 | });
14 |
--------------------------------------------------------------------------------
/src/__tests__/component-testing/Missions.test.js:
--------------------------------------------------------------------------------
1 | import renderer from 'react-test-renderer';
2 | import { Provider } from 'react-redux';
3 | import store from '../../redux/configureStore';
4 | import Missions from '../../pages/Missions';
5 |
6 | test('Testing if Missions component renders correctly', () => {
7 | const missionsSnapshot = renderer.create(
8 |
9 |
10 | ,
11 | ).toJSON();
12 | expect(missionsSnapshot).toMatchSnapshot();
13 | });
14 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/src/__tests__/component-testing/Nav.test.js:
--------------------------------------------------------------------------------
1 | import { Provider } from 'react-redux';
2 | import { BrowserRouter } from 'react-router-dom';
3 | import renderer from 'react-test-renderer';
4 | import NavBar from '../../pages/Nav';
5 | import store from '../../redux/configureStore';
6 |
7 | test('Testing if NavBar component renders correctly', () => {
8 | const navBarSnapshot = renderer.create(
9 |
10 |
11 |
12 |
13 | ,
14 | ).toJSON();
15 | expect(navBarSnapshot).toMatchSnapshot();
16 | });
17 |
--------------------------------------------------------------------------------
/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 | },
23 | "ignorePatterns": [
24 | "dist/",
25 | "build/"
26 | ]
27 | }
28 |
--------------------------------------------------------------------------------
/src/App.css:
--------------------------------------------------------------------------------
1 | .App {
2 | text-align: center;
3 | }
4 |
5 | .App-logo {
6 | height: 40vmin;
7 | pointer-events: none;
8 | }
9 |
10 | @media (prefers-reduced-motion: no-preference) {
11 | .App-logo {
12 | animation: App-logo-spin infinite 20s linear;
13 | }
14 | }
15 |
16 | .App-header {
17 | background-color: #282c34;
18 | min-height: 100vh;
19 | display: flex;
20 | flex-direction: column;
21 | align-items: center;
22 | justify-content: center;
23 | font-size: calc(10px + 2vmin);
24 | color: white;
25 | }
26 |
27 | .App-link {
28 | color: #61dafb;
29 | }
30 |
31 | @keyframes App-logo-spin {
32 | from {
33 | transform: rotate(0deg);
34 | }
35 |
36 | to {
37 | transform: rotate(360deg);
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/src/__tests__/component-testing/__snapshots__/Missions.test.js.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`Testing if Missions component renders correctly 1`] = `
4 |
7 |
10 |
13 |
14 | Mission
15 |
16 |
17 |
20 |
21 | Description
22 |
23 |
24 |
27 |
28 | Status
29 |
30 |
31 |
34 |
35 |
36 | `;
37 |
--------------------------------------------------------------------------------
/src/__tests__/component-testing/__snapshots__/profile.test.js.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`Testing if Profile component renders correctly 1`] = `
4 |
7 |
10 |
13 | My Missions
14 |
15 |
18 |
19 |
22 |
25 | My Rockets
26 |
27 |
30 |
31 |
32 | `;
33 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom/client';
3 | import './index.css';
4 | import { BrowserRouter } from 'react-router-dom';
5 | import { Provider } from 'react-redux';
6 | import App from './App';
7 | import reportWebVitals from './reportWebVitals';
8 | import store from './redux/configureStore';
9 |
10 | const root = ReactDOM.createRoot(document.getElementById('root'));
11 | root.render(
12 |
13 |
14 |
15 |
16 |
17 |
18 | ,
19 | );
20 |
21 | // If you want to start measuring performance in your app, pass a function
22 | // to log results (for example: reportWebVitals(console.log))
23 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
24 | reportWebVitals();
25 |
--------------------------------------------------------------------------------
/src/App.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Routes, Route } from 'react-router-dom';
3 | import { useDispatch } from 'react-redux';
4 | import NavBar from './pages/Nav';
5 | import Rockets from './pages/Rockets';
6 | import Missions from './pages/Missions';
7 | import Profile from './pages/Profile';
8 |
9 | import { fetchMissions } from './redux/missions/missions';
10 | import { fetchRockets } from './redux/rockets/rockets';
11 |
12 | function App() {
13 | const dispatch = useDispatch();
14 | dispatch(fetchMissions());
15 | dispatch(fetchRockets);
16 |
17 | return (
18 |
19 |
20 |
21 |
22 |
23 | } />
24 | } />
25 | } />
26 |
27 |
28 | );
29 | }
30 |
31 | export default App;
32 |
--------------------------------------------------------------------------------
/src/styles/Nav.css:
--------------------------------------------------------------------------------
1 | header {
2 | display: flex;
3 | align-items: center;
4 | justify-content: space-between;
5 | padding: 0 120px 0 200px;
6 | }
7 |
8 | .logo-and-title {
9 | display: flex;
10 | align-items: center;
11 | }
12 |
13 | .logo {
14 | width: 20%;
15 | }
16 |
17 | .navbar {
18 | display: flex;
19 | align-items: center;
20 | list-style: none;
21 | text-decoration: none;
22 | }
23 |
24 | .link-item {
25 | padding-left: 10px;
26 | width: 76px;
27 | text-decoration: none;
28 | }
29 |
30 | .item {
31 | text-decoration: none;
32 | color: rgb(26, 125, 182);
33 | }
34 |
35 | .active-link {
36 | text-decoration: underline;
37 | color: rgb(26, 125, 182);
38 | }
39 |
40 | .vertical-bar-links {
41 | width: 2px;
42 | height: 12px;
43 | background-color: #777;
44 | }
45 |
46 | .horizontal-bar {
47 | width: 975px;
48 | height: 1px;
49 | background-color: #999;
50 | margin: 20px 120px 0 200px;
51 | }
52 |
--------------------------------------------------------------------------------
/src/styles/Profile.css:
--------------------------------------------------------------------------------
1 | .profile {
2 | display: flex;
3 | align-items: flex-start;
4 | padding: 0 120px 0 200px;
5 | }
6 |
7 | .reserved-rockets-wrapper,
8 | .reserved-missions-wrapper {
9 | display: flex;
10 | flex-direction: column;
11 | align-items: flex-start;
12 | justify-content: center;
13 | width: 40vw;
14 | margin-left: 10px;
15 | }
16 |
17 | .reserved-profile {
18 | list-style: none;
19 | border-top: 1px solid #999;
20 | border-right: 1px solid #999;
21 | border-left: 1px solid #999;
22 | width: 100%;
23 | }
24 |
25 | .reserved-item {
26 | border-bottom: 1px solid #999;
27 | padding: 7px 0 20px 10px;
28 | }
29 |
30 | .reserved-rocket-item {
31 | width: 39vw;
32 | }
33 |
34 | .reserved-profile-rocket {
35 | width: 100%;
36 | }
37 |
38 | .flag-text {
39 | padding-top: 20px;
40 | color: blue;
41 | }
42 |
43 | .no-joined-reserved {
44 | padding-top: 10px;
45 | font-size: 24px;
46 | color: #459a;
47 | text-align: center;
48 | }
49 |
--------------------------------------------------------------------------------
/src/styles/Rocket.css:
--------------------------------------------------------------------------------
1 | .rockets-wrapper {
2 | display: flex;
3 | flex-direction: column;
4 | gap: 10px;
5 | padding: 0 120px 0 200px;
6 | }
7 |
8 | .rocket-card {
9 | display: flex;
10 | align-items: flex-start;
11 | gap: 10px;
12 | }
13 |
14 | .rocket-img {
15 | width: 150px;
16 | height: 110px;
17 | display: flex;
18 | align-items: center;
19 | padding-top: 14px;
20 | }
21 |
22 | .rocket-body {
23 | display: flex;
24 | flex-direction: column;
25 | align-items: flex-start;
26 | }
27 |
28 | .rocket-title,
29 | .rocket-description,
30 | .reserve-rocket-btn {
31 | text-align: left;
32 | font-size: 12px;
33 | }
34 |
35 | .rocket-title {
36 | font-weight: 400;
37 | }
38 |
39 | .reserve-rocket-btn {
40 | background-color: rgb(4, 96, 216);
41 | color: #fff;
42 | padding: 7px;
43 | border-radius: 7px;
44 | border: none;
45 | }
46 |
47 | .reserved-text {
48 | color: #fff;
49 | background-color: rgb(24, 157, 161);
50 | margin-right: 10px;
51 | border-radius: 5px;
52 | padding: 0.5px 3px;
53 | }
54 |
--------------------------------------------------------------------------------
/src/__tests__/component-testing/__snapshots__/Nav.test.js.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`Testing if NavBar component renders correctly 1`] = `
4 | Array [
5 |
8 |
11 |
16 |
19 | Space Traveler\`s Hub
20 |
21 |
22 |
60 | ,
61 |
,
64 | ]
65 | `;
66 |
--------------------------------------------------------------------------------
/src/pages/Nav.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { NavLink } from 'react-router-dom';
3 | import planet from '../images/planet.png';
4 | import '../styles/Nav.css';
5 |
6 | const NavBar = () => (
7 | <>
8 |
9 |
10 |
11 |
12 |
Space Traveler`s Hub
13 |
14 |
15 |
16 |
17 | (
19 | isActive ? 'active-link' : 'item')}
20 | to="/"
21 | >
22 | Rockets
23 |
24 |
25 |
26 | (
29 | isActive ? 'active-link' : 'item')}
30 | >
31 | Missions
32 |
33 |
34 |
35 |
36 | (
39 | isActive ? 'active-link' : 'item')}
40 | >
41 | My Profile
42 |
43 |
44 |
45 |
46 |
47 | >
48 |
49 | );
50 | export default NavBar;
51 |
--------------------------------------------------------------------------------
/src/styles/Missions.css:
--------------------------------------------------------------------------------
1 | .missions-wrapper {
2 | margin: 20px 120px 0 200px;
3 | }
4 |
5 | .mission-card {
6 | list-style-type: none;
7 | display: flex;
8 | flex-direction: row;
9 | justify-content: center;
10 | margin: auto;
11 | }
12 |
13 | .mission-card:nth-child(1n) {
14 | background-color: white;
15 | }
16 |
17 | .mission-card:nth-child(2n) {
18 | background-color: lightgray;
19 | }
20 |
21 | .missionName {
22 | width: 10vw;
23 | display: flex;
24 | justify-content: flex-start;
25 | align-items: flex-start;
26 | border: solid 1px #444;
27 | }
28 |
29 | .missionName h3 {
30 | text-align: left;
31 | margin-left: 5px;
32 | }
33 |
34 | .missionDescription {
35 | width: 60vw;
36 | border: solid 1px #444;
37 | }
38 |
39 | .missionDescription p {
40 | margin: 5px;
41 | }
42 |
43 | .missionMember {
44 | width: 10vw;
45 | display: flex;
46 | justify-content: center;
47 | align-items: center;
48 | border: solid 1px #444;
49 | }
50 |
51 | .missionMemberNot {
52 | background-color: #666;
53 | color: white;
54 | border: none;
55 | border-radius: 5px;
56 | }
57 |
58 | .missionMemberActive {
59 | background-color: rgb(3, 201, 164);
60 | color: white;
61 | border: none;
62 | border-radius: 5px;
63 | }
64 |
65 | .missionButton {
66 | width: 10vw;
67 | display: flex;
68 | justify-content: center;
69 | align-items: center;
70 | border: solid 1px #444;
71 | }
72 |
73 | .missionButton button {
74 | background-color: rgba(255, 255, 255, 0);
75 | border-radius: 5px;
76 | height: 40px;
77 | }
78 |
79 | .leavebutton {
80 | color: red;
81 | border: red solid;
82 | }
83 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "space-travellers-hub",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "@reduxjs/toolkit": "^1.9.0",
7 | "@testing-library/jest-dom": "^5.16.5",
8 | "@testing-library/react": "^13.4.0",
9 | "@testing-library/user-event": "^13.5.0",
10 | "react": "^18.2.0",
11 | "react-dom": "^18.2.0",
12 | "react-redux": "^8.0.5",
13 | "react-router-dom": "^6.4.3",
14 | "react-scripts": "5.0.1",
15 | "react-test-renderer": "^18.2.0",
16 | "redux-logger": "^3.0.6",
17 | "redux-thunk": "^2.4.2",
18 | "web-vitals": "^2.1.4"
19 | },
20 | "scripts": {
21 | "start": "react-scripts start",
22 | "build": "react-scripts build",
23 | "test": "react-scripts test",
24 | "eject": "react-scripts eject"
25 | },
26 | "eslintConfig": {
27 | "extends": [
28 | "react-app",
29 | "react-app/jest"
30 | ]
31 | },
32 | "browserslist": {
33 | "production": [
34 | ">0.2%",
35 | "not dead",
36 | "not op_mini all"
37 | ],
38 | "development": [
39 | "last 1 chrome version",
40 | "last 1 firefox version",
41 | "last 1 safari version"
42 | ]
43 | },
44 | "devDependencies": {
45 | "@babel/core": "^7.20.2",
46 | "@babel/eslint-parser": "^7.19.1",
47 | "@babel/plugin-syntax-jsx": "^7.18.6",
48 | "@babel/preset-react": "^7.18.6",
49 | "eslint": "^7.32.0",
50 | "eslint-config-airbnb": "^18.2.1",
51 | "eslint-plugin-import": "^2.26.0",
52 | "eslint-plugin-jsx-a11y": "^6.6.1",
53 | "eslint-plugin-react": "^7.31.11",
54 | "eslint-plugin-react-hooks": "^4.6.0",
55 | "stylelint": "^13.13.1",
56 | "stylelint-config-standard": "^21.0.0",
57 | "stylelint-csstree-validator": "^1.9.0",
58 | "stylelint-scss": "^3.21.0"
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
12 |
13 |
17 |
18 |
27 | React App
28 |
29 |
30 | You need to enable JavaScript to run this app.
31 |
32 |
42 |
43 |
44 |
--------------------------------------------------------------------------------
/src/redux/missions/missions.js:
--------------------------------------------------------------------------------
1 | import { createAsyncThunk, createSlice } from '@reduxjs/toolkit';
2 |
3 | export const fetchMissions = createAsyncThunk(
4 | 'missions/fetchMissions',
5 | async () => {
6 | const response = await fetch('https://api.spacexdata.com/v3/missions');
7 | const missions = await response.json();
8 | return missions;
9 | },
10 | );
11 |
12 | const missionsSlice = createSlice({
13 | name: 'missions',
14 | initialState: {
15 | missions: [],
16 | status: 'idle',
17 | },
18 | reducers: {
19 | reserveMission: (state, action) => ({
20 | ...state,
21 | missions: state.missions.map((thisMission) => {
22 | if (thisMission.mission_id === action.payload) {
23 | return {
24 | ...thisMission,
25 | reserved: true,
26 | };
27 | }
28 | return thisMission;
29 | }),
30 | }),
31 | cancelMission: (state, action) => ({
32 | ...state,
33 | missions: state.missions.map((thisMission) => {
34 | if (thisMission.mission_id === action.payload) {
35 | return {
36 | ...thisMission,
37 | reserved: false,
38 | };
39 | }
40 |
41 | return thisMission;
42 | }),
43 | }),
44 | },
45 | extraReducers: (builder) => {
46 | builder.addCase(fetchMissions.fulfilled, (state, action) => {
47 | const value = state;
48 | value.missions = action.payload.map((mission) => ({
49 | mission_id: mission.mission_id,
50 | mission_name: mission.mission_name,
51 | description: mission.description,
52 | reserved: false,
53 | }));
54 | });
55 |
56 | builder.addCase(fetchMissions.rejected, (state) => {
57 | const failedState = state; failedState.status = 'failed';
58 | });
59 | builder.addCase(fetchMissions.pending, (_, action) => action.payload);
60 | },
61 |
62 | });
63 |
64 | export default missionsSlice.reducer;
65 | export const { reserveMission, cancelMission } = missionsSlice.actions;
66 |
--------------------------------------------------------------------------------
/src/redux/rockets/rockets.js:
--------------------------------------------------------------------------------
1 | import { createAsyncThunk, createSlice } from '@reduxjs/toolkit';
2 |
3 | export const fetchRockets = createAsyncThunk(
4 | 'rockets/fetchRockets',
5 | async () => {
6 | const response = await fetch('https://api.spacexdata.com/v3/rockets');
7 | const responseJSON = await response.json();
8 | return responseJSON;
9 | },
10 | );
11 |
12 | const rocketsSlice = createSlice({
13 | name: 'rockets',
14 | initialState: {
15 | rockets: [],
16 | status: 'Not Requested Yet',
17 | },
18 | reducers: {
19 | reserveRocket: (state, action) => ({
20 | ...state,
21 | rockets: state.rockets.map((rocket) => {
22 | if (rocket.rocket_id === action.payload) {
23 | return {
24 | ...rocket,
25 | reserved: true,
26 | };
27 | }
28 | return rocket;
29 | }),
30 | }),
31 | cancelRocket: (state, action) => ({
32 | ...state,
33 | rockets: state.rockets.map((rocket) => {
34 | if (rocket.rocket_id === action.payload) {
35 | return {
36 | ...rocket,
37 | reserved: false,
38 | };
39 | }
40 | return rocket;
41 | }),
42 | }),
43 | },
44 | extraReducers: (builder) => {
45 | builder.addCase(fetchRockets.fulfilled, (state, action) => {
46 | const rocketsState = state;
47 | rocketsState.rockets = action.payload.map((rocket) => ({
48 | rocket_id: rocket.rocket_id,
49 | rocket_name: rocket.rocket_name,
50 | flickr_images: rocket.flickr_images,
51 | description: rocket.description,
52 | reserved: false,
53 | }));
54 | });
55 |
56 | builder.addCase(fetchRockets.rejected, (state) => {
57 | const failedState = state;
58 | failedState.status = 'failed';
59 | });
60 | builder.addCase(fetchRockets.pending, (state) => {
61 | const pendingState = state;
62 | pendingState.status = 'Pending';
63 | });
64 | },
65 |
66 | });
67 |
68 | export default rocketsSlice.reducer;
69 | export const { reserveRocket, cancelRocket } = rocketsSlice.actions;
70 |
--------------------------------------------------------------------------------
/.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
46 |
--------------------------------------------------------------------------------
/src/pages/Rockets.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect } from 'react';
2 | import { useDispatch, useSelector } from 'react-redux';
3 | import { fetchRockets, reserveRocket, cancelRocket } from '../redux/rockets/rockets';
4 | import '../styles/Rocket.css';
5 |
6 | function Rockets() {
7 | const rocketsArray = useSelector((state) => state.rockets.rockets);
8 | const dispatch = useDispatch();
9 |
10 | useEffect(() => {
11 | if (rocketsArray.length === 0) {
12 | dispatch(fetchRockets());
13 | }
14 | });
15 |
16 | return (
17 |
18 |
19 | { rocketsArray.map((rocket) => (
20 |
21 |
22 |
27 |
28 |
29 | {rocket.rocket_name}
30 |
31 |
32 | {rocket.reserved ? (
33 |
34 | Reserved
35 |
36 | )
37 | : ''}
38 | {rocket.description}
39 |
40 |
41 | {rocket.reserved
42 | ? (
43 |
dispatch(cancelRocket(rocket.rocket_id))}
47 | >
48 | Cancel Reservation
49 |
50 | )
51 | : (
52 |
dispatch(reserveRocket(rocket.rocket_id))}
56 | >
57 | Reserve Rocket
58 |
59 | )}
60 |
61 |
62 |
63 |
64 | ))}
65 |
66 | );
67 | }
68 |
69 | export default Rockets;
70 |
--------------------------------------------------------------------------------
/src/pages/Missions.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect } from 'react';
2 | import { useDispatch, useSelector } from 'react-redux';
3 | import { reserveMission, fetchMissions, cancelMission } from '../redux/missions/missions';
4 | import '../styles/Missions.css';
5 |
6 | const Missions = () => {
7 | const missionsArray = useSelector((state) => state.missions.missions);
8 | const dispatch = useDispatch();
9 |
10 | useEffect(() => {
11 | if (!missionsArray.length) {
12 | dispatch(fetchMissions());
13 | }
14 | });
15 |
16 | return (
17 |
18 |
19 |
20 |
21 |
Mission
22 |
Description
23 |
Status
24 |
25 |
26 |
27 | { missionsArray.map((mission) => (
28 |
29 |
30 |
{mission.mission_name}
31 |
32 |
33 | {mission.reserved ? (Active MEMBER )
34 | : (NOT A MEMBER )}
35 |
36 |
37 | {mission.reserved ? (
38 | dispatch(cancelMission(mission.mission_id))}
42 | >
43 | Leave Mission
44 |
45 | )
46 | : (
47 | dispatch(reserveMission(mission.mission_id))}
50 | >
51 | Join Mission
52 |
53 | )}
54 |
55 |
56 | ))}
57 |
58 |
59 | );
60 | };
61 |
62 | export default Missions;
63 |
--------------------------------------------------------------------------------
/src/logo.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/pages/Profile.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect } from 'react';
2 | import { useDispatch, useSelector } from 'react-redux';
3 | import { fetchRockets } from '../redux/rockets/rockets';
4 | import { fetchMissions } from '../redux/missions/missions';
5 |
6 | import '../styles/Profile.css';
7 |
8 | const Profile = () => {
9 | const rockets = useSelector((state) => state.rockets.rockets);
10 | const missions = useSelector((state) => state.missions.missions);
11 | const dispatch = useDispatch();
12 | useEffect(() => {
13 | if (rockets.length === 0) {
14 | dispatch(fetchRockets());
15 | }
16 | if (missions.length === 0) {
17 | dispatch(fetchMissions());
18 | }
19 | });
20 |
21 | const joinedMissions = missions.filter((mission) => mission.reserved);
22 | let flagMission = false;
23 | if (joinedMissions.length !== 0) {
24 | flagMission = true;
25 | }
26 |
27 | const reservedRockets = rockets.filter((rocket) => rocket.reserved);
28 | let flagRocket = false;
29 | if (reservedRockets.length !== 0) {
30 | flagRocket = true;
31 | }
32 |
33 | return (
34 |
35 |
36 |
37 |
38 | My Missions
39 |
40 |
41 |
42 | {
43 | missions.map((mission) => (
44 | mission.reserved ? (
45 |
46 | {mission.mission_name}
47 |
48 | ) : ''
49 | ))
50 | }
51 |
52 |
53 |
54 |
55 | {flagMission ? '' : 'No Missions Selected Yet!'}
56 |
57 |
58 |
59 |
60 |
61 |
62 | My Rockets
63 |
64 |
65 | {
66 | rockets.map((rocket) => (
67 | rocket.reserved ? (
68 |
69 | {rocket.rocket_name}
70 |
71 |
72 | ) : ''
73 | ))
74 | }
75 |
76 |
77 |
78 | { flagRocket ? '' : 'No Rockets Selected Yet!' }
79 |
80 |
81 |
82 |
83 | );
84 | };
85 |
86 | export default Profile;
87 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
Space travellers hub
9 |
10 |
11 |
12 | # 📗 Table of Contents
13 |
14 | - [📖 About the Project](#about-project)
15 | - [🛠 Built With](#built-with)
16 | - [Tech Stack](#tech-stack)
17 | - [Key Features](#key-features)
18 | - [🚀 Live Demo](#live-demo)
19 | - [💻 Getting Started](#getting-started)
20 | - [Setup](#setup)
21 | - [Prerequisites](#prerequisites)
22 | - [Install](#install)
23 | - [Usage](#usage)
24 | - [Run tests](#run-tests)
25 | - [Deployment](#triangular_flag_on_post-deployment)
26 | - [👥 Authors](#authors)
27 | - [🔭 Future Features](#future-features)
28 | - [🤝 Contributing](#contributing)
29 | - [⭐️ Show your support](#support)
30 | - [🙏 Acknowledgements](#acknowledgements)
31 | - [❓ FAQ](#faq)
32 | - [📝 License](#license)
33 |
34 | # 📖 Space Travellers Hub
35 |
36 | **This is a web application developed using React library and Redux state management framework that allows users to display real data of the space traveller's data. Users can reserve rockets, cancel rocket reservation, join space traveller's mission and leave the mission. The web application also allows users to see their profile, the rockets they reserved and the missions they joined.**
37 |
38 | ## 🛠 Built With
39 |
40 | ### Tech Stack
41 |
42 |
43 |
44 | Client
45 |
51 |
52 |
53 |
54 | Server
55 |
58 |
59 |
60 |
61 | ### Key Features
62 |
63 |
64 | - **Fetch real Rocket and missions data from the Space Travellers Hub hosting API using `fetch` JS API and `createAsyncThunk` redux method**
65 | - **Reserve rocket and cancel rocket reservation**
66 | - **Join and leave space traveller's mission**
67 | - **Allow customers to show the reserved rockets and joined missions**
68 |
69 | (back to top )
70 |
71 | ## 🚀 Live Demo
72 |
73 |
74 | - [Live Demo Link](https://stunning-crumble-1bf3cb.netlify.app/Profile)
75 |
76 | (back to top )
77 |
78 |
79 | ## 💻 Getting Started
80 |
81 | To get a local copy up and running, follow these steps.
82 |
83 | ### Prerequisites
84 |
85 | In order to run this project you need:
86 | - `sudo apt-get install node.js npm`
87 | - `sudo apt-get install git`
88 | - `npx create-react-app .` inside the root directory of your repository.
89 | - `npx install @redux-toolkit`
90 |
91 |
92 | ### Setup
93 |
94 | Clone this repository to your desired folder:
95 |
96 | Use `git clone git@github.com:Oklukeok/space-travellers-hub.git` command to clone our repo in your local folder you want.
97 |
98 | ### Install
99 |
100 | Install this project with:
101 |
102 | `npm run build`
103 |
104 | ### Usage
105 |
106 | To run the project, execute the following command:
107 |
108 | `npm start`
109 |
110 | ### Run tests
111 |
112 | To run tests, run the following command:
113 |
114 | `npm run test`
115 |
116 | ### Deployment
117 |
118 | - You can deploy this project using:
119 | First install `netlify` using the command `npm install netlify-cli -g`
120 |
121 | - Run the command `netlify deploy` in your root directory.
122 |
123 | (back to top )
124 |
125 |
126 | ## 👥 Authors
127 |
128 | 👤 **Lucas Bermudez**
129 |
130 | - GitHub: [@Oklukeok](https://github.com/Oklukeok)
131 | - LinkedIn: [LinkedIn](https://linkedin.com/in/lucas-bermudez/)
132 |
133 | 👤 **Amare Kassa**
134 |
135 | - GitHub: [@githubhandle](https://github.com/amare1990)
136 | - Twitter: [@twitterhandle](https://twitter.com/@amaremek)
137 | - LinkedIn: [LinkedIn](https://linkedin.com/in/amare-kassa-90)
138 |
139 | (back to top )
140 |
141 |
142 | ## 🔭 Future Features
143 |
144 |
145 | - [ ] **Saving the rserved states of the rocket in the database**
146 | - [ ] **Saving the joined states of missions in the database**
147 |
148 | (back to top )
149 |
150 |
151 |
152 | ## 🤝 Contributing
153 |
154 | Contributions, issues, and feature requests are welcome!
155 |
156 | Feel free to check the [issues page](https://github.com/amare1990/space-travellers-hub/issues).
157 |
158 | (back to top )
159 |
160 |
161 | ## ⭐️ Show your support
162 |
163 |
164 | If you like this project, feel free to modify it add new features.
165 |
166 | (back to top )
167 |
168 |
169 | ## 🙏 Acknowledgments
170 |
171 |
172 | I would like to thank my coding partner for his flexible approach to work.
173 |
174 | (back to top )
175 |
176 |
177 | ## ❓ FAQ
178 |
179 |
180 | - **Which way of fetching data is recommended?**
181 |
182 | - I recommend to use `createAsyncThunk` redux method to handle non-unit functions.
183 |
184 | - **Is there a way of posting reserved states of rocket and joined states of missions**
185 |
186 | - No. You need to have permission from the space traveller's hub to post data to the hosting API.
187 |
188 | (back to top )
189 |
190 |
191 | ## 📝 License
192 |
193 | This project is [MIT](https://github.com/amare1990/space-travellers-hub/.LICENSE) licensed.
194 |
195 |
196 |
197 | (back to top )
198 |
--------------------------------------------------------------------------------