├── functions
├── .gitignore
├── package.json
├── index.js
└── .eslintrc.json
├── .firebaserc
├── public
├── favicon.ico
├── assets
│ ├── logo.png
│ ├── user.png
│ └── categoryImages
│ │ ├── film.jpg
│ │ ├── food.jpg
│ │ ├── culture.jpg
│ │ ├── drinks.jpg
│ │ ├── music.jpg
│ │ └── travel.jpg
├── manifest.json
└── index.html
├── src
├── features
│ ├── auth
│ │ ├── authConstants.js
│ │ ├── AuthWrapper.jsx
│ │ ├── authReducer.js
│ │ ├── SocialLogin
│ │ │ └── SocialLogin.jsx
│ │ ├── Login
│ │ │ └── LoginForm.jsx
│ │ ├── Register
│ │ │ └── RegisterForm.jsx
│ │ └── authActions.js
│ ├── modals
│ │ ├── modalConstants.js
│ │ ├── modalActions.js
│ │ ├── modalReducer.js
│ │ ├── TestModal.jsx
│ │ ├── LoginModal.jsx
│ │ ├── RegisterModal.jsx
│ │ ├── ModalManager.jsx
│ │ └── UnauthModal.jsx
│ ├── testarea
│ │ ├── testConstants.js
│ │ ├── testReducer.js
│ │ ├── SimpleMap.jsx
│ │ ├── testActions.js
│ │ ├── TestPlaceInput.jsx
│ │ └── TestComponent.jsx
│ ├── async
│ │ ├── asyncConstants.js
│ │ ├── asyncActions.js
│ │ └── asyncReducer.js
│ ├── event
│ │ ├── eventConstants.js
│ │ ├── 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.js
│ │ ├── EventDashboard
│ │ │ └── EventDashboard.jsx
│ │ ├── eventActions.js
│ │ └── EventForm
│ │ │ └── EventForm.jsx
│ ├── user
│ │ ├── PeopleDashboard
│ │ │ └── PeopleDashboard.jsx
│ │ ├── userQueries.js
│ │ ├── UserDetailed
│ │ │ ├── UserDetailedSidebar.jsx
│ │ │ ├── UserDetailedPhotos.jsx
│ │ │ ├── UserDetailedHeader.jsx
│ │ │ ├── UserDetailedDescription.jsx
│ │ │ ├── UserDetailedEvents.jsx
│ │ │ └── UserDetailedPage.jsx
│ │ ├── Settings
│ │ │ ├── Photos
│ │ │ │ ├── DropzoneInput.jsx
│ │ │ │ ├── CropperInput.jsx
│ │ │ │ ├── UserPhotos.jsx
│ │ │ │ └── PhotosPage.jsx
│ │ │ ├── SettingsNav.jsx
│ │ │ ├── SettingsDashboard.jsx
│ │ │ ├── BasicPage.jsx
│ │ │ ├── AboutPage.jsx
│ │ │ └── AccountPage.jsx
│ │ └── userActions.js
│ ├── nav
│ │ ├── Menus
│ │ │ ├── SignedOutMenu.jsx
│ │ │ └── SignedInMenu.jsx
│ │ └── NavBar
│ │ │ └── NavBar.jsx
│ └── home
│ │ └── HomePage.jsx
├── app
│ ├── common
│ │ ├── util
│ │ │ ├── reducerUtils.js
│ │ │ ├── ScrollToTop.jsx
│ │ │ └── helpers.js
│ │ └── form
│ │ │ ├── RadioInput.jsx
│ │ │ ├── TextInput.jsx
│ │ │ ├── TextArea.jsx
│ │ │ ├── SelectInput.jsx
│ │ │ ├── DateInput.jsx
│ │ │ └── PlaceInput.jsx
│ ├── data
│ │ ├── mockApi.js
│ │ └── sampleData.js
│ ├── layout
│ │ ├── LoadingComponent.jsx
│ │ └── App.jsx
│ ├── config
│ │ └── firebase.js
│ ├── reducers
│ │ └── rootReducer.js
│ └── store
│ │ └── configureStore.js
├── index.js
├── index.css
└── serviceWorker.js
├── .gitignore
├── firebase.json
├── .vscode
└── launch.json
├── package.json
├── README.md
└── firebase-debug.log
/functions/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/
--------------------------------------------------------------------------------
/.firebaserc:
--------------------------------------------------------------------------------
1 | {
2 | "projects": {
3 | "default": "revents-2c2ee"
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TryCatchLearn/ReventsUpdate/HEAD/public/favicon.ico
--------------------------------------------------------------------------------
/public/assets/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TryCatchLearn/ReventsUpdate/HEAD/public/assets/logo.png
--------------------------------------------------------------------------------
/public/assets/user.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TryCatchLearn/ReventsUpdate/HEAD/public/assets/user.png
--------------------------------------------------------------------------------
/src/features/auth/authConstants.js:
--------------------------------------------------------------------------------
1 | export const LOGIN_USER = 'LOGIN_USER';
2 | export const SIGN_OUT_USER = 'SIGN_OUT_USER';
--------------------------------------------------------------------------------
/src/features/modals/modalConstants.js:
--------------------------------------------------------------------------------
1 | export const MODAL_OPEN = 'MODAL_OPEN';
2 | export const MODAL_CLOSE = 'MODAL_CLOSE';
--------------------------------------------------------------------------------
/public/assets/categoryImages/film.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TryCatchLearn/ReventsUpdate/HEAD/public/assets/categoryImages/film.jpg
--------------------------------------------------------------------------------
/public/assets/categoryImages/food.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TryCatchLearn/ReventsUpdate/HEAD/public/assets/categoryImages/food.jpg
--------------------------------------------------------------------------------
/public/assets/categoryImages/culture.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TryCatchLearn/ReventsUpdate/HEAD/public/assets/categoryImages/culture.jpg
--------------------------------------------------------------------------------
/public/assets/categoryImages/drinks.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TryCatchLearn/ReventsUpdate/HEAD/public/assets/categoryImages/drinks.jpg
--------------------------------------------------------------------------------
/public/assets/categoryImages/music.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TryCatchLearn/ReventsUpdate/HEAD/public/assets/categoryImages/music.jpg
--------------------------------------------------------------------------------
/public/assets/categoryImages/travel.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TryCatchLearn/ReventsUpdate/HEAD/public/assets/categoryImages/travel.jpg
--------------------------------------------------------------------------------
/src/features/testarea/testConstants.js:
--------------------------------------------------------------------------------
1 | export const INCREMENT_COUNTER = 'INCREMENT_COUNTER';
2 | export const DECREMENT_COUNTER = 'DECREMENT_COUNTER';
--------------------------------------------------------------------------------
/src/features/async/asyncConstants.js:
--------------------------------------------------------------------------------
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.js:
--------------------------------------------------------------------------------
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/common/util/reducerUtils.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/user/PeopleDashboard/PeopleDashboard.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | const PeopleDashboard = () => {
4 | return (
5 |
6 |
People dashboard
7 |
8 | )
9 | }
10 |
11 | export default PeopleDashboard
12 |
--------------------------------------------------------------------------------
/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 = true}) => {
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": ".",
12 | "display": "standalone",
13 | "theme_color": "#000000",
14 | "background_color": "#ffffff"
15 | }
16 |
--------------------------------------------------------------------------------
/src/features/modals/modalActions.js:
--------------------------------------------------------------------------------
1 | import { MODAL_OPEN, MODAL_CLOSE } 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 | };
18 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # production
12 | /build
13 |
14 | # misc
15 | .DS_Store
16 | .env.local
17 | .env.development.local
18 | .env.test.local
19 | .env.production.local
20 |
21 | npm-debug.log*
22 | yarn-debug.log*
23 | yarn-error.log*
24 |
--------------------------------------------------------------------------------
/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/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 | {label}
10 |
11 |
12 | )
13 | }
14 |
15 | export default RadioInput
16 |
--------------------------------------------------------------------------------
/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.pathname !== prevProps.location.pathname) {
7 | window.scrollTo(0, 0);
8 | }
9 | }
10 |
11 | render() {
12 | return this.props.children;
13 | }
14 | }
15 |
16 | export default withRouter(ScrollToTop);
17 |
--------------------------------------------------------------------------------
/src/features/async/asyncActions.js:
--------------------------------------------------------------------------------
1 | import { ASYNC_ACTION_START, ASYNC_ACTION_FINISH, ASYNC_ACTION_ERROR } 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 = ({
5 | input,
6 | width,
7 | type,
8 | placeholder,
9 | meta: { touched, error }
10 | }) => {
11 | return (
12 |
13 |
14 | {touched && error &&{error} }
15 |
16 | )
17 |
18 | };
19 |
20 | export default TextInput;
21 |
--------------------------------------------------------------------------------
/src/features/modals/modalReducer.js:
--------------------------------------------------------------------------------
1 | import { createReducer } from "../../app/common/util/reducerUtils";
2 | import { MODAL_OPEN, MODAL_CLOSE } from "./modalConstants";
3 |
4 | const initialState = null;
5 |
6 | const openModal = (state, payload) => {
7 | const {modalType, modalProps} = payload;
8 |
9 | return {modalType, modalProps}
10 | }
11 |
12 | const closeModal = (state) => {
13 | return null;
14 | }
15 |
16 | export default createReducer(initialState, {
17 | [MODAL_OPEN]: openModal,
18 | [MODAL_CLOSE]: closeModal
19 | })
--------------------------------------------------------------------------------
/.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 | "type": "chrome",
9 | "request": "launch",
10 | "name": "Launch Chrome against localhost",
11 | "url": "http://localhost:3000",
12 | "webRoot": "${workspaceFolder}/src"
13 | }
14 | ]
15 | }
--------------------------------------------------------------------------------
/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/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 |
15 |
16 | );
17 | };
18 |
19 | export default SignedOutMenu;
20 |
--------------------------------------------------------------------------------
/src/app/common/form/TextArea.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { Form, Label } from 'semantic-ui-react';
3 |
4 | const TextArea = ({
5 | input,
6 | rows,
7 | width,
8 | type,
9 | placeholder,
10 | meta: { touched, error }
11 | }) => {
12 | return (
13 |
14 |
15 | {touched && error &&{error} }
16 |
17 | )
18 | }
19 |
20 | export default TextArea
21 |
--------------------------------------------------------------------------------
/src/features/testarea/testReducer.js:
--------------------------------------------------------------------------------
1 | import { INCREMENT_COUNTER, DECREMENT_COUNTER } from './testConstants';
2 | import { createReducer } from '../../app/common/util/reducerUtils';
3 |
4 | const initialState = {
5 | data: 42
6 | };
7 |
8 | const incrementCounter = (state) => {
9 | return { ...state, data: state.data + 1 };
10 | }
11 |
12 | const decrementCounter = (state) => {
13 | return { ...state, data: state.data - 1 };
14 | }
15 |
16 | export default createReducer(initialState, {
17 | [INCREMENT_COUNTER]: incrementCounter,
18 | [DECREMENT_COUNTER]: decrementCounter
19 | })
--------------------------------------------------------------------------------
/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 |
17 |
18 | );
19 | }
20 | }
21 |
22 | export default EventListAttendee;
23 |
--------------------------------------------------------------------------------
/src/features/user/userQueries.js:
--------------------------------------------------------------------------------
1 | export const userDetailedQuery = ({ auth, userUid }) => {
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 | } else {
17 | return [
18 | {
19 | collection: 'users',
20 | doc: auth.uid,
21 | subcollections: [{ collection: 'photos' }],
22 | storeAs: 'photos'
23 | }
24 | ];
25 | }
26 | };
27 |
--------------------------------------------------------------------------------
/src/features/auth/authReducer.js:
--------------------------------------------------------------------------------
1 | import { createReducer } from "../../app/common/util/reducerUtils";
2 | import { LOGIN_USER, SIGN_OUT_USER } from "./authConstants";
3 |
4 | const initialState = {
5 | authenticated: false,
6 | currentUser: null
7 | }
8 |
9 | const loginUser = (state, payload) => {
10 | return {
11 | authenticated: true,
12 | currentUser: payload.creds.email
13 | }
14 | }
15 |
16 | const signOutUser = () => {
17 | return {
18 | authenticated: false,
19 | currentUser: null
20 | }
21 | }
22 |
23 | export default createReducer(initialState, {
24 | [LOGIN_USER]: loginUser,
25 | [SIGN_OUT_USER]: signOutUser
26 | })
--------------------------------------------------------------------------------
/src/app/config/firebase.js:
--------------------------------------------------------------------------------
1 | import firebase from 'firebase/app';
2 | import 'firebase/firestore';
3 | import 'firebase/database';
4 | import 'firebase/auth';
5 | import 'firebase/storage';
6 |
7 | const firebaseConfig = {
8 | apiKey: "AIzaSyBrlxlOHWLtAxfKJwIp1lDwqp9RD44x-0c",
9 | authDomain: "revents-2c2ee.firebaseapp.com",
10 | databaseURL: "https://revents-2c2ee.firebaseio.com",
11 | projectId: "revents-2c2ee",
12 | storageBucket: "revents-2c2ee.appspot.com",
13 | messagingSenderId: "218617380649",
14 | appId: "1:218617380649:web:c1c7a50ffe8e5eb5"
15 | };
16 |
17 | firebase.initializeApp(firebaseConfig);
18 | firebase.firestore();
19 |
20 | export default firebase;
--------------------------------------------------------------------------------
/src/features/modals/TestModal.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Modal } from 'semantic-ui-react';
3 | import {connect} from 'react-redux';
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 |
--------------------------------------------------------------------------------
/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 | "engines": {
13 | "node": "8"
14 | },
15 | "dependencies": {
16 | "firebase-admin": "~7.0.0",
17 | "firebase-functions": "^2.3.0"
18 | },
19 | "devDependencies": {
20 | "eslint": "^5.12.0",
21 | "eslint-plugin-promise": "^4.0.1",
22 | "firebase-functions-test": "^0.1.6"
23 | },
24 | "private": true
25 | }
26 |
--------------------------------------------------------------------------------
/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 | input.onChange(data.value)}
10 | placeholder={placeholder}
11 | options={options}
12 | multiple={multiple}
13 | />
14 | {touched && error &&{error} }
15 |
16 | )
17 | }
18 |
19 | export default SelectInput
20 |
--------------------------------------------------------------------------------
/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/user/UserDetailed/UserDetailedSidebar.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Segment, Grid, Button } from 'semantic-ui-react';
3 | import { Link } from 'react-router-dom';
4 |
5 | const UserDetailedSidebar = ({ isCurrentUser }) => {
6 | return (
7 |
8 |
9 | {isCurrentUser ? (
10 |
18 | ) : (
19 |
20 | )}
21 |
22 |
23 | );
24 | };
25 |
26 | export default UserDetailedSidebar;
27 |
--------------------------------------------------------------------------------
/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 | socialLogin('facebook')}
9 | type='button'
10 | style={{ marginBottom: '10px' }}
11 | fluid
12 | color='facebook'
13 | >
14 | Login with Facebook
15 |
16 |
17 | socialLogin('google')}
19 | type='button'
20 | fluid
21 | color='google plus'
22 | >
23 |
24 | Login with Google
25 |
26 |
27 | );
28 | };
29 |
30 | export default SocialLogin;
31 |
--------------------------------------------------------------------------------
/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 |
14 | Login to Re-vents
15 |
16 |
17 |
18 |
19 |
20 |
21 | );
22 | }
23 | }
24 |
25 | export default connect(
26 | null,
27 | actions
28 | )(LoginModal);
29 |
--------------------------------------------------------------------------------
/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 |
14 | Sign Up to Re-vents!
15 |
16 |
17 |
18 |
19 |
20 |
21 | );
22 | }
23 | }
24 |
25 | export default connect(
26 | null,
27 | actions
28 | )(RegisterModal);
29 |
--------------------------------------------------------------------------------
/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 zoom = 14;
9 | return (
10 |
11 |
12 |
17 |
18 |
19 |
20 |
21 | );
22 | };
23 |
24 | export default EventDetailedMap;
25 |
--------------------------------------------------------------------------------
/src/features/user/UserDetailed/UserDetailedPhotos.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Segment, Grid, Header, Image } from 'semantic-ui-react';
3 | import LazyLoad from 'react-lazyload';
4 |
5 | const UserDetailedPhotos = ({ photos }) => {
6 | return (
7 |
8 |
9 |
10 |
11 |
12 | {photos && photos.map(photo => (
13 | }
17 | >
18 |
19 |
20 | ))}
21 |
22 |
23 |
24 | );
25 | };
26 |
27 | export default UserDetailedPhotos;
28 |
--------------------------------------------------------------------------------
/src/features/home/HomePage.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {
3 | Segment,
4 | Container,
5 | Header,
6 | Image,
7 | Button,
8 | Icon
9 | } from 'semantic-ui-react';
10 |
11 | const HomePage = ({history}) => {
12 | return (
13 |
14 |
15 |
24 | history.push('/events')}>
25 | Get started
26 |
27 |
28 |
29 |
30 | );
31 | };
32 |
33 | export default HomePage;
34 |
--------------------------------------------------------------------------------
/src/features/user/Settings/Photos/DropzoneInput.jsx:
--------------------------------------------------------------------------------
1 | import React, {useCallback} from 'react'
2 | import {useDropzone} from 'react-dropzone'
3 | import { Icon, Header } from 'semantic-ui-react';
4 |
5 | const DropzoneInput = ({setFiles}) => {
6 | const onDrop = useCallback(acceptedFiles => {
7 | setFiles(acceptedFiles.map(file => Object.assign(file, {
8 | preview: URL.createObjectURL(file)
9 | })))
10 | }, [setFiles])
11 | const {getRootProps, getInputProps, isDragActive} = useDropzone({
12 | onDrop,
13 | multiple: false,
14 | accept: 'image/*'
15 | })
16 |
17 | return (
18 |
19 |
20 |
21 |
22 |
23 | )
24 | }
25 |
26 | export default DropzoneInput
--------------------------------------------------------------------------------
/src/features/event/EventList/EventList.jsx:
--------------------------------------------------------------------------------
1 | import React, { Component, Fragment } 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 && events.length !== 0 && (
11 |
17 | {events &&
18 | events.map(event => (
19 |
20 | ))}
21 |
22 | )}
23 |
24 | );
25 | }
26 | }
27 |
28 | export default EventList;
29 |
--------------------------------------------------------------------------------
/src/features/event/eventReducer.js:
--------------------------------------------------------------------------------
1 | import { createReducer } from '../../app/common/util/reducerUtils';
2 | import { CREATE_EVENT, UPDATE_EVENT, DELETE_EVENT, FETCH_EVENTS } from './eventConstants';
3 |
4 | const initialState = [];
5 |
6 | const createEvent = (state, payload) => {
7 | return [...state, payload.event];
8 | };
9 |
10 | const updateEvent = (state, payload) => {
11 | return [
12 | ...state.filter(event => event.id !== payload.event.id),
13 | payload.event
14 | ];
15 | };
16 |
17 | const deleteEvent = (state, payload) => {
18 | return [...state.filter(event => event.id !== payload.eventId)];
19 | };
20 |
21 | 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 | });
31 |
--------------------------------------------------------------------------------
/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 (
29 | {renderedModal}
30 | )
31 | }
32 |
33 | export default connect(mapState)(ModalManager)
34 |
--------------------------------------------------------------------------------
/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 testReducer from '../../features/testarea/testReducer';
5 | import eventReducer from '../../features/event/eventReducer';
6 | import modalReducer from '../../features/modals/modalReducer';
7 | import authReducer from '../../features/auth/authReducer';
8 | import asyncReducer from '../../features/async/asyncReducer';
9 | import { firebaseReducer } from 'react-redux-firebase';
10 | import { firestoreReducer } from 'redux-firestore';
11 |
12 | const rootReducer = combineReducers({
13 | firebase: firebaseReducer,
14 | firestore: firestoreReducer,
15 | form: FormReducer,
16 | test: testReducer,
17 | events: eventReducer,
18 | modals: modalReducer,
19 | auth: authReducer,
20 | async: asyncReducer,
21 | toastr: ToastrReducer
22 | });
23 |
24 | export default rootReducer;
25 |
--------------------------------------------------------------------------------
/src/features/event/EventDetailed/EventDetailedChatForm.jsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import { Button, Form } from 'semantic-ui-react';
3 | import { reduxForm, Field } 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/testarea/SimpleMap.jsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import GoogleMapReact from 'google-map-react';
3 | import { Icon } from 'semantic-ui-react';
4 |
5 | const AnyReactComponent = () =>
6 |
7 | class SimpleMap extends Component {
8 | static defaultProps = {
9 | zoom: 11
10 | };
11 |
12 | render() {
13 | const {latlng} = this.props;
14 | return (
15 | // Important! Always set the container height explicitly
16 |
28 | );
29 | }
30 | }
31 |
32 | export default SimpleMap;
--------------------------------------------------------------------------------
/src/features/async/asyncReducer.js:
--------------------------------------------------------------------------------
1 | import { createReducer } from "../../app/common/util/reducerUtils";
2 | import { ASYNC_ACTION_START, ASYNC_ACTION_FINISH, ASYNC_ACTION_ERROR } from "./asyncConstants";
3 |
4 | const initialState = {
5 | loading: false,
6 | elementName: null
7 | }
8 |
9 | const asyncActionStarted = (state, payload) => {
10 | return {
11 | ...state,
12 | loading: true,
13 | elementName: payload
14 | }
15 | }
16 |
17 | const asyncActionFinished = (state) => {
18 | return {
19 | ...state,
20 | loading: false,
21 | elementName: null
22 | }
23 | }
24 |
25 | const asyncActionError = (state) => {
26 | return {
27 | ...state,
28 | loading: false,
29 | elementName: null
30 | }
31 | }
32 |
33 | export default createReducer(initialState, {
34 | [ASYNC_ACTION_START]: asyncActionStarted,
35 | [ASYNC_ACTION_FINISH]: asyncActionFinished,
36 | [ASYNC_ACTION_ERROR]: asyncActionError
37 | })
--------------------------------------------------------------------------------
/src/app/store/configureStore.js:
--------------------------------------------------------------------------------
1 | import { createStore, applyMiddleware } from 'redux';
2 | import { reactReduxFirebase, getFirebase } from 'react-redux-firebase';
3 | import { reduxFirestore, getFirestore } from 'redux-firestore';
4 | import { composeWithDevTools } from 'redux-devtools-extension';
5 | import rootReducer from '../reducers/rootReducer';
6 | import thunk from 'redux-thunk';
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 = () => {
17 | const middlewares = [thunk.withExtraArgument({ getFirebase, getFirestore })];
18 |
19 | const composedEnhancer = composeWithDevTools(
20 | applyMiddleware(...middlewares),
21 | reactReduxFirebase(firebase, rrfConfig),
22 | reduxFirestore(firebase)
23 | );
24 |
25 | const store = createStore(rootReducer, composedEnhancer);
26 |
27 | return store;
28 | };
29 |
--------------------------------------------------------------------------------
/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 |
6 | const DateInput = ({
7 | input: { value, onChange, onBlur },
8 | width,
9 | placeholder,
10 | meta: { touched, error },
11 | ...rest
12 | }) => {
13 | return (
14 |
15 | onBlur(val)}
27 | onChangeRaw={e => e.preventDefault()}
28 | />
29 | {touched && error && (
30 |
31 | {error}
32 |
33 | )}
34 |
35 | );
36 | };
37 |
38 | export default DateInput;
39 |
--------------------------------------------------------------------------------
/src/features/user/Settings/SettingsNav.jsx:
--------------------------------------------------------------------------------
1 | import React, { Fragment } from 'react';
2 | import { Header, Menu } from 'semantic-ui-react';
3 | import { NavLink } from 'react-router-dom';
4 |
5 | const SettingsNav = () => {
6 | return (
7 |
8 |
9 |
10 |
11 | Basics
12 |
13 |
14 | About Me
15 |
16 |
17 | My Photos
18 |
19 |
20 |
21 |
28 |
29 | My Account
30 |
31 |
32 |
33 | );
34 | };
35 |
36 | export default SettingsNav;
37 |
--------------------------------------------------------------------------------
/src/features/user/Settings/Photos/CropperInput.jsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import Cropper from 'react-cropper';
3 | import 'cropperjs/dist/cropper.css';
4 |
5 | class CropperInput extends Component {
6 | cropImage = () => {
7 | const {setImage, setCropResult} = this.props;
8 | if (typeof this.refs.cropper.getCroppedCanvas() === 'undefined') {
9 | return;
10 | }
11 |
12 | this.refs.cropper.getCroppedCanvas().toBlob(blob => {
13 | setCropResult(URL.createObjectURL(blob));
14 | setImage(blob)
15 | })
16 | };
17 |
18 | render() {
19 | const { imagePreview } = this.props;
20 | return (
21 |
34 | );
35 | }
36 | }
37 |
38 | export default CropperInput;
39 |
--------------------------------------------------------------------------------
/src/features/testarea/testActions.js:
--------------------------------------------------------------------------------
1 | import { INCREMENT_COUNTER, DECREMENT_COUNTER } from "./testConstants";
2 | import { asyncActionFinish } from "../async/asyncActions";
3 | import { ASYNC_ACTION_START } from "../async/asyncConstants";
4 |
5 | export const incrementCounter = () => {
6 | return {
7 | type: INCREMENT_COUNTER
8 | }
9 | }
10 |
11 | export const decrementCounter = () => {
12 | return {
13 | type: DECREMENT_COUNTER
14 | }
15 | }
16 |
17 | const delay = (ms) => {
18 | return new Promise(resolve => setTimeout(resolve, ms))
19 | }
20 |
21 | export const incrementAsync = (name) => {
22 | return async dispatch => {
23 | dispatch({type: ASYNC_ACTION_START, payload: name})
24 | await delay(1000)
25 | dispatch(incrementCounter())
26 | dispatch(asyncActionFinish())
27 | }
28 | }
29 |
30 | export const decrementAsync = (name) => {
31 | return async dispatch => {
32 | dispatch({type: ASYNC_ACTION_START, payload: name})
33 | await delay(1000)
34 | dispatch({type: DECREMENT_COUNTER})
35 | dispatch(asyncActionFinish())
36 | }
37 | }
--------------------------------------------------------------------------------
/src/app/common/util/helpers.js:
--------------------------------------------------------------------------------
1 | export const objectToArray = (object) => {
2 | if (object) {
3 | return Object.entries(object).map(e => Object.assign({}, e[1], {id: e[0]}))
4 | }
5 | }
6 |
7 | export const createNewEvent = (user, photoURL, event) => {
8 | return {
9 | ...event,
10 | hostUid: user.uid,
11 | hostedBy: user.displayName,
12 | hostPhotoURL: photoURL || '/assets/user.png',
13 | created: new Date(),
14 | attendees: {
15 | [user.uid]: {
16 | going: true,
17 | joinDate: new Date(),
18 | photoURL: photoURL || '/assets/user.png',
19 | displayName: user.displayName,
20 | host: true
21 | }
22 | }
23 | }
24 | }
25 |
26 | export const createDataTree = dataset => {
27 | let hashTable = Object.create(null);
28 | dataset.forEach(a => hashTable[a.id] = {...a, childNodes: []});
29 | let dataTree = [];
30 | dataset.forEach(a => {
31 | if (a.parentId) hashTable[a.parentId].childNodes.push(hashTable[a.id]);
32 | else dataTree.push(hashTable[a.id])
33 | });
34 | return dataTree
35 | };
--------------------------------------------------------------------------------
/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 |
13 |
14 |
15 |
16 |
17 |
18 |
24 |
30 |
31 |
32 |
33 |
34 | );
35 | };
36 |
37 | export default SignedInMenu;
38 |
--------------------------------------------------------------------------------
/src/features/auth/Login/LoginForm.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Form, Segment, Button, Label, Divider } from 'semantic-ui-react';
3 | import { reduxForm, Field } from 'redux-form';
4 | import TextInput from '../../../app/common/form/TextInput';
5 | import { login, socialLogin } from '../../auth/authActions';
6 | import { connect } from 'react-redux';
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(
42 | null,
43 | actions
44 | )(reduxForm({ form: 'loginForm' })(LoginForm));
45 |
--------------------------------------------------------------------------------
/src/features/user/UserDetailed/UserDetailedHeader.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Segment, Item, Header, Grid } from 'semantic-ui-react';
3 | import { differenceInYears } from 'date-fns';
4 | import LazyLoad from 'react-lazyload';
5 |
6 | const UserDetailedHeader = ({ profile }) => {
7 | const age =
8 | profile.dateOfBirth &&
9 | differenceInYears(Date.now(), profile.dateOfBirth.toDate());
10 | return (
11 |
12 |
13 |
14 | -
15 |
}
18 | >
19 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 | {age || 'unknown age'}, Lives in{' '}
33 | {profile.city || 'unknown city'}
34 |
35 |
36 |
37 |
38 |
39 |
40 | );
41 | };
42 |
43 | export default UserDetailedHeader;
44 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import { Provider } from 'react-redux';
4 | import ReduxToastr from 'react-redux-toastr';
5 | import 'react-redux-toastr/lib/css/react-redux-toastr.min.css';
6 | import './index.css';
7 | import App from './app/layout/App';
8 | import * as serviceWorker from './serviceWorker';
9 | import { BrowserRouter } from 'react-router-dom';
10 | import { configureStore } from './app/store/configureStore';
11 | import ScrollToTop from './app/common/util/ScrollToTop';
12 |
13 | const store = configureStore();
14 |
15 | const rootEl = document.getElementById('root');
16 |
17 | let render = () => {
18 | ReactDOM.render(
19 |
20 |
21 |
22 |
27 |
28 |
29 |
30 | ,
31 | rootEl
32 | );
33 | };
34 |
35 | if (module.hot) {
36 | module.hot.accept('./app/layout/App', () => {
37 | setTimeout(render);
38 | });
39 | }
40 |
41 | store.firebaseAuthIsReady.then(() => {
42 | render();
43 | })
44 |
45 | // If you want your app to work offline and load faster, you can change
46 | // unregister() to register() below. Note this comes with some pitfalls.
47 | // Learn more about service workers: https://bit.ly/CRA-PWA
48 | serviceWorker.unregister();
49 |
--------------------------------------------------------------------------------
/src/features/user/Settings/Photos/UserPhotos.jsx:
--------------------------------------------------------------------------------
1 | import React, { Fragment } from 'react';
2 | import { Header, Card, Image, Button } from 'semantic-ui-react';
3 |
4 | const UserPhotos = ({
5 | photos,
6 | profile,
7 | deletePhoto,
8 | setMainPhoto,
9 | loading
10 | }) => {
11 | let filteredPhotos;
12 | if (photos) {
13 | filteredPhotos = photos.filter(photo => {
14 | return photo.url !== profile.photoURL;
15 | });
16 | }
17 | return (
18 |
19 |
20 |
21 |
22 |
23 |
24 | Main Photo
25 |
26 | {photos &&
27 | filteredPhotos.map(photo => (
28 |
29 |
30 |
31 | setMainPhoto(photo)}
34 | basic
35 | color='green'
36 | >
37 | Main
38 |
39 | deletePhoto(photo)}
41 | basic
42 | icon='trash'
43 | color='red'
44 | />
45 |
46 |
47 | ))}
48 |
49 |
50 | );
51 | };
52 |
53 | export default UserPhotos;
54 |
--------------------------------------------------------------------------------
/src/features/user/UserDetailed/UserDetailedDescription.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Segment, Grid, Header, List, Item, Icon } from 'semantic-ui-react';
3 | import { format } from 'date-fns';
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:{' '}
20 |
21 | {profile.createdAt && format(profile.createdAt.toDate(), 'dd LLL yyyy')}
22 |
23 |
24 | {profile.description}
25 |
26 |
27 |
28 |
29 | {profile.interests ? (
30 | profile.interests.map((interest, index) => (
31 | -
32 |
33 | {interest}
34 |
35 | ))
36 | ) : (
37 | No interests
38 | )}
39 |
40 |
41 |
42 |
43 |
44 | );
45 | };
46 |
47 | export default UserDetailedDescription;
48 |
--------------------------------------------------------------------------------
/src/features/event/EventDetailed/EventDetailedSidebar.jsx:
--------------------------------------------------------------------------------
1 | import React, { Fragment } from 'react';
2 | import { Segment, Item, Label } from 'semantic-ui-react';
3 | import { Link } from 'react-router-dom';
4 |
5 | const EventDetailedSidebar = ({ attendees }) => {
6 | const isHost = false;
7 | return (
8 |
9 |
17 | {attendees && attendees.length}{' '}
18 | {attendees && attendees.length === 1 ? 'Person' : 'People'} Going
19 |
20 |
21 |
22 | {attendees &&
23 | attendees.map(attendee => (
24 | -
25 | {isHost && (
26 |
31 | Host
32 |
33 | )}
34 |
35 |
36 |
37 |
38 | {attendee.displayName}
39 |
40 |
41 |
42 |
43 | ))}
44 |
45 |
46 |
47 | );
48 | };
49 |
50 | export default EventDetailedSidebar;
51 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "revents",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "cuid": "^2.1.6",
7 | "date-fns": "^2.0.0-alpha.27",
8 | "firebase": "^6.0.2",
9 | "google-map-react": "^1.1.4",
10 | "moment": "^2.22.0",
11 | "react": "^16.8.6",
12 | "react-cropper": "^1.2.0",
13 | "react-datepicker": "2.5.0",
14 | "react-dom": "^16.8.6",
15 | "react-dropzone": "^10.1.5",
16 | "react-infinite-scroller": "^1.2.4",
17 | "react-lazyload": "^2.5.0",
18 | "react-load-script": "0.0.6",
19 | "react-loadable": "^5.3.1",
20 | "react-places-autocomplete": "^7.2.1",
21 | "react-redux": "^5.1.1",
22 | "react-redux-firebase": "^2.2.6",
23 | "react-redux-toastr": "^7.5.1",
24 | "react-router-dom": "^5.0.0",
25 | "react-scripts": "3.0.0",
26 | "redux": "^4.0.1",
27 | "redux-auth-wrapper": "^2.1.0",
28 | "redux-firestore": "^0.3.2",
29 | "redux-form": "^7.4.2",
30 | "redux-thunk": "^2.3.0",
31 | "revalidate": "^1.2.0",
32 | "semantic-ui-react": "^0.87.1"
33 | },
34 | "scripts": {
35 | "start": "react-scripts start",
36 | "build": "react-scripts build",
37 | "test": "react-scripts test --env=jsdom",
38 | "eject": "react-scripts eject"
39 | },
40 | "eslintConfig": {
41 | "extends": "react-app"
42 | },
43 | "browserslist": {
44 | "production": [
45 | ">0.2%",
46 | "not dead",
47 | "not op_mini all"
48 | ],
49 | "development": [
50 | "last 1 chrome version",
51 | "last 1 firefox version",
52 | "last 1 safari version"
53 | ]
54 | },
55 | "devDependencies": {
56 | "@types/react-redux-toastr": "^7.4.1",
57 | "@types/react-router-dom": "^4.3.3",
58 | "redux-devtools-extension": "^2.13.8",
59 | "source-map-explorer": "^1.5.0",
60 | "typescript": "^3.3"
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/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 { closeModal, openModal } from './modalActions';
5 | import { withRouter } from 'react-router';
6 |
7 | const actions = { closeModal, openModal };
8 |
9 | class UnauthModal extends Component {
10 |
11 | handleCloseModal = () => {
12 | if (this.props.location.pathname.includes('/event')) {
13 | this.props.closeModal();
14 | } else {
15 | this.props.history.goBack();
16 | this.props.closeModal();
17 | }
18 | }
19 |
20 | render() {
21 | const { openModal, closeModal } = this.props;
22 | return (
23 |
24 | You need to be signed in to do that!
25 |
26 |
27 | Please either login or register to see this page
28 |
29 | openModal('LoginModal')}
33 | >
34 | Login
35 |
36 |
37 | openModal('RegisterModal')}>
38 | Register
39 |
40 |
41 |
42 |
43 |
Or click cancel to continue as a guest
44 |
Cancel
45 |
46 |
47 |
48 |
49 | );
50 | }
51 | }
52 |
53 | export default withRouter(connect(
54 | null,
55 | actions
56 | )(UnauthModal));
57 |
--------------------------------------------------------------------------------
/src/features/user/UserDetailed/UserDetailedEvents.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {
3 | Segment,
4 | Header,
5 | Grid,
6 | Card,
7 | Image,
8 | Tab
9 | } from 'semantic-ui-react';
10 | import { Link } from 'react-router-dom';
11 | import { format } from 'date-fns';
12 |
13 | const panes = [
14 | { menuItem: 'All events', pane: { key: 'allEvents' } },
15 | { menuItem: 'Past events', pane: { key: 'pastEvents' } },
16 | { menuItem: 'Future events', pane: { key: 'futureEvents' } },
17 | { menuItem: 'Hosting', pane: { key: 'hosted' } }
18 | ];
19 |
20 | const UserDetailedEvents = ({ events, eventsLoading, changeTab }) => {
21 | return (
22 |
23 |
24 |
25 | changeTab(e, data)}
27 | panes={panes}
28 | menu={{ secondary: true, pointing: true }}
29 | />
30 |
31 |
32 |
33 | {events &&
34 | events.map(event => (
35 |
36 |
37 |
38 | {event.title}
39 |
40 |
41 | {format(event.date && event.date.toDate(), 'dd LLL yyyy')}
42 |
43 |
44 | {format(event.date && event.date.toDate(), 'h:mm a')}
45 |
46 |
47 |
48 |
49 | ))}
50 |
51 |
52 |
53 | );
54 | };
55 |
56 | export default UserDetailedEvents;
57 |
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
9 |
10 |
11 |
12 |
16 |
17 |
26 |
27 | Re-vents
28 |
29 |
30 | You need to enable JavaScript to run this app.
31 | Welcome to re-vents
32 |
42 |
43 |
44 |
--------------------------------------------------------------------------------
/src/app/data/sampleData.js:
--------------------------------------------------------------------------------
1 | const sampleData = [
2 | {
3 | id: '1',
4 | title: 'Trip to Empire State building',
5 | date: '2018-03-21T19:00:00',
6 | category: 'culture',
7 | description:
8 | '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.',
9 | city: 'NY, USA',
10 | venue: 'Empire State Building, 5th Avenue, New York, NY, USA',
11 | venueLatLng: {
12 | lat: 40.7484405,
13 | lng: -73.98566440000002
14 | },
15 | hostedBy: 'Bob',
16 | hostPhotoURL: 'https://randomuser.me/api/portraits/men/20.jpg',
17 | attendees: [
18 | {
19 | id: 'a',
20 | name: 'Bob',
21 | photoURL: 'https://randomuser.me/api/portraits/men/20.jpg'
22 | },
23 | {
24 | id: 'b',
25 | name: 'Tom',
26 | photoURL: 'https://randomuser.me/api/portraits/men/22.jpg'
27 | }
28 | ]
29 | },
30 | {
31 | id: '2',
32 | title: 'Trip to Punch and Judy Pub',
33 | date: '2018-03-18T18:00:00',
34 | category: 'drinks',
35 | description:
36 | '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.',
37 | city: 'London, UK',
38 | venue: 'Punch & Judy, Henrietta Street, London, UK',
39 | venueLatLng: {
40 | lat: 51.5118074,
41 | lng: -0.12300089999996544
42 | },
43 | hostedBy: 'Tom',
44 | hostPhotoURL: 'https://randomuser.me/api/portraits/men/22.jpg',
45 | attendees: [
46 | {
47 | id: 'a',
48 | name: 'Bob',
49 | photoURL: 'https://randomuser.me/api/portraits/men/20.jpg'
50 | },
51 | {
52 | id: 'b',
53 | name: 'Tom',
54 | photoURL: 'https://randomuser.me/api/portraits/men/22.jpg'
55 | }
56 | ]
57 | }
58 | ];
59 |
60 | export default sampleData;
--------------------------------------------------------------------------------
/src/app/common/form/PlaceInput.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PlacesAutocomplete from 'react-places-autocomplete';
3 | import { Form, Label, Segment, List } from 'semantic-ui-react';
4 |
5 | const PlaceInput = ({
6 | input: { value, onChange, onBlur },
7 | width,
8 | options,
9 | placeholder,
10 | onSelect,
11 | meta: { touched, error }
12 | }) => {
13 | return (
14 |
20 | {({ getInputProps, suggestions, getSuggestionItemProps, loading }) => (
21 |
22 |
26 | {touched && error && (
27 |
28 | {error}
29 |
30 | )}
31 | {suggestions.length > 0 && (
32 |
40 | {loading && Loading...
}
41 |
42 | {suggestions.map(suggestion => (
43 |
44 |
45 | {suggestion.formattedSuggestion.mainText}
46 |
47 |
48 | {suggestion.formattedSuggestion.secondaryText}
49 |
50 |
51 | ))}
52 |
53 |
54 | )}
55 |
56 | )}
57 |
58 | );
59 | };
60 |
61 | export default PlaceInput;
62 |
--------------------------------------------------------------------------------
/src/features/testarea/TestPlaceInput.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PlacesAutocomplete from 'react-places-autocomplete';
3 |
4 | class TestPlaceInput extends React.Component {
5 | constructor(props) {
6 | super(props);
7 | this.state = { address: '' };
8 | }
9 |
10 | handleChange = address => {
11 | this.setState({ address });
12 | };
13 |
14 | render() {
15 | const {selectAddress} = this.props;
16 | return (
17 |
22 | {({ getInputProps, suggestions, getSuggestionItemProps, loading }) => (
23 |
24 |
30 |
31 | {loading &&
Loading...
}
32 | {suggestions.map(suggestion => {
33 | const className = suggestion.active
34 | ? 'suggestion-item--active'
35 | : 'suggestion-item';
36 | // inline style for demonstration purpose
37 | const style = suggestion.active
38 | ? { backgroundColor: '#fafafa', cursor: 'pointer' }
39 | : { backgroundColor: '#ffffff', cursor: 'pointer' };
40 | return (
41 |
47 | {suggestion.description}
48 |
49 | );
50 | })}
51 |
52 |
53 | )}
54 |
55 | );
56 | }
57 | }
58 |
59 | export default TestPlaceInput;
--------------------------------------------------------------------------------
/src/features/user/Settings/SettingsDashboard.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Grid } from 'semantic-ui-react';
3 | import SettingsNav from './SettingsNav';
4 | import { Route, Redirect, Switch } from 'react-router-dom';
5 | import { connect } from 'react-redux';
6 | import BasicPage from './BasicPage';
7 | import AboutPage from './AboutPage';
8 | import PhotosPage from './Photos/PhotosPage';
9 | import AccountPage from './AccountPage';
10 | import { updatePassword } from '../../auth/authActions';
11 | import { updateProfile } from '../../user/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 = ({
24 | updatePassword,
25 | providerId,
26 | user,
27 | updateProfile
28 | }) => {
29 | return (
30 |
31 |
32 |
33 |
34 | (
37 |
38 | )}
39 | />
40 | (
43 |
44 | )}
45 | />
46 |
47 | (
50 |
54 | )}
55 | />
56 |
57 |
58 |
59 |
60 |
61 |
62 | );
63 | };
64 |
65 | export default connect(
66 | mapState,
67 | actions
68 | )(SettingsDashboard);
69 |
--------------------------------------------------------------------------------
/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 {formatDistance} from 'date-fns';
5 |
6 | class EventActivityItem extends Component {
7 | renderSummary = activity => {
8 | switch (activity.type) {
9 | case 'newEvent':
10 | return (
11 |
12 | New Event!{' '}
13 |
17 | {activity.hostedBy}
18 | {' '}
19 | is hosting{' '}
20 |
21 | {activity.title}
22 |
23 |
24 | );
25 | case 'cancelledEvent':
26 | return (
27 |
28 | Event Cancelled!{' '}
29 |
33 | {activity.hostedBy}
34 | {' '}
35 | has cancelled{' '}
36 |
37 | {activity.title}
38 |
39 |
40 | );
41 | default:
42 | return;
43 | }
44 | };
45 |
46 | render() {
47 | const { activity } = this.props;
48 |
49 | return (
50 |
51 |
52 |
53 |
54 |
55 | {this.renderSummary(activity)}
56 |
57 |
58 | {formatDistance(activity.timestamp && activity.timestamp.toDate(), Date.now())} ago
59 |
60 |
61 |
62 |
63 | );
64 | }
65 | }
66 |
67 | export default EventActivityItem;
68 |
--------------------------------------------------------------------------------
/src/features/event/EventDetailed/EventDetailedInfo.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import { Segment, Grid, Icon, Button } from 'semantic-ui-react';
3 | import EventDetailedMap from './EventDetailedMap';
4 | import { format } from 'date-fns';
5 |
6 | const EventDetailedInfo = ({ event }) => {
7 | const [isMapOpen, showMapToggle] = useState(false);
8 | return (
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 | {event.description}
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 | {event.date &&
27 |
28 | {format(event.date.toDate(), 'EEEE do LLL')} at{' '}
29 | {format(event.date.toDate(), 'h:mm a')}
30 | }
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 | {event.venue}
41 |
42 |
43 | showMapToggle(!isMapOpen)}
45 | color='teal'
46 | size='tiny'
47 | content={isMapOpen ? 'Hide map' : 'Show map'}
48 | />
49 |
50 |
51 |
52 | {isMapOpen && (
53 |
57 | )}
58 |
59 | );
60 | };
61 |
62 | export default EventDetailedInfo;
63 |
--------------------------------------------------------------------------------
/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 { reduxForm, Field } 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 = ({
21 | handleSubmit,
22 | registerUser,
23 | error,
24 | invalid,
25 | submitting
26 | }) => {
27 | return (
28 |
71 | );
72 | };
73 |
74 | export default connect(
75 | null,
76 | actions
77 | )(reduxForm({ form: 'registerForm', validate })(RegisterForm));
78 |
--------------------------------------------------------------------------------
/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
19 | .document('events/{eventId}')
20 | .onCreate(event => {
21 | let newEvent = event.data();
22 |
23 | console.log(newEvent);
24 |
25 | const activity = newActivity('newEvent', newEvent, event.id);
26 |
27 | console.log(activity);
28 |
29 | return admin
30 | .firestore()
31 | .collection('activity')
32 | .add(activity)
33 | .then(docRef => {
34 | return console.log('Activity created with ID ', docRef.id);
35 | })
36 | .catch(err => {
37 | return console.log('Error adding activity', err);
38 | });
39 | });
40 |
41 | exports.cancelActivity = functions.firestore
42 | .document('events/{eventId}')
43 | .onUpdate((event, context) => {
44 | let updatedEvent = event.after.data();
45 | let previousEventData = event.before.data();
46 | console.log({ event });
47 | console.log({ context });
48 | console.log({ updatedEvent });
49 | console.log({ previousEventData });
50 |
51 | if (
52 | !updatedEvent.cancelled ||
53 | updatedEvent.cancelled === previousEventData.cancelled
54 | )
55 | return false;
56 |
57 | const activity = newActivity(
58 | 'cancelledEvent',
59 | updatedEvent,
60 | context.params.eventId
61 | );
62 |
63 | console.log({ activity });
64 |
65 | return admin
66 | .firestore()
67 | .collection('activity')
68 | .add(activity)
69 | .then(docRef => {
70 | return console.log('Activity created with ID ', docRef.id);
71 | })
72 | .catch(err => {
73 | return console.log('Error adding activity', err);
74 | });
75 | });
--------------------------------------------------------------------------------
/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 EventListAttendee from './EventListAttendee';
4 | import { Link } from 'react-router-dom';
5 | import { format } from 'date-fns';
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 | Hosted by {event.hostedBy}
20 | {event.cancelled && (
21 |
27 | )}
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 | {format(event.date.toDate(), 'EEEE do LLL')} at{' '}
36 | {format(event.date.toDate(), 'h:mm a')} |
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 |
57 |
58 |
59 | );
60 | }
61 | }
62 |
63 | export default EventListItem;
64 |
--------------------------------------------------------------------------------
/src/app/layout/App.jsx:
--------------------------------------------------------------------------------
1 | import React, { Component, Fragment } from 'react';
2 | import EventDashboard from '../../features/event/EventDashboard/EventDashboard';
3 | import NavBar from '../../features/nav/NavBar/NavBar';
4 | import { Container } from 'semantic-ui-react';
5 | import { Route, Switch, withRouter } from 'react-router-dom';
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 |
16 | class App extends Component {
17 | render() {
18 | return (
19 |
20 |
21 |
22 | (
25 |
26 |
27 |
28 |
29 |
30 |
31 |
35 |
39 |
43 |
47 |
48 |
49 |
50 |
51 | )}
52 | />
53 |
54 | );
55 | }
56 | }
57 |
58 | export default withRouter(App);
59 |
--------------------------------------------------------------------------------
/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 DateInput from '../../../app/common/form/DateInput';
5 | import PlaceInput from '../../../app/common/form/PlaceInput';
6 | import TextInput from '../../../app/common/form/TextInput';
7 | import RadioInput from '../../../app/common/form/RadioInput';
8 | import { addYears } from 'date-fns';
9 |
10 | class BasicPage extends Component {
11 | render() {
12 | const { pristine, submitting, handleSubmit, updateProfile } = this.props;
13 | return (
14 |
15 |
16 |
25 | Gender:
26 |
33 |
40 |
41 |
52 |
60 |
61 |
67 |
68 |
69 | );
70 | }
71 | }
72 |
73 | export default reduxForm({
74 | form: 'userProfile',
75 | enableReinitialize: true,
76 | destroyOnUnmount: false
77 | })(BasicPage);
78 |
--------------------------------------------------------------------------------
/src/features/nav/NavBar/NavBar.jsx:
--------------------------------------------------------------------------------
1 | import React, { Component, Fragment } 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 mapState = state => ({
11 | auth: state.firebase.auth,
12 | profile: state.firebase.profile
13 | });
14 |
15 | const actions = {
16 | openModal,
17 | };
18 |
19 | class NavBar extends Component {
20 | handleSignIn = () => {
21 | this.props.openModal('LoginModal');
22 | };
23 |
24 | handleRegister = () => {
25 | this.props.openModal('RegisterModal');
26 | };
27 |
28 | handleSignOut = () => {
29 | this.props.firebase.logout();
30 | this.props.history.push('/');
31 | };
32 |
33 | render() {
34 | const { auth, profile } = this.props;
35 | const authenticated = auth.isLoaded && !auth.isEmpty;
36 | return (
37 |
38 |
39 |
40 |
41 | Re-vents
42 |
43 |
44 | {authenticated && (
45 |
46 |
47 |
48 |
49 |
57 |
58 |
59 | )}
60 |
61 | {authenticated ? (
62 |
67 | ) : (
68 |
72 | )}
73 |
74 |
75 | );
76 | }
77 | }
78 |
79 | export default withRouter(
80 | withFirebase(
81 | connect(
82 | mapState,
83 | actions
84 | )(NavBar)
85 | )
86 | );
87 |
--------------------------------------------------------------------------------
/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 | display: flex;
25 | align-items: center;
26 | background-image: linear-gradient(
27 | 135deg,
28 | rgb(24, 42, 115) 0%,
29 | rgb(33, 138, 174) 69%,
30 | rgb(32, 167, 172) 89%
31 | ) !important;
32 | height: 100vh;
33 | }
34 |
35 | /* .masthead.segment {
36 | min-height: 700px;
37 | padding: 1em 0 !important;
38 | } */
39 |
40 | .masthead .ui.menu .ui.button,
41 | .ui.menu a.ui.inverted.button {
42 | margin-left: 0.5em;
43 | }
44 |
45 | .masthead h1.ui.header {
46 | font-size: 4em;
47 | font-weight: normal;
48 | }
49 |
50 | .masthead h2 {
51 | font-size: 1.7em;
52 | font-weight: normal;
53 | }
54 |
55 | /* .footer.segment {
56 | padding: 5em 0;
57 | } */
58 |
59 | .secondary.inverted.pointing.menu {
60 | border: none;
61 | }
62 |
63 | /*end home page styles*/
64 |
65 | /* navbar styles */
66 |
67 | .ui.menu .item img.logo {
68 | margin-right: 1.5em;
69 | }
70 |
71 | .ui.fixed.menu {
72 | background-image: linear-gradient(
73 | 135deg,
74 | rgb(24, 42, 115) 0%,
75 | rgb(33, 138, 174) 69%,
76 | rgb(32, 167, 172) 89%
77 | ) !important;
78 | }
79 |
80 | .ui.main.container,
81 | .main.segment {
82 | margin-top: 7em;
83 | }
84 |
85 | .ui.center.aligned.segment.attendance-preview {
86 | background-color: #f5f5f5;
87 | }
88 |
89 | .masthead .ui.menu .ui.button,
90 | .ui.menu a.ui.inverted.button {
91 | margin-left: 0.5em;
92 | }
93 |
94 | .ui.menu .item>img:not(.ui) {
95 | margin-right: 1.5em !important;
96 | }
97 |
98 | .ui.menu:not(.vertical) .item>.button {
99 | margin-left: 0.5em;
100 | }
101 |
102 | /*chat comments*/
103 |
104 | .ui.comments .comment .comments {
105 | padding-bottom: 0 !important;
106 | padding-left: 2em !important;
107 | }
108 |
109 | /* dropzone styles */
110 | .dropzone {
111 | border: dashed 3px #eee;
112 | border-radius: 5px;
113 | padding-top: 30px;
114 | text-align: center;
115 | /* min-height: 236px; */
116 | }
117 |
118 | .dropzone--isActive {
119 | border: dashed 3px green;
120 | }
121 |
--------------------------------------------------------------------------------
/src/features/user/UserDetailed/UserDetailedPage.jsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import { Grid } from 'semantic-ui-react';
3 | import { firestoreConnect, isEmpty } from 'react-redux-firebase';
4 | import { connect } from 'react-redux';
5 | import UserDetailedHeader from './UserDetailedHeader';
6 | import UserDetailedSidebar from './UserDetailedSidebar';
7 | import UserDetailedPhotos from './UserDetailedPhotos';
8 | import UserDetailedEvents from './UserDetailedEvents';
9 | import UserDetailedDescription from './UserDetailedDescription';
10 | import { userDetailedQuery } from '../userQueries';
11 | import LoadingComponent from '../../../app/layout/LoadingComponent';
12 | import { getUserEvents } from '../userActions';
13 |
14 | const mapState = (state, ownProps) => {
15 | let userUid = null;
16 | let profile = {};
17 |
18 | if (ownProps.match.params.id === state.auth.uid) {
19 | profile = state.firebase.profile;
20 | } else {
21 | profile =
22 | !isEmpty(state.firestore.ordered.profile) &&
23 | 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 | };
36 | };
37 |
38 | const actions = {
39 | getUserEvents
40 | };
41 |
42 | class UserDetailedPage extends Component {
43 | async componentDidMount() {
44 | let events = await this.props.getUserEvents(this.props.userUid);
45 | console.log(events);
46 | }
47 |
48 | changeTab = (e, data) => {
49 | this.props.getUserEvents(this.props.userUid, data.activeIndex)
50 | };
51 |
52 | render() {
53 | const {
54 | profile,
55 | photos,
56 | auth,
57 | match,
58 | requesting,
59 | events,
60 | eventsLoading
61 | } = this.props;
62 | const isCurrentUser = auth.uid === match.params.id;
63 | const loading = Object.values(requesting).some(a => a === true);
64 | if (loading) return ;
65 | return (
66 |
67 |
68 |
69 |
70 | {photos && }
71 |
76 |
77 | );
78 | }
79 | }
80 |
81 | export default connect(
82 | mapState,
83 | actions
84 | )(
85 | firestoreConnect((auth, userUid) => userDetailedQuery(auth, userUid))(
86 | UserDetailedPage
87 | )
88 | );
89 |
--------------------------------------------------------------------------------
/src/features/auth/authActions.js:
--------------------------------------------------------------------------------
1 | import { SubmissionError, reset } from 'redux-form';
2 | import { closeModal } from '../modals/modalActions';
3 | import { toastr } from 'react-redux-toastr';
4 |
5 | export const login = creds => {
6 | return async (dispatch, getState, { getFirebase }) => {
7 | const firebase = getFirebase();
8 | try {
9 | await firebase
10 | .auth()
11 | .signInWithEmailAndPassword(creds.email, creds.password);
12 | dispatch(closeModal());
13 | } catch (error) {
14 | console.log(error);
15 | throw new SubmissionError({
16 | _error: error.message
17 | });
18 | }
19 | };
20 | };
21 |
22 | export const registerUser = user => async (
23 | dispatch,
24 | getState,
25 | { getFirebase, getFirestore }
26 | ) => {
27 | const firebase = getFirebase();
28 | const firestore = getFirestore();
29 | try {
30 | let createdUser = await firebase
31 | .auth()
32 | .createUserWithEmailAndPassword(user.email, user.password);
33 | console.log(createdUser);
34 | await createdUser.user.updateProfile({
35 | displayName: user.displayName
36 | });
37 | let newUser = {
38 | displayName: user.displayName,
39 | createdAt: firestore.FieldValue.serverTimestamp()
40 | };
41 | await firestore.set(`users/${createdUser.user.uid}`, { ...newUser });
42 | dispatch(closeModal());
43 | } catch (error) {
44 | console.log(error);
45 | throw new SubmissionError({
46 | _error: error.message
47 | });
48 | }
49 | };
50 |
51 | export const socialLogin = (selectedProvider) =>
52 | async (dispatch, getState, {getFirebase, getFirestore}) => {
53 | const firebase = getFirebase();
54 | const firestore = getFirestore();
55 | try {
56 | dispatch(closeModal())
57 | const user = await firebase.login({
58 | provider: selectedProvider,
59 | type: 'popup'
60 | })
61 | if (user.additionalUserInfo.isNewUser) {
62 | await firestore.set(`users/${user.user.uid}`, {
63 | displayName: user.profile.displayName,
64 | photoURL: user.profile.avatarUrl,
65 | createdAt: firestore.FieldValue.serverTimestamp()
66 | })
67 | }
68 | } catch (error) {
69 | console.log(error)
70 | }
71 | }
72 |
73 | export const updatePassword = (creds) =>
74 | async (dispatch, getState, {getFirebase}) => {
75 | const firebase = getFirebase();
76 | const user = firebase.auth().currentUser;
77 | try {
78 | await user.updatePassword(creds.newPassword1);
79 | await dispatch(reset('account'));
80 | toastr.success('Success', 'Your password has been updated')
81 | } catch (error) {
82 | throw new SubmissionError({
83 | _error: error.message
84 | })
85 | }
86 | }
--------------------------------------------------------------------------------
/src/features/event/EventDashboard/EventDashboard.jsx:
--------------------------------------------------------------------------------
1 | import React, { Component, createRef } from 'react';
2 | import { Grid, Loader } from 'semantic-ui-react';
3 | import { connect } from 'react-redux';
4 | import EventList from '../EventList/EventList';
5 | import { getEventsForDashboard } from '../eventActions';
6 | import LoadingComponent from '../../../app/layout/LoadingComponent';
7 | import EventActivity from '../EventActivity/EventActivity';
8 | import { firestoreConnect } from 'react-redux-firebase';
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 | contextRef = createRef();
30 |
31 | state = {
32 | moreEvents: false,
33 | loadingInitial: true,
34 | loadedEvents: []
35 | };
36 |
37 | async componentDidMount() {
38 | let next = await this.props.getEventsForDashboard();
39 |
40 | if (next && next.docs && next.docs.length > 1) {
41 | this.setState({
42 | moreEvents: true,
43 | loadingInitial: false
44 | });
45 | }
46 | }
47 |
48 | componentDidUpdate = prevProps => {
49 | if (this.props.events !== prevProps.events) {
50 | this.setState({
51 | loadedEvents: [...this.state.loadedEvents, ...this.props.events]
52 | });
53 | }
54 | };
55 |
56 | getNextEvents = async () => {
57 | const { events } = this.props;
58 | let lastEvent = events && events[events.length - 1];
59 | let next = await this.props.getEventsForDashboard(lastEvent);
60 | if (next && next.docs && next.docs.length <= 1) {
61 | this.setState({
62 | moreEvents: false
63 | });
64 | }
65 | };
66 |
67 | render() {
68 | const { loading, activities } = this.props;
69 | const { moreEvents, loadedEvents } = this.state;
70 | if (this.state.loadingInitial) return ;
71 | return (
72 |
73 |
74 |
75 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 | );
91 | }
92 | }
93 |
94 | export default connect(
95 | mapState,
96 | actions
97 | )(firestoreConnect(query)(EventDashboard));
98 |
--------------------------------------------------------------------------------
/src/features/event/EventDetailed/EventDetailedHeader.jsx:
--------------------------------------------------------------------------------
1 | import React, { Fragment } from 'react';
2 | import { Segment, Image, Item, Header, Button } from 'semantic-ui-react';
3 | import { Link } from 'react-router-dom';
4 | import { format } from 'date-fns';
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 | event,
21 | isHost,
22 | isGoing,
23 | goingToEvent,
24 | cancelGoingToEvent,
25 | loading,
26 | authenticated,
27 | openModal
28 | }) => {
29 | return (
30 |
31 |
32 |
37 |
38 |
39 |
40 | -
41 |
42 |
47 |
48 | {event.date && format(event.date.toDate(), 'EEEE do LLLL')}
49 |
50 |
51 | Hosted by{' '}
52 |
53 |
54 | {event.hostedBy}
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 | {!isHost && (
66 |
67 | {isGoing &&
68 | cancelGoingToEvent(event)}>
69 | Cancel My Place
70 | }
71 | {!isGoing && authenticated &&
72 | goingToEvent(event)} color='teal'>
73 | JOIN THIS EVENT
74 | }
75 | {!authenticated &&
76 | openModal('UnauthModal')} color='teal'>
77 | JOIN THIS EVENT
78 | }
79 |
80 |
81 | )}
82 |
83 | {isHost && (
84 |
90 | Manage Event
91 |
92 | )}
93 |
94 |
95 | );
96 | };
97 |
98 | export default EventDetailedHeader;
99 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
2 |
3 | ## Available Scripts
4 |
5 | In the project directory, you can run:
6 |
7 | ### `npm start`
8 |
9 | Runs the app in the development mode.
10 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser.
11 |
12 | The page will reload if you make edits.
13 | You will also see any lint errors in the console.
14 |
15 | ### `npm test`
16 |
17 | Launches the test runner in the interactive watch mode.
18 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
19 |
20 | ### `npm run build`
21 |
22 | Builds the app for production to the `build` folder.
23 | It correctly bundles React in production mode and optimizes the build for the best performance.
24 |
25 | The build is minified and the filenames include the hashes.
26 | Your app is ready to be deployed!
27 |
28 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
29 |
30 | ### `npm run eject`
31 |
32 | **Note: this is a one-way operation. Once you `eject`, you can’t go back!**
33 |
34 | If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.
35 |
36 | Instead, it will copy all the configuration files and the transitive dependencies (Webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own.
37 |
38 | You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it.
39 |
40 | ## Learn More
41 |
42 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
43 |
44 | To learn React, check out the [React documentation](https://reactjs.org/).
45 |
46 | ### Code Splitting
47 |
48 | This section has moved here: https://facebook.github.io/create-react-app/docs/code-splitting
49 |
50 | ### Analyzing the Bundle Size
51 |
52 | This section has moved here: https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size
53 |
54 | ### Making a Progressive Web App
55 |
56 | This section has moved here: https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app
57 |
58 | ### Advanced Configuration
59 |
60 | This section has moved here: https://facebook.github.io/create-react-app/docs/advanced-configuration
61 |
62 | ### Deployment
63 |
64 | This section has moved here: https://facebook.github.io/create-react-app/docs/deployment
65 |
66 | ### `npm run build` fails to minify
67 |
68 | This section has moved here: https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify
69 |
--------------------------------------------------------------------------------
/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 | Tell us your status:
27 |
34 |
41 |
48 |
49 |
50 | Tell us about yourself
51 |
52 |
60 |
67 |
74 |
75 |
81 |
82 |
83 | );
84 | };
85 |
86 | export default reduxForm({
87 | form: 'userProfile',
88 | enableReinitialize: true,
89 | destroyOnUnmount: false
90 | })(AboutPage);
91 |
--------------------------------------------------------------------------------
/src/features/user/Settings/AccountPage.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {
3 | Segment,
4 | Header,
5 | Form,
6 | Divider,
7 | Label,
8 | Button,
9 | Icon
10 | } from 'semantic-ui-react';
11 | import {
12 | combineValidators,
13 | matchesField,
14 | isRequired,
15 | composeValidators
16 | } from 'revalidate';
17 | import { Field, reduxForm } from 'redux-form';
18 | import TextInput from '../../../app/common/form/TextInput';
19 |
20 | const validate = combineValidators({
21 | newPassword1: isRequired({ message: 'Please enter a password' }),
22 | newPassword2: composeValidators(
23 | isRequired({ message: 'Please confirm your new password' }),
24 | matchesField('newPassword1')({ message: 'Passwords do not match' })
25 | )()
26 | });
27 |
28 | const AccountPage = ({
29 | error,
30 | invalid,
31 | submitting,
32 | handleSubmit,
33 | updatePassword,
34 | providerId
35 | }) => {
36 | return (
37 |
38 |
39 | {providerId && providerId === 'password' && (
40 |
41 |
42 |
Use this form to update your account settings
43 |
77 |
78 | )}
79 |
80 | {providerId && providerId === 'facebook.com' && (
81 |
82 |
83 |
Please visit Facebook to update your account settings
84 |
85 |
86 | Go to Facebook
87 |
88 |
89 | )}
90 |
91 | {providerId && providerId === 'google.com' && (
92 |
93 |
94 |
Please visit Google to update your account settings
95 |
96 |
97 | Go to Google
98 |
99 |
100 | )}
101 |
102 | );
103 | };
104 |
105 | export default reduxForm({ form: 'account', validate })(AccountPage);
106 |
--------------------------------------------------------------------------------
/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 EventDetailedHeader from './EventDetailedHeader';
5 | import EventDetailedInfo from './EventDetailedInfo';
6 | import EventDetailedChat from './EventDetailedChat';
7 | import EventDetailedSidebar from './EventDetailedSidebar';
8 | import { withFirestore, firebaseConnect, isEmpty } from 'react-redux-firebase';
9 | import { compose } from 'redux';
10 | import { objectToArray, createDataTree } from '../../../app/common/util/helpers';
11 | import { goingToEvent, cancelGoingToEvent } from '../../user/userActions';
12 | import { addEventComment } from '../eventActions';
13 | import {openModal} from '../../modals/modalActions'
14 |
15 | const mapState = (state, ownProps) => {
16 | const eventId = ownProps.match.params.id;
17 |
18 | let event = {};
19 |
20 | if (
21 | state.firestore.ordered.events &&
22 | state.firestore.ordered.events.length > 0
23 | ) {
24 | event =
25 | state.firestore.ordered.events.filter(event => event.id === eventId)[0] ||
26 | {};
27 | }
28 |
29 | return {
30 | event,
31 | loading: state.async.loading,
32 | auth: state.firebase.auth,
33 | eventChat:
34 | !isEmpty(state.firebase.data.event_chat) &&
35 | objectToArray(state.firebase.data.event_chat[ownProps.match.params.id])
36 | };
37 | };
38 |
39 | const actions = {
40 | goingToEvent,
41 | cancelGoingToEvent,
42 | addEventComment,
43 | openModal
44 | };
45 |
46 | class EventDetailedPage extends Component {
47 | async componentDidMount() {
48 | const { firestore, match } = this.props;
49 | await firestore.setListener(`events/${match.params.id}`);
50 | }
51 |
52 | async componentWillUnmount() {
53 | const { firestore, match } = this.props;
54 | await firestore.unsetListener(`events/${match.params.id}`);
55 | }
56 |
57 | render() {
58 | const {
59 | event,
60 | auth,
61 | goingToEvent,
62 | cancelGoingToEvent,
63 | addEventComment,
64 | eventChat,
65 | loading,
66 | openModal
67 | } = this.props;
68 | const attendees =
69 | event && event.attendees && objectToArray(event.attendees);
70 | const isHost = event.hostUid === auth.uid;
71 | const isGoing = attendees && attendees.some(a => a.id === auth.uid);
72 | const chatTree = !isEmpty(eventChat) && createDataTree(eventChat);
73 | const authenticated = auth.isLoaded && !auth.isEmpty;
74 | return (
75 |
76 |
77 |
87 |
88 | {authenticated &&
89 | }
94 |
95 |
96 |
97 |
98 |
99 | );
100 | }
101 | }
102 |
103 | export default compose(
104 | withFirestore,
105 | connect(
106 | mapState,
107 | actions
108 | ),
109 | firebaseConnect(props => [`event_chat/${props.match.params.id}`])
110 | )(EventDetailedPage);
111 |
--------------------------------------------------------------------------------
/functions/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "parserOptions": {
3 | // Required for certain syntax usages
4 | "ecmaVersion": 2017
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/user/Settings/Photos/PhotosPage.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect, Fragment } from 'react';
2 | import { connect } from 'react-redux';
3 | import { compose } from 'redux';
4 | import { firestoreConnect } from 'react-redux-firebase';
5 | import {
6 | Image,
7 | Segment,
8 | Header,
9 | Divider,
10 | Grid,
11 | Button
12 | } from 'semantic-ui-react';
13 | import DropzoneInput from './DropzoneInput';
14 | import CropperInput from './CropperInput';
15 | import {
16 | uploadProfileImage,
17 | deletePhoto,
18 | setMainPhoto
19 | } from '../../userActions';
20 | import { toastr } from 'react-redux-toastr';
21 | import UserPhotos from './UserPhotos';
22 |
23 | const query = ({ auth }) => {
24 | return [
25 | {
26 | collection: 'users',
27 | doc: auth.uid,
28 | subcollections: [{ collection: 'photos' }],
29 | storeAs: 'photos'
30 | }
31 | ];
32 | };
33 |
34 | const actions = {
35 | uploadProfileImage,
36 | deletePhoto,
37 | setMainPhoto
38 | };
39 |
40 | const mapState = state => ({
41 | auth: state.firebase.auth,
42 | profile: state.firebase.profile,
43 | photos: state.firestore.ordered.photos,
44 | loading: state.async.loading
45 | });
46 |
47 | const PhotosPage = ({
48 | uploadProfileImage,
49 | photos,
50 | profile,
51 | deletePhoto,
52 | setMainPhoto,
53 | loading
54 | }) => {
55 | const [files, setFiles] = useState([]);
56 | const [cropResult, setCropResult] = useState('');
57 | const [image, setImage] = useState(null);
58 |
59 | useEffect(() => {
60 | return () => {
61 | files.forEach(file => URL.revokeObjectURL(file.preview));
62 | URL.revokeObjectURL(cropResult);
63 | };
64 | }, [files, cropResult]);
65 |
66 | const handleUploadImage = async () => {
67 | try {
68 | await uploadProfileImage(image, files[0].name);
69 | handleCancelCrop();
70 | toastr.success('Success', 'Photo has been uploaded');
71 | } catch (error) {
72 | toastr.error('Oops', 'Something went wrong');
73 | }
74 | };
75 |
76 | const handleCancelCrop = () => {
77 | setFiles([]);
78 | setImage(null);
79 | setCropResult('');
80 | };
81 |
82 | const handleSetMainPhoto = async photo => {
83 | try {
84 | await setMainPhoto(photo);
85 | } catch (error) {
86 | toastr.error('Oops', error.message);
87 | }
88 | };
89 |
90 | const handleDeletePhoto = async photo => {
91 | try {
92 | await deletePhoto(photo);
93 | } catch (error) {
94 | toastr.error('Oops', error.message);
95 | }
96 | };
97 |
98 | return (
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 | {files.length > 0 && (
111 |
116 | )}
117 |
118 |
119 |
120 |
121 | {files.length > 0 && (
122 |
123 |
127 |
128 |
135 |
141 |
142 |
143 | )}
144 |
145 |
146 |
147 |
148 |
155 |
156 | );
157 | };
158 |
159 | export default compose(
160 | connect(
161 | mapState,
162 | actions
163 | ),
164 | firestoreConnect(auth => query(auth))
165 | )(PhotosPage);
166 |
--------------------------------------------------------------------------------
/src/features/event/EventDetailed/EventDetailedChat.jsx:
--------------------------------------------------------------------------------
1 | import React, { Fragment, 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 { formatDistance } from 'date-fns';
6 |
7 | class EventDetailedChat extends Component {
8 | state = {
9 | showReplyForm: false,
10 | selectedCommentId: null
11 | };
12 |
13 | handleOpenReplyForm = id => () => {
14 | this.setState({
15 | showReplyForm: true,
16 | selectedCommentId: id
17 | });
18 | };
19 |
20 | handleCloseReplyForm = () => {
21 | this.setState({
22 | selectedCommentId: null,
23 | showReplyForm: false
24 | });
25 | };
26 |
27 | render() {
28 | const { addEventComment, eventId, eventChat } = this.props;
29 | const { showReplyForm, selectedCommentId } = this.state;
30 | return (
31 |
32 |
39 |
40 |
41 |
42 |
43 |
44 | {eventChat &&
45 | eventChat.map(comment => (
46 |
47 |
50 |
51 |
52 | {comment.displayName}
53 |
54 |
55 | {formatDistance(comment.date, Date.now())} ago
56 |
57 | {comment.text}
58 |
59 |
62 | Reply
63 |
64 | {showReplyForm && selectedCommentId === comment.id && (
65 |
72 | )}
73 |
74 |
75 |
76 | {comment.childNodes &&
77 | comment.childNodes.map(child => (
78 |
79 |
80 |
83 |
84 |
88 | {child.displayName}
89 |
90 |
91 |
92 | {formatDistance(child.date, Date.now())} ago
93 |
94 |
95 | {child.text}
96 |
97 |
100 | Reply
101 |
102 | {showReplyForm &&
103 | selectedCommentId === child.id && (
104 |
111 | )}
112 |
113 |
114 |
115 |
116 | ))}
117 |
118 | ))}
119 |
120 |
126 |
127 |
128 | );
129 | }
130 | }
131 |
132 | export default EventDetailedChat;
133 |
--------------------------------------------------------------------------------
/src/features/event/eventActions.js:
--------------------------------------------------------------------------------
1 | import { toastr } from 'react-redux-toastr';
2 | import { createNewEvent } from '../../app/common/util/helpers';
3 | import firebase from '../../app/config/firebase';
4 | import { FETCH_EVENTS } from './eventConstants';
5 | import {
6 | asyncActionStart,
7 | asyncActionFinish,
8 | asyncActionError
9 | } from '../async/asyncActions';
10 |
11 | export const createEvent = event => {
12 | return async (dispatch, getState, { getFirestore, getFirebase }) => {
13 | const firestore = getFirestore();
14 | const firebase = getFirebase();
15 | const user = firebase.auth().currentUser;
16 | const photoURL = getState().firebase.profile.photoURL;
17 | const newEvent = createNewEvent(user, photoURL, event);
18 | try {
19 | let createdEvent = await firestore.add('events', newEvent);
20 | await firestore.set(`event_attendee/${createdEvent.id}_${user.uid}`, {
21 | eventId: createdEvent.id,
22 | userUid: user.uid,
23 | eventDate: event.date,
24 | host: true
25 | });
26 | toastr.success('Success!', 'Event has been created');
27 | return createdEvent;
28 | } catch (error) {
29 | toastr.error('Oops', 'Something went wrong');
30 | }
31 | };
32 | };
33 |
34 | export const updateEvent = event => {
35 | return async (dispatch, getState) => {
36 | const firestore = firebase.firestore();
37 | try {
38 | dispatch(asyncActionStart())
39 | let eventDocRef = firestore.collection('events').doc(event.id);
40 | let dateEqual = getState().firestore.ordered.events[0].date.isEqual(event.date);
41 | if (!dateEqual) {
42 | let batch = firestore.batch();
43 | batch.update(eventDocRef, event);
44 |
45 | let eventAttendeeRef = firestore.collection('event_attendee');
46 | let eventAttendeeQuery = await eventAttendeeRef.where('eventId', '==', event.id);
47 | let eventAttendeeQuerySnap = await eventAttendeeQuery.get();
48 |
49 | for (let i=0; i async (
72 | dispatch,
73 | getState,
74 | { getFirestore }
75 | ) => {
76 | const firestore = getFirestore();
77 | const message = cancelled
78 | ? 'Are you sure you want to cancel the event?'
79 | : 'This will reactivate the event, are you sure?';
80 | try {
81 | toastr.confirm(message, {
82 | onOk: async () =>
83 | await firestore.update(`events/${eventId}`, {
84 | cancelled: cancelled
85 | })
86 | });
87 | } catch (error) {
88 | console.log(error);
89 | }
90 | };
91 |
92 | export const getEventsForDashboard = lastEvent => async (
93 | dispatch,
94 | getState
95 | ) => {
96 | let today = new Date();
97 | const firestore = firebase.firestore();
98 | const eventsRef = firestore.collection('events');
99 | try {
100 | dispatch(asyncActionStart());
101 | let startAfter =
102 | lastEvent &&
103 | (await firestore
104 | .collection('events')
105 | .doc(lastEvent.id)
106 | .get());
107 | let query;
108 |
109 | lastEvent
110 | ? (query = eventsRef
111 | .where('date', '>=', today)
112 | .orderBy('date')
113 | .startAfter(startAfter)
114 | .limit(2))
115 | : (query = eventsRef
116 | .where('date', '>=', today)
117 | .orderBy('date')
118 | .limit(2));
119 |
120 | let querySnap = await query.get();
121 |
122 | if (querySnap.docs.length === 0) {
123 | dispatch(asyncActionFinish());
124 | return querySnap;
125 | }
126 |
127 | let events = [];
128 |
129 | for (let i = 0; i < querySnap.docs.length; i++) {
130 | let evt = { ...querySnap.docs[i].data(), id: querySnap.docs[i].id };
131 | events.push(evt);
132 | }
133 | dispatch({ type: FETCH_EVENTS, payload: { events } });
134 | dispatch(asyncActionFinish());
135 | return querySnap;
136 | } catch (error) {
137 | console.log(error);
138 | dispatch(asyncActionError());
139 | }
140 | };
141 |
142 | export const addEventComment = (eventId, values, parentId) =>
143 | async (dispatch, getState, {getFirebase}) => {
144 | const firebase = getFirebase();
145 | const profile = getState().firebase.profile;
146 | const user = firebase.auth().currentUser;
147 | let newComment = {
148 | parentId: parentId,
149 | displayName: profile.displayName,
150 | photoURL: profile.photoURL || '/assets/user.png',
151 | uid: user.uid,
152 | text: values.comment,
153 | date: Date.now()
154 | }
155 | try {
156 | await firebase.push(`event_chat/${eventId}`, newComment);
157 | } catch (error) {
158 | console.log(error)
159 | toastr.error('Oops', 'Problem adding comment')
160 | }
161 | }
--------------------------------------------------------------------------------
/src/serviceWorker.js:
--------------------------------------------------------------------------------
1 | // This optional code is used to register a service worker.
2 | // register() is not called by default.
3 |
4 | // This lets the app load faster on subsequent visits in production, and gives
5 | // it offline capabilities. However, it also means that developers (and users)
6 | // will only see deployed updates on subsequent visits to a page, after all the
7 | // existing tabs open on the page have been closed, since previously cached
8 | // resources are updated in the background.
9 |
10 | // To learn more about the benefits of this model and instructions on how to
11 | // opt-in, read https://bit.ly/CRA-PWA
12 |
13 | const isLocalhost = Boolean(
14 | window.location.hostname === 'localhost' ||
15 | // [::1] is the IPv6 localhost address.
16 | window.location.hostname === '[::1]' ||
17 | // 127.0.0.1/8 is considered localhost for IPv4.
18 | window.location.hostname.match(
19 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
20 | )
21 | );
22 |
23 | export function register(config) {
24 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
25 | // The URL constructor is available in all browsers that support SW.
26 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href);
27 | if (publicUrl.origin !== window.location.origin) {
28 | // Our service worker won't work if PUBLIC_URL is on a different origin
29 | // from what our page is served on. This might happen if a CDN is used to
30 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374
31 | return;
32 | }
33 |
34 | window.addEventListener('load', () => {
35 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
36 |
37 | if (isLocalhost) {
38 | // This is running on localhost. Let's check if a service worker still exists or not.
39 | checkValidServiceWorker(swUrl, config);
40 |
41 | // Add some additional logging to localhost, pointing developers to the
42 | // service worker/PWA documentation.
43 | navigator.serviceWorker.ready.then(() => {
44 | console.log(
45 | 'This web app is being served cache-first by a service ' +
46 | 'worker. To learn more, visit https://bit.ly/CRA-PWA'
47 | );
48 | });
49 | } else {
50 | // Is not localhost. Just register service worker
51 | registerValidSW(swUrl, config);
52 | }
53 | });
54 | }
55 | }
56 |
57 | function registerValidSW(swUrl, config) {
58 | navigator.serviceWorker
59 | .register(swUrl)
60 | .then(registration => {
61 | registration.onupdatefound = () => {
62 | const installingWorker = registration.installing;
63 | if (installingWorker == null) {
64 | return;
65 | }
66 | installingWorker.onstatechange = () => {
67 | if (installingWorker.state === 'installed') {
68 | if (navigator.serviceWorker.controller) {
69 | // At this point, the updated precached content has been fetched,
70 | // but the previous service worker will still serve the older
71 | // content until all client tabs are closed.
72 | console.log(
73 | 'New content is available and will be used when all ' +
74 | 'tabs for this page are closed. See https://bit.ly/CRA-PWA.'
75 | );
76 |
77 | // Execute callback
78 | if (config && config.onUpdate) {
79 | config.onUpdate(registration);
80 | }
81 | } else {
82 | // At this point, everything has been precached.
83 | // It's the perfect time to display a
84 | // "Content is cached for offline use." message.
85 | console.log('Content is cached for offline use.');
86 |
87 | // Execute callback
88 | if (config && config.onSuccess) {
89 | config.onSuccess(registration);
90 | }
91 | }
92 | }
93 | };
94 | };
95 | })
96 | .catch(error => {
97 | console.error('Error during service worker registration:', error);
98 | });
99 | }
100 |
101 | function checkValidServiceWorker(swUrl, config) {
102 | // Check if the service worker can be found. If it can't reload the page.
103 | fetch(swUrl)
104 | .then(response => {
105 | // Ensure service worker exists, and that we really are getting a JS file.
106 | const contentType = response.headers.get('content-type');
107 | if (
108 | response.status === 404 ||
109 | (contentType != null && contentType.indexOf('javascript') === -1)
110 | ) {
111 | // No service worker found. Probably a different app. Reload the page.
112 | navigator.serviceWorker.ready.then(registration => {
113 | registration.unregister().then(() => {
114 | window.location.reload();
115 | });
116 | });
117 | } else {
118 | // Service worker found. Proceed as normal.
119 | registerValidSW(swUrl, config);
120 | }
121 | })
122 | .catch(() => {
123 | console.log(
124 | 'No internet connection found. App is running in offline mode.'
125 | );
126 | });
127 | }
128 |
129 | export function unregister() {
130 | if ('serviceWorker' in navigator) {
131 | navigator.serviceWorker.ready.then(registration => {
132 | registration.unregister();
133 | });
134 | }
135 | }
136 |
--------------------------------------------------------------------------------
/firebase-debug.log:
--------------------------------------------------------------------------------
1 | [debug] [2019-06-02T00:26:01.125Z] ----------------------------------------------------------------------
2 | [debug] [2019-06-02T00:26:01.127Z] Command: /Users/neil/.nvm/versions/node/v12.2.0/bin/node /Users/neil/.nvm/versions/node/v12.2.0/bin/firebase deploy --only functions
3 | [debug] [2019-06-02T00:26:01.127Z] CLI Version: 6.10.0
4 | [debug] [2019-06-02T00:26:01.127Z] Platform: darwin
5 | [debug] [2019-06-02T00:26:01.127Z] Node Version: v12.2.0
6 | [debug] [2019-06-02T00:26:01.128Z] Time: Sun Jun 02 2019 07:26:01 GMT+0700 (Indochina Time)
7 | [debug] [2019-06-02T00:26:01.128Z] ----------------------------------------------------------------------
8 | [debug]
9 | [debug] [2019-06-02T00:26:01.135Z] > command requires scopes: ["email","openid","https://www.googleapis.com/auth/cloudplatformprojects.readonly","https://www.googleapis.com/auth/firebase","https://www.googleapis.com/auth/cloud-platform"]
10 | [debug] [2019-06-02T00:26:01.135Z] > authorizing via signed-in user
11 | [debug] [2019-06-02T00:26:01.135Z] [iam] checking project revents-2c2ee for permissions ["cloudfunctions.functions.create","cloudfunctions.functions.delete","cloudfunctions.functions.get","cloudfunctions.functions.list","cloudfunctions.functions.update","cloudfunctions.operations.get","firebase.projects.get"]
12 | [debug] [2019-06-02T00:26:01.136Z] >>> HTTP REQUEST POST https://cloudresourcemanager.googleapis.com/v1/projects/revents-2c2ee:testIamPermissions
13 |
14 | [debug] [2019-06-02T00:26:02.524Z] <<< HTTP RESPONSE 200
15 | [info]
16 | [info] === Deploying to 'revents-2c2ee'...
17 | [info]
18 | [info] i deploying functions
19 | [info] Running command: npm --prefix "$RESOURCE_DIR" run lint
20 | [info] ✔ functions: Finished running predeploy script.
21 | [debug] [2019-06-02T00:26:04.696Z] > [functions] package.json contents: {
22 | "name": "functions",
23 | "description": "Cloud Functions for Firebase",
24 | "scripts": {
25 | "lint": "eslint .",
26 | "serve": "firebase serve --only functions",
27 | "shell": "firebase functions:shell",
28 | "start": "npm run shell",
29 | "deploy": "firebase deploy --only functions",
30 | "logs": "firebase functions:log"
31 | },
32 | "engines": {
33 | "node": "8"
34 | },
35 | "dependencies": {
36 | "firebase-admin": "~7.0.0",
37 | "firebase-functions": "^2.3.0"
38 | },
39 | "devDependencies": {
40 | "eslint": "^5.12.0",
41 | "eslint-plugin-promise": "^4.0.1",
42 | "firebase-functions-test": "^0.1.6"
43 | },
44 | "private": true
45 | }
46 | [info] i functions: ensuring necessary APIs are enabled...
47 | [debug] [2019-06-02T00:26:04.697Z] >>> HTTP REQUEST GET https://servicemanagement.googleapis.com/v1/services/cloudfunctions.googleapis.com/projectSettings/revents-2c2ee?view=CONSUMER_VIEW
48 |
49 | [debug] [2019-06-02T00:26:04.698Z] >>> HTTP REQUEST GET https://servicemanagement.googleapis.com/v1/services/runtimeconfig.googleapis.com/projectSettings/revents-2c2ee?view=CONSUMER_VIEW
50 |
51 | [debug] [2019-06-02T00:26:06.882Z] <<< HTTP RESPONSE 200
52 | [debug] [2019-06-02T00:26:06.910Z] <<< HTTP RESPONSE 200
53 | [info] ✔ functions: all necessary APIs are enabled
54 | [debug] [2019-06-02T00:26:06.911Z] >>> HTTP REQUEST GET https://cloudresourcemanager.googleapis.com/v1/projects/revents-2c2ee
55 |
56 | [debug] [2019-06-02T00:26:08.367Z] <<< HTTP RESPONSE 200
57 | [debug] [2019-06-02T00:26:08.367Z] >>> HTTP REQUEST GET https://mobilesdk-pa.googleapis.com/v1/projects/218617380649:getServerAppConfig
58 |
59 | [debug] [2019-06-02T00:26:08.768Z] <<< HTTP RESPONSE 200
60 | [info] i functions: preparing functions directory for uploading...
61 | [debug] [2019-06-02T00:26:08.770Z] >>> HTTP REQUEST GET https://runtimeconfig.googleapis.com/v1beta1/projects/revents-2c2ee/configs
62 |
63 | [debug] [2019-06-02T00:26:09.186Z] <<< HTTP RESPONSE 200
64 | [info] i functions: packaged functions (40.31 KB) for uploading
65 | [debug] [2019-06-02T00:26:09.316Z] >>> HTTP REQUEST POST https://cloudfunctions.googleapis.com/v1/projects/revents-2c2ee/locations/us-central1/functions:generateUploadUrl
66 |
67 | [debug] [2019-06-02T00:26:10.169Z] <<< HTTP RESPONSE 200
68 | [debug] [2019-06-02T00:26:10.170Z] >>> HTTP REQUEST PUT https://storage.googleapis.com/gcf-upload-us-central1-411180b3-560a-4084-b7be-8d6f04b63fc5/1add64bf-52e2-47f9-b446-8d1ed67fde4b.zip?GoogleAccessId=service-218617380649@gcf-admin-robot.iam.gserviceaccount.com&Expires=1559436970&Signature=vpZweE5ruM019PallWL%2BtjGCda08eqU38NWFr2ZgOKpRi0xzQsLFuPart%2BkRCAlBYho6Xv4evyiB67AyP5uFEQ0T%2FlRpg2bJ4cBSogWg3%2FJ0%2FDin31en8iquGC3hBgmdYWwr%2BTlCrqAnivcRxDgCTN2grXVXDdQr3%2FNd54A6dcGGEiTu%2FYExPCMLZbdBjlXkQCnsrcTgBUwRp755S%2BphNRnPgevafo9BDnY%2FJ3smyxTZAwvFMJ6uFvv0KyIIWNc5uvitWCwsMuHGtEWIJj%2BXNl33qjwxyD8HV%2FMDcydi2VjviOgkZGqf3foq6DjdtbKBIHfMnbxise%2BXZ4Mp3%2Bzy8w%3D%3D
69 |
70 | [debug] [2019-06-02T00:26:10.896Z] <<< HTTP RESPONSE 200
71 | [info] ✔ functions: functions folder uploaded successfully
72 | [debug] [2019-06-02T00:26:10.898Z] >>> HTTP REQUEST GET https://cloudfunctions.googleapis.com/v1/projects/revents-2c2ee/locations/-/functions
73 |
74 | [debug] [2019-06-02T00:26:11.829Z] <<< HTTP RESPONSE 200
75 | [info] i functions: creating Node.js 8 function createActivity(us-central1)...
76 | [debug] [2019-06-02T00:26:11.830Z] Trigger is: {"eventTrigger":{"resource":"projects/revents-2c2ee/databases/(default)/documents/events/{eventId}","eventType":"providers/cloud.firestore/eventTypes/document.create","service":"firestore.googleapis.com"}}
77 | [info] i functions: creating Node.js 8 function cancelActivity(us-central1)...
78 | [debug] [2019-06-02T00:26:11.831Z] Trigger is: {"eventTrigger":{"resource":"projects/revents-2c2ee/databases/(default)/documents/events/{eventId}","eventType":"providers/cloud.firestore/eventTypes/document.update","service":"firestore.googleapis.com"}}
79 | [info]
80 | The following functions are found in your project but do not exist in your local source code:
81 | helloWorld(us-central1)
82 |
83 | If you are renaming a function or changing its region, it is recommended that you create the new function first before deleting the old one to prevent event loss. For more info, visit https://firebase.google.com/docs/functions/manage-functions#modify
84 |
85 |
--------------------------------------------------------------------------------
/src/features/testarea/TestComponent.jsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import { connect } from 'react-redux';
3 | import { incrementAsync, decrementAsync } from './testActions';
4 | import { Button, Header } from 'semantic-ui-react';
5 | import TestPlaceInput from './TestPlaceInput';
6 | import SimpleMap from './SimpleMap';
7 | import { geocodeByAddress, getLatLng } from 'react-places-autocomplete';
8 | import { openModal } from '../modals/modalActions';
9 | import { toastr } from 'react-redux-toastr';
10 | import firebase from '../../app/config/firebase';
11 |
12 | const mapState = state => ({
13 | data: state.test.data,
14 | loading: state.async.loading,
15 | buttonName: state.async.elementName
16 | });
17 |
18 | const actions = {
19 | incrementAsync,
20 | decrementAsync,
21 | openModal
22 | };
23 |
24 | class TestComponent extends Component {
25 | state = {
26 | latlng: {
27 | lat: 59.95,
28 | lng: 30.33
29 | }
30 | };
31 |
32 | handleSelect = address => {
33 | geocodeByAddress(address)
34 | .then(results => getLatLng(results[0]))
35 | .then(latLng => {
36 | this.setState({
37 | latlng: latLng
38 | });
39 | })
40 | .catch(error => console.error('Error', error));
41 | };
42 |
43 | handleTestUpdateProfile = async () => {
44 | const firestore = firebase.firestore();
45 | // doc = diana's userUid
46 | let userDocRef = await firestore
47 | .collection('users')
48 | .doc('455ZrcNYOCY2AwRDlJidBju368M2');
49 | try {
50 | await userDocRef.update({ displayName: 'testing' });
51 | toastr.success('Success');
52 | } catch (error) {
53 | console.log(error);
54 | toastr.error('Computer says no');
55 | }
56 | };
57 |
58 | handleCreateTestEvent = async () => {
59 | const firestore = firebase.firestore();
60 | let eventDocRef = await firestore.collection('events').doc('DELETEME');
61 | try {
62 | await eventDocRef.set({
63 | title: 'DELETEME'
64 | });
65 | toastr.success('Success');
66 | } catch (error) {
67 | console.log(error);
68 | toastr.error('Computer says no');
69 | }
70 | };
71 |
72 | handleTestJoinEvent = async () => {
73 | const firestore = firebase.firestore();
74 | let eventDocRef = await firestore.collection('events').doc('DELETEME');
75 | const attendee = {
76 | photoURL: '/assets/user.png',
77 | displayName: 'Testing'
78 | };
79 | try {
80 | await eventDocRef.update({
81 | [`attendees.455ZrcNYOCY2AwRDlJidBju368M2`]: attendee
82 | });
83 | toastr.success('Success');
84 | } catch (error) {
85 | console.log(error);
86 | toastr.error('Computer says no');
87 | }
88 | };
89 |
90 | handleTestCancelGoingToEvent = async () => {
91 | const firestore = firebase.firestore();
92 | let eventDocRef = await firestore.collection('events').doc('DELETEME');
93 | try {
94 | await eventDocRef.update({
95 | [`attendees.455ZrcNYOCY2AwRDlJidBju368M2`]: firebase.firestore.FieldValue.delete()
96 | });
97 | toastr.success('Success');
98 | } catch (error) {
99 | console.log(error);
100 | toastr.error('Computer says no');
101 | }
102 | };
103 |
104 | handleTestChangeAttendeePhotoInEvent = async () => {
105 | const firestore = firebase.firestore();
106 | let eventDocRef = await firestore.collection('events').doc('DELETEME');
107 | try {
108 | await eventDocRef.update({
109 | [`attendees.455ZrcNYOCY2AwRDlJidBju368M2.photoURL`]: 'testing123.jpg'
110 | });
111 | toastr.success('Success');
112 | } catch (error) {
113 | console.log(error);
114 | toastr.error('Computer says no');
115 | }
116 | };
117 |
118 | render() {
119 | const {
120 | data,
121 | incrementAsync,
122 | decrementAsync,
123 | openModal,
124 | loading,
125 | buttonName
126 | } = this.props;
127 | return (
128 |
129 |
Test Component
130 | The answer is: {data}
131 | incrementAsync(e.target.name)}
135 | positive
136 | content='Increment'
137 | />
138 | decrementAsync(e.target.name)}
142 | negative
143 | content='Decrement'
144 | />
145 | openModal('TestModal', { data: 42 })}
147 | color='teal'
148 | content='Open Modal'
149 | />
150 |
151 |
152 |
153 |
159 |
165 |
171 |
177 |
183 |
184 |
185 |
186 |
187 |
188 | );
189 | }
190 | }
191 |
192 | export default connect(
193 | mapState,
194 | actions
195 | )(TestComponent);
196 |
--------------------------------------------------------------------------------
/src/features/event/EventForm/EventForm.jsx:
--------------------------------------------------------------------------------
1 | /*global google*/
2 | import React, { Component } from 'react';
3 | import { connect } from 'react-redux';
4 | import { reduxForm, Field } from 'redux-form';
5 | import { geocodeByAddress, getLatLng } from 'react-places-autocomplete';
6 | import {
7 | combineValidators,
8 | composeValidators,
9 | isRequired,
10 | hasLengthGreaterThan
11 | } from 'revalidate';
12 | import { Segment, Form, Button, Grid, Header } from 'semantic-ui-react';
13 | import { createEvent, updateEvent, cancelToggle } from '../eventActions';
14 | import TextInput from '../../../app/common/form/TextInput';
15 | import TextArea from '../../../app/common/form/TextArea';
16 | import SelectInput from '../../../app/common/form/SelectInput';
17 | import DateInput from '../../../app/common/form/DateInput';
18 | import PlaceInput from '../../../app/common/form/PlaceInput';
19 | import { withFirestore } from 'react-redux-firebase';
20 |
21 | const mapState = (state, ownProps) => {
22 | const eventId = ownProps.match.params.id;
23 |
24 | let event = {};
25 |
26 | if (
27 | state.firestore.ordered.events &&
28 | state.firestore.ordered.events.length > 0
29 | ) {
30 | event =
31 | state.firestore.ordered.events.filter(event => event.id === eventId)[0] ||
32 | {};
33 | }
34 | return {
35 | initialValues: event,
36 | event,
37 | loading: state.async.loading
38 | };
39 | };
40 |
41 | const actions = {
42 | createEvent,
43 | updateEvent,
44 | cancelToggle
45 | };
46 |
47 | const validate = combineValidators({
48 | title: isRequired({ message: 'The event title is required' }),
49 | category: isRequired({ message: 'The category is required' }),
50 | description: composeValidators(
51 | isRequired({ message: 'Please enter a description' }),
52 | hasLengthGreaterThan(4)({
53 | message: 'Description needs to be at least 5 characters'
54 | })
55 | )(),
56 | city: isRequired('city'),
57 | venue: isRequired('venue'),
58 | date: isRequired('date')
59 | });
60 |
61 | const category = [
62 | { key: 'drinks', text: 'Drinks', value: 'drinks' },
63 | { key: 'culture', text: 'Culture', value: 'culture' },
64 | { key: 'film', text: 'Film', value: 'film' },
65 | { key: 'food', text: 'Food', value: 'food' },
66 | { key: 'music', text: 'Music', value: 'music' },
67 | { key: 'travel', text: 'Travel', value: 'travel' }
68 | ];
69 |
70 | class EventForm extends Component {
71 | state = {
72 | cityLatLng: {},
73 | venueLatLng: {}
74 | };
75 |
76 | async componentDidMount() {
77 | const { firestore, match } = this.props;
78 | await firestore.setListener(`events/${match.params.id}`);
79 | }
80 |
81 | async componentWillUnmount() {
82 | const { firestore, match } = this.props;
83 | await firestore.unsetListener(`events/${match.params.id}`);
84 | }
85 |
86 | onFormSubmit = async values => {
87 | values.venueLatLng = this.state.venueLatLng;
88 | try {
89 | if (this.props.initialValues.id) {
90 | if (Object.keys(values.venueLatLng).length === 0) {
91 | values.venueLatLng = this.props.event.venueLatLng;
92 | }
93 | await this.props.updateEvent(values);
94 | this.props.history.push(`/events/${this.props.initialValues.id}`);
95 | } else {
96 | let createdEvent = await this.props.createEvent(values);
97 | this.props.history.push(`/events/${createdEvent.id}`);
98 | }
99 | } catch (error) {
100 | console.log(error);
101 | }
102 | };
103 |
104 | handleCitySelect = selectedCity => {
105 | geocodeByAddress(selectedCity)
106 | .then(results => getLatLng(results[0]))
107 | .then(latlng => {
108 | this.setState({
109 | cityLatLng: latlng
110 | });
111 | })
112 | .then(() => {
113 | this.props.change('city', selectedCity);
114 | });
115 | };
116 |
117 | handleVenueSelect = selectedVenue => {
118 | geocodeByAddress(selectedVenue)
119 | .then(results => getLatLng(results[0]))
120 | .then(latlng => {
121 | this.setState({
122 | venueLatLng: latlng
123 | });
124 | })
125 | .then(() => {
126 | this.props.change('venue', selectedVenue);
127 | });
128 | };
129 |
130 | render() {
131 | const {
132 | history,
133 | initialValues,
134 | invalid,
135 | submitting,
136 | pristine,
137 | event,
138 | cancelToggle,
139 | loading
140 | } = this.props;
141 | return (
142 |
143 |
144 |
145 |
146 |
221 |
222 |
223 |
224 | );
225 | }
226 | }
227 |
228 | export default withFirestore(
229 | connect(
230 | mapState,
231 | actions
232 | )(
233 | reduxForm({ form: 'eventForm', validate, enableReinitialize: true })(
234 | EventForm
235 | )
236 | )
237 | );
238 |
--------------------------------------------------------------------------------
/src/features/user/userActions.js:
--------------------------------------------------------------------------------
1 | import { toastr } from 'react-redux-toastr';
2 | import {
3 | asyncActionStart,
4 | asyncActionFinish,
5 | asyncActionError
6 | } from '../async/asyncActions';
7 | import cuid from 'cuid';
8 | import firebase from '../../app/config/firebase';
9 | import { FETCH_EVENTS } from '../event/eventConstants';
10 |
11 | export const updateProfile = user => async (
12 | dispatch,
13 | getState,
14 | { getFirebase }
15 | ) => {
16 | const firebase = getFirebase();
17 | const { isLoaded, isEmpty, ...updatedUser } = user;
18 | try {
19 | await firebase.updateProfile(updatedUser);
20 | toastr.success('Success', 'Your profile has been updated');
21 | } catch (error) {
22 | console.log(error);
23 | }
24 | };
25 |
26 | export const uploadProfileImage = (file, fileName) => async (
27 | dispatch,
28 | getState,
29 | { getFirebase, getFirestore }
30 | ) => {
31 | const imageName = cuid();
32 | const firebase = getFirebase();
33 | const firestore = getFirestore();
34 | const user = firebase.auth().currentUser;
35 | const path = `${user.uid}/user_images`;
36 | const options = {
37 | name: imageName
38 | };
39 | try {
40 | dispatch(asyncActionStart());
41 | // upload the file to firebase storage
42 | let uploadedFile = await firebase.uploadFile(path, file, null, options);
43 | // get url of the image
44 | let downloadURL = await uploadedFile.uploadTaskSnapshot.ref.getDownloadURL();
45 | // get userdoc
46 | let userDoc = await firestore.get(`users/${user.uid}`);
47 | // check if user has photo, if not update profile
48 | if (!userDoc.data().photoURL) {
49 | await firebase.updateProfile({
50 | photoURL: downloadURL
51 | });
52 | await user.updateProfile({
53 | photoURL: downloadURL
54 | });
55 | }
56 | // add the image to firestore
57 | await firestore.add(
58 | {
59 | collection: 'users',
60 | doc: user.uid,
61 | subcollections: [{ collection: 'photos' }]
62 | },
63 | {
64 | name: imageName,
65 | url: downloadURL
66 | }
67 | );
68 | dispatch(asyncActionFinish());
69 | } catch (error) {
70 | console.log(error);
71 | dispatch(asyncActionError());
72 | }
73 | };
74 |
75 | export const deletePhoto = photo => async (
76 | dispatch,
77 | getState,
78 | { getFirebase, getFirestore }
79 | ) => {
80 | const firebase = getFirebase();
81 | const firestore = getFirestore();
82 | const user = firebase.auth().currentUser;
83 | try {
84 | await firebase.deleteFile(`${user.uid}/user_images/${photo.name}`);
85 | await firestore.delete({
86 | collection: 'users',
87 | doc: user.uid,
88 | subcollections: [{ collection: 'photos', doc: photo.id }]
89 | });
90 | } catch (error) {
91 | console.log(error);
92 | throw new Error('Problem deleting the photo');
93 | }
94 | };
95 |
96 | export const setMainPhoto = photo => async (dispatch, getState) => {
97 | const firestore = firebase.firestore();
98 | const user = firebase.auth().currentUser;
99 | const today = new Date();
100 | let userDocRef = firestore.collection('users').doc(user.uid);
101 | let eventAttendeeRef = firestore.collection('event_attendee');
102 | try {
103 | dispatch(asyncActionStart());
104 | let batch = firestore.batch();
105 |
106 | batch.update(userDocRef, {
107 | photoURL: photo.url
108 | });
109 |
110 | let eventQuery = await eventAttendeeRef
111 | .where('userUid', '==', user.uid)
112 | .where('eventDate', '>=', today);
113 |
114 | let eventQuerySnap = await eventQuery.get();
115 |
116 | for (let i = 0; i < eventQuerySnap.docs.length; i++) {
117 | let eventDocRef = await firestore
118 | .collection('events')
119 | .doc(eventQuerySnap.docs[i].data().eventId);
120 | let event = await eventDocRef.get();
121 | if (event.data().hostUid === user.uid) {
122 | batch.update(eventDocRef, {
123 | hostPhotoURL: photo.url,
124 | [`attendees.${user.uid}.photoURL`]: photo.url
125 | });
126 | } else {
127 | batch.update(eventDocRef, {
128 | [`attendees.${user.uid}.photoURL`]: photo.url
129 | });
130 | }
131 | }
132 | console.log(batch);
133 | await batch.commit();
134 | dispatch(asyncActionFinish());
135 | } catch (error) {
136 | console.log(error);
137 | dispatch(asyncActionError());
138 | throw new Error('Problem setting main photo');
139 | }
140 | };
141 |
142 | export const goingToEvent = event => async (dispatch, getState) => {
143 | dispatch(asyncActionStart());
144 | const firestore = firebase.firestore();
145 | const user = firebase.auth().currentUser;
146 | const profile = getState().firebase.profile;
147 | const attendee = {
148 | going: true,
149 | joinDate: new Date(),
150 | photoURL: profile.photoURL || '/assets/user.png',
151 | displayName: profile.displayName,
152 | host: false
153 | };
154 | try {
155 | let eventDocRef = firestore.collection('events').doc(event.id);
156 | let eventAttendeeDocRef = firestore
157 | .collection('event_attendee')
158 | .doc(`${event.id}_${user.uid}`);
159 |
160 | await firestore.runTransaction(async transaction => {
161 | await transaction.get(eventDocRef);
162 | await transaction.update(eventDocRef, {
163 | [`attendees.${user.uid}`]: attendee
164 | });
165 | await transaction.set(eventAttendeeDocRef, {});
166 | });
167 | dispatch(asyncActionFinish());
168 | toastr.success('Success', 'You have signed up to the event');
169 | } catch (error) {
170 | console.log(error);
171 | dispatch(asyncActionFinish());
172 | toastr.error('Oops', 'Problem signing up to the event');
173 | }
174 | };
175 |
176 | export const cancelGoingToEvent = event => async (
177 | dispatch,
178 | getState,
179 | { getFirestore, getFirebase }
180 | ) => {
181 | const firestore = getFirestore();
182 | const firebase = getFirebase();
183 | const user = firebase.auth().currentUser;
184 | try {
185 | await firestore.update(`events/${event.id}`, {
186 | [`attendees.${user.uid}`]: firestore.FieldValue.delete()
187 | });
188 | await firestore.delete(`event_attendee/${event.id}_${user.uid}`);
189 | toastr.success('Success', 'You have removed yourself from the event');
190 | } catch (error) {
191 | console.log(error);
192 | toastr.error('Oops', 'Something went wrong');
193 | }
194 | };
195 |
196 | export const getUserEvents = (userUid, activeTab) => async (
197 | dispatch,
198 | getState
199 | ) => {
200 | dispatch(asyncActionStart());
201 | const firestore = firebase.firestore();
202 | const today = new Date();
203 | let eventsRef = firestore.collection('event_attendee');
204 | let query;
205 | switch (activeTab) {
206 | case 1: // past events
207 | query = eventsRef
208 | .where('userUid', '==', userUid)
209 | .where('eventDate', '<=', today)
210 | .orderBy('eventDate', 'desc');
211 | break;
212 | case 2: // future events
213 | query = eventsRef
214 | .where('userUid', '==', userUid)
215 | .where('eventDate', '>=', today)
216 | .orderBy('eventDate');
217 | break;
218 | case 3: // hosted events
219 | query = eventsRef
220 | .where('userUid', '==', userUid)
221 | .where('host', '==', true)
222 | .orderBy('eventDate', 'desc');
223 | break;
224 | default:
225 | query = eventsRef
226 | .where('userUid', '==', userUid)
227 | .orderBy('eventDate', 'desc');
228 | break;
229 | }
230 |
231 | try {
232 | let querySnap = await query.get();
233 | let events = [];
234 |
235 | for (let i = 0; i < querySnap.docs.length; i++) {
236 | let evt = await firestore
237 | .collection('events')
238 | .doc(querySnap.docs[i].data().eventId)
239 | .get();
240 | events.push({ ...evt.data(), id: evt.id });
241 | }
242 |
243 | dispatch({ type: FETCH_EVENTS, payload: { events } });
244 |
245 | dispatch(asyncActionFinish());
246 | } catch (error) {
247 | console.log(error);
248 | dispatch(asyncActionError());
249 | }
250 | };
251 |
--------------------------------------------------------------------------------