diff --git a/README.md b/README.md index 9893698..0dfbcb5 100644 --- a/README.md +++ b/README.md @@ -1,42 +1,49 @@ -# Frontend template +# teamMate -This is a simple react/redux/jwt template to start a project quickly +See the deployed site [here](https://modest-austin-b02609.netlify.app/) (loading times may vary due to database hosting). ## Table of contents: -- **[Setup](#setup-how-to-use-this-template)** -- **[Create React App docs](#cra-docs)** +- **[About](#about-teamMate)** +- **[Getting started](#getting-started)** +- **[Built with](#built-with)** +- **[User stories and Wireframe](#User-stories-and-Wireframe)** -## SETUP How to use this template +## About teamMate -1. Create a new project based on this template using the `Use this template` button +teamMate allows users to browse and host sport events. users can start a team, join an existing one or ride solo and attend private or public pick-up games. -![HOW_TO_USE](https://user-images.githubusercontent.com/20372832/77003323-70966180-695d-11ea-8abe-b362d57135f3.gif) +## Getting started -2. Clone the app +1. Clone the repo -``` -git clone git@github.com:YOUR_GITHUB_NAME/YOUR_PROJECT_NAME.git -``` +2. cd into the repo on your computer. -3. cd into your project +3. Install dependencies ``` -cd YOUR_PROJECT_NAME +$ npm install ``` -4. install dependencies +4. Compile and run to run the app in a browser ``` -npm install +$ npm run start ``` -5. Start development server with npm start +4. If you like to use the server to see some data go [here](https://github.com/mir4cles/teamMate-server) and repeat steps 1,2,3,4 -``` -npm start -``` +5. Let me know what you think. + +## Built With + +- [Material Ui](https://material-ui.com/) +- [React](https://reactjs.org/) +- [Redux](https://redux.js.org/) +- [Codaisseur's React Redux Template](https://github.com/Codaisseur/express-template) -## CRA docs +## User stories and Wireframe -The normal Create React App docs can be found in [CRA_DOCS.md](./CRA_DOCS.md) +- [User storires](https://github.com/users/mir4cles/projects/1#column-9641210) +- [Wireframe of design](https://wireframepro.mockflow.com/view/teamMate-client-wireframe) +- [Database diagram](https://app.lucidchart.com/invitations/accept/9dd630e3-019a-48af-91cd-c250ff20fdec) diff --git a/package-lock.json b/package-lock.json index b489884..520fd18 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1306,6 +1306,26 @@ } } }, + "@material-ui/icons": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@material-ui/icons/-/icons-4.9.1.tgz", + "integrity": "sha512-GBitL3oBWO0hzBhvA9KxqcowRUsA0qzwKkURyC8nppnC3fw54KPKZ+d4V1Eeg/UnDRSzDaI9nGCdel/eh9AQMg==", + "requires": { + "@babel/runtime": "^7.4.4" + } + }, + "@material-ui/lab": { + "version": "4.0.0-alpha.56", + "resolved": "https://registry.npmjs.org/@material-ui/lab/-/lab-4.0.0-alpha.56.tgz", + "integrity": "sha512-xPlkK+z/6y/24ka4gVJgwPfoCF4RCh8dXb1BNE7MtF9bXEBLN/lBxNTK8VAa0qm3V2oinA6xtUIdcRh0aeRtVw==", + "requires": { + "@babel/runtime": "^7.4.4", + "@material-ui/utils": "^4.10.2", + "clsx": "^1.0.4", + "prop-types": "^15.7.2", + "react-is": "^16.8.0" + } + }, "@material-ui/styles": { "version": "4.10.0", "resolved": "https://registry.npmjs.org/@material-ui/styles/-/styles-4.10.0.tgz", @@ -5761,6 +5781,11 @@ } } }, + "fontsource-roboto": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/fontsource-roboto/-/fontsource-roboto-2.1.4.tgz", + "integrity": "sha512-2NP2idRiX19pjwdNV0NvPaiDziIAFYg6IogPuQUJwVYq8BYqPDWRAGX7maQQFzvEvAUt1u9xaRGVwE7SIeCKDA==" + }, "for-in": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", @@ -6200,16 +6225,11 @@ "integrity": "sha512-l9sfDFsuqtOqKDsQdqrMRk0U85RZc0RtOR9yPI7mRVOa4FsR/BVnZ0shmQRM96Ji99kYZP/7hn1cedc1+ApsTQ==" }, "history": { - "version": "4.10.1", - "resolved": "https://registry.npmjs.org/history/-/history-4.10.1.tgz", - "integrity": "sha512-36nwAD620w12kuzPAsyINPWJqlNbij+hpK1k9XRloDtym8mxzGYl2c17LnV6IAGB2Dmg4tEa7G7DlawS0+qjew==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/history/-/history-5.0.0.tgz", + "integrity": "sha512-3NyRMKIiFSJmIPdq7FxkNMJkQ7ZEtVblOQ38VtKaA0zZMW1Eo6Q6W8oDKEflr1kNNTItSnk4JMCO1deeSgbLLg==", "requires": { - "@babel/runtime": "^7.1.2", - "loose-envify": "^1.2.0", - "resolve-pathname": "^3.0.0", - "tiny-invariant": "^1.0.2", - "tiny-warning": "^1.0.0", - "value-equal": "^1.0.1" + "@babel/runtime": "^7.7.6" } }, "hmac-drbg": { @@ -8694,13 +8714,12 @@ "integrity": "sha1-z8RcN+nsDY8KDsPdTvf3w6vjklY=" }, "mini-create-react-context": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/mini-create-react-context/-/mini-create-react-context-0.3.2.tgz", - "integrity": "sha512-2v+OeetEyliMt5VHMXsBhABoJ0/M4RCe7fatd/fBy6SMiKazUSEt3gxxypfnk2SHMkdBYvorHRoQxuGoiwbzAw==", + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/mini-create-react-context/-/mini-create-react-context-0.4.0.tgz", + "integrity": "sha512-b0TytUgFSbgFJGzJqXPKCFCBWigAjpjo+Fl7Vf7ZbKRDptszpppKxXH6DRXEABZ/gcEQczeb0iZ7JvL8e8jjCA==", "requires": { - "@babel/runtime": "^7.4.0", - "gud": "^1.0.0", - "tiny-warning": "^1.0.2" + "@babel/runtime": "^7.5.5", + "tiny-warning": "^1.0.3" } }, "mini-css-extract-plugin": { @@ -11139,15 +11158,15 @@ } }, "react-router": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/react-router/-/react-router-5.1.2.tgz", - "integrity": "sha512-yjEuMFy1ONK246B+rsa0cUam5OeAQ8pyclRDgpxuSCrAlJ1qN9uZ5IgyKC7gQg0w8OM50NXHEegPh/ks9YuR2A==", + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-5.2.0.tgz", + "integrity": "sha512-smz1DUuFHRKdcJC0jobGo8cVbhO3x50tCL4icacOlcwDOEQPq4TMqwx3sY1TP+DvtTgz4nm3thuo7A+BK2U0Dw==", "requires": { "@babel/runtime": "^7.1.2", "history": "^4.9.0", "hoist-non-react-statics": "^3.1.0", "loose-envify": "^1.3.1", - "mini-create-react-context": "^0.3.0", + "mini-create-react-context": "^0.4.0", "path-to-regexp": "^1.7.0", "prop-types": "^15.6.2", "react-is": "^16.6.0", @@ -11155,6 +11174,19 @@ "tiny-warning": "^1.0.0" }, "dependencies": { + "history": { + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/history/-/history-4.10.1.tgz", + "integrity": "sha512-36nwAD620w12kuzPAsyINPWJqlNbij+hpK1k9XRloDtym8mxzGYl2c17LnV6IAGB2Dmg4tEa7G7DlawS0+qjew==", + "requires": { + "@babel/runtime": "^7.1.2", + "loose-envify": "^1.2.0", + "resolve-pathname": "^3.0.0", + "tiny-invariant": "^1.0.2", + "tiny-warning": "^1.0.0", + "value-equal": "^1.0.1" + } + }, "isarray": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", @@ -11182,6 +11214,61 @@ "react-router": "5.1.2", "tiny-invariant": "^1.0.2", "tiny-warning": "^1.0.0" + }, + "dependencies": { + "history": { + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/history/-/history-4.10.1.tgz", + "integrity": "sha512-36nwAD620w12kuzPAsyINPWJqlNbij+hpK1k9XRloDtym8mxzGYl2c17LnV6IAGB2Dmg4tEa7G7DlawS0+qjew==", + "requires": { + "@babel/runtime": "^7.1.2", + "loose-envify": "^1.2.0", + "resolve-pathname": "^3.0.0", + "tiny-invariant": "^1.0.2", + "tiny-warning": "^1.0.0", + "value-equal": "^1.0.1" + } + }, + "isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=" + }, + "mini-create-react-context": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/mini-create-react-context/-/mini-create-react-context-0.3.2.tgz", + "integrity": "sha512-2v+OeetEyliMt5VHMXsBhABoJ0/M4RCe7fatd/fBy6SMiKazUSEt3gxxypfnk2SHMkdBYvorHRoQxuGoiwbzAw==", + "requires": { + "@babel/runtime": "^7.4.0", + "gud": "^1.0.0", + "tiny-warning": "^1.0.2" + } + }, + "path-to-regexp": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.8.0.tgz", + "integrity": "sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==", + "requires": { + "isarray": "0.0.1" + } + }, + "react-router": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-5.1.2.tgz", + "integrity": "sha512-yjEuMFy1ONK246B+rsa0cUam5OeAQ8pyclRDgpxuSCrAlJ1qN9uZ5IgyKC7gQg0w8OM50NXHEegPh/ks9YuR2A==", + "requires": { + "@babel/runtime": "^7.1.2", + "history": "^4.9.0", + "hoist-non-react-statics": "^3.1.0", + "loose-envify": "^1.3.1", + "mini-create-react-context": "^0.3.0", + "path-to-regexp": "^1.7.0", + "prop-types": "^15.6.2", + "react-is": "^16.6.0", + "tiny-invariant": "^1.0.2", + "tiny-warning": "^1.0.0" + } + } } }, "react-scripts": { diff --git a/package.json b/package.json index 97c2ae7..db61a9c 100644 --- a/package.json +++ b/package.json @@ -4,15 +4,20 @@ "private": true, "dependencies": { "@material-ui/core": "^4.10.2", + "@material-ui/icons": "^4.9.1", + "@material-ui/lab": "^4.0.0-alpha.56", "@testing-library/jest-dom": "^4.2.4", "@testing-library/react": "^9.3.2", "@testing-library/user-event": "^7.1.2", "axios": "^0.19.2", "bootstrap": "^4.4.1", + "fontsource-roboto": "^2.1.4", + "history": "^5.0.0", "react": "^16.13.0", "react-bootstrap": "^1.0.0-beta.17", "react-dom": "^16.13.0", "react-redux": "^7.2.0", + "react-router": "^5.2.0", "react-router-dom": "^5.1.2", "react-scripts": "3.4.0", "redux": "^4.0.5", diff --git a/public/index.html b/public/index.html index aa069f2..f647532 100644 --- a/public/index.html +++ b/public/index.html @@ -3,41 +3,21 @@ - + - - - React App + teamMate
- diff --git a/src/App.js b/src/App.js index 0b92703..be2f0ad 100644 --- a/src/App.js +++ b/src/App.js @@ -1,49 +1,49 @@ import React, { useEffect } from "react"; -import "./App.css"; - import { Switch, Route } from "react-router-dom"; + import Navigation from "./components/Navigation"; -import Loading from "./components/Loading"; import MessageBox from "./components/MessageBox"; +import Home from "./pages/Home"; import SignUp from "./pages/SignUp"; import Login from "./pages/Login"; +import MyProfile from "./pages/MyProfile"; +import Teams from "./pages/Teams"; +import TeamDetails from "./pages/TeamDetails"; +import Events from "./pages/Events"; +import EventDetails from "./pages/EventDetails"; +import CreateEvent from "./pages/CreateEvent"; +import Support from "./pages/Support"; +import NotFound from "./pages/NotFound"; -import { useDispatch, useSelector } from "react-redux"; -import { selectAppLoading } from "./store/appState/selectors"; +import { useDispatch } from "react-redux"; import { getUserWithStoredToken } from "./store/user/actions"; -import { Jumbotron } from "react-bootstrap"; - -const Home = () => ( - -

Home

-
-); -const Other = () => ( - -

Other

-
-); function App() { const dispatch = useDispatch(); - const isLoading = useSelector(selectAppLoading); useEffect(() => { dispatch(getUserWithStoredToken()); }, [dispatch]); return ( -
+ <> - {isLoading ? : null} - - + + + + + + + + + + -
+ ); } diff --git a/src/components/Loading/index.js b/src/components/Loading/index.js index cf3a320..e49bc25 100644 --- a/src/components/Loading/index.js +++ b/src/components/Loading/index.js @@ -1,13 +1,79 @@ import React from "react"; -import Spinner from "react-bootstrap/Spinner"; -import "./spinner.css"; + +import Skeleton from "@material-ui/lab/Skeleton"; +import { + makeStyles, + Container, + Grid, + Card, + CardHeader, + CardContent, +} from "@material-ui/core"; + +const useStyles = makeStyles((theme) => ({ + heroContent: { + padding: theme.spacing(4, 0, 6), + }, + card: { + width: 600, + margin: theme.spacing(2), + }, + media: { + height: 190, + }, +})); export default function Loading() { + const classes = useStyles(); return ( -
- - Loading... - -
+ <> + + + + + + } + title={ + + } + subheader={} + /> + + + + + + + + + + + ); } diff --git a/src/components/MessageBox/index.js b/src/components/MessageBox/index.js index 080f950..5d84613 100644 --- a/src/components/MessageBox/index.js +++ b/src/components/MessageBox/index.js @@ -1,23 +1,44 @@ import React from "react"; -import { useSelector, useDispatch } from "react-redux"; +import { useSelector } from "react-redux"; + +import Alert from "@material-ui/lab/Alert"; +import Collapse from "@material-ui/core/Collapse"; + +import IconButton from "@material-ui/core/IconButton"; +import CloseIcon from "@material-ui/icons/Close"; + import { selectMessage } from "../../store/appState/selectors"; -import { Alert } from "react-bootstrap"; -import { clearMessage } from "../../store/appState/actions"; export default function MessageBox() { + const [open, setOpen] = React.useState(true); const message = useSelector(selectMessage); - const dispatch = useDispatch(); const showMessage = message !== null; + if (!showMessage) return null; - return ( - dispatch(clearMessage()) : null} - > - {message.text} - - ); + if (message.dismissable) { + return ( + + { + setOpen(false); + }} + > + + + } + > + {message.text} + + + ); + } + + return {message.text}; } diff --git a/src/components/Navigation/LoggedIn.js b/src/components/Navigation/LoggedIn.js index 35410ea..51eaebd 100644 --- a/src/components/Navigation/LoggedIn.js +++ b/src/components/Navigation/LoggedIn.js @@ -1,17 +1,19 @@ import React from "react"; -import { useDispatch, useSelector } from "react-redux"; +import { useDispatch } from "react-redux"; + +import { Button } from "@material-ui/core"; + import { logOut } from "../../store/user/actions"; -import Button from "react-bootstrap/Button"; -import { selectUser } from "../../store/user/selectors"; -import Nav from "react-bootstrap/Nav"; export default function LoggedIn() { const dispatch = useDispatch(); - const user = useSelector(selectUser); return ( - <> - {user.email} - - + ); } diff --git a/src/components/Navigation/LoggedOut.js b/src/components/Navigation/LoggedOut.js index ca5f4d5..3df4451 100644 --- a/src/components/Navigation/LoggedOut.js +++ b/src/components/Navigation/LoggedOut.js @@ -1,10 +1,20 @@ import React from "react"; -import NavbarItem from "./NavbarItem"; +import { Link as RouterLink } from "react-router-dom"; +import { Button } from "@material-ui/core"; export default function LoggedOut() { + const ref = React.createRef(); return ( <> - + ); } diff --git a/src/components/Navigation/index.js b/src/components/Navigation/index.js index fb281b3..e74e624 100644 --- a/src/components/Navigation/index.js +++ b/src/components/Navigation/index.js @@ -1,31 +1,85 @@ import React from "react"; -import Navbar from "react-bootstrap/Navbar"; -import Nav from "react-bootstrap/Nav"; -import { NavLink } from "react-router-dom"; import { useSelector } from "react-redux"; -import { selectToken } from "../../store/user/selectors"; -import NavbarItem from "./NavbarItem"; +import { Link as RouterLink } from "react-router-dom"; + +import AppBar from "@material-ui/core/AppBar"; +import Toolbar from "@material-ui/core/Toolbar"; +import Typography from "@material-ui/core/Typography"; +import Link from "@material-ui/core/Link"; +import { makeStyles } from "@material-ui/core/styles"; +import EmojiEventsIcon from "@material-ui/icons/EmojiEvents"; +import { IconButton } from "@material-ui/core"; + import LoggedIn from "./LoggedIn"; import LoggedOut from "./LoggedOut"; +import { selectToken } from "../../store/user/selectors"; +import { selectUser } from "../../store/user/selectors"; + +const useStyles = makeStyles((theme) => ({ + root: { + flexGrow: 1, + }, + menuButton: { + marginRight: theme.spacing(2), + }, + title: { + flexGrow: 1, + }, + link: { + margin: theme.spacing(1, 1.5), + }, +})); + export default function Navigation() { const token = useSelector(selectToken); - const loginLogoutControls = token ? : ; + const classes = useStyles(); + const user = useSelector(selectUser); + const ref = React.createRef(); return ( - - - YOUR PROJECT NAME - - - - - - + + + ); } diff --git a/src/config/constants.js b/src/config/constants.js index 18da8be..fab72b9 100644 --- a/src/config/constants.js +++ b/src/config/constants.js @@ -1,2 +1,5 @@ -export const apiUrl = process.env.API_URL || "http://localhost:4000"; +// export const apiUrl = process.env.API_URL || "http://localhost:4000"; +export const apiUrl = + "https://teammate-server.herokuapp.com" || "http://localhost:4000"; export const DEFAULT_MESSAGE_TIMEOUT = 3000; +export const DEFAULT_PAGINATION_LIMIT = 10; diff --git a/src/images/teammate.jpg b/src/images/teammate.jpg new file mode 100644 index 0000000..50e4abb Binary files /dev/null and b/src/images/teammate.jpg differ diff --git a/src/index.js b/src/index.js index ad5741f..0dea343 100644 --- a/src/index.js +++ b/src/index.js @@ -1,23 +1,20 @@ import React from "react"; import ReactDOM from "react-dom"; -import "./index.css"; import App from "./App"; import * as serviceWorker from "./serviceWorker"; import store from "./store"; import { Provider } from "react-redux"; import { BrowserRouter as Router } from "react-router-dom"; -import "bootstrap/dist/css/bootstrap.min.css"; +import CssBaseline from "@material-ui/core/CssBaseline"; ReactDOM.render( + , document.getElementById("root") ); -// If you want your app to work offline and load faster, you can change -// unregister() to register() below. Note this comes with some pitfalls. -// Learn more about service workers: https://bit.ly/CRA-PWA serviceWorker.unregister(); diff --git a/src/pages/CreateEvent/index.js b/src/pages/CreateEvent/index.js new file mode 100644 index 0000000..dd664b5 --- /dev/null +++ b/src/pages/CreateEvent/index.js @@ -0,0 +1,225 @@ +import React from "react"; +import { useDispatch, useSelector } from "react-redux"; + +import { + Container, + Typography, + Grid, + TextField, + FormControlLabel, + Checkbox, + Button, +} from "@material-ui/core"; +import { makeStyles } from "@material-ui/core/styles"; + +import { selectToken } from "../../store/user/selectors"; +import { createEvent } from "../../store/user/actions"; + +const useStyles = makeStyles((theme) => ({ + heroContent: { + padding: theme.spacing(4, 0, 6), + }, + buttons: { + display: "flex", + justifyContent: "flex-end", + }, + formControl: { + margin: theme.spacing(1), + minWidth: 120, + maxWidth: 300, + }, +})); + +export default function CreateEvent() { + const classes = useStyles(); + const token = useSelector(selectToken); + + const dispatch = useDispatch(); + + const [title, setTitle] = React.useState(""); + const [startDate, setStartDate] = React.useState(""); + const [endDate, setEndDate] = React.useState(""); + const [location, setLocation] = React.useState(""); + const [sportType, setSportType] = React.useState(""); + const [description, setDescription] = React.useState(""); + const [outdoor, setOutdoor] = React.useState(false); + const [maxPlayers, setMaxPlayers] = React.useState(25); + + function submitForm(event) { + event.preventDefault(); + console.log( + "data from form:", + title, + startDate, + endDate, + location, + sportType, + description, + outdoor, + maxPlayers + ); + dispatch( + createEvent( + title, + startDate, + endDate, + location, + sportType, + description, + outdoor, + maxPlayers + ) + ); + // setTitle(""); + // setMinimumBid("0"); + // setImageUrl(""); + } + + if (token === null) { + return ( + <> + + +

