├── .eslintrc
├── .firebaserc
├── public
├── favicon.ico
├── assets
│ ├── logo.png
│ ├── user.png
│ └── categoryImages
│ │ ├── drinks.jpg
│ │ ├── film.jpg
│ │ ├── food.jpg
│ │ ├── music.jpg
│ │ ├── travel.jpg
│ │ └── culture.jpg
├── manifest.json
└── index.html
├── src
├── features
│ ├── modals
│ │ ├── modalConstants.jsx
│ │ ├── modalActions.jsx
│ │ ├── modalReducer.jsx
│ │ ├── TestModal.jsx
│ │ ├── ModalManager.jsx
│ │ ├── LoginModal.jsx
│ │ ├── RegisterModal.jsx
│ │ └── UnauthModal.jsx
│ ├── auth
│ │ ├── authConstants.jsx
│ │ ├── authWrapper.jsx
│ │ ├── SocialLogin
│ │ │ └── SocialLogin.jsx
│ │ ├── authReducer.jsx
│ │ ├── Login
│ │ │ └── LoginForm.jsx
│ │ ├── Register
│ │ │ └── RegisterForm.jsx
│ │ └── authActions.jsx
│ ├── async
│ │ ├── asyncConstants.jsx
│ │ ├── asyncActions.jsx
│ │ └── asyncReducer.jsx
│ ├── event
│ │ ├── eventConstants.jsx
│ │ ├── EventList
│ │ │ ├── EventListAttendee.jsx
│ │ │ ├── EventList.jsx
│ │ │ └── EventListItem.jsx
│ │ ├── EventActivity
│ │ │ ├── EventActivity.jsx
│ │ │ └── EventActivityItem.jsx
│ │ ├── EventDetailed
│ │ │ ├── EventDetailedMap.jsx
│ │ │ ├── EventDetailedChatForm.jsx
│ │ │ ├── EventDetailedSidebar.jsx
│ │ │ ├── EventDetailedInfo.jsx
│ │ │ ├── EventDetailedHeader.jsx
│ │ │ ├── EventDetailedPage.jsx
│ │ │ └── EventDetailedChat.jsx
│ │ ├── eventReducer.jsx
│ │ ├── EventDashboard
│ │ │ └── EventDashboard.jsx
│ │ ├── eventActions.jsx
│ │ └── EventForm
│ │ │ └── EventForm.jsx
│ ├── testarea
│ │ ├── testConstants.jsx
│ │ ├── testActions.jsx
│ │ ├── testReducer.js
│ │ └── TestComponent.jsx
│ ├── nav
│ │ ├── Menus
│ │ │ ├── SignedOutMenu.jsx
│ │ │ └── SignedInMenu.jsx
│ │ └── NavBar
│ │ │ └── NavBar.jsx
│ ├── user
│ │ ├── PeopleDashboard
│ │ │ ├── PersonCard.jsx
│ │ │ └── PeopleDashboard.jsx
│ │ ├── UserDetailed
│ │ │ ├── UserDetailedPhotos.jsx
│ │ │ ├── UserDetailedHeader.jsx
│ │ │ ├── UserDetailedSidebar.jsx
│ │ │ ├── UserDetailedDescription.jsx
│ │ │ ├── UserDetailedEvents.jsx
│ │ │ └── UserDetailedPage.jsx
│ │ ├── userQueries.jsx
│ │ ├── Settings
│ │ │ ├── SettingsNav.jsx
│ │ │ ├── SettingsDashboard.jsx
│ │ │ ├── BasicPage.jsx
│ │ │ ├── AboutPage.jsx
│ │ │ ├── AccountPage.jsx
│ │ │ └── PhotosPage.jsx
│ │ └── userActions.jsx
│ └── home
│ │ └── HomePage.jsx
├── app
│ ├── layout
│ │ ├── NotFound.jsx
│ │ ├── LoadingComponent.jsx
│ │ └── App.jsx
│ ├── common
│ │ ├── util
│ │ │ ├── reducerUtil.js
│ │ │ ├── ScrollToTop.jsx
│ │ │ └── helpers.js
│ │ └── form
│ │ │ ├── RadioInput.jsx
│ │ │ ├── TextArea.jsx
│ │ │ ├── TextInput.jsx
│ │ │ ├── SelectInput.jsx
│ │ │ ├── DateInput.jsx
│ │ │ └── PlaceInput.jsx
│ ├── data
│ │ ├── mockApi.js
│ │ └── sampleData.js
│ ├── config
│ │ └── firebase.js
│ ├── reducers
│ │ └── rootReducer.js
│ └── store
│ │ └── configureStore.js
├── index.js
├── index.css
└── registerServiceWorker.js
├── .gitignore
├── firebase.json
├── .vscode
└── launch.json
├── functions
├── package.json
├── index.js
└── .eslintrc.json
└── package.json
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "react-app"
3 | }
--------------------------------------------------------------------------------
/.firebaserc:
--------------------------------------------------------------------------------
1 | {
2 | "projects": {
3 | "default": "revents-31284"
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TryCatchLearn/revents_archive/HEAD/public/favicon.ico
--------------------------------------------------------------------------------
/public/assets/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TryCatchLearn/revents_archive/HEAD/public/assets/logo.png
--------------------------------------------------------------------------------
/public/assets/user.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TryCatchLearn/revents_archive/HEAD/public/assets/user.png
--------------------------------------------------------------------------------
/src/features/modals/modalConstants.jsx:
--------------------------------------------------------------------------------
1 | export const MODAL_OPEN = "MODAL_OPEN";
2 | export const MODAL_CLOSE = "MODAL_CLOSE";
--------------------------------------------------------------------------------
/src/features/auth/authConstants.jsx:
--------------------------------------------------------------------------------
1 | export const LOGIN_USER = "LOGIN_USER";
2 | export const SIGN_OUT_USER = "SIGN_OUT_USER";
--------------------------------------------------------------------------------
/public/assets/categoryImages/drinks.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TryCatchLearn/revents_archive/HEAD/public/assets/categoryImages/drinks.jpg
--------------------------------------------------------------------------------
/public/assets/categoryImages/film.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TryCatchLearn/revents_archive/HEAD/public/assets/categoryImages/film.jpg
--------------------------------------------------------------------------------
/public/assets/categoryImages/food.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TryCatchLearn/revents_archive/HEAD/public/assets/categoryImages/food.jpg
--------------------------------------------------------------------------------
/public/assets/categoryImages/music.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TryCatchLearn/revents_archive/HEAD/public/assets/categoryImages/music.jpg
--------------------------------------------------------------------------------
/public/assets/categoryImages/travel.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TryCatchLearn/revents_archive/HEAD/public/assets/categoryImages/travel.jpg
--------------------------------------------------------------------------------
/public/assets/categoryImages/culture.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TryCatchLearn/revents_archive/HEAD/public/assets/categoryImages/culture.jpg
--------------------------------------------------------------------------------
/src/features/async/asyncConstants.jsx:
--------------------------------------------------------------------------------
1 | export const ASYNC_ACTION_START = "ASYNC_ACTION_START";
2 | export const ASYNC_ACTION_FINISH = "ASYNC_ACTION_FINISH";
3 | export const ASYNC_ACTION_ERROR = "ASYNC_ACTION_ERROR";
--------------------------------------------------------------------------------
/src/features/event/eventConstants.jsx:
--------------------------------------------------------------------------------
1 | export const CREATE_EVENT = "CREATE_EVENT";
2 | export const UPDATE_EVENT = "UPDATE_EVENT";
3 | export const DELETE_EVENT = "DELETE_EVENT";
4 | export const FETCH_EVENTS = "FETCH_EVENTS";
--------------------------------------------------------------------------------
/src/app/layout/NotFound.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | const NotFound = () => {
4 | return (
5 |
6 |
Error 404 not found!
7 |
8 | )
9 | }
10 |
11 | export default NotFound
12 |
--------------------------------------------------------------------------------
/src/app/common/util/reducerUtil.js:
--------------------------------------------------------------------------------
1 | export const createReducer = (initialState, fnMap) => {
2 | return (state = initialState, {type, payload}) => {
3 | const handler = fnMap[type];
4 |
5 | return handler ? handler(state, payload) : state
6 | }
7 | }
--------------------------------------------------------------------------------
/src/features/testarea/testConstants.jsx:
--------------------------------------------------------------------------------
1 | export const INCREMENT_COUNTER = "INCREMENT_COUNTER";
2 | export const DECREMENT_COUNTER = "DECREMENT_COUNTER";
3 | export const COUNTER_ACTION_STARTED = "COUNTER_ACTION_STARTED";
4 | export const COUNTER_ACTION_FINISHED = "COUNTER_ACTION_FINISHED";
--------------------------------------------------------------------------------
/src/app/data/mockApi.js:
--------------------------------------------------------------------------------
1 | import sampleData from './sampleData';
2 |
3 | const delay = (ms) => {
4 | return new Promise(resolve => setTimeout(resolve, ms))
5 | }
6 |
7 | export const fetchSampleData = () => {
8 | return delay(1000).then(() => {
9 | return Promise.resolve(sampleData)
10 | })
11 | }
--------------------------------------------------------------------------------
/src/app/layout/LoadingComponent.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { Dimmer, Loader } from 'semantic-ui-react'
3 |
4 | const LoadingComponent = ({inverted}) => {
5 | return (
6 |
7 |
8 |
9 | )
10 | }
11 |
12 | export default LoadingComponent
13 |
--------------------------------------------------------------------------------
/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 | "start_url": "./index.html",
12 | "display": "standalone",
13 | "theme_color": "#000000",
14 | "background_color": "#ffffff"
15 | }
16 |
--------------------------------------------------------------------------------
/src/app/common/form/RadioInput.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { Form } from 'semantic-ui-react'
3 |
4 | const RadioInput = ({input, width, type, label}) => {
5 | return (
6 |
7 |
8 | {' '}
9 |
10 |
11 |
12 | )
13 | }
14 |
15 | export default RadioInput
16 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/ignore-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 |
6 | # testing
7 | /coverage
8 |
9 | # production
10 | /build
11 |
12 | # misc
13 | .DS_Store
14 | .env.local
15 | .env.development.local
16 | .env.test.local
17 | .env.production.local
18 |
19 | npm-debug.log*
20 | yarn-debug.log*
21 | yarn-error.log*
22 |
23 | functions/node_modules/**
24 |
--------------------------------------------------------------------------------
/src/features/modals/modalActions.jsx:
--------------------------------------------------------------------------------
1 | import { MODAL_CLOSE, MODAL_OPEN } from './modalConstants';
2 |
3 | export const openModal = (modalType, modalProps) => {
4 | return {
5 | type: MODAL_OPEN,
6 | payload: {
7 | modalType,
8 | modalProps
9 | }
10 | }
11 | }
12 |
13 | export const closeModal = () => {
14 | return {
15 | type: MODAL_CLOSE
16 | }
17 | }
--------------------------------------------------------------------------------
/firebase.json:
--------------------------------------------------------------------------------
1 | {
2 | "functions": {
3 | "predeploy": [
4 | "npm --prefix \"$RESOURCE_DIR\" run lint"
5 | ]
6 | },
7 | "hosting": {
8 | "public": "build",
9 | "ignore": [
10 | "firebase.json",
11 | "**/.*",
12 | "**/node_modules/**"
13 | ],
14 | "rewrites": [
15 | {
16 | "source": "**",
17 | "destination": "/index.html"
18 | }
19 | ]
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/src/app/common/util/ScrollToTop.jsx:
--------------------------------------------------------------------------------
1 | import { Component } from 'react'
2 | import { withRouter } from 'react-router-dom'
3 |
4 | class ScrollToTop extends Component {
5 | componentDidUpdate(prevProps) {
6 | if (this.props.location !== prevProps.location) {
7 | window.scrollTo(0, 0)
8 | }
9 | }
10 |
11 | render() {
12 | return this.props.children
13 | }
14 | }
15 |
16 | export default withRouter(ScrollToTop)
--------------------------------------------------------------------------------
/src/features/nav/Menus/SignedOutMenu.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Menu, Button } from 'semantic-ui-react';
3 |
4 | const SignedOutMenu = ({signIn, register}) => {
5 | return (
6 |
7 |
8 |
9 |
10 | );
11 | };
12 |
13 | export default SignedOutMenu;
14 |
--------------------------------------------------------------------------------
/src/app/common/form/TextArea.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { Form, Label } from 'semantic-ui-react'
3 |
4 | const TextArea = ({input, rows, type, placeholder, meta: {touched, error}}) => {
5 | return (
6 |
7 |
8 | {touched && error && }
9 |
10 | )
11 | }
12 |
13 | export default TextArea
14 |
--------------------------------------------------------------------------------
/src/features/async/asyncActions.jsx:
--------------------------------------------------------------------------------
1 | import { ASYNC_ACTION_ERROR, ASYNC_ACTION_START, ASYNC_ACTION_FINISH } from './asyncConstants';
2 |
3 | export const asyncActionStart = () => {
4 | return {
5 | type: ASYNC_ACTION_START
6 | }
7 | }
8 |
9 | export const asyncActionFinish = () => {
10 | return {
11 | type: ASYNC_ACTION_FINISH
12 | }
13 | }
14 |
15 | export const asyncActionError = () => {
16 | return {
17 | type: ASYNC_ACTION_ERROR
18 | }
19 | }
--------------------------------------------------------------------------------
/src/app/common/form/TextInput.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Form, Label } from 'semantic-ui-react';
3 |
4 | const TextInput = ({input, width, type, placeholder, meta: {touched, error}}) => {
5 | return (
6 |
7 |
8 | {touched && error && }
9 |
10 | )
11 | }
12 |
13 | export default TextInput
14 |
--------------------------------------------------------------------------------
/src/features/event/EventList/EventListAttendee.jsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react'
2 | import { List, Image } from 'semantic-ui-react'
3 | import { Link } from 'react-router-dom'
4 |
5 | class EventListAttendee extends Component {
6 | render() {
7 | const {attendee} = this.props;
8 | return (
9 |
10 |
11 |
12 | )
13 | }
14 | }
15 | export default EventListAttendee
--------------------------------------------------------------------------------
/src/app/config/firebase.js:
--------------------------------------------------------------------------------
1 | import firebase from 'firebase';
2 | import 'firebase/firestore';
3 |
4 | const firebaseConfig = {
5 | apiKey: "AIzaSyAUNBjilavH2T3H4WsM319-CnKkhA4pcco",
6 | authDomain: "revents-31284.firebaseapp.com",
7 | databaseURL: "https://revents-31284.firebaseio.com",
8 | projectId: "revents-31284",
9 | storageBucket: "revents-31284.appspot.com",
10 | messagingSenderId: "189195857891"
11 | }
12 |
13 | firebase.initializeApp(firebaseConfig);
14 | firebase.firestore();
15 |
16 | export default firebase;
--------------------------------------------------------------------------------
/src/features/auth/authWrapper.jsx:
--------------------------------------------------------------------------------
1 | import { connectedReduxRedirect } from 'redux-auth-wrapper/history4/redirect';
2 | import { openModal } from '../modals/modalActions';
3 |
4 | export const UserIsAuthenticated = connectedReduxRedirect({
5 | wrapperDisplayName: 'UserIsAuthenticated',
6 | allowRedirectBack: true,
7 | redirectPath: '/events',
8 | authenticatedSelector: ({firebase: {auth}}) =>
9 | auth.isLoaded && !auth.isEmpty,
10 | redirectAction: newLoc => (dispatch) => {
11 | dispatch(openModal('UnauthModal'))
12 | }
13 | })
--------------------------------------------------------------------------------
/src/features/modals/modalReducer.jsx:
--------------------------------------------------------------------------------
1 | import { MODAL_CLOSE, MODAL_OPEN } from './modalConstants';
2 | import { createReducer } from '../../app/common/util/reducerUtil';
3 |
4 | const initialState = null;
5 |
6 | export const openModal = (state, payload) => {
7 | const {modalType, modalProps} = payload;
8 | return {modalType, modalProps}
9 | }
10 |
11 | export const closeModal = (state, payload) => {
12 | return null;
13 | }
14 |
15 | export default createReducer(initialState, {
16 | [MODAL_OPEN]: openModal,
17 | [MODAL_CLOSE]: closeModal
18 | })
--------------------------------------------------------------------------------
/.vscode/launch.json:
--------------------------------------------------------------------------------
1 | {
2 | // Use IntelliSense to learn about possible attributes.
3 | // Hover to view descriptions of existing attributes.
4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
5 | "version": "0.2.0",
6 | "configurations": [
7 |
8 | {
9 | "type": "chrome",
10 | "request": "launch",
11 | "name": "Launch Chrome against localhost",
12 | "url": "http://localhost:3000",
13 | "webRoot": "${workspaceFolder}/src"
14 | }
15 | ]
16 | }
--------------------------------------------------------------------------------
/functions/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "functions",
3 | "description": "Cloud Functions for Firebase",
4 | "scripts": {
5 | "lint": "eslint .",
6 | "serve": "firebase serve --only functions",
7 | "shell": "firebase functions:shell",
8 | "start": "npm run shell",
9 | "deploy": "firebase deploy --only functions",
10 | "logs": "firebase functions:log"
11 | },
12 | "dependencies": {
13 | "firebase-admin": "~5.12.0",
14 | "firebase-functions": "^1.0.1"
15 | },
16 | "devDependencies": {
17 | "eslint": "^4.12.0",
18 | "eslint-plugin-promise": "^3.6.0"
19 | },
20 | "private": true
21 | }
22 |
--------------------------------------------------------------------------------
/src/features/user/PeopleDashboard/PersonCard.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {Card, Image} from "semantic-ui-react";
3 | import {Link} from 'react-router-dom';
4 |
5 | const PersonCard = ({user}) => {
6 | return (
7 |
8 |
9 |
10 |
11 |
12 |
13 | {user.city}
14 |
15 |
16 | );
17 | };
18 |
19 | export default PersonCard;
--------------------------------------------------------------------------------
/src/app/common/form/SelectInput.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Form, Label, Select } from 'semantic-ui-react'
3 |
4 | const SelectInput = ({input, type, placeholder, multiple, options, meta: {touched, error}}) => {
5 | return (
6 |
7 |
16 | )
17 | }
18 |
19 | export default SelectInput
20 |
--------------------------------------------------------------------------------
/src/features/auth/SocialLogin/SocialLogin.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Button, Icon } from 'semantic-ui-react';
3 |
4 | const SocialLogin = ({socialLogin}) => {
5 | return (
6 |
7 |
10 |
11 |
15 |
16 | );
17 | };
18 |
19 | export default SocialLogin;
20 |
--------------------------------------------------------------------------------
/src/features/modals/TestModal.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { connect } from 'react-redux'
3 | import { Modal } from 'semantic-ui-react';
4 | import { closeModal } from './modalActions'
5 |
6 | const actions = {
7 | closeModal
8 | }
9 |
10 | const TestModal = ({closeModal}) => {
11 | return (
12 |
13 | Test Modal
14 |
15 |
16 | Test Modal... nothing to see here
17 |
18 |
19 |
20 | );
21 | };
22 |
23 | export default connect(null, actions)(TestModal);
24 |
--------------------------------------------------------------------------------
/src/features/event/EventActivity/EventActivity.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { Header, Segment, Feed, Sticky } from 'semantic-ui-react'
3 | import EventActivityItem from './EventActivityItem'
4 |
5 | const EventActivity = ({activities, contextRef}) => {
6 | return (
7 |
8 |
9 |
10 |
11 | {activities && activities.map(activity => (
12 |
13 | ))}
14 |
15 |
16 |
17 | )
18 | }
19 |
20 | export default EventActivity
21 |
--------------------------------------------------------------------------------
/src/features/auth/authReducer.jsx:
--------------------------------------------------------------------------------
1 | import { SIGN_OUT_USER, LOGIN_USER } from './authConstants';
2 | import { createReducer } from '../../app/common/util/reducerUtil';
3 |
4 | const initialState = {
5 | currentUser: {}
6 | }
7 |
8 | export const loginUser = (state, payload) => {
9 | return {
10 | ...state,
11 | authenticated: true,
12 | currentUser: payload.creds.email
13 | }
14 | }
15 |
16 | export const signOutUser = (state, payload) => {
17 | return {
18 | ...state,
19 | authenticated: false,
20 | currentUser: {}
21 | }
22 | }
23 |
24 | export default createReducer(initialState, {
25 | [LOGIN_USER]: loginUser,
26 | [SIGN_OUT_USER]: signOutUser
27 | })
--------------------------------------------------------------------------------
/src/features/async/asyncReducer.jsx:
--------------------------------------------------------------------------------
1 | import { createReducer } from '../../app/common/util/reducerUtil';
2 | import { ASYNC_ACTION_ERROR, ASYNC_ACTION_START, ASYNC_ACTION_FINISH } from './asyncConstants';
3 |
4 | const initialState = {
5 | loading: false
6 | }
7 |
8 | export const aysncActionStarted = (state, payload) => {
9 | return {...state, loading: true}
10 | }
11 |
12 | export const asyncActionFinished = (state) => {
13 | return {...state, loading: false}
14 | }
15 |
16 | export const asyncActionError = (state) => {
17 | return {...state, loading: false}
18 | }
19 |
20 | export default createReducer(initialState, {
21 | [ASYNC_ACTION_START]: aysncActionStarted,
22 | [ASYNC_ACTION_FINISH]: asyncActionFinished,
23 | [ASYNC_ACTION_ERROR]: asyncActionError
24 | })
--------------------------------------------------------------------------------
/src/app/common/form/DateInput.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { Form, Label } from 'semantic-ui-react';
3 | import DatePicker from 'react-datepicker';
4 | import 'react-datepicker/dist/react-datepicker.css';
5 | import moment from 'moment';
6 |
7 | const DateInput = ({input: {value, onChange, ...restInput}, width, placeholder, meta: {touched, error}, ...rest}) => {
8 | return (
9 |
10 |
17 | {touched && error && }
18 |
19 | )
20 | }
21 |
22 | export default DateInput
23 |
--------------------------------------------------------------------------------
/src/features/user/UserDetailed/UserDetailedPhotos.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Grid, Header, Image, Segment } from 'semantic-ui-react';
3 | import LazyLoad from 'react-lazyload';
4 |
5 | const UserDetailedPhotos = ({ photos }) => {
6 | return (
7 |
8 |
9 |
10 |
11 |
12 | {photos &&
13 | photos.map(photo => (
14 | }>
15 |
16 |
17 | ))}
18 |
19 |
20 |
21 | );
22 | };
23 |
24 | export default UserDetailedPhotos;
25 |
--------------------------------------------------------------------------------
/src/features/event/EventDetailed/EventDetailedMap.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Segment, Icon } from 'semantic-ui-react';
3 | import GoogleMapReact from 'google-map-react';
4 |
5 | const Marker = () => ;
6 |
7 | const EventDetailedMap = ({ lat, lng }) => {
8 | const center = [lat, lng];
9 | const zoom = 14;
10 | return (
11 |
12 |
13 |
18 |
19 |
20 |
21 |
22 | );
23 | };
24 |
25 | export default EventDetailedMap;
26 |
--------------------------------------------------------------------------------
/src/features/user/userQueries.jsx:
--------------------------------------------------------------------------------
1 | export const userDetailedQuery = ({ auth, userUid, match }) => {
2 | if (userUid !== null) {
3 | return [
4 | {
5 | collection: 'users',
6 | doc: userUid,
7 | storeAs: 'profile'
8 | },
9 | {
10 | collection: 'users',
11 | doc: userUid,
12 | subcollections: [{ collection: 'photos' }],
13 | storeAs: 'photos'
14 | },
15 | {
16 | collection: 'users',
17 | doc: auth.uid,
18 | subcollections: [{collection: 'following', doc: match.params.id}],
19 | storeAs: 'following'
20 | }
21 | ];
22 | } else {
23 | return [
24 | {
25 | collection: 'users',
26 | doc: auth.uid,
27 | subcollections: [{ collection: 'photos' }],
28 | storeAs: 'photos'
29 | }
30 | ];
31 | }
32 | };
33 |
--------------------------------------------------------------------------------
/src/features/modals/ModalManager.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { connect } from 'react-redux'
3 | import TestModal from './TestModal'
4 | import LoginModal from './LoginModal'
5 | import RegisterModal from './RegisterModal';
6 | import UnauthModal from './UnauthModal'
7 |
8 | const modalLookup = {
9 | TestModal,
10 | LoginModal,
11 | RegisterModal,
12 | UnauthModal
13 | }
14 |
15 | const mapState = (state) => ({
16 | currentModal: state.modals
17 | })
18 |
19 | const ModalManager = ({currentModal}) => {
20 | let renderedModal;
21 |
22 | if (currentModal) {
23 | const {modalType, modalProps} = currentModal;
24 | const ModalComponent = modalLookup[modalType];
25 |
26 | renderedModal =
27 | }
28 | return {renderedModal}
29 | }
30 |
31 | export default connect(mapState)(ModalManager)
32 |
--------------------------------------------------------------------------------
/src/features/user/Settings/SettingsNav.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Grid, Menu, Header } from 'semantic-ui-react';
3 | import { NavLink } from 'react-router-dom'
4 |
5 | const SettingsNav = () => {
6 | return (
7 |
8 |
14 |
15 |
19 |
20 | );
21 | };
22 |
23 | export default SettingsNav;
24 |
--------------------------------------------------------------------------------
/src/features/event/EventList/EventList.jsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import EventListItem from './EventListItem';
3 | import InfiniteScroll from 'react-infinite-scroller';
4 |
5 | class EventList extends Component {
6 | render() {
7 | const { events, getNextEvents, loading, moreEvents } = this.props;
8 | return (
9 |
10 | {events &&
11 | events.length !== 0 && (
12 |
18 | {events &&
19 | events.map(event => (
20 |
21 | ))}
22 |
23 | )}
24 |
25 | );
26 | }
27 | }
28 |
29 | export default EventList;
30 |
--------------------------------------------------------------------------------
/src/features/modals/LoginModal.jsx:
--------------------------------------------------------------------------------
1 | import React, {Component} from 'react';
2 | import {Modal} from 'semantic-ui-react';
3 | import {connect} from 'react-redux';
4 |
5 | import LoginForm from '../auth/Login/LoginForm';
6 | import {closeModal} from "./modalActions";
7 |
8 | const actions = {closeModal};
9 |
10 | class LoginModal extends Component {
11 | render() {
12 | return (
13 |
18 |
19 | Login to Re-vents
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 | );
28 | }
29 | }
30 |
31 | export default connect(null, actions)(LoginModal);
--------------------------------------------------------------------------------
/src/app/reducers/rootReducer.js:
--------------------------------------------------------------------------------
1 | import { combineReducers } from 'redux';
2 | import { reducer as FormReducer } from 'redux-form';
3 | import { reducer as toastrReducer } from 'react-redux-toastr';
4 | import { firebaseReducer } from 'react-redux-firebase';
5 | import { firestoreReducer } from 'redux-firestore'
6 | import testReducer from '../../features/testarea/testReducer';
7 | import eventReducer from '../../features/event/eventReducer';
8 | import modalsReducer from '../../features/modals/modalReducer';
9 | import authReducer from '../../features/auth/authReducer';
10 | import asyncReducer from '../../features/async/asyncReducer';
11 |
12 | const rootReducer = combineReducers({
13 | firebase: firebaseReducer,
14 | firestore: firestoreReducer,
15 | form: FormReducer,
16 | test: testReducer,
17 | events: eventReducer,
18 | modals: modalsReducer,
19 | auth: authReducer,
20 | async: asyncReducer,
21 | toastr: toastrReducer
22 | })
23 |
24 | export default rootReducer;
--------------------------------------------------------------------------------
/src/features/event/EventDetailed/EventDetailedChatForm.jsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import { Form, Button } from 'semantic-ui-react';
3 | import { Field, reduxForm } from 'redux-form';
4 | import TextArea from '../../../app/common/form/TextArea';
5 |
6 | class EventDetailedChatForm extends Component {
7 | handleCommentSubmit = values => {
8 | const { addEventComment, reset, eventId, closeForm, parentId } = this.props;
9 | addEventComment(eventId, values, parentId);
10 | reset();
11 | if (parentId !== 0) {
12 | closeForm();
13 | }
14 | };
15 |
16 | render() {
17 | return (
18 |
22 | );
23 | }
24 | }
25 |
26 | export default reduxForm({ Fields: 'comment' })(EventDetailedChatForm);
27 |
--------------------------------------------------------------------------------
/src/features/modals/RegisterModal.jsx:
--------------------------------------------------------------------------------
1 | import React, {Component} from 'react';
2 | import {Modal} from 'semantic-ui-react';
3 | import {connect} from 'react-redux';
4 |
5 | import {closeModal} from "./modalActions";
6 | import RegisterForm from "../auth/Register/RegisterForm";
7 |
8 | const actions = {closeModal};
9 |
10 | class RegisterModal extends Component {
11 | render() {
12 | return (
13 |
18 |
19 | Sign Up to Re-vents!
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 | );
28 | }
29 | }
30 |
31 | export default connect(null, actions)(RegisterModal);
--------------------------------------------------------------------------------
/src/features/event/eventReducer.jsx:
--------------------------------------------------------------------------------
1 | import { createReducer } from '../../app/common/util/reducerUtil'
2 | import { CREATE_EVENT, DELETE_EVENT, UPDATE_EVENT, FETCH_EVENTS } from './eventConstants';
3 |
4 | const initialState = [];
5 |
6 | export const createEvent = (state, payload) => {
7 | return [...state, Object.assign({}, payload.event)]
8 | }
9 |
10 | export const updateEvent = (state, payload) => {
11 | return [
12 | ...state.filter(event => event.id !== payload.event.id),
13 | Object.assign({}, payload.event)
14 | ]
15 | }
16 |
17 | export const deleteEvent = (state, payload) => {
18 | return [...state.filter(event => event.id !== payload.eventId)]
19 | }
20 |
21 | export const fetchEvents = (state, payload) => {
22 | return payload.events
23 | }
24 |
25 | export default createReducer(initialState, {
26 | [CREATE_EVENT]: createEvent,
27 | [UPDATE_EVENT]: updateEvent,
28 | [DELETE_EVENT]: deleteEvent,
29 | [FETCH_EVENTS]: fetchEvents
30 | })
--------------------------------------------------------------------------------
/src/features/nav/Menus/SignedInMenu.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Menu, Image, Dropdown } from 'semantic-ui-react';
3 | import { Link } from 'react-router-dom'
4 |
5 | const SignedInMenu = ({signOut, profile, auth}) => {
6 | return (
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 | );
21 | };
22 |
23 | export default SignedInMenu;
24 |
--------------------------------------------------------------------------------
/src/features/user/UserDetailed/UserDetailedHeader.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Grid, Header, Item, Segment } from 'semantic-ui-react';
3 | import differenceInYears from 'date-fns/difference_in_years';
4 |
5 | const UserDetailedHeader = ({ profile }) => {
6 | const age = differenceInYears(Date.now(), profile.dateOfBirth);
7 | return (
8 |
9 |
10 |
11 | -
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 | {age || 'Unknown age'}, Lives in {profile.city || 'Unknown city'}
20 |
21 |
22 |
23 |
24 |
25 |
26 | );
27 | };
28 |
29 | export default UserDetailedHeader;
30 |
--------------------------------------------------------------------------------
/src/features/user/UserDetailed/UserDetailedSidebar.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Button, Grid, Segment } from 'semantic-ui-react';
3 | import { Link } from 'react-router-dom';
4 |
5 | const UserDetailedSidebar = ({ unfollowUser, isCurrentUser, followUser, profile, isFollowing }) => {
6 | return (
7 |
8 |
9 | {isCurrentUser && (
10 |
11 | )}
12 | {!isCurrentUser &&
13 | !isFollowing && (
14 |
34 |
35 | );
36 | };
37 |
38 | export default UserDetailedSidebar;
39 |
--------------------------------------------------------------------------------
/src/app/common/util/helpers.js:
--------------------------------------------------------------------------------
1 | import moment from 'moment';
2 |
3 | export const objectToArray = (object) => {
4 | if (object) {
5 | return Object.entries(object).map(e => Object.assign(e[1], {id: e[0]}))
6 | }
7 | }
8 |
9 | export const createNewEvent = (user, photoURL, event) => {
10 | event.date = moment(event.date).toDate();
11 | return {
12 | ...event,
13 | hostUid: user.uid,
14 | hostedBy: user.displayName,
15 | hostPhotoURL: photoURL || '/assets/user.png',
16 | created: Date.now(),
17 | attendees: {
18 | [user.uid]: {
19 | going: true,
20 | joinDate: Date.now(),
21 | photoURL: photoURL || '/assets/user.png',
22 | displayName: user.displayName,
23 | host: true
24 | }
25 | }
26 | }
27 | }
28 |
29 | export const createDataTree = dataset => {
30 | let hashTable = Object.create(null);
31 | dataset.forEach(a => hashTable[a.id] = {...a, childNodes: []});
32 | let dataTree = [];
33 | dataset.forEach(a => {
34 | if (a.parentId) hashTable[a.parentId].childNodes.push(hashTable[a.id]);
35 | else dataTree.push(hashTable[a.id])
36 | });
37 | return dataTree
38 | };
--------------------------------------------------------------------------------
/src/features/testarea/testActions.jsx:
--------------------------------------------------------------------------------
1 | import {
2 | INCREMENT_COUNTER,
3 | DECREMENT_COUNTER,
4 | COUNTER_ACTION_FINISHED,
5 | COUNTER_ACTION_STARTED
6 | } from './testConstants';
7 |
8 | export const incrementCounter = () => {
9 | return {
10 | type: INCREMENT_COUNTER
11 | };
12 | };
13 |
14 | export const decrementCounter = () => {
15 | return {
16 | type: DECREMENT_COUNTER
17 | };
18 | };
19 |
20 | export const startCounterAction = () => {
21 | return {
22 | type: COUNTER_ACTION_STARTED
23 | };
24 | };
25 |
26 | export const finishCounterAction = () => {
27 | return {
28 | type: COUNTER_ACTION_FINISHED
29 | };
30 | };
31 |
32 | const delay = ms => {
33 | return new Promise(resolve => setTimeout(resolve, ms));
34 | };
35 |
36 | export const incrementAsync = () => {
37 | return async dispatch => {
38 | dispatch(startCounterAction())
39 | await delay(1000);
40 | dispatch({type: INCREMENT_COUNTER})
41 | dispatch(finishCounterAction())
42 | }
43 | }
44 |
45 | export const decrementAsync = () => {
46 | return async dispatch => {
47 | dispatch(startCounterAction());
48 | await delay(1000);
49 | dispatch({ type: DECREMENT_COUNTER });
50 | dispatch(finishCounterAction());
51 | };
52 | };
53 |
--------------------------------------------------------------------------------
/src/features/home/HomePage.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | const HomePage = ({history}) => {
4 | return (
5 |
6 |
7 |
8 |
9 |
10 |
Re-vents
11 |
12 |
Do whatever you want to do
13 |
history.push('/events')} className="ui huge white inverted button">
14 | Get Started
15 |
16 |
17 |
18 |
19 |
33 |
34 | );
35 | };
36 |
37 | export default HomePage;
38 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import { BrowserRouter } from 'react-router-dom';
4 | import { Provider } from 'react-redux';
5 | import ReduxToastr from 'react-redux-toastr'
6 | import 'semantic-ui-css/semantic.min.css';
7 | import 'react-redux-toastr/lib/css/react-redux-toastr.min.css';
8 | import './index.css';
9 | import App from './app/layout/App';
10 | import registerServiceWorker from './registerServiceWorker';
11 | import { configureStore } from './app/store/configureStore';
12 | import ScrollToTop from './app/common/util/ScrollToTop';
13 |
14 | const store = configureStore();
15 |
16 | const rootEl = document.getElementById('root');
17 |
18 | let render = () => {
19 | ReactDOM.render(
20 |
21 |
22 |
23 |
28 |
29 |
30 |
31 | ,
32 | rootEl
33 | );
34 | };
35 |
36 | if (module.hot) {
37 | module.hot.accept('./app/layout/App', () => {
38 | setTimeout(render);
39 | });
40 | }
41 |
42 | store.firebaseAuthIsReady.then(() => {
43 | render();
44 | registerServiceWorker();
45 | })
46 |
47 |
48 |
--------------------------------------------------------------------------------
/src/features/auth/Login/LoginForm.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Form, Segment, Button, Label, Divider } from 'semantic-ui-react';
3 | import { connect } from 'react-redux';
4 | import { Field, reduxForm } from 'redux-form';
5 | import TextInput from '../../../app/common/form/TextInput';
6 | import {login, socialLogin} from '../authActions'
7 | import SocialLogin from '../SocialLogin/SocialLogin'
8 |
9 | const actions = {
10 | login,
11 | socialLogin
12 | }
13 |
14 | const LoginForm = ({login, handleSubmit, error, socialLogin}) => {
15 | return (
16 |
38 | );
39 | };
40 |
41 | export default connect(null, actions)(reduxForm({form: 'loginForm'})(LoginForm));
--------------------------------------------------------------------------------
/src/features/testarea/testReducer.js:
--------------------------------------------------------------------------------
1 | import { createReducer } from '../../app/common/util/reducerUtil'
2 | import { INCREMENT_COUNTER, DECREMENT_COUNTER, COUNTER_ACTION_FINISHED, COUNTER_ACTION_STARTED } from './testConstants';
3 |
4 | const initialState = {
5 | data: 41,
6 | loading: false
7 | };
8 |
9 | export const incrementCounter = (state, payload) => {
10 | return { ...state, data: state.data + 1 };
11 | }
12 |
13 | export const decrementCounter = (state, payload) => {
14 | return { ...state, data: state.data - 1 };
15 | }
16 |
17 | export const counterActionStarted = (state, payload) => {
18 | return {...state, loading: true}
19 | }
20 |
21 | export const counterActionFinished = (state, payload) => {
22 | return {...state, loading: false}
23 | }
24 |
25 | // const testReducer = (state = initialState, action) => {
26 | // switch (action.type) {
27 | // case INCREMENT_COUNTER:
28 | // return { ...state, data: state.data + 1 };
29 | // case DECREMENT_COUNTER:
30 | // return { ...state, data: state.data - 1 };
31 | // default:
32 | // return state;
33 | // }
34 | // };
35 |
36 | export default createReducer(initialState, {
37 | [INCREMENT_COUNTER]: incrementCounter,
38 | [DECREMENT_COUNTER]: decrementCounter,
39 | [COUNTER_ACTION_FINISHED]: counterActionFinished,
40 | [COUNTER_ACTION_STARTED]: counterActionStarted
41 | });
42 |
--------------------------------------------------------------------------------
/src/app/common/form/PlaceInput.jsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import { Form, Label } from 'semantic-ui-react';
3 | import Script from 'react-load-script';
4 | import PlacesAutocomplete from 'react-places-autocomplete';
5 |
6 | const styles = {
7 | autocompleteContainer: {
8 | zIndex: 1000
9 | }
10 | }
11 |
12 | class PlaceInput extends Component {
13 | state = {
14 | scriptLoaded: false
15 | };
16 |
17 | handleScriptLoaded = () => this.setState({ scriptLoaded: true });
18 |
19 | render() {
20 | const {
21 | input,
22 | width,
23 | onSelect,
24 | placeholder,
25 | options,
26 | meta: { touched, error }
27 | } = this.props;
28 | return (
29 |
30 |
34 | {this.state.scriptLoaded &&
35 | }
41 | {touched && error && }
42 |
43 | );
44 | }
45 | }
46 |
47 | export default PlaceInput;
48 |
--------------------------------------------------------------------------------
/src/features/event/EventDetailed/EventDetailedSidebar.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Segment, List, Item, Label } from 'semantic-ui-react';
3 | import { Link } from 'react-router-dom'
4 |
5 | const EventDetailedSidebar = ({ attendees }) => {
6 | return (
7 |
8 |
16 | {attendees && attendees.length} {attendees && attendees.length === 1 ? 'Person' : 'People'} Going
17 |
18 |
19 |
20 | {attendees && attendees.map((attendee) => (
21 | -
22 | {attendee.host && (
23 |
26 | )}
27 |
28 |
29 |
30 | {attendee.displayName}
31 |
32 |
33 |
34 | ))}
35 |
36 |
37 |
38 |
39 | );
40 | };
41 |
42 | export default EventDetailedSidebar;
43 |
--------------------------------------------------------------------------------
/src/app/store/configureStore.js:
--------------------------------------------------------------------------------
1 | import { createStore, applyMiddleware } from 'redux';
2 | import { composeWithDevTools } from 'redux-devtools-extension';
3 | import { reactReduxFirebase, getFirebase } from 'react-redux-firebase';
4 | import { reduxFirestore, getFirestore } from 'redux-firestore';
5 | import thunk from 'redux-thunk';
6 | import rootReducer from '../reducers/rootReducer';
7 | import firebase from '../config/firebase';
8 |
9 | const rrfConfig = {
10 | userProfile: 'users',
11 | attachAuthIsReady: true,
12 | useFirestoreForProfile: true,
13 | updateProfileOnLogin: false
14 | };
15 |
16 | export const configureStore = preloadedState => {
17 | const middlewares = [thunk.withExtraArgument({ getFirebase, getFirestore })];
18 | const middlewareEnhancer = applyMiddleware(...middlewares);
19 |
20 | const storeEnhancers = [middlewareEnhancer];
21 |
22 | const composedEnhancer = composeWithDevTools(
23 | ...storeEnhancers,
24 | reactReduxFirebase(firebase, rrfConfig),
25 | reduxFirestore(firebase)
26 | );
27 |
28 | const store = createStore(rootReducer, preloadedState, composedEnhancer);
29 |
30 | if (process.env.NODE_ENV !== 'production') {
31 | if (module.hot) {
32 | module.hot.accept('../reducers/rootReducer', () => {
33 | const newRootReducer = require('../reducers/rootReducer').default;
34 | store.replaceReducer(newRootReducer);
35 | });
36 | }
37 | }
38 |
39 | return store;
40 | };
41 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "revents",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "cuid": "^2.1.1",
7 | "date-fns": "^1.29.0",
8 | "firebase": "^4.12.1",
9 | "google-map-react": "^0.34.0",
10 | "history": "^4.7.2",
11 | "moment": "^2.22.0",
12 | "react": "^16.3.0",
13 | "react-cropper": "^1.0.1",
14 | "react-datepicker": "^1.4.0",
15 | "react-dom": "^16.3.0",
16 | "react-dropzone": "^4.2.9",
17 | "react-infinite-scroller": "^1.1.3",
18 | "react-lazyload": "^2.3.0",
19 | "react-load-script": "0.0.6",
20 | "react-loadable": "^5.3.1",
21 | "react-places-autocomplete": "^6.1.3",
22 | "react-redux": "^5.0.7",
23 | "react-redux-firebase": "^2.0.6",
24 | "react-redux-toastr": "^7.2.3",
25 | "react-router-dom": "^4.2.2",
26 | "react-router-redux": "^5.0.0-alpha.9",
27 | "react-scripts": "1.1.2",
28 | "redux": "^3.7.2",
29 | "redux-auth-wrapper": "^2.0.2",
30 | "redux-firestore": "^0.3.2",
31 | "redux-form": "^7.3.0",
32 | "redux-thunk": "^2.2.0",
33 | "revalidate": "^1.2.0",
34 | "semantic-ui-css": "2.2.14",
35 | "semantic-ui-react": "^0.79.0"
36 | },
37 | "scripts": {
38 | "start": "react-scripts start",
39 | "build": "react-scripts build",
40 | "test": "react-scripts test --env=jsdom",
41 | "eject": "react-scripts eject"
42 | },
43 | "devDependencies": {
44 | "redux-devtools-extension": "^2.13.2",
45 | "source-map-explorer": "^1.5.0"
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/src/features/user/UserDetailed/UserDetailedDescription.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Grid, Header, Icon, Item, List, Segment } from 'semantic-ui-react';
3 | import format from 'date-fns/format';
4 |
5 | const UserDetailedDescription = ({ profile }) => {
6 | return (
7 |
8 |
9 |
10 |
11 |
12 |
13 | I am a: {profile.occupation || 'tbn'}
14 |
15 |
16 | Originally from {profile.origin || 'tbn'}
17 |
18 |
19 | Member Since: {format(profile.createdAt, 'D MMM YYYY')}
20 |
21 | Description of user
22 |
23 |
24 |
25 | {profile.interests ? (
26 |
27 | {profile.interests &&
28 | profile.interests.map((interest, index) => (
29 | -
30 |
31 | {interest}
32 |
33 | ))}
34 |
35 | ) : (
36 | No interests
37 | )}
38 |
39 |
40 |
41 |
42 | );
43 | };
44 |
45 | export default UserDetailedDescription;
46 |
--------------------------------------------------------------------------------
/src/features/user/UserDetailed/UserDetailedEvents.jsx:
--------------------------------------------------------------------------------
1 | import format from 'date-fns/format';
2 | import React from 'react';
3 | import { Link } from 'react-router-dom';
4 | import { Card, Grid, Header, Image, Segment, Tab } from 'semantic-ui-react';
5 |
6 | const panes = [
7 | {menuItem: 'All Events', pane: {key: 'allEvents'}},
8 | {menuItem: 'Past Events', pane: {key: 'pastEvents'}},
9 | {menuItem: 'Future Events', pane: {key: 'futureEvents'}},
10 | {menuItem: 'Hosting', pane: {key: 'hosted'}},
11 | ]
12 |
13 | const UserDetailedEvents = ({ events, eventsLoading, changeTab }) => {
14 | return (
15 |
16 |
17 |
18 | changeTab(e, data)} panes={panes} menu={{secondary: true, pointing: true}}/>
19 |
20 |
21 | {events &&
22 | events.map(event => (
23 |
24 |
25 |
26 | {event.title}
27 |
28 | {format(event.date, 'DD MMM YYYY')}
29 | {format(event.date, 'h:mm A')}
30 |
31 |
32 |
33 | ))}
34 |
35 |
36 |
37 | );
38 | };
39 |
40 | export default UserDetailedEvents;
41 |
--------------------------------------------------------------------------------
/src/features/auth/Register/RegisterForm.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { connect } from 'react-redux';
3 | import { Form, Segment, Button, Label, Divider } from 'semantic-ui-react';
4 | import { Field, reduxForm } from 'redux-form';
5 | import { combineValidators, isRequired } from 'revalidate';
6 | import TextInput from '../../../app/common/form/TextInput';
7 | import { registerUser } from '../authActions';
8 | import SocialLogin from '../SocialLogin/SocialLogin';
9 |
10 | const actions = {
11 | registerUser
12 | };
13 |
14 | const validate = combineValidators({
15 | displayName: isRequired('displayName'),
16 | email: isRequired('email'),
17 | password: isRequired('password')
18 | });
19 |
20 | const RegisterForm = ({ registerUser, handleSubmit, error, invalid, submitting }) => {
21 | return (
22 |
41 | );
42 | };
43 |
44 | export default connect(null, actions)(reduxForm({ form: 'registerForm', validate })(RegisterForm));
45 |
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
11 |
12 |
13 |
22 | Re-vents
23 |
24 |
25 |
28 | Welcome to Re-vents
29 |
39 |
40 |
41 |
--------------------------------------------------------------------------------
/src/features/user/PeopleDashboard/PeopleDashboard.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { connect } from 'react-redux';
3 | import { compose } from 'redux';
4 | import { firestoreConnect } from 'react-redux-firebase';
5 | import { Grid, Segment, Header, Card } from 'semantic-ui-react';
6 | import PersonCard from './PersonCard';
7 |
8 | const query = ({ auth }) => {
9 | return [
10 | {
11 | collection: 'users',
12 | doc: auth.uid,
13 | subcollections: [{ collection: 'following' }],
14 | storeAs: 'following'
15 | },
16 | {
17 | collection: 'users',
18 | doc: auth.uid,
19 | subcollections: [{ collection: 'followers' }],
20 | storeAs: 'followers'
21 | }
22 | ];
23 | };
24 |
25 | const mapState = state => ({
26 | followings: state.firestore.ordered.following,
27 | followers: state.firestore.ordered.followers,
28 | auth: state.firebase.auth
29 | });
30 |
31 | const PeopleDashboard = ({ followings, followers }) => {
32 | return (
33 |
34 |
35 |
36 |
37 |
38 | {followers &&
39 | followers.map(follower => )}
40 |
41 |
42 |
43 |
44 |
45 | {followings &&
46 | followings.map(following => )}
47 |
48 |
49 |
50 |
51 | );
52 | };
53 |
54 | export default compose(connect(mapState), firestoreConnect(props => query(props)))(PeopleDashboard);
55 |
--------------------------------------------------------------------------------
/src/features/modals/UnauthModal.jsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import { Modal, Button, Divider } from 'semantic-ui-react';
3 | import { connect } from 'react-redux';
4 | import { withRouter } from 'react-router-dom';
5 |
6 | import { closeModal, openModal } from './modalActions';
7 |
8 | const actions = { closeModal, openModal };
9 |
10 | class UnauthModal extends Component {
11 |
12 | handleCloseModal = () => {
13 | if (this.props.location.pathname.includes('/event')) {
14 | this.props.closeModal()
15 | } else {
16 | this.props.history.goBack();
17 | this.props.closeModal();
18 | }
19 | }
20 |
21 | render() {
22 | const { openModal} = this.props;
23 | return (
24 |
25 | You need to be signed in to do that!
26 |
27 |
28 | Please either login or register to see this page
29 |
30 | openModal('LoginModal')}>
31 | Login
32 |
33 |
34 | openModal('RegisterModal')}>
35 | Register
36 |
37 |
38 |
39 |
40 |
Or click cancel to continue as a guest
41 |
Cancel
42 |
43 |
44 |
45 |
46 | );
47 | }
48 | }
49 |
50 | export default withRouter(connect(null, actions)(UnauthModal));
51 |
--------------------------------------------------------------------------------
/src/features/user/Settings/SettingsDashboard.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { connect } from 'react-redux';
3 | import { Grid } from 'semantic-ui-react';
4 | import { Switch, Route, Redirect } from 'react-router-dom';
5 | import SettingsNav from './SettingsNav';
6 | import BasicPage from './BasicPage';
7 | import AboutPage from './AboutPage';
8 | import PhotosPage from './PhotosPage';
9 | import AccountPage from './AccountPage';
10 | import { updatePassword } from '../../auth/authActions';
11 | import { updateProfile } from '../userActions';
12 |
13 | const actions = {
14 | updatePassword,
15 | updateProfile
16 | };
17 |
18 | const mapState = state => ({
19 | providerId: state.firebase.auth.providerData[0].providerId,
20 | user: state.firebase.profile
21 | });
22 |
23 | const SettingsDashboard = ({ updatePassword, providerId, user, updateProfile }) => {
24 | return (
25 |
26 |
27 |
28 |
29 | }
32 | />
33 | }
36 | />
37 |
38 | }
41 | />
42 |
43 |
44 |
45 |
46 |
47 |
48 | );
49 | };
50 |
51 | export default connect(mapState, actions)(SettingsDashboard);
52 |
--------------------------------------------------------------------------------
/src/app/data/sampleData.js:
--------------------------------------------------------------------------------
1 | const sampleData = {
2 | events: [
3 | {
4 | id: '1',
5 | title: 'Trip to Empire State building',
6 | date: '2018-03-21T18:00:00',
7 | category: 'culture',
8 | description:
9 | 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Phasellus sollicitudin ligula eu leo tincidunt, quis scelerisque magna dapibus. Sed eget ipsum vel arcu vehicula ullamcorper.',
10 | city: 'NY, USA',
11 | venue: 'Empire State Building, 5th Avenue, New York, NY, USA',
12 | venueLatLng: {
13 | lat: 40.7484405,
14 | lng: -73.98566440000002
15 | },
16 | hostedBy: 'Bob',
17 | hostPhotoURL: 'https://randomuser.me/api/portraits/men/20.jpg',
18 | attendees: [
19 | {
20 | id: 'a',
21 | name: 'Bob',
22 | photoURL: 'https://randomuser.me/api/portraits/men/20.jpg'
23 | },
24 | {
25 | id: 'b',
26 | name: 'Tom',
27 | photoURL: 'https://randomuser.me/api/portraits/men/22.jpg'
28 | }
29 | ]
30 | },
31 | {
32 | id: '2',
33 | title: 'Trip to Punch and Judy Pub',
34 | date: '2018-03-18T14:00:00',
35 | category: 'drinks',
36 | description:
37 | 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Phasellus sollicitudin ligula eu leo tincidunt, quis scelerisque magna dapibus. Sed eget ipsum vel arcu vehicula ullamcorper.',
38 | city: 'London, UK',
39 | venue: 'Punch & Judy, Henrietta Street, London, UK',
40 | venueLatLng: {
41 | lat: 51.5118074,
42 | lng: -0.12300089999996544
43 | },
44 | hostedBy: 'Tom',
45 | hostPhotoURL: 'https://randomuser.me/api/portraits/men/22.jpg',
46 | attendees: [
47 | {
48 | id: 'a',
49 | name: 'Bob',
50 | photoURL: 'https://randomuser.me/api/portraits/men/20.jpg'
51 | },
52 | {
53 | id: 'b',
54 | name: 'Tom',
55 | photoURL: 'https://randomuser.me/api/portraits/men/22.jpg'
56 | }
57 | ]
58 | }
59 | ]
60 | };
61 |
62 | export default sampleData;
--------------------------------------------------------------------------------
/src/features/event/EventActivity/EventActivityItem.jsx:
--------------------------------------------------------------------------------
1 | import React, {Component} from 'react';
2 | import {Feed} from 'semantic-ui-react';
3 | import {Link} from 'react-router-dom';
4 | import distanceInWordsToNow from 'date-fns/distance_in_words_to_now'
5 |
6 | class EventActivityItem extends Component {
7 |
8 | renderSummary = (activity) => {
9 | switch (activity.type) {
10 | case 'newEvent':
11 | return (
12 |
13 | New Event! {' '}
14 | {activity.hostedBy} {' '}
15 | is hosting {' '}
16 | {activity.title}
17 |
18 | );
19 | case 'cancelledEvent':
20 | return (
21 |
22 | Event Cancelled! {' '}
23 | {activity.hostedBy} {' '}
24 | has cancelled {' '}
25 | {activity.title}
26 |
27 | );
28 | default:
29 | return;
30 | }
31 | };
32 |
33 | render() {
34 | const {activity} = this.props;
35 |
36 | return (
37 |
38 |
39 |
40 |
41 |
42 |
43 | {this.renderSummary(activity)}
44 |
45 |
46 | {distanceInWordsToNow(activity.timestamp)} ago
47 |
48 |
49 |
50 | );
51 | }
52 | }
53 |
54 | export default EventActivityItem;
--------------------------------------------------------------------------------
/src/features/event/EventList/EventListItem.jsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import { Segment, Item, Icon, List, Button, Label } from 'semantic-ui-react';
3 | import { Link } from 'react-router-dom';
4 | import format from 'date-fns/format';
5 | import EventListAttendee from './EventListAttendee';
6 | import { objectToArray } from '../../../app/common/util/helpers'
7 |
8 | class EventListItem extends Component {
9 | render() {
10 | const { event } = this.props;
11 | return (
12 |
13 |
14 |
15 | -
16 |
17 |
18 | {event.title}
19 |
20 | Hosted by {event.hostedBy}
21 |
22 | {event.cancelled &&
23 | }
29 |
30 |
31 |
32 |
33 |
34 |
35 | {format(event.date, 'dddd Do MMMM')} at{' '}
36 | {format(event.date, 'HH:mm')} |
37 | {event.venue}
38 |
39 |
40 |
41 |
42 | {event.attendees &&
43 | objectToArray(event.attendees).map((attendee) => (
44 |
45 | ))}
46 |
47 |
48 |
49 | {event.description}
50 |
51 |
52 |
53 | );
54 | }
55 | }
56 |
57 | export default EventListItem;
58 |
--------------------------------------------------------------------------------
/src/features/event/EventDetailed/EventDetailedInfo.jsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import { Segment, Grid, Icon, Button } from 'semantic-ui-react';
3 | import EventDetailedMap from './EventDetailedMap';
4 | import format from 'date-fns/format';
5 |
6 | class EventDetailedInfo extends Component {
7 | state = {
8 | showMap: false
9 | };
10 |
11 | componentWillUnmount() {
12 | this.setState({
13 | showMap: false
14 | })
15 | }
16 |
17 | showMapToggle = () => {
18 | this.setState(prevState => ({
19 | showMap: !prevState.showMap
20 | }));
21 | };
22 |
23 | render() {
24 | const { event } = this.props;
25 | return (
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 | {event.description}
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 | {format(event.date, 'dddd Do MMM')} at {format(event.date, 'h:mm A')}
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 | {event.venue}
54 |
55 |
56 |
57 |
58 |
59 |
60 | {this.state.showMap && (
61 |
62 | )}
63 |
64 | );
65 | }
66 | }
67 |
68 | export default EventDetailedInfo;
69 |
--------------------------------------------------------------------------------
/src/features/nav/NavBar/NavBar.jsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import { connect } from 'react-redux';
3 | import { withFirebase } from 'react-redux-firebase'
4 | import { Menu, Container, Button } from 'semantic-ui-react';
5 | import { NavLink, Link, withRouter } from 'react-router-dom';
6 | import SignedOutMenu from '../Menus/SignedOutMenu';
7 | import SignedInMenu from '../Menus/SignedInMenu';
8 | import { openModal } from '../../modals/modalActions'
9 |
10 | const actions = {
11 | openModal
12 | }
13 |
14 | const mapState = (state) => ({
15 | auth: state.firebase.auth,
16 | profile: state.firebase.profile
17 | })
18 |
19 | class NavBar extends Component {
20 |
21 | handleSignIn = () => {
22 | this.props.openModal('LoginModal')
23 | };
24 |
25 | handleRegister = () => {
26 | this.props.openModal('RegisterModal')
27 | }
28 |
29 | handleSignOut = () => {
30 | this.props.firebase.logout();
31 | this.props.history.push('/')
32 | };
33 |
34 | render() {
35 | const { auth, profile } = this.props;
36 | const authenticated = auth.isLoaded && !auth.isEmpty;
37 | return (
38 |
66 | );
67 | }
68 | }
69 |
70 | export default withRouter(withFirebase(connect(mapState, actions)(NavBar)));
71 |
--------------------------------------------------------------------------------
/src/index.css:
--------------------------------------------------------------------------------
1 | body {
2 | background-color: rgb(234, 234, 234) !important;
3 | }
4 |
5 | /*timepicker style*/
6 | .react-datepicker__time-container .react-datepicker__time .react-datepicker__time-box ul.react-datepicker__time-list {
7 | padding-left: 0;
8 | padding-right: 0;
9 | width: 100px;
10 | }
11 |
12 | .react-datepicker__input-container {
13 | width: inherit;
14 | }
15 |
16 | .react-datepicker-wrapper {
17 | width: 100%;
18 | }
19 |
20 |
21 | /*home page styles*/
22 |
23 | .masthead {
24 | background-image: linear-gradient(
25 | 135deg,
26 | rgb(24, 42, 115) 0%,
27 | rgb(33, 138, 174) 69%,
28 | rgb(32, 167, 172) 89%
29 | ) !important;
30 | }
31 |
32 | .masthead.segment {
33 | min-height: 700px;
34 | padding: 1em 0 !important;
35 | }
36 |
37 | .masthead .ui.menu .ui.button,
38 | .ui.menu a.ui.inverted.button {
39 | margin-left: 0.5em;
40 | }
41 |
42 | .masthead h1.ui.header {
43 | margin-top: 3em;
44 | margin-bottom: 0;
45 | font-size: 4em;
46 | font-weight: normal;
47 | }
48 |
49 | .masthead h2 {
50 | font-size: 1.7em;
51 | font-weight: normal;
52 | }
53 |
54 | .footer.segment {
55 | padding: 5em 0;
56 | }
57 |
58 | .secondary.inverted.pointing.menu {
59 | border: none;
60 | }
61 |
62 | /*end home page styles*/
63 |
64 | /* navbar styles */
65 |
66 | .ui.menu .item img.logo {
67 | margin-right: 1.5em;
68 | }
69 |
70 | .ui.fixed.menu {
71 | background-image: linear-gradient(
72 | 135deg,
73 | rgb(24, 42, 115) 0%,
74 | rgb(33, 138, 174) 69%,
75 | rgb(32, 167, 172) 89%
76 | ) !important;
77 | }
78 |
79 | .ui.main.container,
80 | .main.segment {
81 | margin-top: 7em;
82 | }
83 |
84 | .ui.center.aligned.segment.attendance-preview {
85 | background-color: #f5f5f5;
86 | }
87 |
88 | .masthead .ui.menu .ui.button,
89 | .ui.menu a.ui.inverted.button {
90 | margin-left: 0.5em;
91 | }
92 |
93 | .ui.menu .item>img:not(.ui) {
94 | margin-right: 1.5em !important;
95 | }
96 |
97 | .ui.menu:not(.vertical) .item>.button {
98 | margin-left: 0.5em;
99 | }
100 |
101 | /*chat comments*/
102 |
103 | .ui.comments .comment .comments {
104 | padding-bottom: 0 !important;
105 | padding-left: 2em !important;
106 | }
107 |
--------------------------------------------------------------------------------
/src/features/user/Settings/BasicPage.jsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import { Segment, Form, Header, Divider, Button } from 'semantic-ui-react';
3 | import { Field, reduxForm } from 'redux-form';
4 | import moment from 'moment';
5 | import DateInput from '../../../app/common/form/DateInput';
6 | import PlaceInput from '../../../app/common/form/PlaceInput';
7 | import TextInput from '../../../app/common/form/TextInput';
8 | import RadioInput from '../../../app/common/form/RadioInput';
9 |
10 | class BasicPage extends Component {
11 | render() {
12 | const { pristine, submitting, handleSubmit, updateProfile } = this.props;
13 | return (
14 |
15 |
16 |
25 |
26 |
27 |
28 |
29 |
40 |
48 |
49 |
55 |
56 |
57 | );
58 | }
59 | }
60 |
61 | export default reduxForm({ form: 'userProfile', enableReinitialize: true, destroyOnUnmount: false })(BasicPage);
62 |
--------------------------------------------------------------------------------
/src/app/layout/App.jsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import { Container } from 'semantic-ui-react';
3 | import { Route, Switch } from 'react-router-dom';
4 | import EventDashboard from '../../features/event/EventDashboard/EventDashboard';
5 | import NavBar from '../../features/nav/NavBar/NavBar';
6 | import HomePage from '../../features/home/HomePage';
7 | import EventDetailedPage from '../../features/event/EventDetailed/EventDetailedPage';
8 | import PeopleDashboard from '../../features/user/PeopleDashboard/PeopleDashboard';
9 | import UserDetailedPage from '../../features/user/UserDetailed/UserDetailedPage';
10 | import SettingsDashboard from '../../features/user/Settings/SettingsDashboard';
11 | import EventForm from '../../features/event/EventForm/EventForm';
12 | import TestComponent from '../../features/testarea/TestComponent';
13 | import ModalManager from '../../features/modals/ModalManager';
14 | import { UserIsAuthenticated } from '../../features/auth/authWrapper';
15 | import NotFound from '../../app/layout/NotFound'
16 |
17 | class App extends Component {
18 | render() {
19 | return (
20 |
21 |
22 |
23 |
24 |
25 |
26 |
(
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 | )}
47 | />
48 |
49 | );
50 | }
51 | }
52 |
53 | export default App;
54 |
--------------------------------------------------------------------------------
/src/features/event/EventDetailed/EventDetailedHeader.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Segment, Image, Item, Header, Button, Label } from 'semantic-ui-react';
3 | import format from 'date-fns/format';
4 | import { Link } from 'react-router-dom';
5 |
6 | const eventImageStyle = {
7 | filter: 'brightness(30%)'
8 | };
9 |
10 | const eventImageTextStyle = {
11 | position: 'absolute',
12 | bottom: '5%',
13 | left: '5%',
14 | width: '100%',
15 | height: 'auto',
16 | color: 'white'
17 | };
18 |
19 | const EventDetailedHeader = ({
20 | loading,
21 | event,
22 | isHost,
23 | isGoing,
24 | goingToEvent,
25 | cancelGoingToEvent,
26 | authenticated,
27 | openModal
28 | }) => {
29 | return (
30 |
31 |
32 |
33 |
34 |
35 |
36 | -
37 |
38 |
39 |
{format(event.date, 'dddd Do MMMM')}
40 |
41 | Hosted by {event.hostedBy}
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 | {!isHost && (
51 |
52 | {isGoing && !event.cancelled && cancelGoingToEvent(event)}>Cancel My Place}
53 |
54 | {!isGoing && authenticated && !event.cancelled &&
55 | goingToEvent(event)} color="teal">
56 | JOIN THIS EVENT
57 | }
58 |
59 | {!authenticated && !event.cancelled &&
60 | openModal('UnauthModal')} color="teal">
61 | JOIN THIS EVENT
62 | }
63 |
64 | {event.cancelled && !isHost &&
65 | }
66 |
67 | )}
68 | {isHost && (
69 |
70 | Manage Event
71 |
72 | )}
73 |
74 |
75 | );
76 | };
77 |
78 | export default EventDetailedHeader;
79 |
--------------------------------------------------------------------------------
/src/features/testarea/TestComponent.jsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import { Button } from 'semantic-ui-react';
3 | import { connect } from 'react-redux';
4 | import Script from 'react-load-script';
5 | import PlacesAutocomplete, { geocodeByAddress, getLatLng } from 'react-places-autocomplete';
6 | import { incrementAsync, decrementAsync } from './testActions';
7 | import { openModal } from '../modals/modalActions'
8 |
9 | const mapState = state => ({
10 | data: state.test.data,
11 | loading: state.test.loading
12 | });
13 |
14 | const actions = {
15 | incrementAsync,
16 | decrementAsync,
17 | openModal
18 | };
19 |
20 | class TestComponent extends Component {
21 | static defaultProps = {
22 | center: {
23 | lat: 59.95,
24 | lng: 30.33
25 | },
26 | zoom: 11
27 | };
28 |
29 |
30 | state = {
31 | address: '',
32 | scriptLoaded: false
33 | };
34 |
35 | handleScriptLoad = () => {
36 | this.setState({scriptLoaded: true})
37 | }
38 |
39 | handleFormSubmit = event => {
40 | event.preventDefault();
41 |
42 | geocodeByAddress(this.state.address)
43 | .then(results => getLatLng(results[0]))
44 | .then(latLng => console.log('Success', latLng))
45 | .catch(error => console.error('Error', error));
46 | };
47 |
48 | onChange = address => this.setState({ address });
49 |
50 | render() {
51 | const inputProps = {
52 | value: this.state.address,
53 | onChange: this.onChange
54 | };
55 | const { incrementAsync, decrementAsync, data, openModal, loading } = this.props;
56 | return (
57 |
58 |
62 |
Test Area
63 |
The answer is: {data}
64 |
65 |
66 |
openModal('TestModal', {data: 34})} color="teal" content="Open Modal" />
67 |
68 |
69 |
74 |
75 | );
76 | }
77 | }
78 |
79 | export default connect(mapState, actions)(TestComponent);
80 |
--------------------------------------------------------------------------------
/src/features/auth/authActions.jsx:
--------------------------------------------------------------------------------
1 | import { SubmissionError, reset } from 'redux-form';
2 | import { toastr } from 'react-redux-toastr'
3 | import { closeModal } from '../modals/modalActions';
4 |
5 | export const login = creds => {
6 | return async (dispatch, getState, { getFirebase }) => {
7 | const firebase = getFirebase();
8 | try {
9 | await firebase.auth().signInWithEmailAndPassword(creds.email, creds.password);
10 | dispatch(closeModal());
11 | } catch (error) {
12 | console.log(error);
13 | throw new SubmissionError({
14 | _error: 'Login Failed'
15 | });
16 | }
17 | };
18 | };
19 |
20 | export const registerUser = user => async (dispatch, getState, { getFirebase, getFirestore }) => {
21 | const firebase = getFirebase();
22 | const firestore = getFirestore();
23 | try {
24 | // create the user in firebase auth
25 | let createdUser = await firebase
26 | .auth()
27 | .createUserWithEmailAndPassword(user.email, user.password);
28 | console.log(createdUser);
29 | // update the auth profile
30 | await createdUser.updateProfile({
31 | displayName: user.displayName
32 | })
33 | // create a new profile in firestore
34 | let newUser = {
35 | displayName: user.displayName,
36 | createdAt: firestore.FieldValue.serverTimestamp()
37 | }
38 | await firestore.set(`users/${createdUser.uid}`, {...newUser})
39 | dispatch(closeModal())
40 | } catch (error) {
41 | console.log(error);
42 | throw new SubmissionError({
43 | _error: error.message
44 | });
45 | }
46 | };
47 |
48 | export const socialLogin = (selectedProvider) =>
49 | async (dispatch, getState, {getFirebase, getFirestore}) => {
50 | const firebase = getFirebase();
51 | const firestore = getFirestore();
52 | try {
53 | dispatch(closeModal());
54 | let user = await firebase.login({
55 | provider: selectedProvider,
56 | type: 'popup'
57 | })
58 | if (user.additionalUserInfo.isNewUser) {
59 | await firestore.set(`users/${user.user.uid}`, {
60 | displayName: user.profile.displayName,
61 | photoURL: user.profile.avatarUrl,
62 | createdAt: firestore.FieldValue.serverTimestamp()
63 | })
64 | }
65 | } catch (error) {
66 | console.log(error)
67 | }
68 | }
69 |
70 | export const updatePassword = (creds) =>
71 | async (dispatch, getState, {getFirebase}) => {
72 | const firebase = getFirebase();
73 | const user = firebase.auth().currentUser;
74 | try {
75 | await user.updatePassword(creds.newPassword1);
76 | await dispatch(reset('account'));
77 | toastr.success('Success!', 'Your password has been updated');
78 | } catch (error) {
79 | throw new SubmissionError({
80 | _error: error.message
81 | })
82 | }
83 |
84 | }
--------------------------------------------------------------------------------
/src/features/user/Settings/AboutPage.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Button, Divider, Form, Header, Segment } from 'semantic-ui-react';
3 | import { Field, reduxForm } from 'redux-form';
4 | import RadioInput from '../../../app/common/form/RadioInput';
5 | import TextInput from '../../../app/common/form/TextInput';
6 | import TextArea from '../../../app/common/form/TextArea';
7 | import PlaceInput from '../../../app/common/form/PlaceInput';
8 | import SelectInput from '../../../app/common/form/SelectInput';
9 |
10 | const interests = [
11 | { key: 'drinks', text: 'Drinks', value: 'drinks' },
12 | { key: 'culture', text: 'Culture', value: 'culture' },
13 | { key: 'film', text: 'Film', value: 'film' },
14 | { key: 'food', text: 'Food', value: 'food' },
15 | { key: 'music', text: 'Music', value: 'music' },
16 | { key: 'travel', text: 'Travel', value: 'travel' }
17 | ];
18 |
19 | const AboutPage = ({ pristine, submitting, handleSubmit, updateProfile }) => {
20 | return (
21 |
22 |
23 | Complete your profile to get the most out of this site
24 |
26 |
27 |
28 |
35 |
42 |
43 |
44 |
45 |
46 |
54 |
61 |
68 |
69 |
70 |
71 |
72 | );
73 | };
74 |
75 | export default reduxForm({ form: 'userProfile', enableReinitialize: true, destroyOnUnmount: false })(AboutPage);
--------------------------------------------------------------------------------
/src/features/user/Settings/AccountPage.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Segment, Header, Form, Divider, Label, Button, Icon } from 'semantic-ui-react';
3 | import { combineValidators, matchesField, isRequired, composeValidators } from 'revalidate';
4 | import { Field, reduxForm } from 'redux-form';
5 | import TextInput from '../../../app/common/form/TextInput';
6 |
7 | const validate = combineValidators({
8 | newPassword1: isRequired({message: 'Please enter a password'}),
9 | newPassword2: composeValidators(
10 | isRequired({message: 'Please confirm your new password'}),
11 | matchesField('newPassword1')({message: 'Passwords do not match'})
12 | )()
13 | })
14 |
15 | const AccountPage = ({ error, invalid, submitting, handleSubmit, updatePassword, providerId }) => {
16 | return (
17 |
18 |
19 | {providerId && providerId === 'password' &&
20 |
21 |
22 |
Use this form to update your account settings
23 |
52 |
}
53 |
54 | {providerId && providerId === 'facebook.com' &&
55 |
56 |
57 |
Please visit Facebook to update your account settings
58 |
59 |
60 | Go to Facebook
61 |
62 |
}
63 |
64 | {providerId && providerId === 'google.com' &&
65 |
66 |
67 |
Please visit Google to update your account settings
68 |
69 |
70 | Go to Google
71 |
72 |
}
73 |
74 | );
75 | };
76 |
77 | export default reduxForm({ form: 'account', validate })(AccountPage);
--------------------------------------------------------------------------------
/src/features/event/EventDashboard/EventDashboard.jsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import { Grid, Loader } from 'semantic-ui-react';
3 | import { firestoreConnect } from 'react-redux-firebase';
4 | import { connect } from 'react-redux';
5 | import EventList from '../EventList/EventList';
6 | import { getEventsForDashboard } from '../eventActions';
7 | import LoadingComponent from '../../../app/layout/LoadingComponent';
8 | import EventActivity from '../EventActivity/EventActivity';
9 |
10 | const query = [
11 | {
12 | collection: 'activity',
13 | orderBy: ['timestamp', 'desc'],
14 | limit: 5
15 | }
16 | ];
17 |
18 | const mapState = state => ({
19 | events: state.events,
20 | loading: state.async.loading,
21 | activities: state.firestore.ordered.activity
22 | });
23 |
24 | const actions = {
25 | getEventsForDashboard
26 | };
27 |
28 | class EventDashboard extends Component {
29 | state = {
30 | moreEvents: false,
31 | loadingInitial: true,
32 | loadedEvents: [],
33 | contextRef: {}
34 | };
35 |
36 | async componentDidMount() {
37 | let next = await this.props.getEventsForDashboard();
38 | if (next && next.docs && next.docs.length > 1) {
39 | this.setState({
40 | moreEvents: true,
41 | loadingInitial: false
42 | });
43 | }
44 | }
45 |
46 | componentWillReceiveProps(nextProps) {
47 | if (this.props.events !== nextProps.events) {
48 | this.setState({
49 | loadedEvents: [...this.state.loadedEvents, ...nextProps.events]
50 | });
51 | }
52 | }
53 |
54 | getNextEvents = async () => {
55 | const { events } = this.props;
56 | let lastEvent = events && events[events.length - 1];
57 | let next = await this.props.getEventsForDashboard(lastEvent);
58 | if (next && next.docs && next.docs.length <= 1) {
59 | this.setState({
60 | moreEvents: false
61 | });
62 | }
63 | };
64 |
65 | handleContextRef = contextRef => this.setState({contextRef})
66 |
67 | render() {
68 | const { loading, activities } = this.props;
69 | const { moreEvents, loadedEvents } = this.state;
70 |
71 | if (this.state.loadingInitial) return ;
72 |
73 | return (
74 |
75 |
76 |
77 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 | );
93 | }
94 | }
95 |
96 | export default connect(mapState, actions)(firestoreConnect(query)(EventDashboard));
97 |
--------------------------------------------------------------------------------
/src/features/user/UserDetailed/UserDetailedPage.jsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import { connect } from 'react-redux';
3 | import { firestoreConnect, isEmpty } from 'react-redux-firebase';
4 | import { compose } from 'redux';
5 | import { Grid } from 'semantic-ui-react';
6 | import { toastr } from 'react-redux-toastr'
7 | import UserDetailedDescription from './UserDetailedDescription';
8 | import UserDetailedEvents from './UserDetailedEvents';
9 | import UserDetailedHeader from './UserDetailedHeader';
10 | import UserDetailedPhotos from './UserDetailedPhotos';
11 | import UserDetailedSidebar from './UserDetailedSidebar';
12 | import { userDetailedQuery } from '../userQueries';
13 | import LoadingComponent from '../../../app/layout/LoadingComponent';
14 | import { getUserEvents, followUser, unfollowUser } from '../../user/userActions';
15 |
16 | const mapState = (state, ownProps) => {
17 | let userUid = null;
18 | let profile = {};
19 |
20 | if (ownProps.match.params.id === state.auth.uid) {
21 | profile = state.firebase.profile;
22 | } else {
23 | profile = !isEmpty(state.firestore.ordered.profile) && state.firestore.ordered.profile[0];
24 | userUid = ownProps.match.params.id;
25 | }
26 |
27 | return {
28 | profile,
29 | userUid,
30 | events: state.events,
31 | eventsLoading: state.async.loading,
32 | auth: state.firebase.auth,
33 | photos: state.firestore.ordered.photos,
34 | requesting: state.firestore.status.requesting,
35 | following: state.firestore.ordered.following
36 | };
37 | };
38 |
39 | const actions = {
40 | getUserEvents,
41 | followUser,
42 | unfollowUser
43 | };
44 |
45 | class UserDetailedPage extends Component {
46 | async componentDidMount() {
47 | let user = await this.props.firestore.get(`users/${this.props.match.params.id}`);
48 | if (!user.exists) {
49 | toastr.error('Not found', 'This is not the user you are looking for')
50 | this.props.history.push('/error');
51 | }
52 | await this.props.getUserEvents(this.props.userUid);
53 | }
54 |
55 | changeTab = (e, data) => {
56 | this.props.getUserEvents(this.props.userUid, data.activeIndex);
57 | };
58 |
59 | render() {
60 | const {
61 | profile,
62 | photos,
63 | auth,
64 | match,
65 | requesting,
66 | events,
67 | eventsLoading,
68 | followUser,
69 | following,
70 | unfollowUser
71 | } = this.props;
72 | const isCurrentUser = auth.uid === match.params.id;
73 | const loading = requesting[`users/${match.params.id}`];
74 | const isFollowing = !isEmpty(following);
75 |
76 | if (loading) return ;
77 |
78 | return (
79 |
80 |
81 |
82 |
89 | {photos && photos.length > 0 && }
90 |
95 |
96 | );
97 | }
98 | }
99 |
100 | export default compose(
101 | connect(mapState, actions),
102 | firestoreConnect((auth, userUid, match) => userDetailedQuery(auth, userUid, match))
103 | )(UserDetailedPage);
104 |
--------------------------------------------------------------------------------
/functions/index.js:
--------------------------------------------------------------------------------
1 | const functions = require('firebase-functions');
2 | const admin = require('firebase-admin');
3 | admin.initializeApp(functions.config().firebase);
4 |
5 | const newActivity = (type, event, id) => {
6 | return {
7 | type: type,
8 | eventDate: event.date,
9 | hostedBy: event.hostedBy,
10 | title: event.title,
11 | photoURL: event.hostPhotoURL,
12 | timestamp: admin.firestore.FieldValue.serverTimestamp(),
13 | hostUid: event.hostUid,
14 | eventId: id
15 | };
16 | };
17 |
18 | exports.createActivity = functions.firestore.document('events/{eventId}').onCreate(event => {
19 | let newEvent = event.data();
20 |
21 | console.log(newEvent);
22 |
23 | const activity = newActivity('newEvent', newEvent, event.id);
24 |
25 | console.log(activity);
26 |
27 | return admin
28 | .firestore()
29 | .collection('activity')
30 | .add(activity)
31 | .then(docRef => {
32 | return console.log('Activity created with ID: ', docRef.id);
33 | })
34 | .catch(err => {
35 | return console.log('Error adding activity', err);
36 | });
37 | });
38 |
39 | exports.cancelActivity = functions.firestore
40 | .document('events/{eventId}')
41 | .onUpdate((event, context) => {
42 | let updatedEvent = event.after.data();
43 | let previousEventData = event.before.data();
44 | console.log({ event });
45 | console.log({ context });
46 | console.log({ updatedEvent });
47 | console.log({ previousEventData });
48 |
49 | if (!updatedEvent.cancelled || updatedEvent.cancelled === previousEventData.cancelled)
50 | return false;
51 |
52 | const activity = newActivity('cancelledEvent', updatedEvent, context.params.eventId);
53 |
54 | console.log({ activity });
55 |
56 | return admin
57 | .firestore()
58 | .collection('activity')
59 | .add(activity)
60 | .then(docRef => {
61 | return console.log('Activity created with ID: ', docRef.id);
62 | })
63 | .catch(err => {
64 | return console.log('Error adding activity', err);
65 | });
66 | });
67 |
68 | exports.userFollowing = functions.firestore
69 | .document('users/{followerUid}/following/{followingUid}')
70 | .onCreate((event, context) => {
71 | console.log('v1');
72 | const followerUid = context.params.followerUid;
73 | const followingUid = context.params.followingUid;
74 |
75 | const followerDoc = admin
76 | .firestore()
77 | .collection('users')
78 | .doc(followerUid);
79 |
80 | console.log(followerDoc);
81 |
82 | return followerDoc.get().then(doc => {
83 | let userData = doc.data();
84 | console.log({ userData });
85 | let follower = {
86 | displayName: userData.displayName,
87 | photoURL: userData.photoURL || '/assets/user.png',
88 | city: userData.city || 'unknown city'
89 | };
90 | return admin
91 | .firestore()
92 | .collection('users')
93 | .doc(followingUid)
94 | .collection('followers')
95 | .doc(followerUid)
96 | .set(follower);
97 | });
98 | });
99 |
100 | exports.unfollowUser = functions.firestore
101 | .document('users/{followerUid}/following/{followingUid}')
102 | .onDelete((event, context) => {
103 | return admin
104 | .firestore()
105 | .collection('users')
106 | .doc(context.params.followingUid)
107 | .collection('followers')
108 | .doc(context.params.followerUid)
109 | .delete()
110 | .then(() => {
111 | return console.log('doc deleted');
112 | })
113 | .catch(err => {
114 | return console.log(err);
115 | });
116 | });
117 |
--------------------------------------------------------------------------------
/functions/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "parserOptions": {
3 | // Required for certain syntax usages
4 | "ecmaVersion": 6
5 | },
6 | "plugins": [
7 | "promise"
8 | ],
9 | "extends": "eslint:recommended",
10 | "rules": {
11 | // Removed rule "disallow the use of console" from recommended eslint rules
12 | "no-console": "off",
13 |
14 | // Removed rule "disallow multiple spaces in regular expressions" from recommended eslint rules
15 | "no-regex-spaces": "off",
16 |
17 | // Removed rule "disallow the use of debugger" from recommended eslint rules
18 | "no-debugger": "off",
19 |
20 | // Removed rule "disallow unused variables" from recommended eslint rules
21 | "no-unused-vars": "off",
22 |
23 | // Removed rule "disallow mixed spaces and tabs for indentation" from recommended eslint rules
24 | "no-mixed-spaces-and-tabs": "off",
25 |
26 | // Removed rule "disallow the use of undeclared variables unless mentioned in /*global */ comments" from recommended eslint rules
27 | "no-undef": "off",
28 |
29 | // Warn against template literal placeholder syntax in regular strings
30 | "no-template-curly-in-string": 1,
31 |
32 | // Warn if return statements do not either always or never specify values
33 | "consistent-return": 1,
34 |
35 | // Warn if no return statements in callbacks of array methods
36 | "array-callback-return": 1,
37 |
38 | // Require the use of === and !==
39 | "eqeqeq": 2,
40 |
41 | // Disallow the use of alert, confirm, and prompt
42 | "no-alert": 2,
43 |
44 | // Disallow the use of arguments.caller or arguments.callee
45 | "no-caller": 2,
46 |
47 | // Disallow null comparisons without type-checking operators
48 | "no-eq-null": 2,
49 |
50 | // Disallow the use of eval()
51 | "no-eval": 2,
52 |
53 | // Warn against extending native types
54 | "no-extend-native": 1,
55 |
56 | // Warn against unnecessary calls to .bind()
57 | "no-extra-bind": 1,
58 |
59 | // Warn against unnecessary labels
60 | "no-extra-label": 1,
61 |
62 | // Disallow leading or trailing decimal points in numeric literals
63 | "no-floating-decimal": 2,
64 |
65 | // Warn against shorthand type conversions
66 | "no-implicit-coercion": 1,
67 |
68 | // Warn against function declarations and expressions inside loop statements
69 | "no-loop-func": 1,
70 |
71 | // Disallow new operators with the Function object
72 | "no-new-func": 2,
73 |
74 | // Warn against new operators with the String, Number, and Boolean objects
75 | "no-new-wrappers": 1,
76 |
77 | // Disallow throwing literals as exceptions
78 | "no-throw-literal": 2,
79 |
80 | // Require using Error objects as Promise rejection reasons
81 | "prefer-promise-reject-errors": 2,
82 |
83 | // Enforce “for” loop update clause moving the counter in the right direction
84 | "for-direction": 2,
85 |
86 | // Enforce return statements in getters
87 | "getter-return": 2,
88 |
89 | // Disallow await inside of loops
90 | "no-await-in-loop": 2,
91 |
92 | // Disallow comparing against -0
93 | "no-compare-neg-zero": 2,
94 |
95 | // Warn against catch clause parameters from shadowing variables in the outer scope
96 | "no-catch-shadow": 1,
97 |
98 | // Disallow identifiers from shadowing restricted names
99 | "no-shadow-restricted-names": 2,
100 |
101 | // Enforce return statements in callbacks of array methods
102 | "callback-return": 2,
103 |
104 | // Require error handling in callbacks
105 | "handle-callback-err": 2,
106 |
107 | // Warn against string concatenation with __dirname and __filename
108 | "no-path-concat": 1,
109 |
110 | // Prefer using arrow functions for callbacks
111 | "prefer-arrow-callback": 1,
112 |
113 | // Return inside each then() to create readable and reusable Promise chains.
114 | // Forces developers to return console logs and http calls in promises.
115 | "promise/always-return": 2,
116 |
117 | //Enforces the use of catch() on un-returned promises
118 | "promise/catch-or-return": 2,
119 |
120 | // Warn against nested then() or catch() statements
121 | "promise/no-nesting": 1
122 | }
123 | }
124 |
--------------------------------------------------------------------------------
/src/features/event/EventDetailed/EventDetailedPage.jsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import { Grid } from 'semantic-ui-react';
3 | import { connect } from 'react-redux';
4 | import { toastr } from 'react-redux-toastr'
5 | import { withFirestore, firebaseConnect, isEmpty } from 'react-redux-firebase';
6 | import { compose } from 'redux';
7 | import EventDetailedHeader from './EventDetailedHeader';
8 | import EventDetailedInfo from './EventDetailedInfo';
9 | import EventDetailedChat from './EventDetailedChat';
10 | import EventDetailedSidebar from './EventDetailedSidebar';
11 | import { objectToArray, createDataTree } from '../../../app/common/util/helpers';
12 | import { goingToEvent, cancelGoingToEvent } from '../../user/userActions';
13 | import { addEventComment } from '../eventActions';
14 | import { openModal } from '../../modals/modalActions';
15 | import LoadingComponent from '../../../app/layout/LoadingComponent'
16 |
17 | const mapState = (state, ownProps) => {
18 | let event = {};
19 |
20 | if (state.firestore.ordered.events && state.firestore.ordered.events[0]) {
21 | event = state.firestore.ordered.events[0];
22 | }
23 |
24 | return {
25 | requesting: state.firestore.status.requesting,
26 | event,
27 | loading: state.async.loading,
28 | auth: state.firebase.auth,
29 | eventChat:
30 | !isEmpty(state.firebase.data.event_chat) &&
31 | objectToArray(state.firebase.data.event_chat[ownProps.match.params.id])
32 | };
33 | };
34 |
35 | const actions = {
36 | goingToEvent,
37 | cancelGoingToEvent,
38 | addEventComment,
39 | openModal
40 | };
41 |
42 | class EventDetailedPage extends Component {
43 |
44 | state = {
45 | initialLoading: true
46 | }
47 |
48 | async componentDidMount() {
49 | const { firestore, match } = this.props;
50 | let event = await firestore.get(`events/${match.params.id}`);
51 | if (!event.exists) {
52 | toastr.error('Not Found', 'This is not the event are looking for');
53 | this.props.history.push('/error');
54 | }
55 | await firestore.setListener(`events/${match.params.id}`);
56 | this.setState({
57 | initialLoading: false
58 | })
59 | }
60 |
61 | async componentWillUnmount() {
62 | const { firestore, match } = this.props;
63 | await firestore.unsetListener(`events/${match.params.id}`);
64 | }
65 |
66 | render() {
67 | const {
68 | openModal,
69 | event,
70 | auth,
71 | goingToEvent,
72 | cancelGoingToEvent,
73 | addEventComment,
74 | eventChat,
75 | loading,
76 | requesting,
77 | match
78 | } = this.props;
79 | const attendees = event && event.attendees && objectToArray(event.attendees).sort(function(a, b) {
80 | return a.joinDate - b.joinDate
81 | })
82 | const isHost = event.hostUid === auth.uid;
83 | const isGoing = attendees && attendees.some(a => a.id === auth.uid);
84 | const chatTree = !isEmpty(eventChat) && createDataTree(eventChat);
85 | const authenticated = auth.isLoaded && !auth.isEmpty
86 | const loadingEvent = requesting[`events/${match.params.id}`]
87 |
88 | if (loadingEvent || this.state.initialLoading) return
89 |
90 | return (
91 |
92 |
93 |
103 |
104 | {authenticated &&
105 | }
110 |
111 |
112 |
113 |
114 |
115 | );
116 | }
117 | }
118 |
119 | export default compose(
120 | withFirestore,
121 | connect(mapState, actions),
122 | firebaseConnect(props => [`event_chat/${props.match.params.id}`])
123 | )(EventDetailedPage);
124 |
--------------------------------------------------------------------------------
/src/registerServiceWorker.js:
--------------------------------------------------------------------------------
1 | // In production, we register a service worker to serve assets from local cache.
2 |
3 | // This lets the app load faster on subsequent visits in production, and gives
4 | // it offline capabilities. However, it also means that developers (and users)
5 | // will only see deployed updates on the "N+1" visit to a page, since previously
6 | // cached resources are updated in the background.
7 |
8 | // To learn more about the benefits of this model, read https://goo.gl/KwvDNy.
9 | // This link also includes instructions on opting out of this behavior.
10 |
11 | const isLocalhost = Boolean(
12 | window.location.hostname === 'localhost' ||
13 | // [::1] is the IPv6 localhost address.
14 | window.location.hostname === '[::1]' ||
15 | // 127.0.0.1/8 is considered localhost for IPv4.
16 | window.location.hostname.match(
17 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
18 | )
19 | );
20 |
21 | export default function register() {
22 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
23 | // The URL constructor is available in all browsers that support SW.
24 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location);
25 | if (publicUrl.origin !== window.location.origin) {
26 | // Our service worker won't work if PUBLIC_URL is on a different origin
27 | // from what our page is served on. This might happen if a CDN is used to
28 | // serve assets; see https://github.com/facebookincubator/create-react-app/issues/2374
29 | return;
30 | }
31 |
32 | window.addEventListener('load', () => {
33 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
34 |
35 | if (isLocalhost) {
36 | // This is running on localhost. Lets check if a service worker still exists or not.
37 | checkValidServiceWorker(swUrl);
38 |
39 | // Add some additional logging to localhost, pointing developers to the
40 | // service worker/PWA documentation.
41 | navigator.serviceWorker.ready.then(() => {
42 | console.log(
43 | 'This web app is being served cache-first by a service ' +
44 | 'worker. To learn more, visit https://goo.gl/SC7cgQ'
45 | );
46 | });
47 | } else {
48 | // Is not local host. Just register service worker
49 | registerValidSW(swUrl);
50 | }
51 | });
52 | }
53 | }
54 |
55 | function registerValidSW(swUrl) {
56 | navigator.serviceWorker
57 | .register(swUrl)
58 | .then(registration => {
59 | registration.onupdatefound = () => {
60 | const installingWorker = registration.installing;
61 | installingWorker.onstatechange = () => {
62 | if (installingWorker.state === 'installed') {
63 | if (navigator.serviceWorker.controller) {
64 | // At this point, the old content will have been purged and
65 | // the fresh content will have been added to the cache.
66 | // It's the perfect time to display a "New content is
67 | // available; please refresh." message in your web app.
68 | console.log('New content is available; please refresh.');
69 | } else {
70 | // At this point, everything has been precached.
71 | // It's the perfect time to display a
72 | // "Content is cached for offline use." message.
73 | console.log('Content is cached for offline use.');
74 | }
75 | }
76 | };
77 | };
78 | })
79 | .catch(error => {
80 | console.error('Error during service worker registration:', error);
81 | });
82 | }
83 |
84 | function checkValidServiceWorker(swUrl) {
85 | // Check if the service worker can be found. If it can't reload the page.
86 | fetch(swUrl)
87 | .then(response => {
88 | // Ensure service worker exists, and that we really are getting a JS file.
89 | if (
90 | response.status === 404 ||
91 | response.headers.get('content-type').indexOf('javascript') === -1
92 | ) {
93 | // No service worker found. Probably a different app. Reload the page.
94 | navigator.serviceWorker.ready.then(registration => {
95 | registration.unregister().then(() => {
96 | window.location.reload();
97 | });
98 | });
99 | } else {
100 | // Service worker found. Proceed as normal.
101 | registerValidSW(swUrl);
102 | }
103 | })
104 | .catch(() => {
105 | console.log(
106 | 'No internet connection found. App is running in offline mode.'
107 | );
108 | });
109 | }
110 |
111 | export function unregister() {
112 | if ('serviceWorker' in navigator) {
113 | navigator.serviceWorker.ready.then(registration => {
114 | registration.unregister();
115 | });
116 | }
117 | }
118 |
--------------------------------------------------------------------------------
/src/features/event/EventDetailed/EventDetailedChat.jsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import { Segment, Header, Comment } from 'semantic-ui-react';
3 | import EventDetailedChatForm from './EventDetailedChatForm';
4 | import { Link } from 'react-router-dom';
5 | import distanceInWords from 'date-fns/distance_in_words';
6 |
7 | class EventDetailedChat extends Component {
8 | state = {
9 | showReplyForm: false,
10 | selectedCommentId: null
11 | };
12 |
13 | handleCloseReplyForm = () => {
14 | this.setState({
15 | selectedCommentId: null,
16 | showReplyForm: false
17 | });
18 | };
19 |
20 | handleOpenReplyForm = id => () => {
21 | this.setState({
22 | showReplyForm: true,
23 | selectedCommentId: id
24 | });
25 | };
26 |
27 | render() {
28 | const { addEventComment, eventId, eventChat } = this.props;
29 | const { showReplyForm, selectedCommentId } = this.state;
30 | return (
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 | {eventChat &&
39 | eventChat.map(comment => (
40 |
41 |
42 |
43 |
44 | {comment.displayName}
45 |
46 |
47 | {distanceInWords(comment.date, Date.now())} ago
48 |
49 | {comment.text}
50 |
51 |
52 | Reply
53 |
54 | {showReplyForm &&
55 | selectedCommentId === comment.id && (
56 |
63 | )}
64 |
65 |
66 |
67 | {comment.childNodes &&
68 | comment.childNodes.map(child => (
69 |
70 |
71 |
72 |
73 |
74 | {child.displayName}
75 |
76 |
77 | {distanceInWords(child.date, Date.now())} ago
78 |
79 | {child.text}
80 |
81 |
82 | Reply
83 |
84 | {showReplyForm &&
85 | selectedCommentId === child.id && (
86 |
93 | )}
94 |
95 |
96 |
97 |
98 | ))}
99 |
100 | ))}
101 |
102 |
108 |
109 |
110 | );
111 | }
112 | }
113 |
114 | export default EventDetailedChat;
115 |
--------------------------------------------------------------------------------
/src/features/event/eventActions.jsx:
--------------------------------------------------------------------------------
1 | import moment from 'moment';
2 | import { toastr } from 'react-redux-toastr';
3 | import { createNewEvent } from '../../app/common/util/helpers';
4 | import firebase from '../../app/config/firebase';
5 | import { asyncActionError, asyncActionFinish, asyncActionStart } from '../async/asyncActions';
6 | import { FETCH_EVENTS } from './eventConstants';
7 | import compareAsc from 'date-fns/compare_asc';
8 |
9 | export const createEvent = event => {
10 | return async (dispatch, getState, { getFirestore }) => {
11 | const firestore = getFirestore();
12 | const user = firestore.auth().currentUser;
13 | const photoURL = getState().firebase.profile.photoURL;
14 | let newEvent = createNewEvent(user, photoURL, event);
15 | try {
16 | let createdEvent = await firestore.add(`events`, newEvent);
17 | await firestore.set(`event_attendee/${createdEvent.id}_${user.uid}`, {
18 | eventId: createdEvent.id,
19 | userUid: user.uid,
20 | eventDate: event.date,
21 | host: true
22 | });
23 | toastr.success('Success!', 'Event has been created');
24 | } catch (error) {
25 | toastr.error('Oops', 'Something went wrong');
26 | }
27 | };
28 | };
29 |
30 | export const updateEvent = event => {
31 | return async (dispatch, getState) => {
32 | dispatch(asyncActionStart());
33 | const firestore = firebase.firestore();
34 | event.date = moment(event.date).toDate();
35 | try {
36 | let eventDocRef = firestore.collection('events').doc(event.id);
37 | let dateEqual = compareAsc(getState().firestore.ordered.events[0].date, event.date);
38 | if (dateEqual !== 0) {
39 | let batch = firestore.batch();
40 | await batch.update(eventDocRef, event);
41 |
42 | let eventAttendeeRef = firestore.collection('event_attendee');
43 | let eventAttendeeQuery = await eventAttendeeRef.where('eventId', '==', event.id);
44 | let eventAttendeeQuerySnap = await eventAttendeeQuery.get();
45 |
46 | for (let i = 0; i < eventAttendeeQuerySnap.docs.length; i++) {
47 | let eventAttendeeDocRef = await firestore
48 | .collection('event_attendee')
49 | .doc(eventAttendeeQuerySnap.docs[i].id);
50 |
51 | await batch.update(eventAttendeeDocRef, {
52 | eventDate: event.date
53 | })
54 | }
55 | await batch.commit();
56 | } else {
57 | await eventDocRef.update(event);
58 | }
59 | dispatch(asyncActionFinish());
60 | toastr.success('Success!', 'Event has been updated');
61 | } catch (error) {
62 | dispatch(asyncActionError());
63 | toastr.error('Oops', 'Something went wrong');
64 | }
65 | };
66 | };
67 |
68 | export const cancelToggle = (cancelled, eventId) => async (
69 | dispatch,
70 | getState,
71 | { getFirestore }
72 | ) => {
73 | const firestore = getFirestore();
74 | const message = cancelled
75 | ? 'Are you sure you want to cancel the event?'
76 | : 'This will reactivate the event - are you sure?Ì';
77 | try {
78 | toastr.confirm(message, {
79 | onOk: () =>
80 | firestore.update(`events/${eventId}`, {
81 | cancelled: cancelled
82 | })
83 | });
84 | } catch (error) {
85 | console.log(error);
86 | }
87 | };
88 |
89 | export const getEventsForDashboard = lastEvent => async (dispatch, getState) => {
90 | let today = new Date(Date.now());
91 | const firestore = firebase.firestore();
92 | const eventsRef = firestore.collection('events');
93 | try {
94 | dispatch(asyncActionStart());
95 | let startAfter =
96 | lastEvent &&
97 | (await firestore
98 | .collection('events')
99 | .doc(lastEvent.id)
100 | .get());
101 | let query;
102 |
103 | lastEvent
104 | ? (query = eventsRef
105 | .where('date', '>=', today)
106 | .orderBy('date')
107 | .startAfter(startAfter)
108 | .limit(2))
109 | : (query = eventsRef
110 | .where('date', '>=', today)
111 | .orderBy('date')
112 | .limit(2));
113 |
114 | let querySnap = await query.get();
115 |
116 | if (querySnap.docs.length === 0) {
117 | dispatch(asyncActionFinish());
118 | return querySnap;
119 | }
120 |
121 | let events = [];
122 |
123 | for (let i = 0; i < querySnap.docs.length; i++) {
124 | let evt = { ...querySnap.docs[i].data(), id: querySnap.docs[i].id };
125 | events.push(evt);
126 | }
127 |
128 | dispatch({ type: FETCH_EVENTS, payload: { events } });
129 | dispatch(asyncActionFinish());
130 | return querySnap;
131 | } catch (error) {
132 | console.log(error);
133 | dispatch(asyncActionError());
134 | }
135 | };
136 |
137 | export const addEventComment = (eventId, values, parentId) => async (
138 | dispatch,
139 | getState,
140 | { getFirebase }
141 | ) => {
142 | const firebase = getFirebase();
143 | const profile = getState().firebase.profile;
144 | const user = firebase.auth().currentUser;
145 | let newComment = {
146 | parentId: parentId,
147 | displayName: profile.displayName,
148 | photoURL: profile.photoURL,
149 | uid: user.uid,
150 | text: values.comment,
151 | date: Date.now()
152 | };
153 | try {
154 | await firebase.push(`event_chat/${eventId}`, newComment);
155 | } catch (error) {
156 | console.log(error);
157 | toastr.error('Oops', 'Problem adding comment');
158 | }
159 | };
160 |
--------------------------------------------------------------------------------
/src/features/user/Settings/PhotosPage.jsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import { Image, Segment, Header, Divider, Grid, Button, Card, Icon } from 'semantic-ui-react';
3 | import { toastr } from 'react-redux-toastr';
4 | import { connect } from 'react-redux';
5 | import { firestoreConnect } from 'react-redux-firebase';
6 | import { compose } from 'redux';
7 | import Dropzone from 'react-dropzone';
8 | import Cropper from 'react-cropper';
9 | import 'cropperjs/dist/cropper.css';
10 | import { uploadProfileImage, deletePhoto, setMainPhoto } from '../userActions';
11 |
12 | const query = ({ auth }) => {
13 | return [
14 | {
15 | collection: 'users',
16 | doc: auth.uid,
17 | subcollections: [{ collection: 'photos' }],
18 | storeAs: 'photos'
19 | }
20 | ];
21 | };
22 |
23 | const actions = {
24 | uploadProfileImage,
25 | deletePhoto,
26 | setMainPhoto
27 | };
28 |
29 | const mapState = state => ({
30 | auth: state.firebase.auth,
31 | profile: state.firebase.profile,
32 | photos: state.firestore.ordered.photos,
33 | loading: state.async.loading
34 | });
35 |
36 | class PhotosPage extends Component {
37 | state = {
38 | files: [],
39 | fileName: '',
40 | cropResult: null,
41 | image: {}
42 | };
43 |
44 | uploadImage = async () => {
45 | try {
46 | await this.props.uploadProfileImage(this.state.image, this.state.fileName);
47 | this.cancelCrop();
48 | toastr.success('Success!', 'Photo has been uploaded');
49 | } catch (error) {
50 | toastr.error('Oops', error.message);
51 | }
52 | };
53 |
54 | handlePhotoDelete = (photo) => async () => {
55 | try {
56 | this.props.deletePhoto(photo);
57 | } catch (error) {
58 | toastr.error('Oops', error.message)
59 | }
60 | }
61 |
62 | handleSetMainPhoto = (photo) => async () => {
63 | try {
64 | this.props.setMainPhoto(photo);
65 | } catch (error) {
66 | toastr.error('Oops', error.message)
67 | }
68 | }
69 |
70 | cancelCrop = () => {
71 | this.setState({
72 | files: [],
73 | image: {}
74 | });
75 | };
76 |
77 | cropImage = () => {
78 | if (typeof this.refs.cropper.getCroppedCanvas() === 'undefined') return;
79 |
80 | this.refs.cropper.getCroppedCanvas().toBlob(blob => {
81 | let imageUrl = URL.createObjectURL(blob);
82 | this.setState({
83 | cropResult: imageUrl,
84 | image: blob
85 | });
86 | }, 'image/jpeg');
87 | };
88 |
89 | onDrop = files => {
90 | this.setState({
91 | files,
92 | fileName: files[0].name
93 | });
94 | };
95 |
96 | render() {
97 | const { photos, profile, loading } = this.props;
98 | let filteredPhotos;
99 | if (photos) {
100 | filteredPhotos = photos.filter(photo => {
101 | return photo.url !== profile.photoURL
102 | })
103 | }
104 | return (
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 | {this.state.files[0] && (
122 |
135 | )}
136 |
137 |
138 |
139 |
140 | {this.state.files[0] && (
141 |
142 |
146 |
147 |
154 |
155 |
156 |
157 | )}
158 |
159 |
160 |
161 |
162 |
163 |
164 |
165 |
166 |
167 | Main Photo
168 |
169 | {photos &&
170 | filteredPhotos.map(photo => (
171 |
172 |
173 |
174 |
175 | Main
176 |
177 |
178 |
179 |
180 | ))}
181 |
182 |
183 | );
184 | }
185 | }
186 |
187 | export default compose(connect(mapState, actions), firestoreConnect(auth => query(auth)))(
188 | PhotosPage
189 | );
190 |
--------------------------------------------------------------------------------
/src/features/event/EventForm/EventForm.jsx:
--------------------------------------------------------------------------------
1 | /*global google*/
2 | import React, { Component } from 'react';
3 | import { Form, Segment, Button, Grid, Header } from 'semantic-ui-react';
4 | import { reduxForm, Field } from 'redux-form';
5 | import { withFirestore } from 'react-redux-firebase';
6 | import Script from 'react-load-script';
7 | import { composeValidators, combineValidators, isRequired, hasLengthGreaterThan } from 'revalidate';
8 | import { geocodeByAddress, getLatLng } from 'react-places-autocomplete';
9 | import { connect } from 'react-redux';
10 | import { createEvent, updateEvent, cancelToggle } from '../eventActions';
11 | import TextInput from '../../../app/common/form/TextInput';
12 | import TextArea from '../../../app/common/form/TextArea';
13 | import SelectInput from '../../../app/common/form/SelectInput';
14 | import DateInput from '../../../app/common/form/DateInput';
15 | import PlaceInput from '../../../app/common/form/PlaceInput';
16 |
17 | const mapState = (state, ownProps) => {
18 | let event = {};
19 |
20 | if (state.firestore.ordered.events && state.firestore.ordered.events[0]) {
21 | event = state.firestore.ordered.events[0];
22 | }
23 |
24 | return {
25 | initialValues: event,
26 | event,
27 | loading: state.async.loading
28 | };
29 | };
30 |
31 | const actions = {
32 | createEvent,
33 | updateEvent,
34 | cancelToggle
35 | };
36 |
37 | const category = [
38 | { key: 'drinks', text: 'Drinks', value: 'drinks' },
39 | { key: 'culture', text: 'Culture', value: 'culture' },
40 | { key: 'film', text: 'Film', value: 'film' },
41 | { key: 'food', text: 'Food', value: 'food' },
42 | { key: 'music', text: 'Music', value: 'music' },
43 | { key: 'travel', text: 'Travel', value: 'travel' }
44 | ];
45 |
46 | const validate = combineValidators({
47 | title: isRequired({ message: 'The event title is required' }),
48 | category: isRequired({ message: 'Please provide a category' }),
49 | description: composeValidators(
50 | isRequired({ message: 'Please enter a description' }),
51 | hasLengthGreaterThan(4)({ message: 'Description needs to be at least 5 characters' })
52 | )(),
53 | city: isRequired('city'),
54 | venue: isRequired('venue'),
55 | date: isRequired('date')
56 | });
57 |
58 | class EventForm extends Component {
59 | state = {
60 | cityLatLng: {},
61 | venueLatLng: {},
62 | scriptLoaded: false
63 | };
64 |
65 | async componentDidMount() {
66 | const {firestore, match} = this.props;
67 | await firestore.setListener(`events/${match.params.id}`);
68 | }
69 |
70 | async componentWillUnmount() {
71 | const {firestore, match} = this.props;
72 | await firestore.unsetListener(`events/${match.params.id}`);
73 | }
74 |
75 | handleScriptLoaded = () => this.setState({ scriptLoaded: true });
76 |
77 | handleCitySelect = selectedCity => {
78 | geocodeByAddress(selectedCity)
79 | .then(results => getLatLng(results[0]))
80 | .then(latlng => {
81 | this.setState({
82 | cityLatLng: latlng
83 | });
84 | })
85 | .then(() => {
86 | this.props.change('city', selectedCity);
87 | });
88 | };
89 |
90 | handleVenueSelect = selectedVenue => {
91 | geocodeByAddress(selectedVenue)
92 | .then(results => getLatLng(results[0]))
93 | .then(latlng => {
94 | this.setState({
95 | venueLatLng: latlng
96 | });
97 | })
98 | .then(() => {
99 | this.props.change('venue', selectedVenue);
100 | });
101 | };
102 |
103 | onFormSubmit = async values => {
104 | values.venueLatLng = this.state.venueLatLng;
105 | if (this.props.initialValues.id) {
106 | if (Object.keys(values.venueLatLng).length === 0) {
107 | values.venueLatLng = this.props.event.venueLatLng
108 | }
109 | await this.props.updateEvent(values);
110 | this.props.history.goBack();
111 | } else {
112 | this.props.createEvent(values);
113 | this.props.history.push('/events');
114 | }
115 | };
116 |
117 | render() {
118 | const { invalid, submitting, pristine, event, cancelToggle, loading } = this.props;
119 | return (
120 |
121 |
125 |
126 |
127 |
128 |
196 |
197 |
198 |
199 | );
200 | }
201 | }
202 |
203 | export default withFirestore(
204 | connect(mapState, actions)(
205 | reduxForm({ form: 'eventForm', enableReinitialize: true, validate })(EventForm)
206 | )
207 | );
208 |
--------------------------------------------------------------------------------
/src/features/user/userActions.jsx:
--------------------------------------------------------------------------------
1 | import moment from 'moment';
2 | import cuid from 'cuid';
3 | import { toastr } from 'react-redux-toastr';
4 | import { asyncActionStart, asyncActionFinish, asyncActionError } from '../async/asyncActions';
5 | import firebase from '../../app/config/firebase';
6 | import { FETCH_EVENTS } from '../event/eventConstants';
7 |
8 | export const updateProfile = user => async (dispatch, getState, { getFirebase }) => {
9 | const firebase = getFirebase();
10 | const { isLoaded, isEmpty, ...updatedUser } = user;
11 | if (updatedUser.dateOfBirth) {
12 | updatedUser.dateOfBirth = moment(updatedUser.dateOfBirth).toDate();
13 | }
14 |
15 | try {
16 | await firebase.updateProfile(updatedUser);
17 | toastr.success('Success!', 'Profile updated');
18 | } catch (error) {
19 | console.log(error);
20 | }
21 | };
22 |
23 | export const uploadProfileImage = (file, fileName) => async (
24 | dispatch,
25 | getState,
26 | { getFirebase, getFirestore }
27 | ) => {
28 | const imageName = cuid();
29 | const firebase = getFirebase();
30 | const firestore = getFirestore();
31 | const user = firebase.auth().currentUser;
32 | const path = `${user.uid}/user_images`;
33 | const options = {
34 | name: imageName
35 | };
36 | try {
37 | dispatch(asyncActionStart());
38 | // upload the file to firebase storage
39 | let uploadedFile = await firebase.uploadFile(path, file, null, options);
40 | // get url of the image
41 | let downloadURL = await uploadedFile.uploadTaskSnapshot.downloadURL;
42 | // get userdoc
43 | let userDoc = await firestore.get(`users/${user.uid}`);
44 | // check if user has photo, if not update profile with new image
45 | if (!userDoc.data().photoURL) {
46 | await firebase.updateProfile({
47 | photoURL: downloadURL
48 | });
49 | await user.updateProfile({
50 | photoURL: downloadURL
51 | });
52 | }
53 | // add the new photo to photos collection
54 | await firestore.add(
55 | {
56 | collection: 'users',
57 | doc: user.uid,
58 | subcollections: [{ collection: 'photos' }]
59 | },
60 | {
61 | name: imageName,
62 | url: downloadURL
63 | }
64 | );
65 | dispatch(asyncActionFinish());
66 | } catch (error) {
67 | console.log(error);
68 | dispatch(asyncActionError());
69 | throw new Error('Problem uploading photo');
70 | }
71 | };
72 |
73 | export const deletePhoto = photo => async (dispatch, getState, { getFirebase, getFirestore }) => {
74 | const firebase = getFirebase();
75 | const firestore = getFirestore();
76 | const user = firebase.auth().currentUser;
77 | try {
78 | await firebase.deleteFile(`${user.uid}/user_images/${photo.name}`);
79 | await firestore.delete({
80 | collection: 'users',
81 | doc: user.uid,
82 | subcollections: [{ collection: 'photos', doc: photo.id }]
83 | });
84 | } catch (error) {
85 | console.log(error);
86 | throw new Error('Problem deleting the photo');
87 | }
88 | };
89 |
90 | export const setMainPhoto = photo => async (dispatch, getState) => {
91 | dispatch(asyncActionStart());
92 | const firestore = firebase.firestore();
93 | const user = firebase.auth().currentUser;
94 | const today = new Date(Date.now());
95 | let userDocRef = firestore.collection('users').doc(user.uid);
96 | let eventAttendeeRef = firestore.collection('event_attendee');
97 | try {
98 | let batch = firestore.batch();
99 |
100 | await batch.update(userDocRef, {
101 | photoURL: photo.url
102 | });
103 |
104 | let eventQuery = await eventAttendeeRef
105 | .where('userUid', '==', user.uid)
106 | .where('eventDate', '>', today);
107 |
108 | let eventQuerySnap = await eventQuery.get();
109 |
110 | for (let i = 0; i < eventQuerySnap.docs.length; i++) {
111 | let eventDocRef = await firestore
112 | .collection('events')
113 | .doc(eventQuerySnap.docs[i].data().eventId);
114 |
115 | let event = await eventDocRef.get();
116 |
117 | if (event.data().hostUid === user.uid) {
118 | batch.update(eventDocRef, {
119 | hostPhotoURL: photo.url,
120 | [`attendees.${user.uid}.photoURL`]: photo.url
121 | })
122 | } else {
123 | batch.update(eventDocRef, {
124 | [`attendees.${user.uid}.photoURL`]: photo.url
125 | })
126 | }
127 | }
128 | console.log(batch);
129 | await batch.commit();
130 | dispatch(asyncActionFinish())
131 | } catch (error) {
132 | console.log(error);
133 | dispatch(asyncActionError())
134 | throw new Error('Problem setting main photo');
135 | }
136 | };
137 |
138 | export const goingToEvent = event => async (dispatch, getState) => {
139 | dispatch(asyncActionStart())
140 | const firestore = firebase.firestore();
141 | const user = firebase.auth().currentUser;
142 | const profile = getState().firebase.profile;
143 | const attendee = {
144 | going: true,
145 | joinDate: Date.now(),
146 | photoURL: profile.photoURL || '/assets/user.png',
147 | displayName: profile.displayName,
148 | host: false
149 | };
150 | try {
151 | let eventDocRef = firestore.collection('events').doc(event.id);
152 | let eventAttendeeDocRef = firestore.collection('event_attendee').doc(`${event.id}_${user.uid}`);
153 |
154 | await firestore.runTransaction(async (transaction) => {
155 | await transaction.get(eventDocRef);
156 | await transaction.update(eventDocRef, {
157 | [`attendees.${user.uid}`]: attendee
158 | })
159 | await transaction.set(eventAttendeeDocRef, {
160 | eventId: event.id,
161 | userUid: user.uid,
162 | eventDate: event.date,
163 | host: false
164 | })
165 | })
166 | toastr.success('Success', 'You have signed up to the event');
167 | dispatch(asyncActionFinish())
168 | } catch (error) {
169 | console.log(error);
170 | dispatch(asyncActionError());
171 | toastr.error('Oops', 'Problem signing up to event');
172 | }
173 | };
174 |
175 | export const cancelGoingToEvent = event => async (dispatch, getState, { getFirestore }) => {
176 | const firestore = getFirestore();
177 | const user = firestore.auth().currentUser;
178 | try {
179 | await firestore.update(`events/${event.id}`, {
180 | [`attendees.${user.uid}`]: firestore.FieldValue.delete()
181 | });
182 | await firestore.delete(`event_attendee/${event.id}_${user.uid}`);
183 | toastr.success('Success', 'You have removed yourself from the event');
184 | } catch (error) {
185 | console.log(error);
186 | toastr.error('Oops', 'Something went wrong');
187 | }
188 | };
189 |
190 | export const getUserEvents = (userUid, activeTab) => async (dispatch, getState) => {
191 | dispatch(asyncActionStart());
192 | const firestore = firebase.firestore();
193 | const today = new Date(Date.now());
194 | let eventsRef = firestore.collection('event_attendee');
195 | let query;
196 |
197 | switch (activeTab) {
198 | case 1: // past events
199 | query = eventsRef
200 | .where('userUid', '==', userUid)
201 | .where('eventDate', '<=', today)
202 | .orderBy('eventDate', 'desc');
203 | break;
204 | case 2: // future events
205 | query = eventsRef
206 | .where('userUid', '==', userUid)
207 | .where('eventDate', '>=', today)
208 | .orderBy('eventDate');
209 | break;
210 | case 3: // hosted events
211 | query = eventsRef
212 | .where('userUid', '==', userUid)
213 | .where('host', '==', true)
214 | .orderBy('eventDate', 'desc');
215 | break;
216 | default:
217 | query = eventsRef.where('userUid', '==', userUid).orderBy('eventDate', 'desc');
218 | }
219 | try {
220 | let querySnap = await query.get();
221 | let events = [];
222 |
223 | for (let i = 0; i < querySnap.docs.length; i++) {
224 | let evt = await firestore
225 | .collection('events')
226 | .doc(querySnap.docs[i].data().eventId)
227 | .get();
228 | events.push({ ...evt.data(), id: evt.id });
229 | }
230 |
231 | dispatch({ type: FETCH_EVENTS, payload: { events } });
232 |
233 | dispatch(asyncActionFinish());
234 | } catch (error) {
235 | console.log(error);
236 | dispatch(asyncActionError());
237 | }
238 | };
239 |
240 | export const followUser = userToFollow => async (dispatch, getState, { getFirestore }) => {
241 | const firestore = getFirestore();
242 | const user = firestore.auth().currentUser;
243 | const following = {
244 | photoURL: userToFollow.photoURL || '/assets/user.png',
245 | city: userToFollow.city || 'Unknown city',
246 | displayName: userToFollow.displayName
247 | };
248 | try {
249 | await firestore.set(
250 | {
251 | collection: 'users',
252 | doc: user.uid,
253 | subcollections: [{ collection: 'following', doc: userToFollow.id }]
254 | },
255 | following
256 | );
257 | } catch (error) {
258 | console.log(error);
259 | }
260 | };
261 |
262 | export const unfollowUser = userToUnfollow => async (dispatch, getState, { getFirestore }) => {
263 | const firestore = getFirestore();
264 | const user = firestore.auth().currentUser;
265 | try {
266 | await firestore.delete({
267 | collection: 'users',
268 | doc: user.uid,
269 | subcollections: [{ collection: 'following', doc: userToUnfollow.id }]
270 | });
271 | } catch (error) {
272 | console.log(error);
273 | }
274 | };
275 |
--------------------------------------------------------------------------------