Only registered users can create events.

+
+
+ + ); + } + + return ( + <> + + + Create event + + + + + + setTitle(event.target.value)} + /> + + + setStartDate(event.target.value)} + /> + + + setEndDate(event.target.value)} + /> + + + setLocation(event.target.value)} + /> + + + setSportType(event.target.value)} + /> + + + setMaxPlayers(event.target.value)} + /> + + + setDescription(event.target.value)} + /> + + + } + label="This is an outdoor event" + onChange={() => setOutdoor(!outdoor)} + checked={outdoor} + /> + + +
+ +
+
+ + ); +} diff --git a/src/pages/EventDetails/EditEventForm.js b/src/pages/EventDetails/EditEventForm.js new file mode 100644 index 0000000..e197682 --- /dev/null +++ b/src/pages/EventDetails/EditEventForm.js @@ -0,0 +1,210 @@ +import React from "react"; +import { useDispatch, useSelector } from "react-redux"; + +import { + Container, + Typography, + Grid, + TextField, + FormControlLabel, + Checkbox, + Button, +} from "@material-ui/core"; +import { makeStyles } from "@material-ui/core/styles"; + +import { selectToken } from "../../store/user/selectors"; +import { editEvent } from "../../store/user/actions"; + +const useStyles = makeStyles((theme) => ({ + heroContent: { + padding: theme.spacing(4, 0, 6), + }, + buttons: { + display: "flex", + justifyContent: "flex-end", + }, + formControl: { + margin: theme.spacing(1), + minWidth: 120, + maxWidth: 300, + }, +})); + +export default function EditEventForm(props) { + const classes = useStyles(); + const token = useSelector(selectToken); + const event = props.props; + const eventId = event.id; + + const dispatch = useDispatch(); + + const [title, setTitle] = React.useState(event.title); + const [startDate, setStartDate] = React.useState(event.startDateTime); + const [endDate, setEndDate] = React.useState(event.endDateTime); + const [location, setLocation] = React.useState(event.location); + const [sportType, setSportType] = React.useState(event.sportType); + const [description, setDescription] = React.useState(event.description); + const [outdoor, setOutdoor] = React.useState(event.outdoor); + const [maxPlayers, setMaxPlayers] = React.useState(25); + + function submitForm(event) { + event.preventDefault(); + dispatch( + editEvent( + eventId, + title, + startDate, + endDate, + location, + sportType, + description, + outdoor, + maxPlayers + ) + ); + } + + if (token === null) { + return ( + <> + + +

Only registered users can edit events.

+
+
+ + ); + } + + return ( + <> + + + {event.title} + + + + + + setTitle(event.target.value)} + /> + + + setStartDate(event.target.value)} + /> + + + setEndDate(event.target.value)} + /> + + + setLocation(event.target.value)} + /> + + + setSportType(event.target.value)} + /> + + + setMaxPlayers(event.target.value)} + /> + + + setDescription(event.target.value)} + /> + + + } + label="This is an outdoor event" + onChange={() => setOutdoor(!outdoor)} + defaultValue={event.outdoor} + checked={outdoor} + /> + + +
+ +
+
+ + ); +} diff --git a/src/pages/EventDetails/index.js b/src/pages/EventDetails/index.js new file mode 100644 index 0000000..1f27949 --- /dev/null +++ b/src/pages/EventDetails/index.js @@ -0,0 +1,233 @@ +import React, { useEffect, useState } from "react"; +import { useDispatch, useSelector } from "react-redux"; +import { useParams } from "react-router-dom"; + +import { + Container, + Typography, + Grid, + Card, + CardContent, + Button, + CardActions, + List, + ListItem, + Avatar, + ListItemAvatar, + ListItemText, + ListItemIcon, + CardMedia, +} from "@material-ui/core"; +import { makeStyles } from "@material-ui/core/styles"; +import EventAvailableIcon from "@material-ui/icons/EventAvailable"; +import EventBusyIcon from "@material-ui/icons/EventBusy"; +import EditIcon from "@material-ui/icons/Edit"; +import LocationOnIcon from "@material-ui/icons/LocationOn"; +import SettingsIcon from "@material-ui/icons/Settings"; +import PeopleIcon from "@material-ui/icons/People"; +import ScheduleIcon from "@material-ui/icons/Schedule"; +import PersonPinIcon from "@material-ui/icons/PersonPin"; + +import Loading from "../../components/Loading"; +import EditEventForm from "./EditEventForm"; +import eventHeader from "../../images/teammate.jpg"; + +import { selectEventDetails } from "../../store/eventDetails/selectors"; +import { fetchEventById } from "../../store/eventDetails/actions"; +import { attendEvent, cancelAttendEvent } from "../../store/user/actions"; +import { selectToken, selectUser } from "../../store/user/selectors"; +import { selectAppLoading } from "../../store/appState/selectors"; + +const useStyles = makeStyles((theme) => ({ + heroContent: { + padding: theme.spacing(4, 0, 6), + }, + title: { + fontSize: 14, + }, + button: { + margin: theme.spacing(1), + }, + media: { + height: 140, + }, +})); + +export default function Events() { + const classes = useStyles(); + const { id } = useParams(); + const event = useSelector(selectEventDetails); + const dispatch = useDispatch(); + const token = useSelector(selectToken); + const user = useSelector(selectUser); + const [editMode, setEditMode] = useState(false); + const loading = useSelector(selectAppLoading); + + const attendingIds = event.attending.map((user) => user.id); + const attendButton = attendingIds.includes(user.id) ? ( + + ) : ( + + ); + + useEffect(() => { + dispatch(fetchEventById(id)); + }, [dispatch, id]); + + if (loading) { + return ; + } + + if (!editMode) { + return ( + <> + + + + + + + + {event.title} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Description + + + {event.description} + + {event.attending.length ? ( + + Attending: + {event.attending.map((attendee) => { + return ( + + + + + + + ); + })} + + ) : null} + + + {token ? attendButton : null} + {event.userId === user.id ? ( + + ) : null} + + + + + + + ); + } else { + return ( + <> + + + + + + + + + + + + + ); + } +} diff --git a/src/pages/Events/index.js b/src/pages/Events/index.js new file mode 100644 index 0000000..56e6d7b --- /dev/null +++ b/src/pages/Events/index.js @@ -0,0 +1,148 @@ +import React, { useEffect } from "react"; +import { useDispatch, useSelector } from "react-redux"; +import { Link } from "react-router-dom"; + +import { + Container, + Typography, + Grid, + Card, + CardContent, + Button, + CardActions, + Fab, + CardHeader, + Avatar, + ListItem, + ListItemText, + ListItemIcon, + List, + CardMedia, +} from "@material-ui/core"; +import { makeStyles } from "@material-ui/core/styles"; +import AddIcon from "@material-ui/icons/Add"; +import PeopleIcon from "@material-ui/icons/People"; + +import Loading from "../../components/Loading"; +import eventHeader from "../../images/teammate.jpg"; + +import { selectEvents } from "../../store/events/selectors"; +import { fetchEvents } from "../../store/events/actions"; +import { selectToken } from "../../store/user/selectors"; +import { selectAppLoading } from "../../store/appState/selectors"; + +const useStyles = makeStyles((theme) => ({ + heroContent: { + padding: theme.spacing(4, 0, 6), + }, + title: { + fontSize: 14, + }, + pos: { + marginBottom: 12, + }, + fab: { + position: "fixed", + bottom: theme.spacing(2), + right: theme.spacing(2), + }, + media: { + height: 140, + }, +})); + +export default function Events() { + const classes = useStyles(); + + const dispatch = useDispatch(); + + const events = useSelector(selectEvents); + + const token = useSelector(selectToken); + const loading = useSelector(selectAppLoading); + + useEffect(() => { + dispatch(fetchEvents()); + }, [dispatch]); + + if (loading) { + return ; + } + + return ( + <> + + {token ? ( + + + + ) : null} + + {events.map((event) => ( + + + + + } + title={event.title} + subheader={event.location} + /> + + {event.startDateTime} + + + {event.description} + + {event.attending.length ? ( + + + + + + + + + ) : null} + + + + + + + ))} + + + + ); +} diff --git a/src/pages/Home/index.js b/src/pages/Home/index.js new file mode 100644 index 0000000..4591a72 --- /dev/null +++ b/src/pages/Home/index.js @@ -0,0 +1,131 @@ +import React from "react"; + +import { + Container, + Typography, + Grid, + CardHeader, + CardContent, + CardActions, + Button, + Card, +} from "@material-ui/core"; +import { makeStyles } from "@material-ui/core/styles"; +import { Link } from "react-router-dom"; + +const useStyles = makeStyles((theme) => ({ + "@global": { + ul: { + margin: 0, + padding: 0, + listStyle: "none", + }, + }, + heroContent: { + padding: theme.spacing(4, 0, 6), + }, + cardHeader: { + backgroundColor: + theme.palette.type === "light" + ? theme.palette.grey[200] + : theme.palette.grey[700], + }, +})); + +const landingCards = [ + { + title: "Play", + description: [ + "Find & attend pickup games", + "Whatever type of sport", + "Meet people in real life", + ], + buttonText: "Get started", + buttonVariant: "contained", + url: "/events", + }, + { + title: "Free", + description: ["Life-time fun", "Unlimited games", "No strings attached"], + buttonText: "Sign up for free", + buttonVariant: "outlined", + url: "/signup", + }, +]; + +export default function Home() { + const classes = useStyles(); + + return ( + <> + + + teamMate + + + Look for pickup games or host private events. +
+ Join a team or ride solo. +
+
+ + + {landingCards.map((landingCard) => ( + + + + +
    + {landingCard.description.map((line) => ( + + {line} + + ))} +
+
+ + + +
+
+ ))} +
+
+ + ); +} diff --git a/src/pages/Login/index.js b/src/pages/Login/index.js index d35d780..c3d9e1a 100644 --- a/src/pages/Login/index.js +++ b/src/pages/Login/index.js @@ -1,14 +1,44 @@ import React, { useState, useEffect } from "react"; -import Form from "react-bootstrap/Form"; -import Container from "react-bootstrap/Container"; -import Button from "react-bootstrap/Button"; +import { useSelector, useDispatch } from "react-redux"; + import { login } from "../../store/user/actions"; import { selectToken } from "../../store/user/selectors"; -import { useDispatch, useSelector } from "react-redux"; import { useHistory, Link } from "react-router-dom"; -import { Col } from "react-bootstrap"; + +import CssBaseline from "@material-ui/core/CssBaseline"; +import Avatar from "@material-ui/core/Avatar"; +import Button from "@material-ui/core/Button"; +import TextField from "@material-ui/core/TextField"; +import FormControlLabel from "@material-ui/core/FormControlLabel"; +import Checkbox from "@material-ui/core/Checkbox"; +import Grid from "@material-ui/core/Grid"; +import LockOutlinedIcon from "@material-ui/icons/LockOutlined"; +import Typography from "@material-ui/core/Typography"; +import { makeStyles } from "@material-ui/core/styles"; +import Container from "@material-ui/core/Container"; + +const useStyles = makeStyles((theme) => ({ + paper: { + marginTop: theme.spacing(8), + display: "flex", + flexDirection: "column", + alignItems: "center", + }, + avatar: { + margin: theme.spacing(1), + backgroundColor: theme.palette.secondary.main, + }, + form: { + width: "100%", // Fix IE 11 issue. + marginTop: theme.spacing(1), + }, + submit: { + margin: theme.spacing(3, 0, 2), + }, +})); export default function SignUp() { + const classes = useStyles(); const [email, setEmail] = useState(""); const [password, setPassword] = useState(""); const dispatch = useDispatch(); @@ -24,47 +54,76 @@ export default function SignUp() { function submitForm(event) { console.log("hi"); event.preventDefault(); - dispatch(login(email, password)); - setEmail(""); setPassword(""); } return ( - -
-

Login

- - Email address - setEmail(event.target.value)} - type="email" - placeholder="Enter email" + + +
+ + + + + Log in + + + setEmail(event.target.value)} /> - - - - Password - setPassword(event.target.value)} - type="password" - placeholder="Password" + setPassword(event.target.value)} + /> + } + label="Remember me" /> - - - - - - Click here to sign up - - + + + + Forgot password? + + + + + {"Don't have an account? Sign Up"} + + + + +
); } diff --git a/src/pages/MyProfile/index.js b/src/pages/MyProfile/index.js new file mode 100644 index 0000000..095f012 --- /dev/null +++ b/src/pages/MyProfile/index.js @@ -0,0 +1,26 @@ +import React from "react"; + +import { Container, Typography } from "@material-ui/core"; +import { makeStyles } from "@material-ui/core/styles"; + +const useStyles = makeStyles((theme) => ({ + heroContent: { + padding: theme.spacing(4, 0, 6), + }, +})); + +export default function MyProfile() { + const classes = useStyles(); + return ( + + + My profile + + + ); +} diff --git a/src/pages/NotFound/index.js b/src/pages/NotFound/index.js new file mode 100644 index 0000000..8173f15 --- /dev/null +++ b/src/pages/NotFound/index.js @@ -0,0 +1,26 @@ +import React from "react"; + +import { Container, Typography } from "@material-ui/core"; +import { makeStyles } from "@material-ui/core/styles"; + +const useStyles = makeStyles((theme) => ({ + heroContent: { + padding: theme.spacing(4, 0, 6), + }, +})); + +export default function NotFound() { + const classes = useStyles(); + return ( + + + 404: The page you requested was not found + + + ); +} diff --git a/src/pages/SignUp/index.js b/src/pages/SignUp/index.js index 9e7b90f..12aefff 100644 --- a/src/pages/SignUp/index.js +++ b/src/pages/SignUp/index.js @@ -1,14 +1,42 @@ import React, { useState, useEffect } from "react"; -import Form from "react-bootstrap/Form"; -import Container from "react-bootstrap/Container"; -import Button from "react-bootstrap/Button"; -import { signUp } from "../../store/user/actions"; -import { selectToken } from "../../store/user/selectors"; import { useDispatch, useSelector } from "react-redux"; import { useHistory, Link } from "react-router-dom"; -import { Col } from "react-bootstrap"; + +import Avatar from "@material-ui/core/Avatar"; +import Button from "@material-ui/core/Button"; +import CssBaseline from "@material-ui/core/CssBaseline"; +import TextField from "@material-ui/core/TextField"; +import Grid from "@material-ui/core/Grid"; +import LockOutlinedIcon from "@material-ui/icons/LockOutlined"; +import Typography from "@material-ui/core/Typography"; +import { makeStyles } from "@material-ui/core/styles"; +import Container from "@material-ui/core/Container"; + +import { signUp } from "../../store/user/actions"; +import { selectToken } from "../../store/user/selectors"; + +const useStyles = makeStyles((theme) => ({ + paper: { + marginTop: theme.spacing(8), + display: "flex", + flexDirection: "column", + alignItems: "center", + }, + avatar: { + margin: theme.spacing(1), + backgroundColor: theme.palette.secondary.main, + }, + form: { + width: "100%", // Fix IE 11 issue. + marginTop: theme.spacing(3), + }, + submit: { + margin: theme.spacing(3, 0, 2), + }, +})); export default function SignUp() { + const classes = useStyles(); const [name, setName] = useState(""); const [email, setEmail] = useState(""); const [password, setPassword] = useState(""); @@ -33,50 +61,78 @@ export default function SignUp() { } return ( - -
-

Signup

- - Name - setName(event.target.value)} - type="text" - placeholder="Enter name" - required - /> - - - Email address - setEmail(event.target.value)} - type="email" - placeholder="Enter email" - required - /> - - We'll never share your email with anyone else. - - - - - Password - setPassword(event.target.value)} - type="password" - placeholder="Password" - required - /> - - - - - Click here to log in -
+ + + + Already have an account? Sign in + + + + +
); } diff --git a/src/pages/Support/index.js b/src/pages/Support/index.js new file mode 100644 index 0000000..ed61fc1 --- /dev/null +++ b/src/pages/Support/index.js @@ -0,0 +1,28 @@ +import React from "react"; + +import { Container, Typography } from "@material-ui/core"; +import { makeStyles } from "@material-ui/core/styles"; + +const useStyles = makeStyles((theme) => ({ + heroContent: { + padding: theme.spacing(4, 0, 6), + }, +})); + +export default function Support() { + const classes = useStyles(); + return ( + <> + + + Support coming soon, mate + + + + ); +} diff --git a/src/pages/TeamDetails/index.js b/src/pages/TeamDetails/index.js new file mode 100644 index 0000000..c92a594 --- /dev/null +++ b/src/pages/TeamDetails/index.js @@ -0,0 +1,87 @@ +import React, { useEffect } from "react"; +import { useDispatch, useSelector } from "react-redux"; +import { useParams } from "react-router-dom"; + +import { + Container, + Typography, + Grid, + Card, + CardContent, + CardMedia, +} from "@material-ui/core"; +import { makeStyles } from "@material-ui/core/styles"; + +import { selectTeamDetails } from "../../store/teamDetails/selectors"; +import { fetchTeamById } from "../../store/teamDetails/actions"; + +const useStyles = makeStyles((theme) => ({ + heroContent: { + padding: theme.spacing(4, 0, 6), + }, + bullet: { + display: "inline-block", + margin: "0 2px", + transform: "scale(0.8)", + }, + title: { + fontSize: 14, + }, + pos: { + marginBottom: 12, + }, + media: { + height: 400, + }, +})); + +export default function TeamDetails() { + const classes = useStyles(); + + const { id } = useParams(); + const team = useSelector(selectTeamDetails); + const dispatch = useDispatch(); + + useEffect(() => { + dispatch(fetchTeamById(id)); + }, [dispatch, id]); + + return ( + <> + + + Team Details + + + + + + + + + + {team.teamName} + + + Hometown: {team.city} + + + {team.description} + + + + + + + + ); +} diff --git a/src/pages/Teams/index.js b/src/pages/Teams/index.js new file mode 100644 index 0000000..a00c42e --- /dev/null +++ b/src/pages/Teams/index.js @@ -0,0 +1,90 @@ +import React, { useEffect } from "react"; +import { useDispatch, useSelector } from "react-redux"; +import { Link } from "react-router-dom"; + +import { + Container, + Typography, + Grid, + Card, + CardContent, + Button, + CardActions, + CardMedia, +} from "@material-ui/core"; +import { makeStyles } from "@material-ui/core/styles"; + +import { selectTeams } from "../../store/teams/selectors"; +import { fetchTeams } from "../../store/teams/actions"; + +const useStyles = makeStyles((theme) => ({ + heroContent: { + padding: theme.spacing(4, 0, 6), + }, + bullet: { + display: "inline-block", + margin: "0 2px", + transform: "scale(0.8)", + }, + title: { + fontSize: 14, + }, + pos: { + marginBottom: 12, + }, + media: { + height: 250, + }, +})); + +export default function Teams() { + const classes = useStyles(); + + const dispatch = useDispatch(); + + const teams = useSelector(selectTeams); + + useEffect(() => { + dispatch(fetchTeams()); + }, [dispatch]); + + return ( + <> + + + {teams.map((team) => ( + + + + + + {team.teamName} + + + {team.city} + + + {team.description} + + + + + + + + ))} + + + + ); +} diff --git a/src/store/appState/actions.js b/src/store/appState/actions.js index 57fcfce..6b8b5a6 100644 --- a/src/store/appState/actions.js +++ b/src/store/appState/actions.js @@ -15,8 +15,8 @@ export const setMessage = (variant, dismissable, text) => { payload: { variant, dismissable, - text - } + text, + }, }; }; @@ -26,7 +26,7 @@ export const showMessageWithTimeout = ( text, timeOutMilliSeconds ) => { - return dispatch => { + return (dispatch) => { dispatch(setMessage(variant, dismissable, text)); const timeout = timeOutMilliSeconds || DEFAULT_MESSAGE_TIMEOUT; diff --git a/src/store/eventDetails/actions.js b/src/store/eventDetails/actions.js new file mode 100644 index 0000000..77dcaa8 --- /dev/null +++ b/src/store/eventDetails/actions.js @@ -0,0 +1,19 @@ +import axios from "axios"; +import { apiUrl } from "../../config/constants"; +import { appLoading, appDoneLoading } from "../appState/actions"; + +export const EVENT_DETAILS_FETCHED = "EVENT_DETAILS_FETCHED"; + +const eventDetailsFetched = (event) => ({ + type: EVENT_DETAILS_FETCHED, + payload: event, +}); + +export const fetchEventById = (id) => { + return async (dispatch, getState) => { + dispatch(appLoading()); + const response = await axios.get(`${apiUrl}/events/${id}`); + dispatch(eventDetailsFetched(response.data.event)); + dispatch(appDoneLoading()); + }; +}; diff --git a/src/store/eventDetails/reducer.js b/src/store/eventDetails/reducer.js new file mode 100644 index 0000000..a572bb2 --- /dev/null +++ b/src/store/eventDetails/reducer.js @@ -0,0 +1,29 @@ +import { EVENT_DETAILS_FETCHED } from "./actions"; +import { ATTEND_EVENT, CANCEL_ATTEND_EVENT } from "../user/actions"; + +const initialState = { + attending: [], + user: [], +}; + +export default (state = initialState, { type, payload }) => { + switch (type) { + case EVENT_DETAILS_FETCHED: + return { ...state, ...payload }; + + case ATTEND_EVENT: + return { + ...state, + attending: [...state.attending, payload], + }; + + case CANCEL_ATTEND_EVENT: + return { + ...state, + attending: [...state.attending.filter((rsvp) => rsvp.id !== payload)], + }; + + default: + return state; + } +}; diff --git a/src/store/eventDetails/selectors.js b/src/store/eventDetails/selectors.js new file mode 100644 index 0000000..1aee5a3 --- /dev/null +++ b/src/store/eventDetails/selectors.js @@ -0,0 +1 @@ +export const selectEventDetails = (state) => state.eventDetails; diff --git a/src/store/events/actions.js b/src/store/events/actions.js new file mode 100644 index 0000000..b177540 --- /dev/null +++ b/src/store/events/actions.js @@ -0,0 +1,22 @@ +import { apiUrl, DEFAULT_PAGINATION_LIMIT } from "../../config/constants"; +import axios from "axios"; +import { appLoading, appDoneLoading } from "../appState/actions"; + +export const FETCH_EVENTS_SUCCESS = "FETCH_EVENTS_SUCCESS"; + +export const fetchEventsSuccess = (events) => ({ + type: FETCH_EVENTS_SUCCESS, + payload: events, +}); + +export const fetchEvents = () => { + return async (dispatch, getState) => { + const eventsCount = getState().events.length; + dispatch(appLoading()); + const response = await axios.get( + `${apiUrl}/events?limit=${DEFAULT_PAGINATION_LIMIT}&offset=${eventsCount}` + ); + dispatch(fetchEventsSuccess(response.data.events.rows)); + dispatch(appDoneLoading()); + }; +}; diff --git a/src/store/events/reducer.js b/src/store/events/reducer.js new file mode 100644 index 0000000..bb45075 --- /dev/null +++ b/src/store/events/reducer.js @@ -0,0 +1,13 @@ +import { FETCH_EVENTS_SUCCESS } from "./actions"; + +const initialState = []; + +export default (state = initialState, action) => { + switch (action.type) { + case FETCH_EVENTS_SUCCESS: + return [...state, ...action.payload]; + + default: + return state; + } +}; diff --git a/src/store/events/selectors.js b/src/store/events/selectors.js new file mode 100644 index 0000000..fbc7567 --- /dev/null +++ b/src/store/events/selectors.js @@ -0,0 +1 @@ +export const selectEvents = (state) => state.events; diff --git a/src/store/rootReducer.js b/src/store/rootReducer.js index 03483da..9e7bc6b 100644 --- a/src/store/rootReducer.js +++ b/src/store/rootReducer.js @@ -1,8 +1,16 @@ import { combineReducers } from "redux"; import appState from "./appState/reducer"; import user from "./user/reducer"; +import events from "./events/reducer"; +import eventDetails from "./eventDetails/reducer"; +import teams from "./teams/reducer"; +import teamDetails from "./teamDetails/reducer"; export default combineReducers({ appState, - user + user, + events, + eventDetails, + teams, + teamDetails, }); diff --git a/src/store/teamDetails/actions.js b/src/store/teamDetails/actions.js new file mode 100644 index 0000000..54f0d0f --- /dev/null +++ b/src/store/teamDetails/actions.js @@ -0,0 +1,16 @@ +import axios from "axios"; +import { apiUrl } from "../../config/constants"; + +export const TEAM_DETAILS_FETCHED = "TEAM_DETAILS_FETCHED"; + +const teamDetailsFetched = (team) => ({ + type: TEAM_DETAILS_FETCHED, + payload: team, +}); + +export const fetchTeamById = (id) => { + return async (dispatch, getState) => { + const response = await axios.get(`${apiUrl}/teams/${id}`); + dispatch(teamDetailsFetched(response.data.team)); + }; +}; diff --git a/src/store/teamDetails/reducer.js b/src/store/teamDetails/reducer.js new file mode 100644 index 0000000..09786e2 --- /dev/null +++ b/src/store/teamDetails/reducer.js @@ -0,0 +1,13 @@ +import { TEAM_DETAILS_FETCHED } from "./actions"; + +const initialState = {}; + +export default (state = initialState, { type, payload }) => { + switch (type) { + case TEAM_DETAILS_FETCHED: + return { ...state, ...payload }; + + default: + return state; + } +}; diff --git a/src/store/teamDetails/selectors.js b/src/store/teamDetails/selectors.js new file mode 100644 index 0000000..f1009ca --- /dev/null +++ b/src/store/teamDetails/selectors.js @@ -0,0 +1 @@ +export const selectTeamDetails = (state) => state.teamDetails; diff --git a/src/store/teams/actions.js b/src/store/teams/actions.js new file mode 100644 index 0000000..f62cf66 --- /dev/null +++ b/src/store/teams/actions.js @@ -0,0 +1,23 @@ +import { apiUrl, DEFAULT_PAGINATION_LIMIT } from "../../config/constants"; +import axios from "axios"; + +export const FETCH_TEAMS_SUCCESS = "FETCH_TEAMS_SUCCESS"; + +export const fetchTeamsSuccess = (teams) => ({ + type: FETCH_TEAMS_SUCCESS, + payload: teams, +}); + +export const fetchTeams = () => { + return async (dispatch, getState) => { + const teamsCount = getState().teams.length; + // const response = await axios.get( + // `http://localhost:4000/events?limit=${DEFAULT_PAGINATION_LIMIT}` + // ); + const response = await axios.get( + `${apiUrl}/teams?limit=${DEFAULT_PAGINATION_LIMIT}&offset=${teamsCount}` + ); + // console.log(response.data); + dispatch(fetchTeamsSuccess(response.data.teams.rows)); + }; +}; diff --git a/src/store/teams/reducer.js b/src/store/teams/reducer.js new file mode 100644 index 0000000..09baba7 --- /dev/null +++ b/src/store/teams/reducer.js @@ -0,0 +1,21 @@ +import { FETCH_TEAMS_SUCCESS } from "./actions"; +// import { HEART_INCREMENT } from "../artworkDetails/actions"; + +const initialState = []; + +export default (state = initialState, action) => { + switch (action.type) { + case FETCH_TEAMS_SUCCESS: + return [...state, ...action.payload]; + + // case HEART_INCREMENT: + // return state.map((artwork) => + // artwork.id === action.payload + // ? { ...artwork, hearts: artwork.hearts + 1 } + // : artwork + // ); + + default: + return state; + } +}; diff --git a/src/store/teams/selectors.js b/src/store/teams/selectors.js new file mode 100644 index 0000000..a7f7f17 --- /dev/null +++ b/src/store/teams/selectors.js @@ -0,0 +1 @@ +export const selectTeams = (state) => state.teams; diff --git a/src/store/user/actions.js b/src/store/user/actions.js index 0dc25f0..353b659 100644 --- a/src/store/user/actions.js +++ b/src/store/user/actions.js @@ -1,27 +1,32 @@ import { apiUrl } from "../../config/constants"; + import axios from "axios"; -import { selectToken } from "./selectors"; + +import { selectToken, selectUser } from "./selectors"; import { appLoading, appDoneLoading, showMessageWithTimeout, - setMessage + setMessage, } from "../appState/actions"; export const LOGIN_SUCCESS = "LOGIN_SUCCESS"; export const TOKEN_STILL_VALID = "TOKEN_STILL_VALID"; export const LOG_OUT = "LOG_OUT"; +export const ATTEND_EVENT = "ATTEND_EVENT"; +export const UPDATE_EVENTS = "UPDATE_EVENTS"; +export const CANCEL_ATTEND_EVENT = "CANCEL_ATTEND_EVENT"; -const loginSuccess = userWithToken => { +const loginSuccess = (userWithToken) => { return { type: LOGIN_SUCCESS, - payload: userWithToken + payload: userWithToken, }; }; -const tokenStillValid = userWithoutToken => ({ +const tokenStillValid = (userWithoutToken) => ({ type: TOKEN_STILL_VALID, - payload: userWithoutToken + payload: userWithoutToken, }); export const logOut = () => ({ type: LOG_OUT }); @@ -33,19 +38,21 @@ export const signUp = (name, email, password) => { const response = await axios.post(`${apiUrl}/signup`, { name, email, - password + password, }); dispatch(loginSuccess(response.data)); - dispatch(showMessageWithTimeout("success", true, "account created")); + dispatch( + showMessageWithTimeout("success", false, "Account created. Have fun!") + ); dispatch(appDoneLoading()); } catch (error) { if (error.response) { console.log(error.response.data.message); - dispatch(setMessage("danger", true, error.response.data.message)); + dispatch(setMessage("error", false, error.response.data.message)); } else { console.log(error.message); - dispatch(setMessage("danger", true, error.message)); + dispatch(setMessage("error", false, error.message)); } dispatch(appDoneLoading()); } @@ -58,19 +65,22 @@ export const login = (email, password) => { try { const response = await axios.post(`${apiUrl}/login`, { email, - password + password, }); - dispatch(loginSuccess(response.data)); - dispatch(showMessageWithTimeout("success", false, "welcome back!", 1500)); + dispatch( + showMessageWithTimeout( + "success", + false, + `Welcome back ${response.data.name}!` + ) + ); dispatch(appDoneLoading()); } catch (error) { if (error.response) { - console.log(error.response.data.message); - dispatch(setMessage("danger", true, error.response.data.message)); + dispatch(setMessage("error", false, error.response.data.message)); } else { - console.log(error.message); - dispatch(setMessage("danger", true, error.message)); + dispatch(setMessage("error", false, error.message)); } dispatch(appDoneLoading()); } @@ -79,21 +89,14 @@ export const login = (email, password) => { export const getUserWithStoredToken = () => { return async (dispatch, getState) => { - // get token from the state const token = selectToken(getState()); - - // if we have no token, stop if (token === null) return; dispatch(appLoading()); try { - // if we do have a token, - // check wether it is still valid or if it is expired const response = await axios.get(`${apiUrl}/me`, { - headers: { Authorization: `Bearer ${token}` } + headers: { Authorization: `Bearer ${token}` }, }); - - // token is still valid dispatch(tokenStillValid(response.data)); dispatch(appDoneLoading()); } catch (error) { @@ -102,10 +105,152 @@ export const getUserWithStoredToken = () => { } else { console.log(error); } - // if we get a 4xx or 5xx response, - // get rid of the token by logging out dispatch(logOut()); dispatch(appDoneLoading()); } }; }; + +export const createEvent = ( + title, + startDate, + endDate, + location, + sportType, + description, + outdoor, + maxPlayers +) => { + return async (dispatch, getState) => { + const { id, token } = selectUser(getState()); + dispatch(appLoading()); + + const response = await axios.post( + `${apiUrl}/events`, + { + title, + startDate, + endDate, + location, + sportType, + description, + outdoor, + maxPlayers, + id, + }, + { + headers: { + Authorization: `Bearer ${token}`, + }, + } + ); + dispatch(setMessage("success", true, response.data.message)); + dispatch(appDoneLoading()); + }; +}; + +export const attendEvent = (eventId) => { + return async (dispatch, getState) => { + const { id, token } = selectUser(getState()); + dispatch(appLoading()); + + try { + const response = await axios.post( + `${apiUrl}/events/${eventId}/rsvp`, + { + id, + }, + { + headers: { + Authorization: `Bearer ${token}`, + }, + } + ); + dispatch(attendEventSuccess(response.data.user)); + dispatch(appDoneLoading()); + } catch (error) { + dispatch( + showMessageWithTimeout( + "succes", + false, + "You are already attending this event", + 3000 + ) + ); + } + }; +}; + +const attendEventSuccess = (user) => { + return { + type: ATTEND_EVENT, + payload: user, + }; +}; + +export const cancelAttendEvent = (eventId) => { + return async (dispatch, getState) => { + const { id, token } = selectUser(getState()); + dispatch(appLoading()); + try { + await axios.delete(`${apiUrl}/events/${eventId}/rsvp`, { + headers: { + Authorization: `Bearer ${token}`, + }, + }); + dispatch(cancelAttendEventSuccess(id)); + dispatch(appDoneLoading()); + } catch (error) { + dispatch( + setMessage("warning", false, "You already cancelled this event") + ); + } + }; +}; + +const cancelAttendEventSuccess = (userId) => { + return { + type: CANCEL_ATTEND_EVENT, + payload: userId, + }; +}; + +export const editEvent = ( + eventId, + title, + startDate, + endDate, + location, + sportType, + description, + outdoor, + maxPlayers +) => { + return async (dispatch, getState, history) => { + const { token } = selectUser(getState()); + dispatch(appLoading()); + + await axios.patch( + `${apiUrl}/events/${eventId}`, + { + title, + startDate, + endDate, + location, + sportType, + description, + outdoor, + maxPlayers, + }, + { + headers: { + Authorization: `Bearer ${token}`, + }, + } + ); + dispatch( + showMessageWithTimeout("success", false, "Event successfully updated") + ); + dispatch(appDoneLoading()); + }; +}; diff --git a/src/store/user/reducer.js b/src/store/user/reducer.js index 5b93e1a..b14b7c1 100644 --- a/src/store/user/reducer.js +++ b/src/store/user/reducer.js @@ -3,7 +3,7 @@ import { LOG_OUT, LOGIN_SUCCESS, TOKEN_STILL_VALID } from "./actions"; const initialState = { token: localStorage.getItem("token"), name: null, - email: null + email: null, }; export default (state = initialState, action) => { diff --git a/src/store/user/selectors.js b/src/store/user/selectors.js index 57c0253..0cff551 100644 --- a/src/store/user/selectors.js +++ b/src/store/user/selectors.js @@ -1,3 +1,3 @@ -export const selectToken = state => state.user.token; +export const selectToken = (state) => state.user.token; -export const selectUser = state => state.user; +export const selectUser = (state) => state.user; diff --git a/teamMate-gimme5.jpg b/teamMate-gimme5.jpg deleted file mode 100644 index b58c725..0000000 Binary files a/teamMate-gimme5.jpg and /dev/null differ