├── public
├── app-icon.png
├── app-icon@2x.png
├── app-icon@3x.png
└── index.html
├── src
├── images
│ ├── Cover.jpg
│ ├── directs.png
│ ├── dashboard.png
│ ├── direct-show.png
│ ├── followUp-edit.png
│ ├── followUp-show.png
│ ├── meeting-edit.png
│ ├── meeting-show.png
│ └── directs-by-team.png
├── __tests__
│ ├── index.test.jsx
│ └── components
│ │ └── Home.test.jsx
├── firebase
│ └── config.js.example
├── colors.js
├── actions
│ ├── header.js
│ ├── teams.js
│ ├── categoriesQuestions.js
│ ├── followUps.js
│ ├── meetings.js
│ ├── directs.js
│ └── auth.js
├── HOCs
│ ├── archive
│ │ ├── ArchiveHocFactory.js
│ │ ├── __tests__
│ │ │ ├── ArchiveHocFactory.js
│ │ │ ├── HideOnArchivedHOC.js
│ │ │ ├── ShowOnArchivedHOC.js
│ │ │ └── OnArchivedContainerHOC.js
│ │ ├── HideOnArchivedHOC.js
│ │ ├── ShowOnArchivedHOC.js
│ │ └── OnArchivedContainerHOC.js
│ ├── ShowLoaderHOC.js
│ ├── ShowLoaderHOC.css
│ └── SetTextHOC.js
├── components
│ ├── 404.jsx
│ ├── common
│ │ ├── InnerHtml.js
│ │ ├── crud-dialog-box
│ │ │ ├── AddItemBtn.js
│ │ │ └── CrudDialogBox.js
│ │ ├── InnerHtmlStripTags.js
│ │ └── RichTextEditor.js
│ ├── meetings
│ │ ├── questions
│ │ │ ├── MeetingQuestionsContainer.js
│ │ │ ├── dialog
│ │ │ │ ├── QuestionsTools.js
│ │ │ │ ├── QuestionsImportContainer.js
│ │ │ │ ├── QuestionsDialogBoxContainer.js
│ │ │ │ ├── QuestionsImportBtn.js
│ │ │ │ ├── QuestionsCrudDialogBox.js
│ │ │ │ ├── QuestionItem.js
│ │ │ │ ├── QuestionForm.js
│ │ │ │ ├── QuestionsList.js
│ │ │ │ └── CategoriesQuestions.js
│ │ │ ├── QuestionsSettings.js
│ │ │ ├── QuestionsList.js
│ │ │ ├── MeetingQuestions.js
│ │ │ └── QuestionSingle.js
│ │ ├── MeetingItem.jsx
│ │ ├── MeetingNew.jsx
│ │ ├── MeetingList.jsx
│ │ ├── MeetingForm.jsx
│ │ └── MeetingEdit.jsx
│ ├── directs
│ │ ├── list
│ │ │ ├── DirectListContainer.js
│ │ │ ├── DirectArchivedListContainer.js
│ │ │ ├── DirectItemList.js
│ │ │ ├── DirectItemDividerList.js
│ │ │ ├── AddNewDirectBtn.js
│ │ │ ├── item
│ │ │ │ ├── ArchivedLinkGenerator.js
│ │ │ │ ├── DirectItemDivider.js
│ │ │ │ ├── DirectItemNewMeetingIcon.js
│ │ │ │ └── DirectItem.jsx
│ │ │ ├── DirectList.js
│ │ │ └── ArchivedDirectCount.js
│ │ ├── single
│ │ │ ├── tabs
│ │ │ │ ├── follow-up
│ │ │ │ │ ├── FollowUpListContainer.js
│ │ │ │ │ ├── FollowUpListArchivedContainer.js
│ │ │ │ │ ├── DirectFollowUpList.jsx
│ │ │ │ │ └── FollowUpList.js
│ │ │ │ ├── DirectTabs.js
│ │ │ │ └── meetings
│ │ │ │ │ └── DirectMeetingList.jsx
│ │ │ ├── DirectSingleContainer.js
│ │ │ ├── additional-info
│ │ │ │ ├── Notes.js
│ │ │ │ ├── Phone.js
│ │ │ │ ├── StartDate.js
│ │ │ │ └── AdditionalInfo.js
│ │ │ ├── DirectArchivedSingleContainer.js
│ │ │ ├── DirectShow.jsx
│ │ │ ├── head-info
│ │ │ │ ├── EditIconLink.js
│ │ │ │ ├── ActionsButtons.js
│ │ │ │ ├── HeadInfo.js
│ │ │ │ └── UnarchiveBtn.js
│ │ │ └── DirectSingle.js
│ │ ├── DirectHome.jsx
│ │ ├── common
│ │ │ └── DirectAvatar.js
│ │ └── form
│ │ │ ├── DirectNew.jsx
│ │ │ ├── DirectEdit.jsx
│ │ │ └── DirectForm.jsx
│ ├── teams
│ │ ├── dialog
│ │ │ ├── TeamCrudDialogBoxContainer.js
│ │ │ ├── TeamsCrudDialogBox.js
│ │ │ ├── TeamList.js
│ │ │ ├── TeamItem.js
│ │ │ └── TeamForm.js
│ │ └── TeamsDropDownField.js
│ ├── header
│ │ ├── DirectSortBtnContainer.js
│ │ ├── MeetingNavigationIconsContainers.js
│ │ ├── HeaderRightIcon.js
│ │ ├── DirectSortBtn.js
│ │ └── MeetingNavigationIcons.js
│ ├── About.jsx
│ ├── Auth.jsx
│ ├── Account.jsx
│ ├── drawer
│ │ ├── TeamMenuItem.js
│ │ └── QuestionMenuItem.js
│ ├── Header.jsx
│ ├── AboutMe.jsx
│ ├── App.jsx
│ ├── Footer.jsx
│ ├── Privacy.jsx
│ ├── followUps
│ │ ├── FollowUpNew.jsx
│ │ ├── FollowUpList.jsx
│ │ ├── FollowUpItem.jsx
│ │ ├── FollowUpForm.jsx
│ │ └── FollowUpEdit.jsx
│ ├── Features.jsx
│ ├── BottomNav.jsx
│ ├── LeftDrawer.jsx
│ ├── Home.jsx
│ └── Dashboard.jsx
├── reducers
│ ├── header.js
│ ├── teams.js
│ ├── auth.js
│ ├── categoriesQuestions.js
│ ├── index.js
│ ├── meetings.js
│ ├── directs.js
│ ├── followUps.js
│ └── questions.js
├── selectors
│ ├── routing.js
│ ├── categoriesQuestions.js
│ ├── questions.js
│ ├── teams.js
│ ├── meetings.js
│ ├── archivedDirects.js
│ └── direct.js
├── store
│ ├── store.js
│ └── initialState.js
├── utility
│ └── uniqueRandomNumber.js
├── index.js
├── constants
│ └── general.js
└── routes.js
├── .gitignore
├── firebase.json
├── .eslintrc
├── package.json
├── database.rules.json
└── README.md
/public/app-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/VGraupera/1on1tracker/HEAD/public/app-icon.png
--------------------------------------------------------------------------------
/src/images/Cover.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/VGraupera/1on1tracker/HEAD/src/images/Cover.jpg
--------------------------------------------------------------------------------
/public/app-icon@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/VGraupera/1on1tracker/HEAD/public/app-icon@2x.png
--------------------------------------------------------------------------------
/public/app-icon@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/VGraupera/1on1tracker/HEAD/public/app-icon@3x.png
--------------------------------------------------------------------------------
/src/images/directs.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/VGraupera/1on1tracker/HEAD/src/images/directs.png
--------------------------------------------------------------------------------
/src/images/dashboard.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/VGraupera/1on1tracker/HEAD/src/images/dashboard.png
--------------------------------------------------------------------------------
/src/images/direct-show.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/VGraupera/1on1tracker/HEAD/src/images/direct-show.png
--------------------------------------------------------------------------------
/src/images/followUp-edit.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/VGraupera/1on1tracker/HEAD/src/images/followUp-edit.png
--------------------------------------------------------------------------------
/src/images/followUp-show.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/VGraupera/1on1tracker/HEAD/src/images/followUp-show.png
--------------------------------------------------------------------------------
/src/images/meeting-edit.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/VGraupera/1on1tracker/HEAD/src/images/meeting-edit.png
--------------------------------------------------------------------------------
/src/images/meeting-show.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/VGraupera/1on1tracker/HEAD/src/images/meeting-show.png
--------------------------------------------------------------------------------
/src/images/directs-by-team.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/VGraupera/1on1tracker/HEAD/src/images/directs-by-team.png
--------------------------------------------------------------------------------
/src/__tests__/index.test.jsx:
--------------------------------------------------------------------------------
1 | describe('App', () => {
2 | it('should be able to run tests', () => {
3 | expect(1).toEqual(1);
4 | });
5 | });
6 |
--------------------------------------------------------------------------------
/src/firebase/config.js.example:
--------------------------------------------------------------------------------
1 | export const firebaseConfig = {
2 | apiKey: '',
3 | authDomain: '',
4 | databaseURL: '',
5 | storageBucket: '',
6 | messagingSenderId: '',
7 | };
8 |
--------------------------------------------------------------------------------
/src/colors.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | richBlack80: '#342F2E',
3 | richBlack50: '#716C6B',
4 | richBlack20: '#BBB8B8',
5 | richBlack10: '#D8D6D6',
6 | brightGreen: '#4caf50',
7 | brightGreen10: '#daf2db',
8 | };
9 |
--------------------------------------------------------------------------------
/src/__tests__/components/Home.test.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { shallow } from 'enzyme';
3 |
4 | import { Home } from '../../components/Home';
5 |
6 | it('renders without crashing', () => {
7 | shallow( );
8 | });
9 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See http://help.github.com/ignore-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | node_modules
5 |
6 | # testing
7 | coverage
8 |
9 | # production
10 | build
11 | index.css
12 |
13 | # misc
14 | .DS_Store
15 | npm-debug.log
16 |
17 | .idea
18 |
--------------------------------------------------------------------------------
/firebase.json:
--------------------------------------------------------------------------------
1 | {
2 | "database": {
3 | "rules": "database.rules.json"
4 | },
5 | "hosting": {
6 | "public": "build",
7 | "rewrites": [
8 | {
9 | "source": "**",
10 | "destination": "/index.html"
11 | }
12 | ]
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/src/actions/header.js:
--------------------------------------------------------------------------------
1 | import * as types from './types';
2 |
3 | export function setText(text) {
4 | return {
5 | type: types.HEADER_SET_TEXT,
6 | payload: text,
7 | };
8 | }
9 |
10 | export function reset() {
11 | return {
12 | type: types.HEADER_RESET,
13 | };
14 | }
15 |
--------------------------------------------------------------------------------
/src/HOCs/archive/ArchiveHocFactory.js:
--------------------------------------------------------------------------------
1 | import OnArchivedContainerHOC from './OnArchivedContainerHOC';
2 |
3 | const ArchiveHocFactory = (WrappedComponent, factory) => {
4 | const HocComponent = factory(WrappedComponent);
5 | return OnArchivedContainerHOC(HocComponent);
6 | };
7 |
8 | export default ArchiveHocFactory;
9 |
--------------------------------------------------------------------------------
/src/components/404.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Link } from 'react-router';
3 |
4 | const NotFound = () => {
5 | return (
6 |
7 |
Page Not Found
8 |
Return to the homepage
9 |
10 | );
11 | };
12 |
13 | export default NotFound;
14 |
--------------------------------------------------------------------------------
/src/reducers/header.js:
--------------------------------------------------------------------------------
1 | import * as types from '../actions/types';
2 |
3 | export default (state = {}, action) => {
4 | switch (action.type) {
5 | case types.HEADER_SET_TEXT:
6 | return { ...state,
7 | text: action.payload,
8 | };
9 | case types.HEADER_RESET:
10 | return state;
11 | default:
12 | return state;
13 | }
14 | };
15 |
--------------------------------------------------------------------------------
/src/components/common/InnerHtml.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | function createMarkup(html) {
4 | return { __html: html };
5 | }
6 |
7 | /**
8 | * @function InnerHtml
9 | * @param props
10 | * @returns {XML}
11 | */
12 | function InnerHtml(props) {
13 | return
;
14 | }
15 |
16 | export default InnerHtml;
17 |
--------------------------------------------------------------------------------
/src/components/meetings/questions/MeetingQuestionsContainer.js:
--------------------------------------------------------------------------------
1 | import { connect } from 'react-redux';
2 |
3 | import MeetingQuestions from './MeetingQuestions';
4 | import { getQuestionsArray } from '../../../selectors/questions';
5 |
6 | const mapStateToProps = state => ({
7 | questions: getQuestionsArray(state),
8 | });
9 |
10 | export default connect(mapStateToProps)(MeetingQuestions);
11 |
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "env": {
3 | "browser": true,
4 | },
5 | "extends": "airbnb",
6 | "parser": "babel-eslint",
7 | "plugins": [ "react" ],
8 | "rules": {
9 | "no-underscore-dangle": 0,
10 | "no-console": 0,
11 | "arrow-body-style": 0,
12 | "anchor-has-content": 0,
13 | "react/forbid-prop-types": 0,
14 | "react/jsx-boolean-value": 0,
15 | "react/jsx-filename-extension": [1, { "extensions": [".js", ".jsx"] }],
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/src/actions/teams.js:
--------------------------------------------------------------------------------
1 | import * as types from './types';
2 | import { FirebaseApi } from '../firebase/firebase';
3 |
4 | export default new FirebaseApi('teams', {
5 | LOAD_REQUEST: types.LOAD_TEAMS_REQUEST,
6 | LOAD_SUCCESS: types.LOAD_TEAMS_SUCCESS,
7 | UNLOAD_SUCCESS: types.UNLOAD_TEAMS_SUCCESS,
8 | SET_ACTIVE: types.SET_ACTIVE_TEAM,
9 | RESET_ACTIVE: types.RESET_ACTIVE_TEAM,
10 | CREATE: types.CREATE_TEAM,
11 | SET_MATCHING: types.SET_MATCHING_TEAMS,
12 | },'name');
13 |
--------------------------------------------------------------------------------
/src/selectors/routing.js:
--------------------------------------------------------------------------------
1 | import { createSelector } from 'reselect';
2 |
3 | /**
4 | * @description return routing properties from state
5 | * @param {Object} state app state
6 | */
7 | const getRouting = state => state.routing.locationBeforeTransitions;
8 |
9 | /**
10 | * @description Check does routing state has prop state and isArchived
11 | */
12 | export const getIsArchived = createSelector(getRouting, (routing) => {
13 | const { state } = routing;
14 | return (state && state.isArchived) || false;
15 | });
16 |
--------------------------------------------------------------------------------
/src/components/directs/list/DirectListContainer.js:
--------------------------------------------------------------------------------
1 | import { connect } from 'react-redux';
2 |
3 | import { getDirectsArrayWithTeam } from '../../../selectors/direct';
4 | import DirectList from './DirectList';
5 | import HideOnArchivedHOC from '../../../HOCs/archive/HideOnArchivedHOC';
6 |
7 | const mapStateToProps = (state) => {
8 | return {
9 | directs: getDirectsArrayWithTeam(state),
10 | sortBy: state.directs.sortBy,
11 | };
12 | };
13 |
14 | export default HideOnArchivedHOC(connect(mapStateToProps)(DirectList));
--------------------------------------------------------------------------------
/src/components/directs/list/DirectArchivedListContainer.js:
--------------------------------------------------------------------------------
1 | import { connect } from 'react-redux';
2 |
3 | import ShowOnArchivedHOC from '../../../HOCs/archive/ShowOnArchivedHOC';
4 | import { getArchivedDirectsArrayWithTeam } from '../../../selectors/archivedDirects';
5 | import DirectList from './DirectList';
6 |
7 | const mapStateToProps = state => ({
8 | directs: getArchivedDirectsArrayWithTeam(state),
9 | sortBy: state.archivedDirects.sortBy,
10 | });
11 |
12 | export default ShowOnArchivedHOC(connect(mapStateToProps)(DirectList));
13 |
--------------------------------------------------------------------------------
/src/selectors/categoriesQuestions.js:
--------------------------------------------------------------------------------
1 | import { createSelector } from 'reselect';
2 |
3 | const getCategoriesQuestions = (state) => {
4 | if (!(state.categoriesQuestions.list instanceof Map)) {
5 | return [];
6 | }
7 | return state.categoriesQuestions.list;
8 | };
9 |
10 | export const getCategoriesQuestionsArray = createSelector(
11 | getCategoriesQuestions,
12 | (categoriesQuestions) => {
13 | const arr = [];
14 | categoriesQuestions.forEach((question, id) => {
15 | arr.push({ ...question, ...{ id } });
16 | });
17 | return arr;
18 | },
19 | );
20 |
--------------------------------------------------------------------------------
/src/actions/categoriesQuestions.js:
--------------------------------------------------------------------------------
1 | import * as types from './types';
2 | import { FirebaseApi } from '../firebase/firebase';
3 |
4 | export default new FirebaseApi('categoriesQuestions', {
5 | LOAD_REQUEST: types.LOAD_CATEGORIES_QUESTIONS_REQUEST,
6 | LOAD_SUCCESS: types.LOAD_CATEGORIES_QUESTIONS_SUCCESS,
7 | UNLOAD_SUCCESS: types.UNLOAD_CATEGORIES_QUESTIONS_SUCCESS,
8 | SET_ACTIVE: types.SET_ACTIVE_CATEGORIES_QUESTIONS,
9 | RESET_ACTIVE: types.RESET_ACTIVE_CATEGORIES_QUESTIONS,
10 | CREATE: types.CREATE_CATEGORIES_QUESTIONS,
11 | SET_MATCHING: types.SET_MATCHING_CATEGORIES_QUESTIONS,
12 | }, 'name');
13 |
--------------------------------------------------------------------------------
/src/components/meetings/questions/dialog/QuestionsTools.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import CategoriesQuestions from './CategoriesQuestions';
4 | import QuestionsImportContainer from './QuestionsImportContainer';
5 |
6 | /**
7 | * @function QuestionsTools
8 | * @returns {XML}
9 | */
10 | function QuestionsTools() {
11 | return (
12 |
16 | );
17 | }
18 |
19 | export default QuestionsTools;
20 |
--------------------------------------------------------------------------------
/src/components/teams/dialog/TeamCrudDialogBoxContainer.js:
--------------------------------------------------------------------------------
1 | import { connect } from 'react-redux';
2 |
3 | import TeamsCrudDialogBox from './TeamsCrudDialogBox';
4 | import teamAction from '../../../actions/teams';
5 |
6 |
7 | const mapDispatchToProps = dispatch => ({
8 | onDelete: (id) => {
9 | dispatch(teamAction.remove(id));
10 | },
11 | submitForm: (data) => {
12 | if (data.id) {
13 | dispatch(teamAction.update(data.id, { name: data.name }));
14 | } else {
15 | dispatch(teamAction.create(data));
16 | }
17 | },
18 | });
19 |
20 | export default connect(null, mapDispatchToProps)(TeamsCrudDialogBox);
21 |
--------------------------------------------------------------------------------
/src/components/directs/single/tabs/follow-up/FollowUpListContainer.js:
--------------------------------------------------------------------------------
1 | import { connect } from 'react-redux';
2 |
3 | import followUpActions from '../../../../../actions/followUps';
4 | import FollowUpList from './FollowUpList';
5 | import HideOnArchivedHOC from '../../../../../HOCs/archive/HideOnArchivedHOC';
6 |
7 | const mapStateToProps = (state, ownProps) => {
8 | return {
9 | followUps: state.followUps.matchingList,
10 | directId: ownProps.directId,
11 | };
12 | };
13 |
14 | export default HideOnArchivedHOC(connect(
15 | mapStateToProps,
16 | {
17 | followUpsEqualTo: followUpActions.equalTo,
18 | },
19 | )(FollowUpList));
20 |
--------------------------------------------------------------------------------
/src/components/directs/single/DirectSingleContainer.js:
--------------------------------------------------------------------------------
1 | import { connect } from 'react-redux';
2 |
3 | import directActions from '../../../actions/directs';
4 | import DirectSingle from './DirectSingle';
5 | import { getDirect } from '../../../selectors/direct';
6 | import HideOnArchivedHOC from '../../../HOCs/archive/HideOnArchivedHOC';
7 |
8 | const mapStateToProps = (state, ownProps) => ({
9 | direct: getDirect(state, ownProps.id),
10 | loading: state.directs.loading,
11 | error: state.directs.error,
12 | });
13 |
14 | export default HideOnArchivedHOC(
15 | connect(
16 | mapStateToProps,
17 | { find: directActions.find })(DirectSingle),
18 | );
19 |
--------------------------------------------------------------------------------
/src/components/directs/single/tabs/follow-up/FollowUpListArchivedContainer.js:
--------------------------------------------------------------------------------
1 | import { connect } from 'react-redux';
2 |
3 | import { archivedFollowUps } from '../../../../../actions/followUps';
4 | import FollowUpList from './FollowUpList';
5 | import ShowOnArchivedHOC from '../../../../../HOCs/archive/ShowOnArchivedHOC';
6 |
7 | const mapStateToProps = (state, ownProps) => {
8 | return {
9 | followUps: state.archivedFollowUp.matchingList,
10 | directId: ownProps.directId,
11 | };
12 | };
13 |
14 | export default ShowOnArchivedHOC(connect(
15 | mapStateToProps,
16 | {
17 | followUpsEqualTo: archivedFollowUps.equalTo,
18 | },
19 | )(FollowUpList));
20 |
--------------------------------------------------------------------------------
/src/components/meetings/questions/dialog/QuestionsImportContainer.js:
--------------------------------------------------------------------------------
1 | import { connect } from 'react-redux';
2 |
3 | import questionAction from '../../../../actions/questions';
4 | import categoriesQuestionsAction from '../../../../actions/categoriesQuestions';
5 | import QuestionsImportBtn from './QuestionsImportBtn';
6 |
7 | const mapStateToProps = state => ({
8 | loading: state.questions.loading,
9 | });
10 |
11 | const mapDispachToProps = dispatch => ({
12 | importQuestion: () => {
13 | dispatch(questionAction.importQuestionsTo(categoriesQuestionsAction));
14 | },
15 | });
16 |
17 | export default connect(mapStateToProps, mapDispachToProps)(QuestionsImportBtn);
18 |
--------------------------------------------------------------------------------
/src/components/common/crud-dialog-box/AddItemBtn.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import FloatingActionButton from 'material-ui/FloatingActionButton';
3 | import ContentAdd from 'material-ui/svg-icons/content/add';
4 | import PropTypes from 'prop-types';
5 |
6 | const propTypes = {
7 | handleAddItem: PropTypes.func.isRequired,
8 | };
9 |
10 | /**
11 | * @function AddItemBtn
12 | * @param {Function} handleAddItem
13 | * @returns {XML}
14 | */
15 | function AddItemBtn({ handleAddItem }) {
16 | return (
17 |
18 |
19 |
20 | );
21 | }
22 |
23 | AddItemBtn.propTypes = propTypes;
24 |
25 | export default AddItemBtn;
26 |
--------------------------------------------------------------------------------
/src/components/directs/single/additional-info/Notes.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { CardText } from 'material-ui/Card';
3 | import PropTypes from 'prop-types';
4 | import NotesIcon from 'material-ui/svg-icons/communication/comment';
5 |
6 | const propTypes = {
7 | notes: PropTypes.string.isRequired,
8 | };
9 |
10 | /**
11 | * @function Notes
12 | * @param {String} notes
13 | * @param {Object} cardTextProps CardText props
14 | * @returns {XML}
15 | */
16 | function Notes({ notes, ...cardTextProps }) {
17 | return (
18 |
19 |
20 | {notes}
21 |
22 | );
23 | }
24 |
25 | Notes.propTypes = propTypes;
26 |
27 | export default Notes;
28 |
--------------------------------------------------------------------------------
/src/components/directs/DirectHome.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import AddNewDirectBtn from './list/AddNewDirectBtn';
4 | import DirectListContainer from './list/DirectListContainer';
5 | import DirectArchivedListContainer from './list/DirectArchivedListContainer';
6 | import ArchivedDirectCount from './list/ArchivedDirectCount';
7 |
8 | /**
9 | * @function DirectHome
10 | * @returns {XML}
11 | * @constructor
12 | */
13 | function DirectHome() {
14 | return (
15 |
21 | );
22 | }
23 |
24 | export default DirectHome;
25 |
26 |
--------------------------------------------------------------------------------
/src/components/directs/single/DirectArchivedSingleContainer.js:
--------------------------------------------------------------------------------
1 | import { connect } from 'react-redux';
2 |
3 | import { archivedDirects as archivedDirectsActions } from '../../../actions/directs';
4 | import { getArchivedDirect } from '../../../selectors/archivedDirects';
5 | import DirectSingle from './DirectSingle';
6 | import ShowOnArchivedHOC from '../../../HOCs/archive/ShowOnArchivedHOC';
7 |
8 | const mapStateToProps = (state, ownProps) => ({
9 | direct: getArchivedDirect(state,ownProps.id),
10 | loading: state.archivedDirects.loading,
11 | error: state.archivedDirects.error,
12 | });
13 |
14 | export default ShowOnArchivedHOC(
15 | connect(
16 | mapStateToProps,
17 | { find: archivedDirectsActions.find })(DirectSingle),
18 | );
19 |
--------------------------------------------------------------------------------
/src/reducers/teams.js:
--------------------------------------------------------------------------------
1 | import * as types from '../actions/types';
2 |
3 | export default (state = {}, action) => {
4 | switch (action.type) {
5 | case types.LOAD_TEAMS_REQUEST:
6 | return {
7 | ...state,
8 | ...{
9 | loading: true,
10 | },
11 | };
12 | case types.LOAD_TEAMS_SUCCESS:
13 | return {
14 | ...state,
15 | ...{
16 | list: action.payload,
17 | loading: false,
18 | },
19 | };
20 | case types.UNLOAD_TEAMS_SUCCESS:
21 | return {
22 | ...state,
23 | ...{
24 | list: {},
25 | loading: false,
26 | error: null,
27 | },
28 | };
29 | default:
30 | return state;
31 | }
32 | };
33 |
--------------------------------------------------------------------------------
/src/components/header/DirectSortBtnContainer.js:
--------------------------------------------------------------------------------
1 | import { connect } from 'react-redux';
2 |
3 | import { setSortBy } from '../../actions/directs';
4 | import DirectSort from './DirectSortBtn';
5 |
6 | /**
7 | * @description Map state to props for DirectSort component
8 | * @param {Object} state app store
9 | */
10 | const mapStateToProps = state => ({
11 | selected: state.directs.sortBy,
12 | });
13 |
14 | /**
15 | * @description Map dispatch to props for DirectSort component
16 | * @param {Function} dispatch the redux dispatch function
17 | */
18 | const mapDispatchToProps = dispatch => ({
19 | handleChange: (value) => {
20 | dispatch(setSortBy(value));
21 | },
22 | });
23 |
24 | export default connect(mapStateToProps, mapDispatchToProps)(DirectSort);
25 |
--------------------------------------------------------------------------------
/src/components/directs/single/additional-info/Phone.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { CardText } from 'material-ui/Card';
3 | import PropTypes from 'prop-types';
4 | import PhoneIcon from 'material-ui/svg-icons/communication/phone';
5 |
6 | const propTypes = {
7 | phone: PropTypes.string.isRequired,
8 | };
9 |
10 | /**
11 | * @function Phone
12 | * @param {Number} phone
13 | * @param {Object} cardTextProps CardText props
14 | * @returns {XML}
15 | */
16 | function Phone({ phone, ...cardTextProps }) {
17 | return (
18 |
19 |
20 |
21 | {phone}
22 |
23 |
24 | );
25 | }
26 |
27 | Phone.propTypes = propTypes;
28 |
29 | export default Phone;
30 |
31 |
--------------------------------------------------------------------------------
/src/components/header/MeetingNavigationIconsContainers.js:
--------------------------------------------------------------------------------
1 | import { connect } from 'react-redux';
2 | import meetingActions from '../../actions/meetings';
3 |
4 | import {
5 | getMeetingsInSameDirectAsActive,
6 | getIndexOfActiveMeating,
7 | } from '../../selectors/meetings';
8 | import MeetingNavigationIcons from './MeetingNavigationIcons';
9 |
10 | const mapStateToProps = (state, ownProps) => {
11 | return {
12 | indexOfActive: getIndexOfActiveMeating(state, ownProps),
13 | meetings: getMeetingsInSameDirectAsActive(state, ownProps),
14 | }};
15 |
16 | const mapDispatchToProps = (dispatch) => ({
17 | setActiveMeating : (id) => {
18 | dispatch(meetingActions.find(id));
19 | }
20 | });
21 |
22 | export default connect(mapStateToProps,mapDispatchToProps)(MeetingNavigationIcons);
23 |
--------------------------------------------------------------------------------
/src/HOCs/archive/__tests__/ArchiveHocFactory.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable import/newline-after-import,import/first,no-undef */
2 | import React from 'react';
3 | import ArchiveHocFactory from '../ArchiveHocFactory';
4 | jest.mock('../OnArchivedContainerHOC');
5 | import OnArchivedContainerHOC from '../OnArchivedContainerHOC';
6 |
7 |
8 | describe('ArchiveHocFactory', () => {
9 | const TestComponent = () => (Test component
);
10 | const factoryMock = jest.fn();
11 |
12 | ArchiveHocFactory(TestComponent, factoryMock);
13 |
14 | it('Should call passed factory function', () => {
15 | expect(factoryMock.mock.calls.length).toBe(1);
16 | });
17 |
18 | it('Should call OnArchivedContainerHOC', () => {
19 | expect(OnArchivedContainerHOC.mock.calls.length).toBe(1);
20 | });
21 | });
22 |
--------------------------------------------------------------------------------
/src/components/directs/list/DirectItemList.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { List } from 'material-ui/List';
3 | import PropTypes from 'prop-types';
4 |
5 | import DirectItem from './item/DirectItem';
6 |
7 | const propTypes = {
8 | directs: PropTypes.array.isRequired,
9 | };
10 |
11 | /**
12 | * @function DirectItemList
13 | * @param {Array} directs array of direct object
14 | * @returns {Array} array of XML
15 | * @constructor
16 | */
17 | function DirectItemList({ directs }) {
18 | const content = directs.map(direct => (
19 |
24 | ));
25 |
26 | return ({content}
);
27 | }
28 |
29 | DirectItemList.propTypes = propTypes;
30 |
31 | export default DirectItemList;
32 |
--------------------------------------------------------------------------------
/src/store/store.js:
--------------------------------------------------------------------------------
1 | import { routerMiddleware, syncHistoryWithStore } from 'react-router-redux';
2 | import { browserHistory } from 'react-router';
3 | import { applyMiddleware, compose, createStore } from 'redux';
4 | import thunk from 'redux-thunk';
5 |
6 | import rootReducer from '../reducers/index';
7 | import initialState from './initialState';
8 |
9 | const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
10 |
11 | const store = createStore(
12 | rootReducer,
13 | initialState,
14 | composeEnhancers(
15 | applyMiddleware(thunk, routerMiddleware(browserHistory)),
16 | // applyMiddleware(thunk),
17 | ),
18 | );
19 |
20 | export const history = syncHistoryWithStore(browserHistory, store);
21 | // export const history = browserHistory;
22 |
23 | export default store;
24 |
--------------------------------------------------------------------------------
/src/components/directs/single/additional-info/StartDate.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { CardText } from 'material-ui/Card';
3 | import PropTypes from 'prop-types';
4 | import StartDateIcon from 'material-ui/svg-icons/notification/event-note';
5 |
6 | const propTypes = {
7 | startDate: PropTypes.string.isRequired,
8 | };
9 |
10 | /**
11 | * @function StartDate
12 | * @param {String} startDate
13 | * @param {Object} cardTextProps CardText props
14 | * @returns {XML}
15 | */
16 | function StartDate({ startDate, ...cardTextProps }) {
17 | const date = new Date(startDate).toLocaleDateString();
18 | return (
19 |
20 |
21 | {date}
22 |
23 | );
24 | }
25 |
26 | StartDate.propTypes = propTypes;
27 |
28 | export default StartDate;
29 |
--------------------------------------------------------------------------------
/src/HOCs/ShowLoaderHOC.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import './ShowLoaderHOC.css';
3 |
4 | /**
5 | * @description Loader show/hide HOC.
6 | * @param {String} propKey
7 | * @param {*} expectedValue
8 | * @return {XML} Loader | WrappedComponent
9 | */
10 | const ShowLoaderHOC = (propKey, expectedValue) => (WrappedComponent) => {
11 | /**
12 | * @function Loader
13 | * @param props
14 | * @returns {XML}
15 | */
16 | function Loader(props) {
17 | const loader = (
18 |
22 | );
23 |
24 | return props[propKey] === expectedValue ? loader : ;
25 | }
26 |
27 | return Loader;
28 | };
29 |
30 |
31 | export default ShowLoaderHOC;
32 |
--------------------------------------------------------------------------------
/src/components/directs/list/DirectItemDividerList.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { List } from 'material-ui/List';
3 | import PropTypes from 'prop-types';
4 |
5 | import DirectItemDivider from './item/DirectItemDivider';
6 |
7 | const propTypes = {
8 | directs: PropTypes.object.isRequired,
9 | };
10 |
11 | /**
12 | * @function DirectItemDividerList
13 | * @param {Object} directs Object with team name as properties
14 | * @returns {XML}
15 | * @constructor
16 | */
17 | function DirectItemDividerList({ directs }) {
18 | const content = Object.keys(directs).map((team) => {
19 | return (
20 |
21 | );
22 | });
23 | return ({content}
);
24 | }
25 |
26 | DirectItemDividerList.propTypes = propTypes;
27 |
28 | export default DirectItemDividerList;
29 |
--------------------------------------------------------------------------------
/src/utility/uniqueRandomNumber.js:
--------------------------------------------------------------------------------
1 | import uniqueRandom from 'unique-random';
2 |
3 | /**
4 | * @description Improved version of unique-random. Always shows unique number
5 | * between two numbers until all numbers have shown then starts new cycle
6 | * @param {Number} start
7 | * @param {Number} end
8 | * @return {Function}
9 | */
10 | function uniqueRandomNumber(start, end) {
11 | const rand = uniqueRandom(start, end);
12 | let showedNumber = [];
13 |
14 | return function random() {
15 | if ((showedNumber.length - 1) === end) {
16 | showedNumber = [];
17 | }
18 |
19 | const nextNumber = rand();
20 |
21 | if (showedNumber.indexOf(nextNumber) === -1) {
22 | showedNumber.push(nextNumber);
23 | return nextNumber;
24 | }
25 |
26 | return random();
27 | };
28 | }
29 |
30 | export default uniqueRandomNumber;
31 |
--------------------------------------------------------------------------------
/src/components/common/InnerHtmlStripTags.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import String from 'string';
3 | import PropTypes from 'prop-types';
4 |
5 | /**
6 | * @description propTypes for InnerHtmlStripTags
7 | * @type {Object}
8 | */
9 | const propTypes = {
10 | html: PropTypes.string.isRequired,
11 | };
12 |
13 | /**
14 | * @function InnerHtmlStripTags
15 | * @param {String} html html string
16 | * @returns {XML}
17 | */
18 | function InnerHtmlStripTags({ html }) {
19 | if (!html) {
20 | return null;
21 | }
22 |
23 | const stripedHtml = String(html)
24 | .trim()
25 | .replaceAll(' ', ' ')
26 | .stripTags()
27 | .s;
28 |
29 | return ;
30 | }
31 |
32 | InnerHtmlStripTags.propTypes = propTypes;
33 |
34 | export default InnerHtmlStripTags;
35 |
--------------------------------------------------------------------------------
/src/components/meetings/questions/dialog/QuestionsDialogBoxContainer.js:
--------------------------------------------------------------------------------
1 | import { connect } from 'react-redux';
2 |
3 | import questionAction from '../../../../actions/questions';
4 | import QuestionsCrudDialogBox from './QuestionsCrudDialogBox';
5 | import { getQuestionsArray } from '../../../../selectors/questions';
6 |
7 | const mapStateToProps = state => ({
8 | questions: getQuestionsArray(state),
9 | });
10 |
11 | const mapDispatchToProps = dispatch => ({
12 | submitQuestionForm: (data) => {
13 | if (data.id) {
14 | dispatch(questionAction.update(data.id, { question: data.question }));
15 | } else {
16 | dispatch(questionAction.create(data));
17 | }
18 | },
19 | onDeleteQuestion: (id) => {
20 | dispatch(questionAction.remove(id));
21 | },
22 | });
23 |
24 | export default connect(mapStateToProps, mapDispatchToProps)(QuestionsCrudDialogBox);
25 |
--------------------------------------------------------------------------------
/src/components/directs/single/tabs/follow-up/DirectFollowUpList.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 |
4 | import FollowUpListContainer from './FollowUpListContainer';
5 | import FollowUpListArchivedContainer from './FollowUpListArchivedContainer';
6 |
7 | /**
8 | * @description propTypes for DirectFollowUpList
9 | * @type {Object}
10 | */
11 | const propTypes = {
12 | directId: PropTypes.string.isRequired,
13 | };
14 |
15 | /**
16 | * @function DirectFollowUpList
17 | * @param props
18 | * @returns {XML}
19 | */
20 | function DirectFollowUpList(props) {
21 | return (
22 |
23 |
24 |
25 |
26 | );
27 | }
28 |
29 | DirectFollowUpList.propTypes = propTypes;
30 |
31 | export default DirectFollowUpList;
32 |
--------------------------------------------------------------------------------
/src/selectors/questions.js:
--------------------------------------------------------------------------------
1 | import { createSelector } from 'reselect';
2 |
3 | const getQuestions = (state) => {
4 | if (!(state.questions.list instanceof Map)) {
5 | return [];
6 | }
7 | return state.questions.list;
8 | };
9 | const getFilterByCategory = state => state.questions.filterByCategory;
10 |
11 | export const getQuestionsArray = createSelector(
12 | getQuestions,
13 | (questions) => {
14 | const arr = [];
15 | questions.forEach((question, key) => {
16 | arr.push({ ...question, ...{ id: key } });
17 | });
18 | return arr;
19 | },
20 | );
21 |
22 | export const getFiltreredQuestionsArray = createSelector(
23 | getQuestionsArray, getFilterByCategory,
24 | (questions, filter) => {
25 | if (!filter) {
26 | return questions;
27 | }
28 | return questions.filter(question => question.categoriesQuestionsID === filter);
29 | },
30 | );
31 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { render } from 'react-dom';
3 | import { Router } from 'react-router';
4 | import injectTapEventPlugin from 'react-tap-event-plugin';
5 | import { Provider } from 'react-redux';
6 | import ReactGA from 'react-ga';
7 |
8 | import store, { history } from './store/store';
9 | import './index.css';
10 | import routes from './routes';
11 |
12 | injectTapEventPlugin();
13 |
14 | ReactGA.initialize('UA-2153940-18');
15 |
16 | history.listen((location) => {
17 | if (process.env.NODE_ENV === 'production') {
18 | ReactGA.set({ page: location.pathname });
19 | ReactGA.pageview(location.pathname);
20 | }
21 | });
22 |
23 | const router = (
24 |
25 |
29 |
30 | );
31 |
32 | render(router, document.getElementById('root'));
33 |
--------------------------------------------------------------------------------
/src/components/directs/single/DirectShow.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 |
4 | import DirectSingleContainer from './DirectSingleContainer';
5 | import DirectArchivedSingleContainer from './DirectArchivedSingleContainer';
6 |
7 | /**
8 | * @description propTypes for DirectShow
9 | * @type {{id: (*)}}
10 | */
11 | const propTypes = {
12 | params: PropTypes.shape({
13 | id: PropTypes.string.isRequired,
14 | }).isRequired,
15 | };
16 | /**
17 | * @function DirectShow
18 | * @param {Object} props props object passed by route
19 | * @returns {XML}
20 | */
21 | function DirectShow(props) {
22 | const { id } = props.params;
23 | return (
24 |
25 |
26 |
27 |
28 | );
29 | }
30 |
31 | DirectShow.propTypes = propTypes;
32 |
33 | export default DirectShow;
34 |
--------------------------------------------------------------------------------
/src/HOCs/ShowLoaderHOC.css:
--------------------------------------------------------------------------------
1 | .spinner {
2 | width: 40px;
3 | height: 40px;
4 | position: relative;
5 | margin: 200px auto; }
6 |
7 | .double-bounce1, .double-bounce2 {
8 | width: 100%;
9 | height: 100%;
10 | border-radius: 50%;
11 | background-color: #333;
12 | opacity: 0.6;
13 | position: absolute;
14 | top: 0;
15 | left: 0;
16 | -webkit-animation: sk-bounce 2.0s infinite ease-in-out;
17 | animation: sk-bounce 2.0s infinite ease-in-out; }
18 |
19 | .double-bounce2 {
20 | -webkit-animation-delay: -1.0s;
21 | animation-delay: -1.0s; }
22 |
23 | @-webkit-keyframes sk-bounce {
24 | 0%, 100% {
25 | -webkit-transform: scale(0); }
26 | 50% {
27 | -webkit-transform: scale(1); } }
28 |
29 | @keyframes sk-bounce {
30 | 0%, 100% {
31 | transform: scale(0);
32 | -webkit-transform: scale(0); }
33 | 50% {
34 | transform: scale(1);
35 | -webkit-transform: scale(1); } }
36 |
--------------------------------------------------------------------------------
/src/components/directs/single/head-info/EditIconLink.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Link } from 'react-router';
3 | import IconButton from 'material-ui/IconButton';
4 | import EditIcon from 'material-ui/svg-icons/editor/mode-edit';
5 | import PropTypes from 'prop-types';
6 |
7 | import HideOnArchivedHOC from '../../../../HOCs/archive/HideOnArchivedHOC';
8 |
9 | const propTypes = {
10 | id: PropTypes.string.isRequired,
11 | };
12 |
13 | /**
14 | * @function EditIcon
15 | * @param {String} id
16 | * @returns {XML}
17 | */
18 | function EditIconLink({ id }) {
19 | return (
20 |
21 | }
23 | >
24 |
25 |
26 |
27 | );
28 | }
29 |
30 | EditIconLink.propTypes = propTypes;
31 |
32 | export default HideOnArchivedHOC(EditIconLink);
33 |
--------------------------------------------------------------------------------
/src/components/meetings/questions/dialog/QuestionsImportBtn.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import IconButton from 'material-ui/IconButton';
3 | import AutoRenew from 'material-ui/svg-icons/action/autorenew';
4 | import PropTypes from 'prop-types';
5 |
6 | const propTypes = {
7 | importQuestion: PropTypes.func.isRequired,
8 | loading: PropTypes.bool.isRequired,
9 | };
10 |
11 | /**
12 | * @function QuestionsImportBtn
13 | * @param {Function} importQuestion
14 | * @param {Boolean} loading
15 | * @returns {XML}
16 | */
17 | function QuestionsImportBtn({ importQuestion, loading }) {
18 | return (
19 |
20 |
21 |
22 | );
23 | }
24 |
25 | QuestionsImportBtn.propTypes = propTypes;
26 |
27 | export default QuestionsImportBtn;
28 |
29 |
--------------------------------------------------------------------------------
/src/components/directs/list/AddNewDirectBtn.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Link } from 'react-router';
3 | import FloatingActionButton from 'material-ui/FloatingActionButton';
4 | import ContentAdd from 'material-ui/svg-icons/content/add';
5 |
6 | import HideOnArchivedHOC from '../../../HOCs/archive/HideOnArchivedHOC';
7 |
8 | /**
9 | * @description Button Style
10 | * @type {Object}
11 | */
12 | const buttonStyle = {
13 | margin: 0,
14 | top: 'auto',
15 | right: 20,
16 | bottom: 76,
17 | left: 'auto',
18 | position: 'fixed',
19 | };
20 |
21 | /**
22 | * @function AddNewDirectBtn
23 | * @returns {XML}
24 | * @constructor
25 | */
26 | function AddNewDirectBtn() {
27 | return (
28 | }
31 | >
32 |
33 |
34 | );
35 | }
36 |
37 | export default HideOnArchivedHOC(AddNewDirectBtn);
38 |
--------------------------------------------------------------------------------
/src/components/directs/list/item/ArchivedLinkGenerator.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Link } from 'react-router';
3 | import { connect } from 'react-redux';
4 |
5 | import { getIsArchived } from '../../../../selectors/routing';
6 | import { ARCHIVED_URL_SUFFIX } from '../../../../constants/general';
7 |
8 | /**
9 | * @function ArchivedLinkGenerator
10 | * @param {Object} props
11 | * @returns {XML}
12 | */
13 | function ArchivedLinkGenerator(props) {
14 | const { to, isArchived,dispatch, ...linkProps } = props;
15 | let newTo = {
16 | pathname: to,
17 | };
18 | if (isArchived) {
19 | newTo = {
20 | ...newTo,
21 | ...{
22 | pathname: `${to}/${ARCHIVED_URL_SUFFIX}`,
23 | state: { isArchived },
24 | },
25 | };
26 | }
27 | return ;
28 | }
29 |
30 | const mapStateToProps = state => ({
31 | isArchived: getIsArchived(state),
32 | });
33 |
34 | export default connect(mapStateToProps)(ArchivedLinkGenerator);
35 |
--------------------------------------------------------------------------------
/src/reducers/auth.js:
--------------------------------------------------------------------------------
1 | import {
2 | AUTH_AWAITING_RESPONSE,
3 | AUTH_LOGGED_IN,
4 | AUTH_ERROR,
5 | AUTH_LOGOUT,
6 | AUTH_ANONYMOUS,
7 | AUTH_LOGIN,
8 | AUTH_OPEN } from '../actions/types';
9 |
10 | export default (state = {}, action) => {
11 | switch (action.type) {
12 | case AUTH_OPEN:
13 | return {
14 | status: AUTH_AWAITING_RESPONSE,
15 | displayName: 'guest',
16 | uid: null,
17 | email: null,
18 | photoURL: null,
19 | };
20 | case AUTH_LOGIN:
21 | return {
22 | status: AUTH_LOGGED_IN,
23 | displayName: action.displayName,
24 | uid: action.uid,
25 | email: action.email,
26 | photoURL: action.photoURL,
27 | };
28 | case AUTH_LOGOUT:
29 | case AUTH_ERROR:
30 | return {
31 | status: AUTH_ANONYMOUS,
32 | displayName: 'guest',
33 | uid: null,
34 | email: null,
35 | photoURL: null,
36 | };
37 | default: return state;
38 | }
39 | };
40 |
--------------------------------------------------------------------------------
/src/components/About.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | const About = () => {
4 | return (
5 |
6 |
About 1on1tracker.com
7 |
1on1tracker is a web application to keep track of 1 on 1 meetings between managers and the people that report to them (aka "direct reports", or "directs"). It also includes a list of directs that can be grouped by teams, sample questions to ask at 1 on 1 meetings, and follow up items.
8 |
It's been designed for mobile web because I wanted to use it on my phone. I don't like taking notes on paper that can get lost, and using a laptop is IMHO not so great for 1 on 1 meetings.
9 |
There are some commercial applications that deal with 1 on 1 meetings. My approach was to create something simple for myself and share it with other managers. This app was designed by me, Vidal Graupera and engineering manager in the SF Bay Area, and I use this application myself.
10 |
11 | );
12 | }
13 |
14 | export default About;
15 |
--------------------------------------------------------------------------------
/src/HOCs/archive/HideOnArchivedHOC.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 |
4 | import ArchiveHocFactory from './ArchiveHocFactory';
5 |
6 |
7 | const propTypes = {
8 | isArchived: PropTypes.bool,
9 | };
10 |
11 | const defaultProps = {
12 | isArchived: false,
13 | };
14 | export const componentFactory = (WrappedComponent) => {
15 | const HideOnArchived = (props) => {
16 | const { isArchived, ...restProps } = props;
17 | if (isArchived) {
18 | return null;
19 | }
20 |
21 | return ;
22 | };
23 |
24 | HideOnArchived.propTypes = propTypes;
25 | HideOnArchived.defaultProps = defaultProps;
26 |
27 | return HideOnArchived;
28 | };
29 |
30 | /**
31 | * @description Hide wrapped component when isArchived prop true
32 | * @param WrappedComponent
33 | */
34 | const HideOnArchivedHOC = (WrappedComponent) => {
35 | return ArchiveHocFactory(WrappedComponent, componentFactory);
36 | };
37 |
38 | export default HideOnArchivedHOC;
39 |
--------------------------------------------------------------------------------
/src/components/directs/single/tabs/DirectTabs.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Paper from 'material-ui/Paper';
3 | import {
4 | Tabs,
5 | Tab,
6 | } from 'material-ui/Tabs';
7 | import PropTypes from 'prop-types';
8 |
9 | import DirectMeetingList from './meetings/DirectMeetingList';
10 | import DirectFollowUpList from './follow-up/DirectFollowUpList';
11 |
12 | const propTypes = {
13 | directId: PropTypes.string.isRequired,
14 | };
15 |
16 | /**
17 | * @function DirectTabs
18 | * @param {String} directId
19 | * @returns {XML}
20 | */
21 | function DirectTabs({ directId }) {
22 | return (
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 | );
34 | }
35 |
36 | DirectTabs.propTypes = propTypes;
37 |
38 | export default DirectTabs;
39 |
--------------------------------------------------------------------------------
/src/components/directs/list/DirectList.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { List, ListItem } from 'material-ui/List';
3 | import PropTypes from 'prop-types';
4 |
5 | import DirectItemDividerList from './DirectItemDividerList';
6 | import DirectItemList from './DirectItemList';
7 | import { SORT_BY_NAME } from '../../../constants/general';
8 |
9 |
10 | const propTypes = {
11 | directs: PropTypes.any.isRequired,
12 | sortBy: PropTypes.string.isRequired,
13 | };
14 | /**
15 | * @function DirectList
16 | * @param props
17 | * @returns {XML}
18 | * @constructor
19 | */
20 | function DirectList(props) {
21 | const { directs, sortBy } = props;
22 | if (directs.length === 0) {
23 | return (
24 |
25 |
26 |
27 | );
28 | }
29 |
30 | if (sortBy === SORT_BY_NAME) {
31 | return ;
32 | }
33 | return ;
34 | }
35 |
36 | DirectList.propTypes = propTypes;
37 |
38 | export default DirectList;
39 |
--------------------------------------------------------------------------------
/src/components/Auth.jsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import { connect } from 'react-redux';
3 | import FlatButton from 'material-ui/FlatButton';
4 | import * as types from '../actions/types';
5 | import { openAuth, logoutUser } from '../actions/auth';
6 |
7 | class Auth extends Component {
8 | // static muiName = 'FlatButton';
9 |
10 | render() {
11 | switch (this.props.auth.status) {
12 | case types.AUTH_LOGGED_IN: return (
13 |
14 | );
15 | case types.AUTH_AWAITING_RESPONSE: return (
16 |
17 | );
18 | default: return (
19 |
20 | );
21 | }
22 | }
23 | }
24 |
25 | const mapStateToProps = (state) => {
26 | return {
27 | auth: state.auth,
28 | };
29 | };
30 |
31 | const mapDispatchToProps = {
32 | openAuth,
33 | logoutUser,
34 | };
35 |
36 | export default connect(mapStateToProps, mapDispatchToProps)(Auth);
37 |
--------------------------------------------------------------------------------
/src/HOCs/archive/ShowOnArchivedHOC.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 |
4 | import ArchiveHocFactory from './ArchiveHocFactory';
5 |
6 | const propTypes = {
7 | isArchived: PropTypes.bool,
8 | };
9 |
10 |
11 | const defaultProps = {
12 | isArchived: false,
13 | };
14 |
15 | export const componentFactory = (WrappedComponent) => {
16 | const ShowOnArchived = (props) => {
17 | const { isArchived, ...restProps } = props;
18 |
19 | if (!isArchived) {
20 | return null;
21 | }
22 |
23 | return ;
24 | };
25 |
26 | ShowOnArchived.propTypes = propTypes;
27 | ShowOnArchived.defaultProps = defaultProps;
28 |
29 | return ShowOnArchived;
30 | };
31 |
32 | /**
33 | * @description Show wrapped component when is isArchived prop true
34 | * @param WrappedComponent
35 | * @constructor
36 | */
37 | const ShowOnArchivedHOC = (WrappedComponent) => {
38 | return ArchiveHocFactory(WrappedComponent, componentFactory);
39 | };
40 |
41 | export default ShowOnArchivedHOC;
42 |
--------------------------------------------------------------------------------
/src/actions/followUps.js:
--------------------------------------------------------------------------------
1 | import * as types from './types';
2 | import { FirebaseApi } from '../firebase/firebase';
3 | import { ARCHIVED_PATH_PREFIX } from '../constants/general';
4 |
5 | export const archivedFollowUps = new FirebaseApi(`${ARCHIVED_PATH_PREFIX}followUps`, {
6 | LOAD_REQUEST: types.ARCHIVED_LOAD_FOLLOW_UPS_REQUEST,
7 | LOAD_SUCCESS: types.ARCHIVED_LOAD_FOLLOW_UPS_SUCCESS,
8 | UNLOAD_SUCCESS: types.ARCHIVED_UNLOAD_FOLLOW_UPS_SUCCESS,
9 | SET_ACTIVE: types.ARCHIVED_SET_ACTIVE_FOLLOW_UP,
10 | RESET_ACTIVE: types.ARCHIVED_RESET_ACTIVE_FOLLOW_UP,
11 | CREATE: types.ARCHIVED_CREATE_FOLLOW_UP,
12 | SET_MATCHING: types.ARCHIVED_SET_MATCHING_FOLLOW_UPS,
13 | });
14 |
15 | export default new FirebaseApi('followUps', {
16 | LOAD_REQUEST: types.LOAD_FOLLOW_UPS_REQUEST,
17 | LOAD_SUCCESS: types.LOAD_FOLLOW_UPS_SUCCESS,
18 | UNLOAD_SUCCESS: types.UNLOAD_FOLLOW_UPS_SUCCESS,
19 | SET_ACTIVE: types.SET_ACTIVE_FOLLOW_UP,
20 | RESET_ACTIVE: types.RESET_ACTIVE_FOLLOW_UP,
21 | CREATE: types.CREATE_FOLLOW_UP,
22 | SET_MATCHING: types.SET_MATCHING_FOLLOW_UPS,
23 | });
24 |
--------------------------------------------------------------------------------
/src/components/Account.jsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import PropTypes from 'prop-types';
3 | import { connect } from 'react-redux';
4 |
5 | /**
6 | * @description PropTypes for Account component
7 | * @type {{displayName: (*), email: (*), photoURL: (*)}}
8 | */
9 | const propTypes = {
10 | displayName: PropTypes.string.isRequired,
11 | email: PropTypes.string.isRequired,
12 | photoURL: PropTypes.string.isRequired,
13 | };
14 |
15 | class Account extends Component {
16 | render() {
17 | return (
18 |
19 |
{this.props.displayName}
20 |
{this.props.email}
21 |
22 |
23 |
24 |
25 | );
26 | }
27 | }
28 |
29 | Account.propTypes = propTypes;
30 |
31 | const mapStateToProps = (state) => {
32 | return {
33 | displayName: state.auth.displayName,
34 | email: state.auth.email,
35 | photoURL: state.auth.photoURL,
36 | };
37 | };
38 |
39 | export default connect(mapStateToProps)(Account);
40 |
--------------------------------------------------------------------------------
/src/actions/meetings.js:
--------------------------------------------------------------------------------
1 | import * as types from './types';
2 | import { FirebaseApi } from '../firebase/firebase';
3 | import { ARCHIVED_PATH_PREFIX } from '../constants/general';
4 |
5 | export const archivedMeetings = new FirebaseApi(`${ARCHIVED_PATH_PREFIX}meetings`, {
6 | LOAD_REQUEST: types.ARCHIVED_LOAD_MEETINGS_REQUEST,
7 | LOAD_SUCCESS: types.ARCHIVED_LOAD_MEETINGS_SUCCESS,
8 | UNLOAD_SUCCESS: types.ARCHIVED_UNLOAD_MEETINGS_SUCCESS,
9 | SET_ACTIVE: types.ARCHIVED_SET_ACTIVE_MEETING,
10 | RESET_ACTIVE: types.ARCHIVED_RESET_ACTIVE_MEETING,
11 | CREATE: types.ARCHIVED_CREATE_MEETING,
12 | SET_MATCHING: types.ARCHIVED_SET_MATCHING_MEETINGS,
13 | },
14 | 'meetingDateReverse');
15 |
16 | export default new FirebaseApi('meetings', {
17 | LOAD_REQUEST: types.LOAD_MEETINGS_REQUEST,
18 | LOAD_SUCCESS: types.LOAD_MEETINGS_SUCCESS,
19 | UNLOAD_SUCCESS: types.UNLOAD_MEETINGS_SUCCESS,
20 | SET_ACTIVE: types.SET_ACTIVE_MEETING,
21 | RESET_ACTIVE: types.RESET_ACTIVE_MEETING,
22 | CREATE: types.CREATE_MEETING,
23 | SET_MATCHING: types.SET_MATCHING_MEETINGS,
24 | },
25 | 'meetingDateReverse');
26 |
--------------------------------------------------------------------------------
/src/reducers/categoriesQuestions.js:
--------------------------------------------------------------------------------
1 | import * as types from '../actions/types';
2 |
3 | export default (state = {}, action) => {
4 | switch (action.type) {
5 | case types.LOAD_CATEGORIES_QUESTIONS_REQUEST:
6 | return {
7 | ...state,
8 | ...{ loading: true },
9 | };
10 | case types.LOAD_CATEGORIES_QUESTIONS_SUCCESS:
11 | return {
12 | ...state,
13 | ...{
14 | list: action.payload,
15 | loading: false,
16 | error: null,
17 | },
18 | };
19 | case types.UNLOAD_CATEGORIES_QUESTIONS_SUCCESS:
20 | return {
21 | ...state,
22 | ...{
23 | activeQuestion: null,
24 | list: [],
25 | error: null,
26 | },
27 | };
28 | case types.SET_ACTIVE_CATEGORIES_QUESTIONS:
29 | return {
30 | ...state,
31 | activeQuestion: action.payload,
32 | };
33 | case types.RESET_ACTIVE_CATEGORIES_QUESTIONS:
34 | return {
35 | ...state,
36 | activeQuestion: null,
37 | };
38 | default:
39 | return state;
40 | }
41 | };
42 |
--------------------------------------------------------------------------------
/src/components/meetings/questions/QuestionsSettings.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 |
3 | import QuestionsDialogBoxContainer from './dialog/QuestionsDialogBoxContainer';
4 |
5 | /**
6 | * @class QuestionsSettings
7 | * @extends React.Component
8 | * @description Render component
9 | */
10 | class QuestionsSettings extends Component {
11 |
12 | state = {
13 | openDialog: false,
14 | };
15 |
16 | openDialog = (e) => {
17 | e.preventDefault();
18 | this.setState({ openDialog: true });
19 | };
20 |
21 | handleCloseDialog = () => {
22 | this.setState({ openDialog: false });
23 | };
24 |
25 | /**
26 | * @description render
27 | * @return {Object} JSX HTML Content
28 | */
29 | render() {
30 | return (
31 |
38 | );
39 | }
40 | }
41 |
42 | export default QuestionsSettings;
43 |
--------------------------------------------------------------------------------
/src/components/drawer/TeamMenuItem.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import MenuItem from 'material-ui/MenuItem';
3 | import TeamCrudDialogBoxContainer from '../teams/dialog/TeamCrudDialogBoxContainer';
4 | /**
5 | * @class TeamMenuItem
6 | * @extends React.Component
7 | * @description Render component
8 | */
9 | class TeamMenuItem extends Component {
10 |
11 | state = {
12 | openDialog: false,
13 | };
14 | handleItemClick = () => {
15 | this.props.closeDrawer(false);
16 | this.setState({ openDialog: true });
17 | };
18 | handleCloseDialog = () => {
19 | this.setState({ openDialog: false });
20 | };
21 | /**
22 | * @description render
23 | * @return {Object} JSX HTML Content
24 | */
25 | render() {
26 | return (
27 |
28 |
32 |
36 |
37 | );
38 | }
39 | }
40 |
41 | export default TeamMenuItem;
42 |
--------------------------------------------------------------------------------
/src/reducers/index.js:
--------------------------------------------------------------------------------
1 | import { combineReducers } from 'redux';
2 | import { routerReducer } from 'react-router-redux';
3 | import { reducer as formReducer } from 'redux-form';
4 |
5 | import { ARCHIVED_ACTION_PREFIX } from '../constants/general';
6 | import directsReducerFactory from './directs';
7 | import meetingsReducerFactory from './meetings';
8 | import followUpsReducerFactory from './followUps';
9 | import teams from './teams';
10 | import questions from './questions';
11 | import categoriesQuestions from './categoriesQuestions';
12 |
13 | import auth from './auth';
14 | import header from './header';
15 |
16 | const rootReducer = combineReducers({
17 | directs: directsReducerFactory(),
18 | archivedDirects: directsReducerFactory(ARCHIVED_ACTION_PREFIX),
19 | meetings: meetingsReducerFactory(),
20 | archivedMeetings: meetingsReducerFactory(ARCHIVED_ACTION_PREFIX),
21 | followUps: followUpsReducerFactory(),
22 | archivedFollowUp: followUpsReducerFactory(ARCHIVED_ACTION_PREFIX),
23 | auth,
24 | header,
25 | teams,
26 | questions,
27 | categoriesQuestions,
28 | form: formReducer,
29 | routing: routerReducer });
30 |
31 | export default rootReducer;
32 |
--------------------------------------------------------------------------------
/src/components/teams/dialog/TeamsCrudDialogBox.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 |
4 | import CrudDialogBox from '../../common/crud-dialog-box/CrudDialogBox';
5 | import TeamList from './TeamList';
6 | import TeamForm from './TeamForm';
7 |
8 | const propTypes = {
9 | openDialog: PropTypes.bool.isRequired,
10 | handleCloseDialog: PropTypes.func.isRequired,
11 | onDelete: PropTypes.func.isRequired,
12 | submitForm: PropTypes.func.isRequired,
13 | };
14 |
15 | /**
16 | * @function TeamsCrudDialogBox
17 | * @param props
18 | * @returns {XML}
19 | */
20 | function TeamsCrudDialogBox(props) {
21 | const {
22 | openDialog,
23 | handleCloseDialog,
24 | onDelete,
25 | submitForm,
26 | } = props;
27 |
28 | const dialogProps = {
29 | title: 'Team',
30 | openDialog,
31 | handleCloseDialog,
32 | ListComponent: TeamList,
33 | onDeleteItem: onDelete,
34 | FormComponent: TeamForm,
35 | submitForm,
36 | };
37 |
38 | return (
39 |
42 | );
43 | }
44 |
45 | TeamsCrudDialogBox.propTypes = propTypes;
46 |
47 | export default TeamsCrudDialogBox;
48 |
--------------------------------------------------------------------------------
/src/components/drawer/QuestionMenuItem.js:
--------------------------------------------------------------------------------
1 | import React, {Component} from "react";
2 | import MenuItem from "material-ui/MenuItem";
3 | import QuestionsDialogBoxContainer from "../meetings/questions/dialog/QuestionsDialogBoxContainer";
4 | /**
5 | * @class TeamMenuItem
6 | * @extends React.Component
7 | * @description Render component
8 | */
9 | class QuestionMenuItem extends Component {
10 |
11 | state = {
12 | openDialog: false,
13 | };
14 | handleItemClick = () => {
15 | this.props.closeDrawer(false);
16 | this.setState({ openDialog: true });
17 | };
18 | handleCloseDialog = () => {
19 | this.setState({ openDialog: false });
20 | };
21 | /**
22 | * @description render
23 | * @return {Object} JSX HTML Content
24 | */
25 | render() {
26 | return (
27 |
28 |
32 |
36 |
37 | );
38 | }
39 | }
40 |
41 | export default QuestionMenuItem;
42 |
--------------------------------------------------------------------------------
/src/HOCs/archive/OnArchivedContainerHOC.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { connect } from 'react-redux';
3 | import PropTypes from 'prop-types';
4 |
5 | import { getIsArchived } from '../../selectors/routing';
6 |
7 |
8 | const propTypes = {
9 | isArchived: PropTypes.bool,
10 | dispatch: PropTypes.func.isRequired,
11 | };
12 |
13 | const defaultProps = {
14 | isArchived: false,
15 | };
16 |
17 | export const mapStateToProps = state => ({
18 | isArchived: getIsArchived(state),
19 | });
20 |
21 | export const componentFactory = (WrappedComponent) => {
22 | function OnArchivedContainer(props) {
23 | const { dispatch, ...restProps } = props;
24 | return ;
25 | }
26 |
27 | OnArchivedContainer.propTypes = propTypes;
28 | OnArchivedContainer.defaultProps = defaultProps;
29 | return OnArchivedContainer;
30 | };
31 |
32 | /**
33 | * @description HOC provide props over redux connect function
34 | * @param WrappedComponent
35 | * @return {XML}
36 | uctor
37 | */
38 | const OnArchivedContainerHOC = (WrappedComponent) => {
39 | const OnArchivedContainer = componentFactory(WrappedComponent);
40 |
41 | return connect(mapStateToProps)(OnArchivedContainer);
42 | };
43 |
44 | export default OnArchivedContainerHOC;
45 |
--------------------------------------------------------------------------------
/src/reducers/meetings.js:
--------------------------------------------------------------------------------
1 | import * as types from '../actions/types';
2 |
3 | export default (name = '') => {
4 | return (state = {}, action) => {
5 | switch (action.type) {
6 | case name + types.LOAD_MEETINGS_REQUEST:
7 | return {
8 | ...state,
9 | ...{
10 | loading: true,
11 | },
12 | };
13 | case name + types.LOAD_MEETINGS_SUCCESS:
14 | return {
15 | ...state,
16 | ...{
17 | list: action.payload,
18 | loading: false,
19 | },
20 | };
21 | case name + types.UNLOAD_MEETINGS_SUCCESS:
22 | return {
23 | ...state,
24 | ...{
25 | list: {},
26 | activeMeeting: null,
27 | activeMeetingKey: null,
28 | loading: false,
29 | error: null,
30 | },
31 | };
32 | case name + types.SET_ACTIVE_MEETING:
33 | return { ...state,
34 | activeMeeting: action.payload,
35 | activeMeetingKey: action.key };
36 | case name + types.RESET_ACTIVE_MEETING:
37 | return { ...state,
38 | activeMeeting: null,
39 | activeMeetingKey: null };
40 | default:
41 | return state;
42 | }
43 | };
44 | };
45 |
--------------------------------------------------------------------------------
/src/reducers/directs.js:
--------------------------------------------------------------------------------
1 | import * as types from '../actions/types';
2 |
3 | export default function (name = '') {
4 | return (state = {}, action) => {
5 | switch (action.type) {
6 | case name + types.LOAD_DIRECTS_REQUEST:
7 | return {
8 | ...state,
9 | ...{ loading: true },
10 | };
11 | case name + types.LOAD_DIRECTS_SUCCESS:
12 | return {
13 | ...state,
14 | ...{
15 | list: action.payload,
16 | loading: false,
17 | },
18 | };
19 | case name + types.UNLOAD_DIRECTS_SUCCESS:
20 | return {
21 | ...state,
22 | ...{
23 | activeDirect: null,
24 | list: [],
25 | loading: false,
26 | error: null,
27 | },
28 | };
29 | case name + types.SET_ACTIVE_DIRECT:
30 | return {
31 | ...state,
32 | activeDirect: action.payload,
33 | };
34 | case name + types.RESET_ACTIVE_DIRECT:
35 | return {
36 | ...state,
37 | activeDirect: null,
38 | };
39 | case name + types.SET_DIRECTS_SORT_BY:
40 | return { ...state, ...{ sortBy: action.payload } };
41 | default:
42 | return state;
43 | }
44 | };
45 | }
46 |
--------------------------------------------------------------------------------
/src/components/meetings/questions/QuestionsList.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { List, ListItem } from 'material-ui/List';
3 | import Paper from 'material-ui/Paper';
4 | import QuestionAnswer from 'material-ui/svg-icons/action/question-answer';
5 | import PropTypes from 'prop-types';
6 |
7 | /**
8 | * @description PropTypes for QuestionsList
9 | * @type {{questions}}
10 | */
11 | const propTypes = {
12 | questions: PropTypes.arrayOf(PropTypes.shape({
13 | id: PropTypes.string.isRequired,
14 | question: PropTypes.string.isRequired,
15 | })).isRequired,
16 | };
17 |
18 | /**
19 | * @function QuestionsList
20 | * @param {Array} questions
21 | * @returns {XML}
22 | */
23 | function QuestionsList({ questions }) {
24 | return (
25 |
26 |
27 | {questions.map((singleQuestion) => {
28 | return (
29 | }
32 | primaryText={singleQuestion.question}
33 | disabled={true}
34 | />
35 | );
36 | })}
37 |
38 |
39 | );
40 | }
41 |
42 | QuestionsList.propTypes = propTypes;
43 |
44 | export default QuestionsList;
45 |
--------------------------------------------------------------------------------
/src/components/meetings/questions/dialog/QuestionsCrudDialogBox.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 |
4 | import CrudDialogBox from '../../../common/crud-dialog-box/CrudDialogBox';
5 | import QuestionsList from './QuestionsList';
6 | import QuestionForm from './QuestionForm';
7 |
8 | const propTypes = {
9 | handleCloseDialog: PropTypes.func.isRequired,
10 | submitQuestionForm: PropTypes.func.isRequired,
11 | onDeleteQuestion: PropTypes.func.isRequired,
12 | openDialog: PropTypes.bool.isRequired,
13 | };
14 |
15 | /**
16 | * @function QuestionsCrudDialogBox
17 | * @param props
18 | * @returns {XML}
19 | */
20 | function QuestionsCrudDialogBox(props) {
21 | const {
22 | openDialog,
23 | handleCloseDialog,
24 | onDeleteQuestion,
25 | submitQuestionForm,
26 | ...rest
27 | } = props;
28 |
29 | const dialogProps = {
30 | title: 'Question',
31 | openDialog,
32 | handleCloseDialog,
33 | ListComponent: QuestionsList,
34 | onDeleteItem: onDeleteQuestion,
35 | FormComponent: QuestionForm,
36 | submitForm: submitQuestionForm,
37 | ...rest,
38 | };
39 |
40 | return ( );
41 | }
42 |
43 | QuestionsCrudDialogBox.propTypes = propTypes;
44 |
45 | export default QuestionsCrudDialogBox;
46 |
--------------------------------------------------------------------------------
/src/components/Header.jsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import PropTypes from 'prop-types';
3 | import { connect } from 'react-redux';
4 | import AppBar from 'material-ui/AppBar';
5 |
6 | import { isAuthenticated, rootPath } from '../actions/auth';
7 | import HeaderRightIcon from './header/HeaderRightIcon';
8 |
9 |
10 | class Header extends Component {
11 | returnHome = () => {
12 | this.context.router.push(rootPath(isAuthenticated(this.props)));
13 | }
14 |
15 | render() {
16 | return (
17 | }
23 | />
24 | );
25 | }
26 | }
27 |
28 | Header.propTypes = {
29 | onLeftIconButtonTouchTap: PropTypes.func.isRequired,
30 | header: PropTypes.object,
31 | };
32 |
33 | Header.contextTypes = {
34 | router: PropTypes.object,
35 | };
36 |
37 | const mapStateToProps = (state) => {
38 | return {
39 | auth: state.auth,
40 | header: state.header,
41 | };
42 | };
43 |
44 | export default connect(mapStateToProps)(Header);
45 |
--------------------------------------------------------------------------------
/src/components/directs/list/item/DirectItemDivider.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { ListItem } from 'material-ui/List';
3 | import PropTypes from 'prop-types';
4 |
5 | import DirectItem from './DirectItem';
6 | import { SORT_WITHOUT_TEAM_NAME } from '../../../../constants/general';
7 |
8 | /**
9 | * @description PropTypes for DirectItemDivider
10 | */
11 | const propTypes = {
12 | team: PropTypes.string.isRequired,
13 | directs: PropTypes.array.isRequired,
14 | };
15 |
16 | /**
17 | * @function DirectItemDivider
18 | * @param props
19 | * @returns {XML}
20 | * @constructor
21 | */
22 | function DirectItemDivider(props) {
23 | const { team, directs } = props;
24 | return (
25 | {
32 | return ( );
37 | })}
38 | />
39 | );
40 | }
41 |
42 | DirectItemDivider.propTypes = propTypes;
43 |
44 | export default DirectItemDivider;
45 |
46 |
--------------------------------------------------------------------------------
/src/constants/general.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @description Sort by key name. Used for local storage
3 | * @const {string}
4 | * @default
5 | */
6 | export const SORT_BY_KEY_NAME = 'directsSortBy';
7 |
8 | export const LOCAL_STORAGE_AUTH_STATE_KEY = 'authState';
9 |
10 | /**
11 | * @description Sort by name constant
12 | * @const {string}
13 | * @default
14 | */
15 | export const SORT_BY_NAME = 'name';
16 |
17 | /**
18 | * @description Sort by team constant
19 | * @const {string}
20 | * @default
21 | */
22 | export const SORT_BY_TEAM_NAME = 'teamName';
23 |
24 | /**
25 | * @description use instead empty team name
26 | * @const {string}
27 | * @default
28 | */
29 | export const SORT_WITHOUT_TEAM_NAME = 'SORT_WITHOUT_TEAM_NAME';
30 |
31 | /**
32 | * @description prefix for archived actions
33 | * @const {string}
34 | * @default
35 | */
36 | export const ARCHIVED_ACTION_PREFIX = 'ARCHIVED_';
37 |
38 | /**
39 | * @description archived prefix for firebase paths
40 | * @const {string}
41 | * @default
42 | */
43 | export const ARCHIVED_PATH_PREFIX = 'archived/';
44 |
45 | /**
46 | * @description Archived url suffix
47 | * @const {string}
48 | * @default
49 | */
50 | export const ARCHIVED_URL_SUFFIX = 'archived';
51 |
52 | export const SUGGESTED_QUESTION_URL = 'https://s3-us-west-1.amazonaws.com/1on1tracker/questions.json';
--------------------------------------------------------------------------------
/src/actions/directs.js:
--------------------------------------------------------------------------------
1 | import locStore from 'store';
2 | import * as types from './types';
3 | import { FirebaseApi } from '../firebase/firebase';
4 | import { ARCHIVED_PATH_PREFIX, SORT_BY_KEY_NAME } from '../constants/general';
5 |
6 | export const setSortBy = (value) => {
7 | return (dispatch) => {
8 | locStore.set(SORT_BY_KEY_NAME,value);
9 | dispatch({
10 | type: types.SET_DIRECTS_SORT_BY,
11 | payload: value,
12 | });
13 | };
14 | };
15 |
16 | export const archivedDirects = new FirebaseApi(`${ARCHIVED_PATH_PREFIX}directs`, {
17 | LOAD_REQUEST: types.ARCHIVED_LOAD_DIRECTS_REQUEST,
18 | LOAD_SUCCESS: types.ARCHIVED_LOAD_DIRECTS_SUCCESS,
19 | UNLOAD_SUCCESS: types.ARCHIVED_UNLOAD_DIRECTS_SUCCESS,
20 | SET_ACTIVE: types.ARCHIVED_SET_ACTIVE_DIRECT,
21 | RESET_ACTIVE: types.ARCHIVED_RESET_ACTIVE_DIRECT,
22 | CREATE: types.ARCHIVED_CREATE_DIRECT,
23 | SET_MATCHING: types.ARCHIVED_SET_MATCHING_DIRECTS,
24 | }, 'name');
25 |
26 | export default new FirebaseApi('directs', {
27 | LOAD_REQUEST: types.LOAD_DIRECTS_REQUEST,
28 | LOAD_SUCCESS: types.LOAD_DIRECTS_SUCCESS,
29 | UNLOAD_SUCCESS: types.UNLOAD_DIRECTS_SUCCESS,
30 | SET_ACTIVE: types.SET_ACTIVE_DIRECT,
31 | RESET_ACTIVE: types.RESET_ACTIVE_DIRECT,
32 | CREATE: types.CREATE_DIRECT,
33 | SET_MATCHING: types.SET_MATCHING_DIRECTS,
34 | }, 'name');
35 |
--------------------------------------------------------------------------------
/src/components/directs/list/ArchivedDirectCount.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { connect } from 'react-redux';
3 | import { List, ListItem } from 'material-ui/List';
4 | import { Link } from 'react-router';
5 | import PropTypes from 'prop-types';
6 |
7 | import { getArchivedArrayCount } from '../../../selectors/archivedDirects';
8 | import HideOnArchivedHOC from '../../../HOCs/archive/HideOnArchivedHOC';
9 |
10 | /**
11 | * @description PropTypes for ArchivedDirectCount
12 | * @type {{count: (*)}}
13 | */
14 | const propTypes = {
15 | count: PropTypes.number.isRequired,
16 | };
17 |
18 | /**
19 | * @function ArchivedDirectCount
20 | * @param {Number} count
21 | * @returns {XML}
22 | * @constructor
23 | */
24 | function ArchivedDirectCount({ count }) {
25 | if (count === 0) {
26 | return null;
27 | }
28 | return (
29 |
30 | }
34 | />
35 |
36 | );
37 | }
38 |
39 | ArchivedDirectCount.propTypes = propTypes;
40 |
41 | const mapStateToProps = state => ({
42 | count: getArchivedArrayCount(state),
43 | });
44 |
45 | export default HideOnArchivedHOC(connect(mapStateToProps)(ArchivedDirectCount));
46 |
47 |
--------------------------------------------------------------------------------
/src/components/header/HeaderRightIcon.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import PropTypes from 'prop-types';
3 |
4 | import DirectSortContainer from './DirectSortBtnContainer';
5 | import MeetingNavigationIconsContainers from './MeetingNavigationIconsContainers';
6 |
7 | /**
8 | * @description PropTypes for HeaderRightIcon
9 | * @type {{location: (*)}}
10 | */
11 | const propType = {
12 | router: PropTypes.object.isRequired,
13 | };
14 |
15 | /**
16 | * @class HeaderRightIcon
17 | * @extends React.Component
18 | * @description Render component
19 | */
20 | class HeaderRightIcon extends Component {
21 |
22 | /**
23 | * @description render
24 | * @return {Object} JSX HTML Content
25 | */
26 | render() {
27 | const { location } = this.props.router;
28 | const { pathname } = location;
29 | let icon;
30 |
31 | switch (pathname) {
32 | case '/directs':
33 | icon = ;
34 | break;
35 | case (((route) => {
36 | return `/meetings/${route.params.id}`;
37 | })(this.props.router)):
38 | icon = ;
39 | break;
40 | default:
41 | icon = null;
42 | }
43 |
44 | return icon;
45 | }
46 | }
47 |
48 | HeaderRightIcon.propTypes = propType;
49 |
50 | export default HeaderRightIcon;
51 |
--------------------------------------------------------------------------------
/src/components/directs/single/additional-info/AdditionalInfo.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Paper from 'material-ui/Paper';
3 | import { Card } from 'material-ui/Card';
4 | import PropTypes from 'prop-types';
5 |
6 | import Phone from './Phone';
7 | import StartDate from './StartDate';
8 | import Notes from './Notes';
9 |
10 | const propTypes = {
11 | phone: PropTypes.string,
12 | notes: PropTypes.string,
13 | startDate: PropTypes.string,
14 | };
15 |
16 | const defaultProps = {
17 | phone: '',
18 | notes: '',
19 | startDate: '',
20 | };
21 |
22 | const style = {
23 | paper: { marginBottom: 10, marginTop: 10 },
24 | text: { paddingBottom: 10, paddingTop: 10 },
25 |
26 | };
27 |
28 | /**
29 | * @function DirectAdditionalInfo
30 | * @param props
31 | * @returns {XML}
32 | */
33 | function DirectAdditionalInfo(props) {
34 | const { phone, notes, startDate } = props;
35 | return (
36 |
37 |
38 | {phone && }
39 | {startDate && }
40 | {notes && }
41 |
42 |
43 | );
44 | }
45 |
46 | DirectAdditionalInfo.propTypes = propTypes;
47 | DirectAdditionalInfo.defaultProps = defaultProps;
48 |
49 | export default DirectAdditionalInfo;
50 |
--------------------------------------------------------------------------------
/src/components/directs/list/item/DirectItemNewMeetingIcon.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { browserHistory } from 'react-router';
3 | import IconButton from 'material-ui/IconButton';
4 | import MeetingIcon from 'material-ui/svg-icons/communication/chat';
5 | import { grey400 } from 'material-ui/styles/colors';
6 | import PropTypes from 'prop-types';
7 |
8 | import HideOnArchivedHOC from '../../../../HOCs/archive/HideOnArchivedHOC';
9 |
10 | /**
11 | * @description PropTypes for DirectItemNewMeetingIcon
12 | * @type {Object}
13 | */
14 | const propTypes = {
15 | id: PropTypes.string.isRequired,
16 | isArchived: PropTypes.bool.isRequired,
17 | };
18 |
19 | /**
20 | * @function DirectItemNewMeetingIcon
21 | * @param props
22 | * @returns {XML}
23 | * @constructor
24 | */
25 | function DirectItemNewMeetingIcon(props) {
26 | const { id, isArchived, ...IconButtonProps } = props;
27 |
28 | const handleClick = (e) => {
29 | e.preventDefault();
30 | browserHistory.push(`/directs/${props.id}/meetings/new`);
31 | };
32 |
33 | return (
34 |
39 |
40 |
41 | );
42 | }
43 |
44 | DirectItemNewMeetingIcon.propTypes = propTypes;
45 |
46 | export default HideOnArchivedHOC(DirectItemNewMeetingIcon);
47 |
--------------------------------------------------------------------------------
/src/components/directs/common/DirectAvatar.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Avatar from 'material-ui/Avatar';
3 | import tinycolor from 'tinycolor2';
4 | import { grey400, grey900 } from 'material-ui/styles/colors';
5 | import PropTypes from 'prop-types';
6 |
7 | const propTypes = {
8 | name: PropTypes.string.isRequired,
9 | category: PropTypes.string,
10 | };
11 |
12 | const defaultProps = {
13 | category: '',
14 | };
15 |
16 | const initials = (name) => {
17 | return name.split(' ').map(w => w[0]).join('');
18 | };
19 |
20 | /**
21 | * @function DirectAvatar
22 | * @param {String} name Direct name
23 | * @param {String} category Direct color catagory
24 | * @param {Object} avatarProps Props for Avatar component
25 | * @returns {XML}
26 | */
27 | function DirectAvatar({ name, category, ...avatarProps }) {
28 | let avatarColor = '#ffffff';
29 | let avatarBgColor = grey400;
30 |
31 | if (category) {
32 | avatarBgColor = category;
33 | avatarColor = tinycolor.mostReadable(category, [grey900, '#ffffff']).toHexString();
34 | }
35 |
36 | return (
37 |
42 | {initials(name)}
43 |
44 | );
45 | }
46 |
47 | DirectAvatar.propTypes = propTypes;
48 | DirectAvatar.defaultProps = defaultProps;
49 |
50 | export default DirectAvatar;
51 |
--------------------------------------------------------------------------------
/src/reducers/followUps.js:
--------------------------------------------------------------------------------
1 | import * as types from '../actions/types';
2 |
3 | export default (name = '') => {
4 | return (state = {}, action) => {
5 | switch (action.type) {
6 | case name + types.LOAD_FOLLOW_UPS_REQUEST:
7 | return {
8 | ...state,
9 | ...{ loading: true },
10 | };
11 | case name + types.LOAD_FOLLOW_UPS_SUCCESS:
12 | return {
13 | ...state,
14 | ...{
15 | list: action.payload,
16 | loading: false,
17 | },
18 | };
19 | case name + types.SET_MATCHING_FOLLOW_UPS:
20 | return { ...state,
21 | matchingList: action.payload };
22 | case name + types.UNLOAD_FOLLOW_UPS_SUCCESS:
23 | return {
24 | ...state,
25 | ...{
26 | list: {},
27 | activeFollowUp: null,
28 | activeFollowUpKey: null,
29 | loading: false,
30 | error: null,
31 | },
32 | };
33 | case name + types.SET_ACTIVE_FOLLOW_UP:
34 | return { ...state,
35 | activeFollowUp: action.payload,
36 | activeFollowUpKey: action.key };
37 | case name + types.RESET_ACTIVE_FOLLOW_UP:
38 | return { ...state,
39 | activeFollowUp: null,
40 | activeFollowUpKey: null };
41 | default:
42 | return state;
43 | }
44 | };
45 | };
46 |
--------------------------------------------------------------------------------
/src/selectors/teams.js:
--------------------------------------------------------------------------------
1 | import { createSelector } from 'reselect';
2 |
3 | import { getDirectsArray } from './direct';
4 |
5 | /**
6 | * @description Return team list from state
7 | * @param {Object} state app state
8 | */
9 | const getTeams = (state) => {
10 | if (!(state.teams.list instanceof Map)) {
11 | return [];
12 | }
13 | return state.teams.list;
14 | };
15 |
16 | /**
17 | * @description directs array
18 | */
19 | const directArray = state => getDirectsArray(state);
20 |
21 | /**
22 | * @description selector for team array
23 | * @return {Array} array of teams objects
24 | */
25 | export const getTeamsArray = createSelector(getTeams, (teams) => {
26 | const arr = [];
27 | teams.forEach((team, id) => {
28 | arr.push({ ...team, ...{ id } });
29 | });
30 | return arr;
31 | });
32 |
33 | export const getTeamsArrayWithDeleteFlag = createSelector(
34 | [getTeamsArray, directArray],
35 | (teams, directs) => {
36 | return teams.map((team) => {
37 | const assigned = directs.find(direct => team.id === direct.team);
38 | return {
39 | ...team,
40 | ...{ allowedDelete: assigned === undefined } };
41 | });
42 | },
43 | );
44 |
45 | /**
46 | * @description return single team item from state
47 | * @param {String} id ID of team
48 | */
49 | export const getTeam = id => createSelector(getTeamsArray, (teams) => {
50 | return teams.find(team => team.id === id);
51 | });
52 |
--------------------------------------------------------------------------------
/src/HOCs/SetTextHOC.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import { connect } from 'react-redux';
3 | import PropTypes from 'prop-types';
4 |
5 | import * as headerActions from '../actions/header';
6 |
7 | /**
8 | * @description High Order Component for set Header
9 | * @param {XML} WrappedComponent
10 | * @param text
11 | * @return {XML} return react component
12 | * @constructor
13 | */
14 | const SetTextHOC = (WrappedComponent, text) => {
15 | /**
16 | * @description propTypes for SetText component
17 | * @type {{setText: (*)}}
18 | */
19 | const propTypes = {
20 | setText: PropTypes.func.isRequired,
21 | };
22 | /**
23 | * @class SetText
24 | * @extends React.Component
25 | * @description Render component
26 | */
27 | class SetText extends Component {
28 |
29 | componentDidMount() {
30 | this.props.setText(text);
31 | }
32 |
33 | /**
34 | * @description render
35 | * @return {Object} JSX HTML Content
36 | */
37 | render() {
38 | const { setText,dispatch, ...props } = this.props;
39 | return ;
40 | }
41 | }
42 |
43 | SetText.propTypes = propTypes;
44 |
45 | const mapDispatchToProps = dispatch => ({
46 | setText: (settedText) => {
47 | dispatch(headerActions.setText(settedText));
48 | },
49 | });
50 |
51 | return connect(null, mapDispatchToProps)(SetText);
52 | };
53 |
54 | export default SetTextHOC;
55 |
--------------------------------------------------------------------------------
/src/components/teams/dialog/TeamList.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { connect } from 'react-redux';
3 | import { List, ListItem } from 'material-ui/List';
4 | import PropTypes from 'prop-types';
5 |
6 | import { getTeamsArrayWithDeleteFlag } from '../../../selectors/teams';
7 | import TeamItem from './TeamItem';
8 |
9 |
10 | /**
11 | * @description propTypes for TeamList
12 | * @type {Object}
13 | */
14 | const propTypes = {
15 | list: PropTypes.arrayOf(PropTypes.shape({
16 | id: PropTypes.string.isRequired,
17 | name: PropTypes.string.isRequired,
18 | })).isRequired,
19 | onDelete: PropTypes.func.isRequired,
20 | clickOnItem: PropTypes.func.isRequired,
21 | };
22 |
23 | /**
24 | * @function TeamList
25 | * @param {Array} list
26 | * @param {Function} clickOnItem
27 | * @param {Function} onDelete
28 | * @returns {XML}
29 | */
30 | function TeamList({ list, clickOnItem, onDelete }) {
31 | const isEmptyList = list.length === 0;
32 | return (
33 |
34 | {isEmptyList && }
35 | {!isEmptyList && list.map(team => (
36 |
37 | ))}
38 |
39 |
40 | );
41 | }
42 |
43 | TeamList.propTypes = propTypes;
44 | const mapStateToProps = state => ({
45 | list: getTeamsArrayWithDeleteFlag(state),
46 | });
47 | export default connect(mapStateToProps)(TeamList);
48 |
49 |
--------------------------------------------------------------------------------
/src/components/directs/single/tabs/follow-up/FollowUpList.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import PropTypes from 'prop-types';
3 | import {
4 | List,
5 | ListItem,
6 | } from 'material-ui/List';
7 | import FollowUpItem from '../../../../followUps/FollowUpItem';
8 |
9 | class FollowUpList extends Component {
10 | componentDidMount() {
11 | this.selectFollowUps();
12 | }
13 |
14 | selectFollowUps() {
15 | this.props.followUpsEqualTo('directKey', this.props.directId);
16 | }
17 |
18 | renderItems() {
19 | const rows = [];
20 | if (this.props.followUps && this.props.followUps.size > 0) {
21 | this.props.followUps.forEach((item, key) => {
22 | rows.push(
23 | );
30 | });
31 | } else {
32 | rows.push(
33 | );
37 | }
38 | return rows;
39 | }
40 |
41 | render() {
42 | return (
43 |
44 |
45 | {this.renderItems()}
46 |
47 |
48 | );
49 | }
50 | }
51 |
52 |
53 | FollowUpList.propTypes = {
54 | directId: PropTypes.string.isRequired,
55 | followUps: PropTypes.object,
56 | };
57 |
58 | export default FollowUpList;
--------------------------------------------------------------------------------
/src/components/AboutMe.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | const AboutMe = () => {
4 | return (
5 |
6 |
About Me
7 |
Hello! :)
8 |
I'm Vidal Graupera and I live in the SF Bay Area. I am originally from Miami. I work as an
9 | engineering manager. And I am a product person. I love to create stuff. I am a serial app creator.
10 |
I used to have a company that focussed on productivty apps. I am really interested in tools,
11 | productivity hacks, and technology. I created 1on1tracker as a side project for my own use,
12 | and also to learn about React, Redux, Firebase and Material UI. I've had a little help since
13 | with this app, but I designed it and coded most of it.
14 |
Vidal Graupera
15 |
Feel free to get in touch with me for ANYTHING at vgraupera@gmail.com !
16 |
Some links:
17 |
32 |
33 | );
34 | }
35 |
36 | export default AboutMe;
37 |
--------------------------------------------------------------------------------
/src/reducers/questions.js:
--------------------------------------------------------------------------------
1 | import * as types from '../actions/types';
2 |
3 | export default (state = {}, action) => {
4 | switch (action.type) {
5 | case types.LOAD_QUESTIONS_REQUEST:
6 | case types.IMPORT_QUESTIONS_REQUEST:
7 | return {
8 | ...state,
9 | ...{ loading: true },
10 | };
11 | case types.LOAD_QUESTIONS_SUCCESS:
12 | return {
13 | ...state,
14 | ...{
15 | list: action.payload,
16 | loading: false,
17 | },
18 | };
19 | case types.UNLOAD_QUESTIONS_SUCCESS:
20 | return {
21 | ...state,
22 | ...{
23 | activeQuestion: null,
24 | list: [],
25 | error: null,
26 | },
27 | };
28 | case types.SET_ACTIVE_QUESTIONS:
29 | return {
30 | ...state,
31 | activeQuestion: action.payload,
32 | };
33 | case types.RESET_ACTIVE_QUESTIONS:
34 | return {
35 | ...state,
36 | activeQuestion: null,
37 | };
38 | case types.IMPORT_QUESTIONS_SUCCESS:
39 | return {
40 | ...state,
41 | ...{
42 | loading: false,
43 | error: null,
44 | },
45 | };
46 | case types.IMPORT_QUESTIONS_FAILURE:
47 | return {
48 | ...state,
49 | ...{
50 | loading: false,
51 | error: action.payload,
52 | },
53 | };
54 | case types.SET_QUESTIONS_FILTER:
55 | return { ...state, ...{ filterByCategory: action.payload } };
56 | default:
57 | return state;
58 | }
59 | };
60 |
--------------------------------------------------------------------------------
/src/selectors/meetings.js:
--------------------------------------------------------------------------------
1 | import { createSelector } from 'reselect';
2 |
3 | /**
4 | * @description Retrun meetings list from state
5 | * @param {Object} state app state
6 | * @return {Array|Map}
7 | */
8 | const getMeetings = (state) => {
9 | if (!(state.meetings.list instanceof Map)) {
10 | return [];
11 | }
12 | return state.meetings.list;
13 | };
14 |
15 | /**
16 | * @description Return active meeting
17 | * @param {Object} state app state
18 | * @param {{id}} props
19 | * @return {Object} active meeting
20 | */
21 | const getActiveMeeting = (state, props) => ({
22 | ...state.meetings.activeMeeting,
23 | ...{ id: props.id },
24 | });
25 |
26 | /**
27 | * @description selector for meetings array
28 | * @return {Array} array of meetings
29 | */
30 | export const getMeetingsArray = createSelector(getMeetings, (meetings) => {
31 | const arr = [];
32 | meetings.forEach((meeting, id) => {
33 | arr.push({ ...meeting, ...{ id } });
34 | });
35 | return arr;
36 | });
37 |
38 | /**
39 | * @description Return array of meetings that belongs to same direct
40 | */
41 | export const getMeetingsInSameDirectAsActive = createSelector(
42 | [getMeetingsArray, getActiveMeeting],
43 | (meetings, active) => {
44 | return meetings.filter(meeting => meeting.directKey === active.directKey);
45 | },
46 | );
47 |
48 | /**
49 | * @description return index of active meetings in array of meetings that belong same direct
50 | */
51 | export const getIndexOfActiveMeating = createSelector(
52 | [getMeetingsInSameDirectAsActive, getActiveMeeting],
53 | (meetings, active) => {
54 | return meetings.findIndex(meeting => meeting.id === active.id);
55 | },
56 | );
57 |
--------------------------------------------------------------------------------
/src/components/directs/list/item/DirectItem.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { ListItem } from 'material-ui/List';
3 | import PropTypes from 'prop-types';
4 |
5 | import { SORT_WITHOUT_TEAM_NAME } from '../../../../constants/general';
6 | import DirectItemNewMeetingIcon from './DirectItemNewMeetingIcon';
7 | import ArchivedLinkGenerator from './ArchivedLinkGenerator';
8 | import DirectAvatar from '../../common/DirectAvatar';
9 |
10 | const propTypes = {
11 | direct: PropTypes.shape({
12 | teamName: PropTypes.string,
13 | category: PropTypes.string,
14 | phone: PropTypes.string,
15 | name: PropTypes.string.isRequired,
16 | id: PropTypes.string.isRequired,
17 | }).isRequired,
18 | id: PropTypes.string.isRequired,
19 | };
20 |
21 | const DirectItem = (props) => {
22 | let teamName = props.direct.teamName;
23 | if (SORT_WITHOUT_TEAM_NAME === teamName) {
24 | teamName = null;
25 | }
26 |
27 | const line1 = [props.direct.title, props.direct.phone].filter(val => val).join("\u00a0")
28 |
29 | return (
30 | {line1}
{teamName}
}
35 | secondaryTextLines={2}
36 | containerElement={ }
37 | leftAvatar={ }
38 | rightIconButton={ }
39 | />
40 | );
41 | };
42 |
43 | DirectItem.propTypes = propTypes;
44 |
45 | export default DirectItem;
46 |
--------------------------------------------------------------------------------
/src/components/meetings/MeetingItem.jsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import PropTypes from 'prop-types';
3 | import { connect } from 'react-redux';
4 | import { Link } from 'react-router';
5 | import { ListItem } from 'material-ui/List';
6 |
7 | import { getDirectsArray } from '../../selectors/direct';
8 | import InnerHtmlStripTags from '../common/InnerHtmlStripTags';
9 |
10 | class MeetingItem extends Component {
11 | static summary(meeting) {
12 | const dateString = new Date(meeting.meetingDate).toLocaleDateString();
13 | const notesString = meeting.directsNotes ? meeting.directsNotes : meeting.managersNotes;
14 | return `${dateString} : ${notesString || ''}`;
15 | }
16 |
17 | render() {
18 | const { directs, meeting, id } = this.props;
19 |
20 | if (directs && meeting) {
21 | const direct = directs.find(singleDirect => singleDirect.id === meeting.directKey);
22 |
23 | return (
29 | }
30 | containerElement={ }
31 | />
32 | );
33 | }
34 |
35 | return null;
36 | }
37 | }
38 |
39 | MeetingItem.propTypes = {
40 | directs: PropTypes.array.isRequired,
41 | meeting: PropTypes.object.isRequired,
42 | id: PropTypes.string.isRequired,
43 | };
44 |
45 | const mapStateToProps = (state) => {
46 | return {
47 | directs: getDirectsArray(state),
48 | };
49 | };
50 |
51 | export default connect(mapStateToProps)(MeetingItem);
52 |
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
11 | 1on1 Tracker
12 |
13 |
14 |
15 |
16 |
17 |
30 |
40 |
41 |
42 |
--------------------------------------------------------------------------------
/src/components/teams/dialog/TeamItem.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import { ListItem } from 'material-ui/List';
3 | import IconButton from 'material-ui/IconButton';
4 | import ActionDelete from 'material-ui/svg-icons/action/delete';
5 | import { red500, red200 } from 'material-ui/styles/colors';
6 | import PropTypes from 'prop-types';
7 |
8 | const propTypes = {
9 | team: PropTypes.shape({
10 | id: PropTypes.string.isRequired,
11 | name: PropTypes.string.isRequired,
12 | }).isRequired,
13 | onDelete: PropTypes.func.isRequired,
14 | clickOnItem: PropTypes.func.isRequired,
15 | };
16 |
17 | /**
18 | * @class TeamItem
19 | * @extends React.Component
20 | * @description Render component
21 | */
22 | class TeamItem extends Component {
23 |
24 | handleClickOnItem = () => {
25 | const { team, clickOnItem } = this.props;
26 | clickOnItem(team);
27 | };
28 |
29 | handleOnDelete = () => {
30 | const { team, onDelete } = this.props;
31 | onDelete(team.id);
32 | };
33 |
34 | /**
35 | * @description render
36 | * @return {Object} JSX HTML Content
37 | */
38 | render() {
39 | const { id, name, allowedDelete } = this.props.team;
40 | return (
41 |
51 |
55 |
56 | }
57 | />
58 | );
59 | }
60 | }
61 |
62 | TeamItem.propTypes = propTypes;
63 |
64 | export default TeamItem;
65 |
--------------------------------------------------------------------------------
/src/components/meetings/questions/dialog/QuestionItem.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import { ListItem } from 'material-ui/List';
3 | import IconButton from 'material-ui/IconButton';
4 | import ActionDelete from 'material-ui/svg-icons/action/delete';
5 | import { red500, red200 } from 'material-ui/styles/colors';
6 | import PropTypes from 'prop-types';
7 |
8 | const propTypes = {
9 | question: PropTypes.shape({
10 | id: PropTypes.string.isRequired,
11 | question: PropTypes.string.isRequired,
12 | }).isRequired,
13 | clickOnItem: PropTypes.func.isRequired,
14 | onDelete: PropTypes.func.isRequired,
15 | };
16 |
17 | /**
18 | * @class QuestionItem
19 | * @extends React.Component
20 | * @description Render component
21 | */
22 | class QuestionItem extends Component {
23 |
24 | handleClickOnItem = () => {
25 | const { question, clickOnItem } = this.props;
26 | clickOnItem(question);
27 | };
28 |
29 | handleOnDelete = () => {
30 | const { question, onDelete } = this.props;
31 | onDelete(question.id);
32 | };
33 |
34 | /**
35 | * @description render
36 | * @return {Object} JSX HTML Content
37 | */
38 | render() {
39 | const { id, question } = this.props.question;
40 | return (
41 |
50 |
54 |
55 | }
56 | />
57 | );
58 | }
59 | }
60 |
61 | QuestionItem.propTypes = propTypes;
62 |
63 | export default QuestionItem;
64 |
--------------------------------------------------------------------------------
/src/components/directs/single/DirectSingle.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import PropTypes from 'prop-types';
3 |
4 | import HeadInfo from './head-info/HeadInfo';
5 | import AdditionalInfo from './additional-info/AdditionalInfo';
6 | import DirectTabs from './tabs/DirectTabs';
7 |
8 | const propTypes = {
9 | direct: PropTypes.shape({
10 | id: PropTypes.string.isRequired,
11 | name: PropTypes.string.isRequired,
12 | title: PropTypes.string,
13 | teamName: PropTypes.string,
14 | phone: PropTypes.string,
15 | notes: PropTypes.string,
16 | startDate: PropTypes.string,
17 | category: PropTypes.string,
18 | }).isRequired,
19 | find: PropTypes.func.isRequired,
20 | };
21 |
22 | class DirectSingle extends Component {
23 | componentDidMount() {
24 | this.props.find(this.props.direct.id);
25 | }
26 |
27 | render() {
28 | const { direct, loading, error } = this.props;
29 |
30 | if (loading) {
31 | return Loading...
;
32 | } else if (error) {
33 | return {error.message}
;
34 | } else if (!direct) {
35 | return ;
36 | }
37 |
38 | return (
39 |
40 |
57 | );
58 | }
59 | }
60 | DirectSingle.propTypes = propTypes;
61 |
62 | export default DirectSingle;
63 |
--------------------------------------------------------------------------------
/src/components/meetings/questions/MeetingQuestions.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import Toggle from 'material-ui/Toggle';
3 | import PropTypes from 'prop-types';
4 |
5 | import QuestionSingle from './QuestionSingle';
6 | import QuestionsList from './QuestionsList';
7 | import QuestionsSettings from './QuestionsSettings';
8 |
9 | const propTypes = {
10 | questions: PropTypes.arrayOf(PropTypes.shape({
11 | id: PropTypes.string.isRequired,
12 | question: PropTypes.string.isRequired,
13 | })).isRequired,
14 | };
15 |
16 | /**
17 | * @class MeetingQuestions
18 | * @extends React.Component
19 | * @description Render component
20 | */
21 | class MeetingQuestions extends Component {
22 | state = {
23 | showAllQuestions: false,
24 | };
25 |
26 | handleToggleList = () => {
27 | this.setState(prevState => ({
28 | showAllQuestions: !prevState.showAllQuestions,
29 | }));
30 | };
31 |
32 | /**
33 | * @description render
34 | * @return {Object} JSX HTML Content
35 | */
36 | render() {
37 | const { showAllQuestions } = this.state;
38 | const { questions } = this.props;
39 | const isEmpty = questions.length === 0;
40 |
41 | return (
42 |
43 | {!isEmpty && }
49 | {!isEmpty && !showAllQuestions && }
50 | {!isEmpty && showAllQuestions && }
51 | {(isEmpty || showAllQuestions) && }
52 |
53 | );
54 | }
55 | }
56 |
57 | MeetingQuestions.propTypes = propTypes;
58 |
59 | export default MeetingQuestions;
60 |
--------------------------------------------------------------------------------
/src/components/meetings/MeetingNew.jsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import PropTypes from 'prop-types';
3 | import { connect } from 'react-redux';
4 | import { browserHistory } from 'react-router';
5 | import MeetingForm from './MeetingForm';
6 | import meetingActions from '../../actions/meetings';
7 |
8 | class MeetingNew extends Component {
9 | constructor() {
10 | super();
11 | this.onSubmit = this.onSubmit.bind(this);
12 | }
13 |
14 | componentWillMount() {
15 | if (!this.props.initialValues.directKey) {
16 | this.props.initialValues.directKey = this.props.params.id;
17 | }
18 | this.props.reset();
19 | }
20 |
21 | /* eslint no-param-reassign: ["error", { "props": false }] */
22 | onSubmit(meeting) {
23 | if (meeting.meetingDate instanceof Date) {
24 | meeting.meetingDateReverse = 0 - meeting.meetingDate;
25 | meeting.meetingDate = meeting.meetingDate.toISOString();
26 | }
27 | this.props.create(meeting).then(() => {
28 | browserHistory.goBack();
29 | });
30 | }
31 |
32 | render() {
33 | return (
34 |
35 |
39 |
40 | );
41 | }
42 | }
43 |
44 | MeetingNew.propTypes = {
45 | create: PropTypes.func.isRequired,
46 | reset: PropTypes.func.isRequired,
47 | };
48 |
49 | const mapStateToProps = (state) => {
50 | const initialValues = {
51 | meetingDate: new Date(),
52 | };
53 | return {
54 | initialValues,
55 | formType: 'create',
56 | error: state.meetings.error,
57 | directs: state.directs.list,
58 | };
59 | };
60 |
61 | export default connect(
62 | mapStateToProps,
63 | { create: meetingActions.create,
64 | reset: meetingActions.resetActive,
65 | })(MeetingNew);
66 |
--------------------------------------------------------------------------------
/src/components/header/DirectSortBtn.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import IconMenu from 'material-ui/IconMenu';
3 | import MenuItem from 'material-ui/MenuItem';
4 | import IconButton from 'material-ui/IconButton';
5 | import FontIcon from 'material-ui/FontIcon';
6 | import { white } from 'material-ui/styles/colors';
7 | import PropTypes from 'prop-types';
8 |
9 | import {SORT_BY_NAME, SORT_BY_TEAM_NAME} from '../../constants/general';
10 |
11 | /**
12 | * @description PropTypes for DirectSort
13 | * @type {{handleChange: (*)}}
14 | */
15 | const propTypes = {
16 | handleChange: PropTypes.func.isRequired,
17 | selected: PropTypes.string.isRequired,
18 | };
19 |
20 | /**
21 | * @class DirectSort
22 | * @extends React.Component
23 | * @description Render component
24 | */
25 | class DirectSort extends Component {
26 |
27 | /**
28 | * @description handle on change
29 | * @param {Object} e synthetic event
30 | * @param {String} val new value
31 | */
32 | handleOnChange = (e, val) => {
33 | this.props.handleChange(val);
34 | };
35 |
36 | /**
37 | * @description render
38 | * @return {Object} JSX HTML Content
39 | */
40 | render() {
41 | const icon = (
42 |
43 | sort
44 |
45 | );
46 | return (
47 |
48 |
53 |
54 |
55 |
56 |
57 | );
58 | }
59 | }
60 |
61 | DirectSort.propTypes = propTypes;
62 |
63 | export default DirectSort;
64 |
--------------------------------------------------------------------------------
/src/components/directs/form/DirectNew.jsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import PropTypes from 'prop-types';
3 | import { reduxForm } from 'redux-form';
4 | import { connect } from 'react-redux';
5 | import { browserHistory } from 'react-router';
6 | import RaisedButton from 'material-ui/RaisedButton';
7 |
8 | import DirectForm, { validate } from './DirectForm';
9 | import directActions from '../../../actions/directs';
10 | import { getTeamsArray } from '../../../selectors/teams';
11 |
12 | class DirectNew extends Component {
13 | constructor() {
14 | super();
15 | this.onSubmit = this.onSubmit.bind(this);
16 | this.onCancelClick = this.onCancelClick.bind(this);
17 | }
18 |
19 | onSubmit(direct) {
20 | this.props.create(direct).then(() => {
21 | browserHistory.push('/directs');
22 | });
23 | }
24 |
25 | onCancelClick(event) {
26 | event.preventDefault(); // Fix double touchtap bug
27 | browserHistory.push('/directs');
28 | }
29 |
30 | render() {
31 | return (
32 |
33 |
34 |
39 |
* Indicates required field
40 |
41 | );
42 | }
43 | }
44 |
45 | DirectNew.propTypes = {
46 | create: PropTypes.func.isRequired,
47 | };
48 |
49 | const mapStateToProps = (state) => {
50 | return {
51 | formType: 'create',
52 | error: state.directs.error,
53 | teams: getTeamsArray(state),
54 | };
55 | };
56 |
57 | const mapDispatchToProps = {
58 | create: directActions.create,
59 | }
60 |
61 | export default connect(
62 | mapStateToProps,
63 | mapDispatchToProps,
64 | )(reduxForm({ form: 'direct', validate })(DirectNew));
65 |
--------------------------------------------------------------------------------
/src/components/teams/dialog/TeamForm.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Field, reduxForm } from 'redux-form';
3 | import { TextField } from 'redux-form-material-ui';
4 | import RaisedButton from 'material-ui/RaisedButton';
5 | import PropTypes from 'prop-types';
6 |
7 | /**
8 | * @description PropTypes for TeamFrom
9 | */
10 | const propTypes = {
11 | initialValues: PropTypes.object,
12 | handleSubmit: PropTypes.func.isRequired,
13 | handleFormCancel: PropTypes.func.isRequired,
14 | handleFormSubmit: PropTypes.func.isRequired,
15 | };
16 |
17 | const defaultProps = {
18 | initialValues: {},
19 | };
20 |
21 | /**
22 | * @description Validation for TeamForm
23 | * @param {Object} values form values
24 | * @return {Object} errors object
25 | */
26 | export const validate = (values) => {
27 | const errors = {};
28 | if (!values.name) {
29 | errors.name = 'Required';
30 | }
31 | return errors;
32 | };
33 | /**
34 | * @function TeamForm
35 | * @param props
36 | * @returns {XML}
37 | */
38 | function TeamForm(props) {
39 | const { handleSubmit, handleFormSubmit, handleFormCancel } = props;
40 | return (
41 |
62 | );
63 | }
64 | TeamForm.propTypes = propTypes;
65 | TeamForm.defaultProps = defaultProps;
66 |
67 | export default reduxForm({
68 | form: 'team',
69 | validate,
70 | })(TeamForm);
71 |
72 |
--------------------------------------------------------------------------------
/src/store/initialState.js:
--------------------------------------------------------------------------------
1 | import locStore from 'store';
2 | import { SORT_BY_NAME, SORT_BY_KEY_NAME, LOCAL_STORAGE_AUTH_STATE_KEY } from '../constants/general';
3 | import { AUTH_ANONYMOUS } from '../actions/types';
4 |
5 | const initialState = {
6 | directs: {
7 | activeDirect: null,
8 | list: [],
9 | loading: false,
10 | error: null,
11 | sortBy: locStore.get(SORT_BY_KEY_NAME) || SORT_BY_NAME,
12 | },
13 | archivedDirects: {
14 | activeDirect: null,
15 | list: [],
16 | loading: false,
17 | error: null,
18 | sortBy: SORT_BY_NAME,
19 | },
20 | meetings: {
21 | list: {},
22 | activeMeeting: null,
23 | activeMeetingKey: null,
24 | loading: false,
25 | error: null,
26 | },
27 | archivedMeetings: {
28 | list: {},
29 | activeMeeting: null,
30 | activeMeetingKey: null,
31 | loading: false,
32 | error: null,
33 | },
34 | followUps: {
35 | list: {},
36 | activeFollowUp: null,
37 | activeFollowUpKey: null,
38 | loading: false,
39 | error: null,
40 | },
41 | archivedFollowUp: {
42 | list: {},
43 | activeFollowUp: null,
44 | activeFollowUpKey: null,
45 | loading: false,
46 | error: null,
47 | },
48 | auth: {
49 | displayName: null,
50 | uid: null,
51 | email: null,
52 | photoURL: null,
53 | status: locStore.get(LOCAL_STORAGE_AUTH_STATE_KEY) || AUTH_ANONYMOUS,
54 | },
55 | header: {
56 | text: '1on1 Tracker',
57 | },
58 | teams: {
59 | list: {},
60 | loading: false,
61 | error: null,
62 | },
63 | questions: {
64 | activeQuestion: null,
65 | list: [],
66 | loading: false,
67 | error: null,
68 | filterByCategory: false,
69 | },
70 | categoriesQuestions: {
71 | activeCategoriesQuestions: null,
72 | list: [],
73 | loading: false,
74 | error: null,
75 | },
76 | };
77 |
78 | export default initialState;
79 |
--------------------------------------------------------------------------------
/src/components/meetings/questions/QuestionSingle.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import { List, ListItem } from 'material-ui/List';
3 | import QuestionAnswer from 'material-ui/svg-icons/action/question-answer';
4 | import Paper from 'material-ui/Paper';
5 | import PropTypes from 'prop-types';
6 |
7 | import uniqueRandomNumber from '../../../utility/uniqueRandomNumber';
8 |
9 | /**
10 | * @description PropTypes for QuestionSingle
11 | * @type {{questions}}
12 | */
13 | const propTypes = {
14 | questions: PropTypes.arrayOf(PropTypes.shape({
15 | id: PropTypes.string.isRequired,
16 | question: PropTypes.string.isRequired,
17 | })).isRequired,
18 | };
19 |
20 | /**
21 | * @class QuestionSingle
22 | * @extends React.Component
23 | * @description Render component
24 | */
25 | class QuestionSingle extends Component {
26 |
27 | constructor(props) {
28 | super(props);
29 | this.uniqueRandom = uniqueRandomNumber(0, (this.props.questions.length - 1));
30 | this.state = {
31 | currentItemIndex: this.uniqueRandom(),
32 | };
33 | }
34 |
35 |
36 | handleClickOnItem = () => {
37 | this.setState({
38 | currentItemIndex: this.uniqueRandom(),
39 | });
40 | };
41 |
42 | /**
43 | * @description render
44 | * @return {Object} JSX HTML Content
45 | */
46 | render() {
47 | const { currentItemIndex } = this.state;
48 | const { questions } = this.props;
49 | return (
50 |
51 |
52 | }
57 | />
58 |
59 |
60 | );
61 | }
62 | }
63 |
64 | QuestionSingle.propTypes = propTypes;
65 |
66 | export default QuestionSingle;
67 |
--------------------------------------------------------------------------------
/src/components/meetings/questions/dialog/QuestionForm.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Field, reduxForm } from 'redux-form';
3 | import { TextField } from 'redux-form-material-ui';
4 | import RaisedButton from 'material-ui/RaisedButton';
5 | import PropTypes from 'prop-types';
6 |
7 | /**
8 | * @description PropTypes for QuestionForm
9 | * @type {Object}
10 | */
11 | const propTypes = {
12 | handleSubmit: PropTypes.func.isRequired,
13 | handleFormSubmit: PropTypes.func.isRequired,
14 | handleFormCancel: PropTypes.func.isRequired,
15 | };
16 |
17 | /**
18 | * @description validation for Question form
19 | * @param values
20 | * @return {{}}
21 | */
22 | export const validate = (values) => {
23 | const errors = {};
24 | if (!values.question) {
25 | errors.question = 'Required';
26 | }
27 | return errors;
28 | };
29 |
30 | /**
31 | * @description QuestionForm
32 | * @param {Function} handleSubmit
33 | * @param {Function} handleFormSubmit
34 | * @param {Function} handleFormCancel
35 | * @return {XML}
36 | */
37 | function QuestionForm({ handleSubmit, handleFormSubmit, handleFormCancel }) {
38 | return (
39 |
62 | );
63 | }
64 |
65 | QuestionForm.propTypes = propTypes;
66 |
67 | export default reduxForm({
68 | form: 'questions',
69 | validate,
70 | })(QuestionForm);
71 |
--------------------------------------------------------------------------------
/src/components/App.jsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import PropTypes from 'prop-types';
3 | import getMuiTheme from 'material-ui/styles/getMuiTheme';
4 | import MuiThemeProvider from 'material-ui/styles/MuiThemeProvider';
5 | import { green100, green500, green700 } from 'material-ui/styles/colors';
6 | import { browserHistory } from 'react-router';
7 |
8 | import Header from './Header';
9 | import LeftDrawer from './LeftDrawer';
10 | import BottomNav from './BottomNav';
11 | import store from '../store/store';
12 | import { listenToAuth } from '../actions/auth';
13 |
14 | const muiTheme = getMuiTheme({
15 | palette: {
16 | primary1Color: green500,
17 | primary2Color: green700,
18 | primary3Color: green100,
19 | },
20 | });
21 |
22 | class App extends Component {
23 | constructor(props) {
24 | super(props);
25 | this.state = { open: false };
26 | }
27 |
28 | componentDidMount() {
29 | store.dispatch(listenToAuth());
30 | }
31 |
32 | handleMenuTap = () => this.setState({ open: !this.state.open });
33 |
34 | handleNavigate = (path) => {
35 | this.setState({ open: false });
36 | browserHistory.push(path);
37 | }
38 |
39 | render() {
40 | return (
41 |
42 |
43 |
47 | this.setState({ open })}
50 | handleNavigate={this.handleNavigate}
51 | />
52 | {this.props.children}
53 |
56 |
57 |
58 | );
59 | }
60 | }
61 |
62 | App.propTypes = {
63 | children: PropTypes.object.isRequired,
64 | };
65 |
66 | export default App;
67 |
--------------------------------------------------------------------------------
/src/components/Footer.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Link } from 'react-router';
3 | import ArrowIcon from 'material-ui/svg-icons/hardware/keyboard-arrow-up';
4 |
5 | const Footer = () => (
6 |
7 |
8 |
9 |
10 | About
11 |
12 |
13 | Features
14 |
15 |
16 | About 1on1tracker
17 |
18 |
19 | About me
20 |
21 |
22 |
23 |
24 | Support
25 |
26 |
27 | Contact
28 |
29 |
30 | Terms and Conditions
31 |
32 |
33 | Privacy Policy
34 |
35 |
36 |
50 |
51 |
Use of this website signifies your agreement to the Terms and Conditions and Privacy Policy.
52 |
55 |
56 | );
57 |
58 | export default Footer;
59 |
--------------------------------------------------------------------------------
/src/components/directs/single/head-info/ActionsButtons.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { CardActions } from 'material-ui/Card';
3 | import { Link } from 'react-router';
4 | import MeetingIcon from 'material-ui/svg-icons/action/speaker-notes';
5 | import FollowUpIcon from 'material-ui/svg-icons/action/assignment';
6 | import FloatingActionButton from 'material-ui/FloatingActionButton';
7 | import PropTypes from 'prop-types';
8 | import { lightBlue500, cyan500 } from 'material-ui/styles/colors';
9 |
10 | import HideOnArchivedHOC from '../../../../HOCs/archive/HideOnArchivedHOC';
11 |
12 | /**
13 | * @description propTypes for DirectSingleActions
14 | * @type {{id: (*)}}
15 | */
16 | const propTypes = {
17 | id: PropTypes.string.isRequired,
18 | };
19 |
20 | const style = {
21 | wrapperDiv: {
22 | display: 'inline-block',
23 | marginBottom: 5,
24 | marginTop: 5,
25 | marginRight: 30,
26 | marginLeft: 30,
27 | },
28 | };
29 |
30 | /**
31 | * @function DirectSingleActions
32 | * @param {String} id id of direct
33 | * @returns {XML}
34 | */
35 | function DirectSingleActions({ id }) {
36 | return (
37 |
38 |
39 | }
41 | backgroundColor={lightBlue500}
42 | >
43 |
44 |
45 |
Meeting
46 |
47 |
48 | }
50 | backgroundColor={cyan500}
51 | >
52 |
53 |
54 |
Follow Up
55 |
56 |
57 | );
58 | }
59 |
60 | DirectSingleActions.propTypes = propTypes;
61 |
62 | export default HideOnArchivedHOC(DirectSingleActions);
63 |
--------------------------------------------------------------------------------
/src/HOCs/archive/__tests__/HideOnArchivedHOC.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-undef,import/first */
2 | jest.mock('../ArchiveHocFactory', () => jest.fn());
3 | import ArchiveHocFactory from '../ArchiveHocFactory';
4 | import React from 'react';
5 | import { shallow } from 'enzyme';
6 |
7 | import HideOnArchivedHOC, { componentFactory } from '../HideOnArchivedHOC';
8 |
9 | const TestComponent = () => (I am test
);
10 |
11 | describe('HideOnArchived', () => {
12 | let HideOnArhive;
13 |
14 | beforeEach(() => {
15 | HideOnArhive = componentFactory(TestComponent);
16 | });
17 |
18 | it('Should return wrapped component if isArchived props is not passed ', () => {
19 | const wrapped = shallow( );
20 | expect(wrapped.find(TestComponent).exists()).toBeTruthy();
21 | });
22 |
23 | it('Should return null', () => {
24 | const wrapper = shallow( );
25 | expect(wrapper.type()).toBe(null);
26 | });
27 |
28 | it('Should return passed component', () => {
29 | const wrapped = shallow( );
30 | expect(wrapped.find(TestComponent).exists()).toBeTruthy();
31 | });
32 |
33 | describe('Props of HideOnArchived', () => {
34 | it('Should pass additional prop isArchived to wrapped component ', () => {
35 | const wrapper = shallow( );
36 | expect(wrapper.prop('isArchived')).toBeDefined();
37 | });
38 |
39 | it('Should pass additional props to wrapped component', () => {
40 | const wrapper = shallow( );
41 | expect(wrapper.props()).toEqual({
42 | foo: 'bar',
43 | isArchived: false,
44 | });
45 | });
46 | });
47 |
48 | describe('HideOnArchivedHOC', () => {
49 | it('Should call ArchiveHocFactory ', () => {
50 | HideOnArchivedHOC(TestComponent);
51 | expect(ArchiveHocFactory.mock.calls.length).toBe(1);
52 | });
53 | });
54 | });
55 |
--------------------------------------------------------------------------------
/src/HOCs/archive/__tests__/ShowOnArchivedHOC.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-undef,import/first */
2 | jest.mock('../ArchiveHocFactory', () => jest.fn());
3 | import React from 'react';
4 | import { shallow } from 'enzyme';
5 |
6 | import ShowOnArchivedHOC, { componentFactory } from '../ShowOnArchivedHOC';
7 | import ArchiveHocFactory from '../ArchiveHocFactory';
8 |
9 |
10 | const TestComponent = () => (I am test
);
11 | describe('ShowOnArchived', () => {
12 | let ShowOnArchived;
13 |
14 | beforeEach(() => {
15 | ShowOnArchived = componentFactory(TestComponent);
16 | });
17 |
18 | it('Should return null if isArchived not passed as props ', () => {
19 | const wrapper = shallow( );
20 | expect(wrapper.type()).toBe(null);
21 | });
22 |
23 | it('Should return null', () => {
24 | const wrapper = shallow( );
25 | expect(wrapper.type()).toBe(null);
26 | });
27 |
28 |
29 | it('Should return passed component', () => {
30 | const wrapper = shallow( );
31 | expect(wrapper.find(TestComponent).exists()).toBeTruthy();
32 | });
33 |
34 | describe('Props of ShowOnArchived', () => {
35 | it('Should pass additional prop isArchived to wrapped component ', () => {
36 | const wrapper = shallow( );
37 | expect(wrapper.prop('isArchived')).toBeDefined();
38 | });
39 |
40 | it('Should pass additional props to wrapped component', () => {
41 | const wrapper = shallow( );
42 | expect(wrapper.props()).toEqual({
43 | foo: 'bar',
44 | isArchived: true,
45 | });
46 | });
47 | });
48 |
49 | describe('ShowOnArchivedHOC', () => {
50 | it('Should call ArchiveHocFactory ', () => {
51 | ShowOnArchivedHOC(TestComponent);
52 | expect(ArchiveHocFactory.mock.calls.length).toBe(1);
53 | });
54 | });
55 | });
56 |
--------------------------------------------------------------------------------
/src/components/directs/single/head-info/HeadInfo.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Paper from 'material-ui/Paper';
3 | import {
4 | Card,
5 | CardHeader,
6 | } from 'material-ui/Card';
7 | import PropTypes from 'prop-types';
8 |
9 | import EditIconLink from './EditIconLink';
10 | import DirectAvatar from '../../common/DirectAvatar';
11 | import ActionsButtons from './ActionsButtons';
12 | import UnarchiveBtn from './UnarchiveBtn';
13 |
14 | const propTypes = {
15 | id: PropTypes.string.isRequired,
16 | name: PropTypes.string.isRequired,
17 | title: PropTypes.string,
18 | teamName: PropTypes.string,
19 | category: PropTypes.string,
20 | };
21 |
22 | const defaultProps = {
23 | title: '',
24 | teamName: '',
25 | category: '',
26 | };
27 |
28 | /**
29 | * @function HeadInfo
30 | * @param props
31 | * @returns {XML}
32 | */
33 | function HeadInfo(props) {
34 | const { id, name, title, category, teamName } = props;
35 | return (
36 |
41 |
42 |
43 |
44 | {name}}
47 | textStyle={{ paddingRight: 0 }}
48 | avatar={
49 |
50 |
55 |
56 | }
57 | subtitle={
58 |
59 |
{title}
60 |
{teamName}
61 |
62 | }
63 | />
64 |
65 |
66 |
67 |
68 | );
69 | }
70 |
71 | HeadInfo.propTypes = propTypes;
72 | HeadInfo.defaultProps = defaultProps;
73 |
74 | export default HeadInfo;
75 |
--------------------------------------------------------------------------------
/src/components/meetings/questions/dialog/QuestionsList.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { connect } from 'react-redux';
3 | import { List, ListItem } from 'material-ui/List';
4 | import PropTypes from 'prop-types';
5 |
6 | import QuestionItem from './QuestionItem';
7 | import QuestionsTools from './QuestionsTools';
8 | import { getFiltreredQuestionsArray } from '../../../../selectors/questions';
9 |
10 | /**
11 | * @description PropTypes for QuestionsList
12 | * @type {Object}
13 | */
14 | const propTypes = {
15 | list: PropTypes.arrayOf(PropTypes.shape({
16 | id: PropTypes.string.isRequired,
17 | question: PropTypes.string.isRequired,
18 | })).isRequired,
19 | clickOnItem: PropTypes.func.isRequired,
20 | onDelete: PropTypes.func.isRequired,
21 | loading: PropTypes.bool.isRequired,
22 | };
23 |
24 | /**
25 | * @function QuestionsList
26 | * @param {Array} list array of questions
27 | * @param {Function} clickOnItem
28 | * @param {Function} onDelete
29 | * @param {Boolean} loading
30 | * @returns {XML}
31 | */
32 | function QuestionsList({ list, clickOnItem, onDelete, loading }) {
33 | const isEmptyList = list.length === 0;
34 | const emptyListPrimaryText = loading ? 'Please wait' : 'No items. Add or/and Import Questions';
35 | return (
36 |
37 |
38 |
39 | { isEmptyList && }
40 | { !isEmptyList && list.map(item => (
41 |
47 | ))}
48 |
49 |
50 | );
51 | }
52 |
53 | QuestionsList.propTypes = propTypes;
54 |
55 | const mapStateToProps = state => ({
56 | list: getFiltreredQuestionsArray(state),
57 | loading: state.questions.loading,
58 | });
59 |
60 | export default connect(mapStateToProps)(QuestionsList);
61 |
62 |
--------------------------------------------------------------------------------
/src/components/meetings/MeetingList.jsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import PropTypes from 'prop-types';
3 | import { Link } from 'react-router';
4 | import { connect } from 'react-redux';
5 | import { List, ListItem } from 'material-ui/List';
6 | import FloatingActionButton from 'material-ui/FloatingActionButton';
7 | import ContentAdd from 'material-ui/svg-icons/content/add';
8 |
9 | import MeetingItem from './MeetingItem';
10 |
11 | class MeetingList extends Component {
12 |
13 | renderMeetings() {
14 | const rows = [];
15 | if (this.props.meetings && this.props.meetings.size > 0) {
16 | this.props.meetings.forEach((meeting, key) => {
17 | rows.push(
18 | ,
23 | );
24 | });
25 | }
26 | if (rows.length === 0) {
27 | rows.push(
28 | ,
32 | );
33 | }
34 | return rows;
35 | }
36 |
37 | render() {
38 | const buttonStyle = {
39 | margin: 0,
40 | top: 'auto',
41 | right: 20,
42 | bottom: 76,
43 | left: 'auto',
44 | position: 'fixed',
45 | zIndex: 1,
46 | };
47 |
48 | return (
49 |
50 |
51 | {this.renderMeetings()}
52 | }
55 | >
56 |
57 |
58 |
59 |
60 | );
61 | }
62 | }
63 |
64 | MeetingList.propTypes = {
65 | meetings: PropTypes.object.isRequired,
66 | };
67 |
68 | const mapStateToProps = (state) => {
69 | return {
70 | meetings: state.meetings.list,
71 | };
72 | };
73 |
74 | export default connect(mapStateToProps)(MeetingList);
75 |
--------------------------------------------------------------------------------
/src/components/Privacy.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Divider from 'material-ui/Divider';
3 |
4 | const Privacy = () => (
5 |
6 |
Privacy Policy
7 |
Last updated: March 24, 2017
8 |
9 |
10 |
11 |
12 | Your privacy is very important to us. Accordingly, we have developed this Policy in order
13 | for you to understand how we collect, use, communicate and disclose and make use of
14 | personal information. The following outlines our privacy policy.
15 |
16 |
17 |
18 |
19 | We will collect and use of personal information solely with the objective of bettering
20 | the user's experience or for other compatible purposes.
21 |
22 |
23 | We will only retain personal information as long as necessary for the fulfillment of
24 | those purposes.
25 |
26 |
27 | We will collect personal information by lawful and fair means and, where appropriate,
28 | with the knowledge or consent of the individual concerned.
29 |
30 |
31 | We will attempt to protect personal information by reasonable security safeguards
32 | against loss or theft, as well as unauthorized access, disclosure, copying, use or
33 | modification.
34 |
35 |
36 | We will make readily available to users information about our policies and
37 | practices relating to the management of personal information.
38 |
39 |
40 | We may use analytics software, including but not limited to, Google Analytics, etc.
41 | Such data collected will be used solely for the purpose of improving the user
42 | experience.
43 |
44 |
45 | We use local storage technologies, including Cookies, localStorage, etc. to enhance
46 | your experience.
47 |
48 |
49 |
50 | );
51 |
52 | export default Privacy;
53 |
--------------------------------------------------------------------------------
/src/components/followUps/FollowUpNew.jsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import PropTypes from 'prop-types';
3 | import { connect } from 'react-redux';
4 | import { browserHistory } from 'react-router';
5 |
6 | import FollowUpForm from './FollowUpForm';
7 | import followUpActions from '../../actions/followUps';
8 |
9 | class FollowUpNew extends Component {
10 | constructor() {
11 | super();
12 | this.onSubmit = this.onSubmit.bind(this);
13 | }
14 |
15 | componentWillMount() {
16 | if (!this.props.initialValues.directKey) {
17 | this.props.initialValues.directKey = this.props.params.id;
18 | }
19 | if (!this.props.initialValues.meetingKey && this.props.params.meetingId) {
20 | this.props.initialValues.meetingKey = this.props.params.meetingId;
21 | }
22 | this.props.reset();
23 | }
24 |
25 | /* eslint no-param-reassign: ["error", { "props": false }] */
26 | onSubmit(followUp) {
27 | if (followUp.followUpDate instanceof Date) {
28 | followUp.followUpDateReverse = 0 - followUp.followUpDate;
29 | followUp.followUpDate = followUp.followUpDate.toISOString();
30 | }
31 | this.props.create(followUp).then(() => {
32 | browserHistory.goBack();
33 | });
34 | }
35 |
36 | render() {
37 | return (
38 |
39 |
43 |
* Indicates required field
44 |
45 | );
46 | }
47 | }
48 |
49 | FollowUpNew.propTypes = {
50 | create: PropTypes.func.isRequired,
51 | reset: PropTypes.func.isRequired,
52 | };
53 |
54 | const mapStateToProps = (state) => {
55 | const initialValues = {
56 | followUpDate: new Date(),
57 | meetingKey: null,
58 | };
59 | return {
60 | initialValues,
61 | formType: 'create',
62 | error: state.followUps.error,
63 | directs: state.directs.list,
64 | };
65 | };
66 |
67 | export default connect(mapStateToProps,
68 | { create: followUpActions.create,
69 | reset: followUpActions.resetActive,
70 | })(FollowUpNew);
71 |
--------------------------------------------------------------------------------
/src/components/header/MeetingNavigationIcons.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import IconButton from 'material-ui/IconButton';
3 | import NavigationChevronRight from 'material-ui/svg-icons/navigation/chevron-right';
4 | import NavigationChevronLeft from 'material-ui/svg-icons/navigation/chevron-left';
5 | import { Link } from 'react-router';
6 | import { white } from 'material-ui/styles/colors';
7 | import PropTypes from 'prop-types';
8 |
9 | const propTypes = {
10 | indexOfActive: PropTypes.number.isRequired,
11 | meetings: PropTypes.array.isRequired,
12 | };
13 |
14 | /**
15 | * @function MeetingNavigationIcons
16 | * @param props
17 | * @returns {XML}
18 | * @constructor
19 | */
20 | function MeetingNavigationIcons(props) {
21 | const { indexOfActive, meetings } = props;
22 |
23 | if (meetings.length === 1) {
24 | return null;
25 | }
26 |
27 | const iconStyle = {
28 | width: 36,
29 | height: 36,
30 | color: white,
31 | opacity: 0.5,
32 | };
33 |
34 | let prevLink = { iconStyle };
35 | let nextLink = { iconStyle };
36 |
37 | const prevIndex = indexOfActive - 1;
38 | const nextIndex = indexOfActive + 1;
39 |
40 | if (meetings[prevIndex]) {
41 | prevLink = {
42 | ...prevLink,
43 | ...{
44 | containerElement: ,
45 | iconStyle: { ...prevLink.iconStyle, ...{ opacity: 1 } },
46 | },
47 | };
48 | }
49 | if (meetings[nextIndex]) {
50 | nextLink = {
51 | ...nextLink,
52 | ...{
53 | containerElement: ,
54 | iconStyle: { ...prevLink.iconStyle, ...{ opacity: 1 } },
55 | },
56 | };
57 | }
58 | return (
59 |
60 |
64 |
65 |
66 |
70 |
71 |
72 |
73 | );
74 | }
75 |
76 | MeetingNavigationIcons.propTypes = propTypes;
77 |
78 | export default MeetingNavigationIcons;
79 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "one-on-one",
3 | "version": "0.7.2",
4 | "description": "A simple tracking app for one-on-one meetings",
5 | "private": true,
6 | "devDependencies": {
7 | "enzyme": "^2.9.1",
8 | "eslint": "^3.8.0",
9 | "eslint-config-airbnb": "^15.0.1",
10 | "eslint-plugin-import": "^2.2.0",
11 | "eslint-plugin-jsx-a11y": "^5.0.3",
12 | "eslint-plugin-react": "^7.0.1",
13 | "node-sass": "^4.5.1",
14 | "npm-run-all": "^4.0.2",
15 | "react-scripts": "1.0.16",
16 | "react-test-renderer": "^15.6.1",
17 | "redux-mock-store": "^1.2.3"
18 | },
19 | "dependencies": {
20 | "axios": "^0.16.2",
21 | "firebase": "^4.1.2",
22 | "group-array": "^0.3.3",
23 | "material-ui": "^0.18.3",
24 | "mout": "^1.0.0",
25 | "object-property-natural-sort": "^0.0.4",
26 | "prop-types": "^15.5.10",
27 | "re-reselect": "^0.6.3",
28 | "react": "^15.5.4",
29 | "react-color": "^2.13.1",
30 | "react-dom": "^15.5.4",
31 | "react-ga": "^2.1.2",
32 | "react-quill": "^1.1.0",
33 | "react-redux": "^5.0.1",
34 | "react-router": "^3.0.5",
35 | "react-router-redux": "^4.0.7",
36 | "react-tap-event-plugin": "^2.0.0",
37 | "react-transition-group": "1.x",
38 | "reactcss": "^1.2.2",
39 | "redux": "^3.6.0",
40 | "redux-form": "^6.4.1",
41 | "redux-form-material-ui": "^4.1.2",
42 | "redux-thunk": "^2.1.0",
43 | "reselect": "^3.0.1",
44 | "source-map-explorer": "^1.5.0",
45 | "store": "^2.0.12",
46 | "string": "^3.3.3",
47 | "tinycolor2": "^1.4.1",
48 | "unique-random": "^1.0.0",
49 | "validator": "^7.0.0"
50 | },
51 | "scripts": {
52 | "analyze": "source-map-explorer build/static/js/main.*",
53 | "build-css": "node-sass src/ -o src/",
54 | "watch-css": "npm run build-css && node-sass src/ -o src/ --watch --recursive",
55 | "start-js": "react-scripts start",
56 | "start": "npm-run-all -p watch-css start-js",
57 | "build": "npm run build-css && react-scripts build",
58 | "test": "react-scripts test --env=jsdom",
59 | "eject": "react-scripts eject"
60 | },
61 | "eslintConfig": {
62 | "extends": "react-app"
63 | },
64 | "author": "Vidal Graupera (http://www.vidalgraupera.com)",
65 | "license": "Apache 2.0"
66 | }
67 |
--------------------------------------------------------------------------------
/database.rules.json:
--------------------------------------------------------------------------------
1 | {
2 | "rules": {
3 | "directs": {
4 | ".indexOn": ["name"],
5 | "$uid": {
6 | ".read": "auth != null && auth.uid == $uid",
7 | ".write": "auth != null && auth.uid == $uid"
8 | }
9 | },
10 | "meetings": {
11 | ".indexOn": ["meetingDateReverse", "directKey"],
12 | "$uid": {
13 | ".read": "auth != null && auth.uid == $uid",
14 | ".write": "auth != null && auth.uid == $uid"
15 | }
16 | },
17 | "teams": {
18 | ".indexOn": ["name"],
19 | "$uid": {
20 | ".read": "auth != null && auth.uid == $uid",
21 | ".write": "auth != null && auth.uid == $uid"
22 | }
23 | },
24 | "followUps": {
25 | ".indexOn": ["meetingKey", "directKey"],
26 | "$uid": {
27 | ".read": "auth != null && auth.uid == $uid",
28 | ".write": "auth != null && auth.uid == $uid"
29 | }
30 | },
31 | "questions": {
32 | "$uid": {
33 | ".indexOn": ["question","externalID"],
34 | ".read": "auth != null && auth.uid == $uid",
35 | ".write": "auth != null && auth.uid == $uid"
36 | }
37 | },
38 | "categoriesQuestions": {
39 | "$uid": {
40 | ".indexOn": ["name"],
41 | ".read": "auth != null && auth.uid == $uid",
42 | ".write": "auth != null && auth.uid == $uid"
43 | }
44 | },
45 | "accounts": {
46 | "$uid": {
47 | ".read": "auth != null && auth.uid == $uid",
48 | ".write": "auth != null && auth.uid == $uid"
49 | }
50 | },
51 | "archived": {
52 | "directs": {
53 | ".indexOn": ["name"],
54 | "$uid": {
55 | ".read": "auth != null && auth.uid == $uid",
56 | ".write": "auth != null && auth.uid == $uid"
57 | }
58 | },
59 | "meetings": {
60 | ".indexOn": ["meetingDateReverse", "directKey"],
61 | "$uid": {
62 | ".read": "auth != null && auth.uid == $uid",
63 | ".write": "auth != null && auth.uid == $uid"
64 | }
65 | },
66 | "followUps": {
67 | ".indexOn": ["meetingKey", "directKey"],
68 | "$uid": {
69 | ".read": "auth != null && auth.uid == $uid",
70 | ".write": "auth != null && auth.uid == $uid"
71 | }
72 | }
73 | }
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/src/selectors/archivedDirects.js:
--------------------------------------------------------------------------------
1 | import { createSelector } from 'reselect';
2 | import createCachedSelector from 're-reselect';
3 | import { getTeamsArray } from './teams';
4 | import { SORT_WITHOUT_TEAM_NAME } from '../constants/general';
5 |
6 | /**
7 | * @description Return directs from state
8 | * @param {Object} state app state
9 | */
10 | const getArchivedDirects = (state) => {
11 | if (!(state.archivedDirects.list instanceof Map)) {
12 | return [];
13 | }
14 | return state.archivedDirects.list;
15 | };
16 |
17 | const getTeams = state => getTeamsArray(state);
18 |
19 | /**
20 | * @description selector for directs array
21 | * @return {Array} array of direct objects
22 | */
23 | export const getArchivedArray = createSelector(getArchivedDirects, (directs) => {
24 | const arr = [];
25 | directs.forEach((direct, key) => {
26 | arr.push({ ...direct, ...{ id: key } });
27 | });
28 |
29 | return arr;
30 | });
31 |
32 | /**
33 | * @description selector for archived array count
34 | * @return {Number} number of array items
35 | */
36 | export const getArchivedArrayCount = createSelector(
37 | getArchivedArray,
38 | arr => arr.length,
39 | );
40 |
41 | /**
42 | * @description return array of archived directs with teamName
43 | * @return {Array}
44 | */
45 | export const getArchivedDirectsArrayWithTeam = createSelector(
46 | [getArchivedArray, getTeams],
47 | (directs, teams) => {
48 | return directs.map((direct) => {
49 | let teamName = SORT_WITHOUT_TEAM_NAME;
50 | if (direct.team) {
51 | const teamDirect = teams.find(team => team.id === direct.team);
52 | if (typeof teamDirect !== 'undefined') {
53 | teamName = teamDirect.name;
54 | }
55 | }
56 | return { ...direct, ...{ teamName } };
57 | });
58 | });
59 |
60 | export const getArchivedDirect = createCachedSelector(
61 | [getArchivedArray, getTeams, (state, id) => id],
62 | (directs, teams, id) => {
63 | const direct = directs.find(directSingle => directSingle.id === id);
64 | let teamName = '';
65 | if (direct.team) {
66 | const teamDirect = teams.find(team => team.id === direct.team);
67 | if (typeof teamDirect !== 'undefined') {
68 | teamName = teamDirect.name;
69 | }
70 | }
71 | return { ...direct, ...{ teamName } };
72 | },
73 | )((state, id) => id);
74 |
--------------------------------------------------------------------------------
/src/components/Features.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import meetingShowScreenshot from '../images/meeting-show.png';
3 | import meetingEditScreenshot from '../images/meeting-edit.png';
4 | import directsTeamScreenshot from '../images/directs-by-team.png';
5 | import followUpEditScreenshot from '../images/followUp-edit.png';
6 |
7 | const Features = () => {
8 | return (
9 |
10 |
Features
11 |
Things to make tracking 1 on 1s easier
12 |
13 |
14 |
19 |
24 |
25 |
26 |
Meetings
27 |
28 | Two sections for notes
29 | Includes sample questions you can ask
30 | Links to follow up items
31 |
32 |
33 |
34 |
35 |
36 |
Team members
37 |
38 | List of direct reports
39 | Categorize by color
40 | Group into teams
41 |
42 |
43 |
44 |
49 |
50 |
51 |
52 |
53 |
58 |
59 |
60 |
Follow Up Items
61 |
62 | Linked to directs and meetings
63 | Due dates
64 |
65 |
66 |
67 |
68 | );
69 | };
70 |
71 | export default Features;
72 |
--------------------------------------------------------------------------------
/src/components/BottomNav.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import FontIcon from 'material-ui/FontIcon';
4 | import { BottomNavigation, BottomNavigationItem } from 'material-ui/BottomNavigation';
5 | import Paper from 'material-ui/Paper';
6 | import Footer from './Footer';
7 |
8 | /**
9 | * @description propTypes for BottomNav
10 | * @type {Object}
11 | */
12 | const propTypes = {
13 | handleNavigate: PropTypes.func.isRequired,
14 | };
15 |
16 | /**
17 | * @function BottomNav
18 | * @param {Function} handleNavigate
19 | * @returns {XML}
20 | */
21 | function BottomNav({ handleNavigate }) {
22 | const dashboardIcon = dashboard ;
23 | const meetingsIcon = chat ;
24 | const directsIcon = people ;
25 | const followUpsIcon = assignment ;
26 |
27 | const paths = [/^\/dashboard/i, /^\/directs/i, /^\/meetings/i, /^\/followUps/i];
28 | let selectedIndex = -1;
29 | for (let i = 0; i < paths.length; i += 1) {
30 | if (paths[i].test(window.location.pathname)) {
31 | selectedIndex = i;
32 | break;
33 | }
34 | }
35 |
36 | if (selectedIndex !== -1) {
37 | return (
38 |
39 |
40 | handleNavigate('/dashboard')}
44 | />
45 | handleNavigate('/directs')}
49 | />
50 | handleNavigate('/meetings')}
54 | />
55 | handleNavigate('/followUps')}
59 | />
60 |
61 |
62 | );
63 | }
64 | return ;
65 | }
66 |
67 | BottomNav.propTypes = propTypes;
68 |
69 | export default BottomNav;
70 |
71 |
--------------------------------------------------------------------------------
/src/HOCs/archive/__tests__/OnArchivedContainerHOC.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-undef,no-unused-vars,import/first */
2 | jest.mock('../../../selectors/routing');
3 | import React from 'react';
4 | import { Provider } from 'react-redux';
5 | import { shallow } from 'enzyme';
6 | import configMockStore from 'redux-mock-store';
7 | import { getIsArchived } from '../../../selectors/routing';
8 | import OnArchivedContainerHOC, { componentFactory, mapStateToProps } from '../OnArchivedContainerHOC';
9 |
10 |
11 | const TestComponent = () => Test component
;
12 |
13 | describe('OnArchivedContainerHOC', () => {
14 | describe('mapStateToProps', () => {
15 | getIsArchived.mockImplementationOnce(() => ({ isArchived: false }));
16 | const mapProps = mapStateToProps({});
17 |
18 | it('Should call selector getIsArchived ', () => {
19 | expect(getIsArchived.mock.calls.length).toBe(1);
20 | });
21 |
22 | it('Should provide isArchived prop', () => {
23 | expect(mapProps.isArchived).toBeDefined();
24 | });
25 | });
26 |
27 | describe('componentFactory', () => {
28 | let OnArchivedContainer;
29 |
30 | const dispatch = jest.fn();
31 |
32 | beforeEach(() => {
33 | OnArchivedContainer = componentFactory(TestComponent);
34 | });
35 |
36 | it('Should be instance of OnArchivedContainer', () => {
37 | const wrapper = shallow( );
38 | expect(wrapper.find(TestComponent).exists()).toBeTruthy();
39 | });
40 |
41 | it('Should contains default prop isArchived', () => {
42 | const wrapper = shallow( );
43 | expect(wrapper.prop('isArchived')).toBeDefined();
44 | });
45 |
46 | it('Should pass own props to wrapped component', () => {
47 | const wrapper = shallow( );
48 | expect(wrapper.prop('foo')).toBe('bar');
49 | });
50 | });
51 |
52 | describe('OnArchivedContainerHOC', () => {
53 | const moskStore = configMockStore();
54 | const OnArchivedContainer = OnArchivedContainerHOC(TestComponent);
55 | const wrapper = shallow(
56 |
57 |
58 | );
59 |
60 | it('Should exists OnArchivedContainer', () => {
61 | expect(wrapper.find(OnArchivedContainer).exists()).toBeTruthy();
62 | });
63 | });
64 | });
65 |
66 |
--------------------------------------------------------------------------------
/src/components/teams/TeamsDropDownField.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import { Field } from 'redux-form';
3 | import Divider from 'material-ui/Divider';
4 | import { SelectField } from 'redux-form-material-ui';
5 | import MenuItem from 'material-ui/MenuItem';
6 | import PropTypes from 'prop-types';
7 |
8 | import TeamCrudDialogBoxContainer from './dialog/TeamCrudDialogBoxContainer';
9 |
10 | /**
11 | * @description propTypes for TeamsDropDownField
12 | * @type {{teams}}
13 | */
14 | const propTypes = {
15 | teams: PropTypes.arrayOf(
16 | PropTypes.shape({
17 | id: PropTypes.string.isRequired,
18 | name: PropTypes.string.isRequired,
19 | createdAt: PropTypes.string,
20 | updatedAt: PropTypes.string,
21 | }).isRequired,
22 | ).isRequired,
23 | };
24 |
25 | /**
26 | * @class TeamsDropDownField
27 | * @extends React.Component
28 | * @description Render component
29 | */
30 | class TeamsDropDownField extends Component {
31 |
32 | state = {
33 | openDialog: false,
34 | };
35 |
36 | handleOpenDialog = () => {
37 | this.setState({ openDialog: true });
38 | };
39 |
40 | handleCloseDialog = () => {
41 | this.setState({ openDialog: false });
42 | };
43 |
44 | render() {
45 | const { teams } = this.props;
46 | return (
47 |
48 |
56 |
61 |
62 | {teams.map(team => (
63 |
68 | ))}
69 |
70 |
75 |
76 |
80 |
81 | );
82 | }
83 | }
84 |
85 | TeamsDropDownField.propTypes = propTypes;
86 |
87 | export default TeamsDropDownField;
88 |
--------------------------------------------------------------------------------
/src/components/meetings/questions/dialog/CategoriesQuestions.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import { connect } from 'react-redux';
3 | import SelectField from 'material-ui/SelectField';
4 | import MenuItem from 'material-ui/MenuItem';
5 | import PropTypes from 'prop-types';
6 |
7 | import { getCategoriesQuestionsArray } from '../../../../selectors/categoriesQuestions';
8 | import { setCategoryFilter } from '../../../../actions/questions';
9 |
10 | const propTypes = {
11 | categoriesQuestions: PropTypes.array.isRequired,
12 | filterByCategory: PropTypes.oneOfType([
13 | PropTypes.string,
14 | PropTypes.bool,
15 | ]).isRequired,
16 | setQuestionFilter: PropTypes.func.isRequired,
17 | loadingQuestions: PropTypes.bool.isRequired,
18 | };
19 |
20 | /**
21 | * @class CategoriesQuestions
22 | * @extends React.Component
23 | * @description Render component
24 | */
25 | class CategoriesQuestions extends Component {
26 |
27 | state = {
28 | value: this.props.filterByCategory,
29 | };
30 |
31 | handleChange = (event, index, value) => {
32 | this.setState({ value });
33 | this.props.setQuestionFilter(value);
34 | };
35 |
36 | renderSelectField = () => {
37 | const { categoriesQuestions } = this.props;
38 | return (
43 |
44 | {categoriesQuestions.map(({ id, name }) => (
45 |
46 | ))}
47 | );
48 | };
49 |
50 | /**
51 | * @description render
52 | * @return {Object} JSX HTML Content
53 | */
54 | render() {
55 | const isEmpty = this.props.categoriesQuestions.length === 0;
56 | const { loadingQuestions } = this.props;
57 | return (isEmpty || loadingQuestions) ? null : this.renderSelectField();
58 | }
59 | }
60 |
61 | CategoriesQuestions.propTypes = propTypes;
62 |
63 | const mapStateToProps = state => ({
64 | categoriesQuestions: getCategoriesQuestionsArray(state),
65 | filterByCategory: state.questions.filterByCategory,
66 | loadingQuestions: state.questions.loading,
67 | });
68 |
69 | const mapDispatchToProp = dispatch => ({
70 | setQuestionFilter: (categoryID) => {
71 | dispatch(setCategoryFilter(categoryID));
72 | },
73 | });
74 |
75 | export default connect(mapStateToProps, mapDispatchToProp)(CategoriesQuestions);
76 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # 1 on 1 Tracker
2 |
3 |
4 |
5 | One-on-one tracking app for managers and their direct reports written in React and Firebase
6 | built and designed for the mobile web.
7 |
8 | Instead of continuing to use paper, or forgetting things, I wanted an
9 | app to keep track of my one-on-ones, plus I wanted to learn more about React and Firebase,
10 | so I created this.
11 |
12 | A *live* version can be found at [https://www.1on1tracker.com](https://www.1on1tracker.com)
13 |
14 | [](https://www.youtube.com/watch?v=n42TtZIKzEU "Quick Demo of 1on1tracker.com mobile website")
15 |
16 | ### Stack
17 |
18 | - React
19 | - Redux
20 | - React-Redux
21 | - React-Router
22 | - React-Router-Redux
23 | - Redux-Thunk
24 | - Redux-Form
25 | - Firebase
26 | - Material-UI
27 | - React-GA for Google Analytics
28 |
29 | This project was bootstrapped with [Create React App](https://github.com/facebookincubator/create-react-app).
30 |
31 | ### Gallery
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 | ### Deploy
41 |
42 | - Create a firebase project
43 | - Enable Google Authentication
44 | - Fork this repo
45 | - Copy src/firebase/config.js.example to src/firebase/config.js and fill out
46 | - npm run build
47 | - install firebase CLI tools
48 | - firebase deploy
49 |
50 | ### Code Style
51 |
52 | For the most part, this code attempts to follow Airbnb's styleguide.
53 |
54 | ## Roadmap
55 |
56 | The basic feature set is a list of meetings with notes linked to direct reports.
57 |
58 | - Better UI design and styling
59 | - Sorting
60 | - Search
61 | - Google hangout integration
62 | - Slack integration
63 | - Reports
64 | - Test coverage
65 |
66 | Want to talk about one on ones? Have an idea or question? Contact me.
67 |
68 | ## Contact
69 |
70 | If you find this useful, follow me [@vgraupera](https://twitter.com/vgraupera)
71 |
--------------------------------------------------------------------------------
/src/components/common/RichTextEditor.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import ReactQuill from 'react-quill'; // ES6
3 | import 'react-quill/dist/quill.snow.css'; // ES6
4 |
5 | import { TextField } from 'redux-form-material-ui';
6 | import PropTypes from 'prop-types';
7 |
8 | /**
9 | * @description propTypes for RichTextEditor
10 | * @type {{input}}
11 | */
12 | const propTypes = {
13 | input: PropTypes.shape({
14 | value: PropTypes.string.isRequired,
15 | onChange: PropTypes.func.isRequired,
16 | }).isRequired,
17 | floatingLabelText: PropTypes.string.isRequired,
18 | };
19 |
20 | /**
21 | * @description component Style
22 | * @type {Object}
23 | */
24 | const style = {
25 | wrapperDiv: { marginTop: 10, marginBottom: 10 },
26 | inputStyle: { display: 'none' },
27 | floatingLabelStyle: { top: '10%' },
28 | textFieldStyle: { height: '28px', width: '100%' },
29 | };
30 |
31 | /**
32 | * @class RichTextEditor
33 | * @extends React.Component
34 | * @description Render component
35 | */
36 | class RichTextEditor extends Component {
37 |
38 | state = {
39 | value: this.props.input.value
40 | };
41 |
42 | /**
43 | * @description Handle Change in react-rte and pass to redux-form
44 | * @param {Object} value react-rte value obect
45 | */
46 | handleChange = (value) => {
47 | this.setState({ value });
48 | this.props.input.onChange(value);
49 | };
50 |
51 | /**
52 | * @description render
53 | * @return {Object} JSX HTML Content
54 | */
55 | render() {
56 |
57 | const modules = {
58 | toolbar: [
59 | [{ 'header': [1, 2, false] }],
60 | ['bold', 'italic', 'underline','strike', 'blockquote'],
61 | [{'list': 'ordered'}, {'list': 'bullet'}, {'indent': '-1'}, {'indent': '+1'}],
62 | ],
63 | };
64 |
65 | const formats = [
66 | 'header',
67 | 'bold', 'italic', 'underline', 'strike', 'blockquote',
68 | 'list', 'bullet', 'indent',
69 | ];
70 |
71 | return (
72 |
73 |
80 |
87 |
88 | );
89 | }
90 | }
91 |
92 | RichTextEditor.propTypes = propTypes;
93 |
94 | export default RichTextEditor;
95 |
--------------------------------------------------------------------------------
/src/components/LeftDrawer.jsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import PropTypes from 'prop-types';
3 | import { connect } from 'react-redux';
4 | import Drawer from 'material-ui/Drawer';
5 | import MenuItem from 'material-ui/MenuItem';
6 | import Divider from 'material-ui/Divider';
7 |
8 | import Auth from './Auth';
9 | import { isAuthenticated, rootPath } from '../actions/auth';
10 | import QuestionMenuItem from './drawer/QuestionMenuItem';
11 | import TeamMenuItem from './drawer/TeamMenuItem';
12 |
13 | class LeftDrawer extends Component {
14 | render() {
15 | let signedInItems;
16 | if (isAuthenticated(this.props)) {
17 | signedInItems = (
18 |
19 |
{
22 | this.props.handleNavigate('/account');
23 | }}
24 | />
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 | );
33 | }
34 |
35 | return (
36 |
41 | {
43 | this.props.handleNavigate(rootPath(isAuthenticated(this.props)));
44 | }}
45 | >
46 | 1on1 Tracker
47 |
48 | {signedInItems}
49 |
50 | this.props.handleNavigate('/terms')}
52 | primaryText="Terms and Conditions"
53 | />
54 | this.props.handleNavigate('/privacy')}
56 | primaryText="Privacy Policy"
57 | />
58 |
59 | this.props.handleNavigate('/about')}
61 | primaryText="About 1on1tracker"
62 | />
63 | this.props.handleNavigate('/about-me')}
65 | primaryText="About me"
66 | />
67 |
68 | );
69 | }
70 | }
71 |
72 | LeftDrawer.propTypes = {
73 | handleNavigate: PropTypes.func.isRequired,
74 | open: PropTypes.bool,
75 | };
76 |
77 | LeftDrawer.defaultProps = {
78 | open: false,
79 | };
80 |
81 | const mapStateToProps = (state) => {
82 | return {
83 | auth: state.auth,
84 | };
85 | };
86 |
87 | export default connect(mapStateToProps)(LeftDrawer);
88 |
--------------------------------------------------------------------------------
/src/components/directs/single/tabs/meetings/DirectMeetingList.jsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import PropTypes from 'prop-types';
3 | import { Link } from 'react-router';
4 | import { connect } from 'react-redux';
5 | import { List, ListItem } from 'material-ui/List';
6 |
7 | import { getIsArchived } from '../../../../../selectors/routing';
8 | import InnerHtmlStripTags from '../../../../common/InnerHtmlStripTags';
9 |
10 |
11 | class DirectMeetingList extends Component {
12 |
13 | constructor() {
14 | super();
15 | this.state = { selectedMeetings: null };
16 | }
17 |
18 | componentDidMount() {
19 | this.onMount();
20 | }
21 |
22 | onMount() {
23 | const selectedMeetings = new Map([...this.props.meetings]
24 | .filter(([key, value]) =>
25 | value.directKey === this.props.directId));
26 | this.setState({ selectedMeetings });
27 | }
28 |
29 | renderMeetings() {
30 | const rows = [];
31 | if (this.props.meetings &&
32 | this.state.selectedMeetings &&
33 | this.state.selectedMeetings.size > 0) {
34 | this.state.selectedMeetings.forEach((meeting, key) => {
35 | const notesString = meeting.directsNotes ? meeting.directsNotes : meeting.managersNotes;
36 | rows.push(
37 |
42 |
43 |
44 | }
45 | containerElement={ }
46 | disabled={this.props.isArchived}
47 | />);
48 | });
49 | } else {
50 | rows.push(
51 | );
55 | }
56 | return rows;
57 | }
58 |
59 | render() {
60 | return (
61 |
62 |
63 | {this.renderMeetings()}
64 |
65 |
66 | );
67 | }
68 | }
69 |
70 | DirectMeetingList.propTypes = {
71 | directId: PropTypes.string.isRequired,
72 | meetings: PropTypes.object.isRequired,
73 | isArchived: PropTypes.bool.isRequired,
74 | };
75 |
76 | const mapStateToProps = (state) => {
77 | let meetings = 'meetings';
78 | const isArchived = getIsArchived(state);
79 | if (isArchived) {
80 | meetings = 'archivedMeetings';
81 | }
82 | return {
83 | meetings: state[meetings].list,
84 | isArchived,
85 | };
86 | };
87 |
88 | export default connect(mapStateToProps)(DirectMeetingList);
89 |
--------------------------------------------------------------------------------
/src/components/directs/single/head-info/UnarchiveBtn.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import { connect } from 'react-redux';
3 | import { browserHistory } from 'react-router';
4 | import { CardActions } from 'material-ui/Card';
5 | import RaisedButton from 'material-ui/RaisedButton';
6 | import { white , teal400 } from 'material-ui/styles/colors';
7 | import PropTypes from 'prop-types';
8 |
9 | import directAction, { archivedDirects as archivedDirectsAction } from '../../../../actions/directs';
10 | import followUpActions, { archivedFollowUps as archivedFollowUpsAction } from '../../../../actions/followUps';
11 | import meetingActions, { archivedMeetings as archivedMeetingsAction } from '../../../../actions/meetings';
12 | import ShowOnArchivedHOC from '../../../../HOCs/archive/ShowOnArchivedHOC';
13 | import { ARCHIVED_URL_SUFFIX } from '../../../../constants/general';
14 |
15 | /**
16 | * @description propTypes for UnarchiveBtn
17 | * @type {Object}
18 | */
19 | const propTypes = {
20 | id: PropTypes.string.isRequired,
21 | onMoveTo: PropTypes.func.isRequired,
22 | meetingsMoveEqualTo: PropTypes.func.isRequired,
23 | followUpsMoveEqualTo: PropTypes.func.isRequired,
24 | };
25 |
26 | /**
27 | * @class UnarchiveBtn
28 | * @extends React.Component
29 | * @description Render component
30 | */
31 | class UnarchiveBtn extends Component {
32 |
33 | onUnArchived = (event) => {
34 | event.preventDefault();
35 | if (window.confirm('Unarchive the direct?')) {
36 | const { id } = this.props;
37 | this.props.onMoveTo(id, directAction)
38 | .then(() => {
39 | this.props.meetingsMoveEqualTo('directKey', id, meetingActions);
40 | this.props.followUpsMoveEqualTo('directKey', id, followUpActions);
41 | });
42 | browserHistory.push({
43 | pathname: `/directs/${ARCHIVED_URL_SUFFIX}`,
44 | state: { isArchived: true },
45 | });
46 | }
47 | };
48 |
49 | /**
50 | * @description render
51 | * @return {Object} JSX HTML Content
52 | */
53 | render() {
54 | return (
55 |
56 |
62 |
63 | );
64 | }
65 | }
66 |
67 | UnarchiveBtn.propTypes = propTypes;
68 |
69 | const mapDispatchToProps = {
70 | onMoveTo: archivedDirectsAction.moveTo,
71 | followUpsMoveEqualTo: archivedFollowUpsAction.moveEqualTo,
72 | meetingsMoveEqualTo: archivedMeetingsAction.moveEqualTo,
73 | };
74 |
75 | export default ShowOnArchivedHOC(connect(null, mapDispatchToProps)(UnarchiveBtn));
76 |
--------------------------------------------------------------------------------
/src/components/followUps/FollowUpList.jsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import PropTypes from 'prop-types';
3 | import { Link } from 'react-router';
4 | import { connect } from 'react-redux';
5 | import {
6 | List,
7 | ListItem,
8 | } from 'material-ui/List';
9 | import Divider from 'material-ui/Divider';
10 | import FloatingActionButton from 'material-ui/FloatingActionButton';
11 | import ContentAdd from 'material-ui/svg-icons/content/add';
12 |
13 | import FollowUpItem from './FollowUpItem';
14 |
15 | class FollowUpList extends Component {
16 | constructor() {
17 | super();
18 | this.toggleCompleted = this.toggleCompleted.bind(this);
19 | this.state = {
20 | showCompleted: false,
21 | };
22 | }
23 |
24 | toggleCompleted() {
25 | this.setState({ showCompleted: !this.state.showCompleted });
26 | }
27 |
28 | renderFollowUps() {
29 | const rows = [];
30 | rows.push(
31 | ,
37 | ,
38 | );
39 |
40 | if (this.props.followUps && this.props.followUps.size > 0) {
41 | this.props.followUps.forEach((followUp, key) => {
42 | if (!this.state.showCompleted && followUp.completed) {
43 | return;
44 | }
45 | rows.push(
46 |
51 | );
52 | });
53 | };
54 |
55 | if (rows.length === 2) {
56 | rows.push(
57 |
61 | );
62 | }
63 | return rows;
64 | }
65 |
66 | render() {
67 | const buttonStyle = {
68 | margin: 0,
69 | top: 'auto',
70 | right: 20,
71 | bottom: 76,
72 | left: 'auto',
73 | position: 'fixed',
74 | };
75 |
76 | return (
77 |
78 |
79 | {this.renderFollowUps()}
80 | }
83 | >
84 |
85 |
86 |
87 |
88 | );
89 | }
90 | }
91 |
92 | FollowUpList.propTypes = {
93 | followUps: PropTypes.object,
94 | };
95 |
96 | const mapStateToProps = (state) => {
97 | return {
98 | followUps: state.followUps.list,
99 | };
100 | };
101 |
102 | export default connect(mapStateToProps)(FollowUpList);
103 |
--------------------------------------------------------------------------------
/src/components/followUps/FollowUpItem.jsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import PropTypes from 'prop-types';
3 | import { connect } from 'react-redux';
4 | import { ListItem } from 'material-ui/List';
5 | import Checkbox from 'material-ui/Checkbox';
6 | import { browserHistory } from 'react-router';
7 |
8 | import followUpActions from '../../actions/followUps';
9 | import { getDirectsArray } from '../../selectors/direct';
10 | import { getIsArchived } from '../../selectors/routing';
11 |
12 | class FollowUpItem extends Component {
13 | static summary(followUp) {
14 | const dateString = new Date(followUp.followUpDate).toLocaleDateString();
15 | const notesString = followUp.description;
16 | return `${dateString} : ${notesString || 'TBD'}`;
17 | }
18 |
19 | constructor() {
20 | super();
21 | this.onClick = this.onClick.bind(this);
22 | }
23 |
24 | onClick() {
25 | browserHistory.push(`/followUps/${this.props.id}`);
26 | }
27 |
28 | handleCheck = (event, isInputChecked) => {
29 | const { id, followUp } = this.props;
30 | followUp.completed = !followUp.completed;
31 | this.props.update(id, followUp);
32 | }
33 |
34 | render() {
35 | const { directs, followUp, isArchived } = this.props;
36 |
37 | if (directs && followUp) {
38 | const direct = directs.find(directSingle => directSingle.id === followUp.directKey);
39 | return ( {
45 | event.stopPropagation();
46 | this.handleCheck(event, isInputChecked);
47 | }}
48 | />
49 | }
50 | primaryText={this.props.primaryText || ((direct && typeof direct.name !== 'undefined') ? direct.name : '???')}
51 | secondaryText={this.props.secondaryText || FollowUpItem.summary(followUp)}
52 | onClick={isArchived ? null : this.onClick}
53 | style={isArchived ? {cursor:'default'} : {}}
54 | />
55 | );
56 | }
57 |
58 | return null;
59 | }
60 | }
61 |
62 | FollowUpItem.contextTypes = {
63 | router: PropTypes.object,
64 | };
65 |
66 | FollowUpItem.propTypes = {
67 | directs: PropTypes.array.isRequired,
68 | followUp: PropTypes.object.isRequired,
69 | id: PropTypes.string.isRequired,
70 | primaryText: PropTypes.string,
71 | secondaryText: PropTypes.string,
72 | isArchived: PropTypes.bool.isRequired,
73 | };
74 |
75 | const mapStateToProps = (state) => {
76 | return {
77 | directs: getDirectsArray(state),
78 | isArchived: getIsArchived(state),
79 | };
80 | };
81 |
82 |
83 | const mapDispatchToProps = (dispatch) => {
84 | return {
85 | update: (key, value) => {
86 | dispatch(followUpActions.update(key, value));
87 | },
88 | };
89 | };
90 |
91 | export default connect(
92 | mapStateToProps,
93 | mapDispatchToProps,
94 | )(FollowUpItem);
95 |
--------------------------------------------------------------------------------
/src/components/followUps/FollowUpForm.jsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import PropTypes from 'prop-types';
3 | import { Field, reduxForm } from 'redux-form';
4 | import {
5 | DatePicker,
6 | TextField,
7 | SelectField,
8 | Checkbox,
9 | } from 'redux-form-material-ui';
10 | import RaisedButton from 'material-ui/RaisedButton';
11 | import MenuItem from 'material-ui/MenuItem';
12 |
13 | export const validate = (values) => {
14 | const errors = {};
15 | const requiredFields = ['directKey', 'followUpDate'];
16 | requiredFields.forEach((field) => {
17 | if (!values[field]) {
18 | errors[field] = 'Required';
19 | }
20 | });
21 | return errors;
22 | };
23 |
24 | class FollowUpForm extends Component {
25 | static formatDate(date) {
26 | return date.toLocaleDateString();
27 | }
28 |
29 | renderDirects() {
30 | const rows = [];
31 |
32 | this.props.directs.forEach((direct, key) => {
33 | rows.push( );
34 | });
35 |
36 | return rows;
37 | }
38 |
39 | render() {
40 | const { formType, handleSubmit, pristine, submitting } = this.props;
41 | const submitText = formType === 'edit' ? 'Update' : 'Create';
42 |
43 | return (
44 |
86 | );
87 | }
88 | }
89 |
90 | FollowUpForm.propTypes = {
91 | formType: PropTypes.oneOf(['create', 'edit']),
92 | handleSubmit: PropTypes.func,
93 | onSubmit: PropTypes.func,
94 | directs: PropTypes.object.isRequired,
95 | pristine: PropTypes.bool,
96 | submitting: PropTypes.bool,
97 | };
98 |
99 | export default reduxForm({ form: 'followUp', validate })(FollowUpForm);
100 |
--------------------------------------------------------------------------------
/src/components/Home.jsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import PropTypes from 'prop-types';
3 | import { connect } from 'react-redux';
4 | import { Link } from 'react-router';
5 | import RaisedButton from 'material-ui/RaisedButton';
6 |
7 | import { openAuth } from '../actions/auth';
8 |
9 | import dashboardScreenshot from '../images/dashboard.png';
10 |
11 | import ShowLoaderHOC from '../HOCs/ShowLoaderHOC';
12 | import { AUTH_AWAITING_RESPONSE } from '../actions/types';
13 |
14 | export class Home extends Component {
15 |
16 | render() {
17 | return (
18 |
19 |
20 |
Designed to help with your 1 on 1s and for use on your smartphone.
21 |
22 |
27 |
28 |
29 | VIDEO
35 |
36 |
37 |
38 |
39 |
40 | No more taking notes on paper that can get lost.
41 | Link follow up actions to people and meetings.
42 | Group your people into teams
43 | Includes a library of 300 suggested questions.
44 | Designed for easy note taking and follow up on your phone
45 | while you are away from your desk or computer.
46 |
47 |
48 | }
50 | label="Features"
51 | primary={true}
52 | />
53 |
54 |
55 |
56 |
57 |
58 |
Dashboard
59 |
64 |
See recent meetings and pending follow up items at a glance
65 |
66 |
67 |
68 |
69 |
70 | Create a free account using Google
71 |
72 |
77 |
78 |
79 | );
80 | }
81 | }
82 |
83 | Home.propTypes = {
84 | openAuth: PropTypes.func.isRequired,
85 | authStatus: PropTypes.string.isRequired,
86 | };
87 |
88 | const mapStateToProps = state => ({
89 | authStatus: state.auth.status,
90 | });
91 |
92 | const mapDispatchToProps = {
93 | openAuth,
94 | };
95 |
96 | export default connect(mapStateToProps, mapDispatchToProps)(
97 | ShowLoaderHOC('authStatus', AUTH_AWAITING_RESPONSE)(Home),
98 | );
99 |
--------------------------------------------------------------------------------
/src/selectors/direct.js:
--------------------------------------------------------------------------------
1 | import { createSelector } from 'reselect';
2 | import createCachedSelector from 're-reselect';
3 | import objNaturalSort from 'object-property-natural-sort';
4 | import groupArray from 'group-array';
5 | import sort from 'mout/array/sort';
6 |
7 | import { SORT_BY_TEAM_NAME, SORT_WITHOUT_TEAM_NAME } from '../constants/general';
8 | import { getTeamsArray } from './teams';
9 |
10 | /**
11 | * @description Return directs from state
12 | * @param {Object} state app state
13 | */
14 | const getDirects = (state) => {
15 | if (!(state.directs.list instanceof Map)) {
16 | return [];
17 | }
18 | return state.directs.list;
19 | };
20 | const getTeams = state => getTeamsArray(state);
21 | const sortBy = state => state.directs.sortBy;
22 |
23 | /**
24 | * @description selector for directs array
25 | * @return {Array} array of direct objects
26 | */
27 | export const getDirectsArray = createSelector(getDirects, (directs) => {
28 | const arr = [];
29 | directs.forEach((direct, key) => {
30 | arr.push({ ...direct, ...{ id: key } });
31 | });
32 |
33 | return arr;
34 | });
35 |
36 | /**
37 | * @description natural sort of array
38 | * @param {Array} arr
39 | * @param {String} sortByValue name of property
40 | * @return {Array.}
41 | */
42 | const arraySort = (arr, sortByValue) => {
43 | return sort(arr, objNaturalSort(sortByValue));
44 | };
45 |
46 | function putEmptyTeamToEnd(arr, sortByValue) {
47 | return sort(arr, (a, b) => {
48 | if (a[sortByValue] === SORT_WITHOUT_TEAM_NAME) {
49 | return 1;
50 | }
51 | if (b[sortByValue] === SORT_WITHOUT_TEAM_NAME) {
52 | return -1;
53 | }
54 | return 0;
55 | });
56 | }
57 |
58 | function groupByTeam(arr, sortByValue) {
59 | return groupArray(arr, sortByValue);
60 | }
61 |
62 | /**
63 | * @description return sorted array of directs with teamName
64 | * @return {Array}
65 | */
66 | export const getDirectsArrayWithTeam = createSelector(
67 | [getDirectsArray, getTeams, sortBy],
68 | (directs, teams, sortByValue) => {
69 | const arr = directs.map((direct) => {
70 | let teamName = SORT_WITHOUT_TEAM_NAME;
71 | if (direct.team) {
72 | const teamDirect = teams.find(team => team.id === direct.team);
73 | if (typeof teamDirect !== 'undefined') {
74 | teamName = teamDirect.name;
75 | }
76 | }
77 | return { ...direct, ...{ teamName } };
78 | });
79 |
80 | let directsArray = arraySort(arr, sortByValue);
81 |
82 | if (sortByValue === SORT_BY_TEAM_NAME) {
83 | directsArray = putEmptyTeamToEnd(directsArray, sortByValue);
84 | return groupByTeam(directsArray, sortByValue);
85 | }
86 |
87 | return directsArray;
88 | });
89 |
90 | export const getDirect = createCachedSelector(
91 | [getDirectsArray, getTeams, (state, id) => id],
92 | (directs, teams, id) => {
93 | const direct = directs.find(directSingle => directSingle.id === id);
94 | let teamName = '';
95 | if (direct.team) {
96 | const teamDirect = teams.find(team => team.id === direct.team);
97 | if (typeof teamDirect !== 'undefined') {
98 | teamName = teamDirect.name;
99 | }
100 | }
101 | return { ...direct, ...{ teamName } };
102 | },
103 | )((state, id) => id);
104 |
--------------------------------------------------------------------------------
/src/components/meetings/MeetingForm.jsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import PropTypes from 'prop-types';
3 | import { Field, reduxForm } from 'redux-form';
4 | import {
5 | DatePicker,
6 | SelectField,
7 | } from 'redux-form-material-ui';
8 | import RaisedButton from 'material-ui/RaisedButton';
9 | import MenuItem from 'material-ui/MenuItem';
10 |
11 | import RichTextEditor from '../common/RichTextEditor';
12 | import MeetingQuestionsContainer from './questions/MeetingQuestionsContainer';
13 |
14 | export const validate = (values) => {
15 | const errors = {};
16 | const requiredFields = ['directKey', 'meetingDate'];
17 | requiredFields.forEach((field) => {
18 | if (!values[field]) {
19 | errors[field] = 'Required';
20 | }
21 | });
22 | return errors;
23 | };
24 |
25 | class MeetingForm extends Component {
26 | static formatDate(date) {
27 | return date.toLocaleDateString();
28 | }
29 |
30 | renderDirects() {
31 | const rows = [];
32 |
33 | this.props.directs.forEach((direct, key) => {
34 | rows.push( );
35 | });
36 |
37 | return rows;
38 | }
39 |
40 | render() {
41 | const { formType, handleSubmit, pristine, submitting } = this.props;
42 | const submitText = formType === 'edit' ? 'Update' : 'Create';
43 |
44 | return (
45 |
91 | );
92 | }
93 | }
94 |
95 | MeetingForm.propTypes = {
96 | formType: PropTypes.oneOf(['create', 'edit']).isRequired,
97 | handleSubmit: PropTypes.func.isRequired,
98 | onSubmit: PropTypes.func.isRequired,
99 | directs: PropTypes.object.isRequired,
100 | pristine: PropTypes.bool.isRequired,
101 | submitting: PropTypes.bool.isRequired,
102 | };
103 |
104 | export default reduxForm({ form: 'meeting', validate })(MeetingForm);
105 |
--------------------------------------------------------------------------------
/src/components/meetings/MeetingEdit.jsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import PropTypes from 'prop-types';
3 | import { isDirty, hasSubmitSucceeded } from 'redux-form';
4 | import { connect } from 'react-redux';
5 | import RaisedButton from 'material-ui/RaisedButton';
6 | import { withRouter, browserHistory } from 'react-router';
7 | import MeetingForm from './MeetingForm';
8 |
9 | import meetingActions from '../../actions/meetings';
10 |
11 | class MeetingEdit extends Component {
12 | constructor() {
13 | super();
14 | this.onDelete = this.onDelete.bind(this);
15 | this.onSubmit = this.onSubmit.bind(this);
16 | this.warnIfUnsavedChanges = this.warnIfUnsavedChanges.bind(this);
17 | }
18 |
19 | componentDidMount() {
20 | this.props.router.setRouteLeaveHook(
21 | this.props.route,
22 | this.warnIfUnsavedChanges,
23 | );
24 | window.onbeforeunload = () => this.warnIfUnsavedChanges();
25 | }
26 |
27 | componentDidUpdate(prevProps) {
28 | if (this.props.route.path !== prevProps.route.path) {
29 | this.props.router.setRouteLeaveHook(
30 | this.props.route,
31 | this.warnIfUnsavedChanges,
32 | );
33 | }
34 | }
35 |
36 | onDelete(event) {
37 | event.preventDefault(); // Fix double touchtap bug
38 | browserHistory.push('/meetings');
39 | this.props.remove(this.props.params.id).then(() => {
40 | });
41 | }
42 |
43 | onSubmit(meeting) {
44 | if (meeting.meetingDate instanceof Date) {
45 | meeting.meetingDateReverse = 0 - meeting.meetingDate;
46 | meeting.meetingDate = meeting.meetingDate.toISOString();
47 | }
48 | this.props.update(this.props.params.id, meeting).then(() => {
49 | browserHistory.goBack();
50 | });
51 | }
52 |
53 | warnIfUnsavedChanges(nextLocation) {
54 | if (this.props.dirty && !this.props.submitted) {
55 | return 'Are you sure you want to leave this page? You have unsaved changes.';
56 | }
57 | }
58 |
59 | render() {
60 | return (
61 |
62 |
66 |
75 |
76 | );
77 | }
78 | }
79 |
80 | MeetingEdit.propTypes = {
81 | update: PropTypes.func.isRequired,
82 | remove: PropTypes.func.isRequired,
83 | params: PropTypes.object.isRequired,
84 | route: PropTypes.object.isRequired,
85 | dirty: PropTypes.bool.isRequired,
86 | submitted: PropTypes.bool.isRequired,
87 | router: PropTypes.shape({
88 | setRouteLeaveHook: PropTypes.func,
89 | }).isRequired,
90 | };
91 |
92 | const mapStateToProps = (state) => {
93 | const meeting = state.meetings.activeMeeting;
94 | const initialValues = {
95 | ...meeting,
96 | meetingDate: meeting.meetingDate ? new Date(meeting.meetingDate) : null,
97 | };
98 | return {
99 | initialValues,
100 | formType: 'edit',
101 | dirty: isDirty('meeting')(state),
102 | submitted: hasSubmitSucceeded('meeting')(state),
103 | error: state.meetings.error,
104 | directs: state.directs.list,
105 | };
106 | };
107 |
108 | const mapDispatchToProps = {
109 | update: meetingActions.update,
110 | remove: meetingActions.remove,
111 | };
112 |
113 | export default connect(mapStateToProps, mapDispatchToProps)(withRouter(MeetingEdit));
114 |
--------------------------------------------------------------------------------
/src/components/followUps/FollowUpEdit.jsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import PropTypes from 'prop-types';
3 | import { isDirty, hasSubmitSucceeded } from 'redux-form';
4 | import { connect } from 'react-redux';
5 | import RaisedButton from 'material-ui/RaisedButton';
6 | import { withRouter, browserHistory } from 'react-router';
7 | import FollowUpForm from './FollowUpForm';
8 |
9 | import followUpActions from '../../actions/followUps';
10 |
11 | class FollowUpEdit extends Component {
12 | constructor() {
13 | super();
14 | this.onDelete = this.onDelete.bind(this);
15 | this.onSubmit = this.onSubmit.bind(this);
16 | this.warnIfUnsavedChanges = this.warnIfUnsavedChanges.bind(this);
17 | }
18 |
19 | componentDidMount() {
20 | this.props.router.setRouteLeaveHook(
21 | this.props.route,
22 | this.warnIfUnsavedChanges,
23 | );
24 | window.onbeforeunload = () => this.warnIfUnsavedChanges();
25 | }
26 |
27 | componentDidUpdate(prevProps) {
28 | if (this.props.route.path !== prevProps.route.path) {
29 | this.props.router.setRouteLeaveHook(
30 | this.props.route,
31 | this.warnIfUnsavedChanges,
32 | );
33 | }
34 | }
35 |
36 | onDelete(event) {
37 | event.preventDefault(); // Fix double touchtap bug
38 | browserHistory.push('/followUps');
39 | this.props.remove(this.props.params.id).then(() => {
40 | });
41 | }
42 |
43 | onSubmit(followUp) {
44 | if (followUp.followUpDate instanceof Date) {
45 | followUp.followUpDateReverse = 0 - followUp.followUpDate;
46 | followUp.followUpDate = followUp.followUpDate.toISOString();
47 | }
48 |
49 | this.props.update(this.props.params.id, followUp).then(() => {
50 | browserHistory.goBack();
51 | });
52 | }
53 |
54 | warnIfUnsavedChanges(nextLocation) {
55 | if (this.props.dirty && !this.props.submitted) {
56 | return 'Are you sure you want to leave this page? You have unsaved changes.';
57 | }
58 | }
59 |
60 | render() {
61 | return (
62 |
63 |
67 |
76 |
77 | );
78 | }
79 | }
80 |
81 | FollowUpEdit.propTypes = {
82 | update: PropTypes.func.isRequired,
83 | remove: PropTypes.func.isRequired,
84 | params: PropTypes.object.isRequired,
85 | route: PropTypes.object.isRequired,
86 | dirty: PropTypes.bool.isRequired,
87 | submitted: PropTypes.bool.isRequired,
88 | router: PropTypes.shape({
89 | setRouteLeaveHook: PropTypes.func,
90 | }).isRequired,
91 | };
92 |
93 | const mapStateToProps = (state) => {
94 | const followUp = state.followUps.activeFollowUp;
95 | const initialValues = {
96 | ...followUp,
97 | followUpDate: new Date(followUp.followUpDate),
98 | };
99 | return {
100 | initialValues,
101 | formType: 'edit',
102 | dirty: isDirty('followUp')(state),
103 | submitted: hasSubmitSucceeded('followUp')(state),
104 | error: state.followUps.error,
105 | directs: state.directs.list,
106 | };
107 | };
108 |
109 | const mapDispatchToProps = {
110 | update: followUpActions.update,
111 | remove: followUpActions.remove,
112 | };
113 |
114 | export default connect(mapStateToProps, mapDispatchToProps)(withRouter(FollowUpEdit));
115 |
--------------------------------------------------------------------------------
/src/components/common/crud-dialog-box/CrudDialogBox.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import Dialog from 'material-ui/Dialog';
3 | import PropTypes from 'prop-types';
4 |
5 | import AddItemBtn from './AddItemBtn';
6 |
7 | const propTypes = {
8 | title: PropTypes.string.isRequired,
9 | openDialog: PropTypes.bool.isRequired,
10 | handleCloseDialog: PropTypes.func.isRequired,
11 | ListComponent: PropTypes.func.isRequired,
12 | onDeleteItem: PropTypes.func.isRequired,
13 | FormComponent: PropTypes.func.isRequired,
14 | submitForm: PropTypes.func.isRequired,
15 | };
16 |
17 | /**
18 | * @class CrudDialogBox
19 | * @extends React.Component
20 | * @description Render component
21 | */
22 | class CrudDialogBox extends Component {
23 |
24 | state = {
25 | showForm: false,
26 | clickedItem: null,
27 | };
28 |
29 | setDialogPropsForList = () => {
30 | return {
31 | title: `${this.props.title}s`,
32 | actions: ,
33 | actionsContainerStyle: { padding: 20, textAlign: 'center' },
34 | };
35 | };
36 |
37 | setDialogPropsForForm = () => ({
38 | title: `${this.state.clickedItem ? 'Update' : 'Add'} ${this.props.title}`,
39 | actions: null,
40 | actionsContainerStyle: {},
41 | });
42 |
43 | handleAddItem = () => {
44 | this.setState({ showForm: true });
45 | };
46 |
47 | handleClose = () => {
48 | this.hideForm();
49 | this.props.handleCloseDialog();
50 | };
51 |
52 | hideForm = () => {
53 | this.setState({ showForm: false, clickedItem: null });
54 | };
55 |
56 | handleFormSubmit = (data) => {
57 | this.props.submitForm(data);
58 | this.hideForm();
59 | };
60 |
61 |
62 | handleClickOnItem = (data) => {
63 | this.setState({
64 | showForm: true,
65 | clickedItem: data,
66 | });
67 | };
68 |
69 | handleOnDeleteItem = (id) => {
70 | if (window.confirm(`Delete ${this.props.title}?`)) {
71 | this.props.onDeleteItem(id);
72 | }
73 | };
74 |
75 | renderList = () => {
76 | const { ListComponent, ...rest } = this.props;
77 | return (
78 | );
83 | };
84 |
85 | renderForm = () => {
86 | const { clickedItem } = this.state;
87 | const { FormComponent } = this.props;
88 | return (
89 |
94 | );
95 | };
96 |
97 | /**
98 | * @description render
99 | * @return {Object} JSX HTML Content
100 | */
101 | render() {
102 | const { showForm } = this.state;
103 | const { openDialog } = this.props;
104 | const dialogProps = showForm ? this.setDialogPropsForForm() : this.setDialogPropsForList();
105 | const list = this.renderList();
106 | const form = this.renderForm();
107 |
108 | const customContentStyle = {
109 | width: '90%',
110 | maxWidth: '600',
111 | };
112 |
113 | return (
114 |
122 | {!showForm && list}
123 | {showForm && form}
124 |
125 | );
126 | }
127 | }
128 |
129 | CrudDialogBox.propTypes = propTypes;
130 |
131 | export default CrudDialogBox;
132 |
--------------------------------------------------------------------------------
/src/actions/auth.js:
--------------------------------------------------------------------------------
1 | import * as firebase from 'firebase';
2 | import { push } from 'react-router-redux';
3 | import { browserHistory } from 'react-router';
4 | import locStore from 'store';
5 |
6 | import { firebaseAuth, firebaseDb } from '../firebase/firebase';
7 | import * as types from './types';
8 | import { LOCAL_STORAGE_AUTH_STATE_KEY } from '../constants/general';
9 |
10 | import directActions, { archivedDirects as archivedDirectsAction } from './directs';
11 | import meetingActions, { archivedMeetings as archivedMeetingsAction } from './meetings';
12 | import followUpActions, { archivedFollowUps as archivedFollowUpsAction } from './followUps';
13 | import teamsActions from './teams';
14 | import questions from './questions';
15 | import categoriesQuestions from './categoriesQuestions';
16 |
17 | const recordLogin = (accountData) => {
18 | firebaseDb.ref('accounts').child(accountData.uid).set(accountData)
19 | .catch((error) => {
20 | console.log(error);
21 | });
22 | };
23 |
24 | export function isAuthenticated(state) {
25 | return state.auth.status === types.AUTH_LOGGED_IN;
26 | }
27 |
28 | export function rootPath(authenticated) {
29 | return authenticated ? '/dashboard' : '/';
30 | }
31 |
32 | export const listenToAuth = () => {
33 | return (dispatch, getState) => {
34 | firebaseAuth.onAuthStateChanged((authData) => {
35 | if (authData) {
36 | dispatch({
37 | ...authData.toJSON(),
38 | type: types.AUTH_LOGIN,
39 | });
40 |
41 | // reload on auth update.
42 | directActions.subscribe(dispatch, getState);
43 | archivedDirectsAction.subscribe(dispatch,getState);
44 | meetingActions.subscribe(dispatch, getState);
45 | archivedMeetingsAction.subscribe(dispatch, getState);
46 | followUpActions.subscribe(dispatch, getState);
47 | archivedFollowUpsAction.subscribe(dispatch, getState);
48 | teamsActions.subscribe(dispatch, getState);
49 | questions.subscribe(dispatch, getState);
50 | categoriesQuestions.subscribe(dispatch,getState);
51 | browserHistory.push(rootPath(true));
52 |
53 | recordLogin({
54 | ...authData.toJSON(),
55 | lastLogin: new Date().toISOString(),
56 | });
57 | } else if (getState().auth.status !== types.AUTH_ANONYMOUS) {
58 | dispatch({ type: types.AUTH_LOGOUT });
59 | }
60 | locStore.remove(LOCAL_STORAGE_AUTH_STATE_KEY);
61 | });
62 | };
63 | };
64 |
65 | export const openAuth = () => {
66 | return (dispatch) => {
67 | const provider = new firebase.auth.GoogleAuthProvider();
68 | firebaseAuth.signInWithRedirect(provider);
69 | firebaseAuth.getRedirectResult().then(()=>{
70 | dispatch({ type: types.AUTH_OPEN });
71 | locStore.set(LOCAL_STORAGE_AUTH_STATE_KEY,types.AUTH_AWAITING_RESPONSE);
72 | }).catch((error) => {
73 | const errorMessage = error.message;
74 | console.log(errorMessage);
75 | alert(errorMessage);
76 | dispatch({
77 | type: types.AUTH_ERROR,
78 | error: `Login failed! ${errorMessage}`,
79 | });
80 | dispatch({ type: types.AUTH_LOGOUT });
81 | });
82 | };
83 | };
84 |
85 | export const logoutUser = () => {
86 | return (dispatch, getState) => {
87 | directActions.unsubscribe(dispatch, getState);
88 | firebaseAuth.signOut().then(() => {
89 | dispatch({ type: types.AUTH_LOGOUT });
90 | dispatch(push(rootPath(false)));
91 | });
92 | };
93 | };
94 |
95 | export function requireAuth(nextState, replace) {
96 | return (dispatch, getState) => {
97 | if (!isAuthenticated(getState())) {
98 | replace({
99 | pathname: rootPath(false),
100 | state: { nextPathname: nextState.location.pathname },
101 | });
102 | }
103 | };
104 | }
105 |
--------------------------------------------------------------------------------
/src/components/directs/form/DirectEdit.jsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import PropTypes from 'prop-types';
3 | import { reduxForm } from 'redux-form';
4 | import { connect } from 'react-redux';
5 | import { browserHistory } from 'react-router';
6 | import RaisedButton from 'material-ui/RaisedButton';
7 | import { teal400, white } from 'material-ui/styles/colors';
8 |
9 | import DirectForm, { validate } from './DirectForm';
10 | import directActions, { archivedDirects } from '../../../actions/directs';
11 | import followUpActions, { archivedFollowUps } from '../../../actions/followUps';
12 | import meetingActions, { archivedMeetings } from '../../../actions/meetings';
13 | import { getTeamsArray } from '../../../selectors/teams';
14 |
15 | class DirectEdit extends Component {
16 | constructor() {
17 | super();
18 | this.onDelete = this.onDelete.bind(this);
19 | this.onSubmit = this.onSubmit.bind(this);
20 | }
21 |
22 | onDelete(event) {
23 | event.preventDefault(); // Fix double touchtap bug
24 | if (window.confirm('Delete the direct?')) {
25 | browserHistory.push('/directs');
26 | this.props.remove(this.props.params.id).then(() => {
27 | this.props.followUpsRemoveEqualTo('directKey', this.props.params.id);
28 | this.props.meetingsRemoveEqualTo('directKey', this.props.params.id);
29 | });
30 | }
31 | }
32 |
33 | onSubmit(direct) {
34 | this.props.update(this.props.params.id, direct).then(() => {
35 | browserHistory.push('/directs');
36 | });
37 | }
38 |
39 | onArchived = () => {
40 | if (window.confirm('Archive the direct?')) {
41 | this.props.onMoveTo(this.props.params.id, archivedDirects)
42 | .then(() => {
43 | this.props.meetingsMoveEqualTo('directKey', this.props.params.id, archivedMeetings);
44 | this.props.followUpsMoveEqualTo('directKey', this.props.params.id, archivedFollowUps);
45 | });
46 | browserHistory.push('/directs');
47 | }
48 | }
49 |
50 | render() {
51 | return (
52 |
53 |
57 |
66 |
76 |
77 | );
78 | }
79 | }
80 |
81 | DirectEdit.propTypes = {
82 | update: PropTypes.func.isRequired,
83 | remove: PropTypes.func.isRequired,
84 | params: PropTypes.object.isRequired,
85 | };
86 |
87 | const mapStateToProps = (state) => {
88 | const direct = state.directs.activeDirect;
89 | const initialValues = { ...direct,
90 | startDate: direct.startDate ? new Date(direct.startDate) : null,
91 | };
92 | return {
93 | initialValues,
94 | formType: 'edit',
95 | error: state.directs.error,
96 | teams: getTeamsArray(state),
97 | };
98 | };
99 |
100 | const mapDispatchToProps = {
101 | update: directActions.update,
102 | remove: directActions.remove,
103 | followUpsRemoveEqualTo: followUpActions.removeEqualTo,
104 | meetingsRemoveEqualTo: meetingActions.removeEqualTo,
105 | onMoveTo: directActions.moveTo,
106 | followUpsMoveEqualTo: followUpActions.moveEqualTo,
107 | meetingsMoveEqualTo: meetingActions.moveEqualTo,
108 | };
109 |
110 | export default connect(
111 | mapStateToProps,
112 | mapDispatchToProps,
113 | )(reduxForm({ form: 'direct', validate })(DirectEdit));
114 |
--------------------------------------------------------------------------------
/src/components/directs/form/DirectForm.jsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import PropTypes from 'prop-types';
3 | import { Field } from 'redux-form';
4 | import {
5 | DatePicker,
6 | TextField,
7 | } from 'redux-form-material-ui';
8 | import RaisedButton from 'material-ui/RaisedButton';
9 | import isMobilePhone from 'validator/lib/isMobilePhone';
10 |
11 | import TeamsDropDownField from '../../teams/TeamsDropDownField';
12 | import DirectColorPicker from './DirectColorPicker';
13 |
14 | const isValidPhone = (phone) => {
15 | return isMobilePhone(phone.replace(/-/g, ''), 'en-US');
16 | };
17 |
18 | export const validate = (values) => {
19 | const errors = {};
20 | const requiredFields = ['name'];
21 | requiredFields.forEach((field) => {
22 | if (!values[field]) {
23 | errors[field] = 'Required';
24 | }
25 | });
26 | const {phone} = values;
27 | if (phone && !isValidPhone(phone)) {
28 | errors.phone = 'Invalid phone number.';
29 | }
30 | return errors;
31 | };
32 |
33 | export default class DirectForm extends Component {
34 | constructor() {
35 | super();
36 | this.onSubmit = this.onSubmit.bind(this);
37 | }
38 |
39 | componentDidMount() {
40 | this.refs.name // the Field
41 | .getRenderedComponent() // on Field, returns ReduxFormMaterialUITextField
42 | .getRenderedComponent() // on ReduxFormMaterialUITextField, returns TextField
43 | .focus(); // on TextField
44 | }
45 |
46 | onSubmit(direct) {
47 | this.props.onSubmit(direct);
48 | }
49 |
50 | formatDate(date) {
51 | return date.toLocaleDateString();
52 | }
53 |
54 | render() {
55 | const {formType, handleSubmit, pristine, submitting, teams} = this.props;
56 | const submitText = formType === 'edit' ? 'Update' : 'Create';
57 | return (
58 |
116 | );
117 | }
118 | }
119 |
120 | DirectForm.propTypes = {
121 | formType: PropTypes.oneOf(['create', 'edit']),
122 | handleSubmit: PropTypes.func,
123 | onSubmit: PropTypes.func,
124 | };
125 |
--------------------------------------------------------------------------------
/src/routes.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Route, IndexRoute } from 'react-router';
3 |
4 | import { requireAuth } from './actions/auth';
5 |
6 | import App from './components/App';
7 | import MeetingShow from './components/meetings/MeetingShow';
8 | import MeetingList from './components/meetings/MeetingList';
9 | import MeetingNew from './components/meetings/MeetingNew';
10 | import MeetingEdit from './components/meetings/MeetingEdit';
11 | import DirectShow from './components/directs/single/DirectShow';
12 | import DirectHome from './components/directs/DirectHome';
13 | import DirectNew from './components/directs/form/DirectNew';
14 | import DirectEdit from './components/directs/form/DirectEdit';
15 | import FollowUpShow from './components/followUps/FollowUpShow';
16 | import FollowUpList from './components/followUps/FollowUpList';
17 | import FollowUpNew from './components/followUps/FollowUpNew';
18 | import FollowUpEdit from './components/followUps/FollowUpEdit';
19 |
20 |
21 | import Home from './components/Home';
22 | import Dashboard from './components/Dashboard';
23 | import About from './components/About';
24 | import AboutMe from './components/AboutMe';
25 | import Features from './components/Features';
26 | import Account from './components/Account';
27 | import Terms from './components/Terms';
28 | import Privacy from './components/Privacy';
29 |
30 | import NotFound from './components/404';
31 |
32 | import SetTextHOC from './HOCs/SetTextHOC';
33 | import { ARCHIVED_URL_SUFFIX } from './constants/general';
34 |
35 | export default function Routes(store) {
36 | const checkAuth = (nextState, replace) => {
37 | store.dispatch(requireAuth(nextState, replace));
38 | };
39 |
40 | return (
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 | );
76 | }
77 |
--------------------------------------------------------------------------------
/src/components/Dashboard.jsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import { connect } from 'react-redux';
3 | import { Link } from 'react-router';
4 | import Subheader from 'material-ui/Subheader';
5 | import {
6 | List,
7 | ListItem,
8 | } from 'material-ui/List';
9 | import FloatingActionButton from 'material-ui/FloatingActionButton';
10 | import MeetingIcon from 'material-ui/svg-icons/action/speaker-notes';
11 | import FollowUpIcon from 'material-ui/svg-icons/action/assignment';
12 | import DirectIcon from 'material-ui/svg-icons/social/person-add';
13 | import { lightBlue500, cyan500 } from 'material-ui/styles/colors';
14 |
15 | import MeetingItem from './meetings/MeetingItem';
16 | import FollowUpItem from './followUps/FollowUpItem';
17 | import ShowLoaderHOC from '../HOCs/ShowLoaderHOC';
18 |
19 | export class Dashboard extends Component {
20 |
21 | renderMeetings() {
22 | const rows = [];
23 | if (this.props.meetings && this.props.meetings.size > 0) {
24 | this.props.meetings.forEach((meeting, key) => {
25 | rows.push(
26 | ,
31 | );
32 | });
33 | } else {
34 | rows.push(
35 | ,
39 | );
40 | }
41 | return rows.slice(0, 5);
42 | }
43 |
44 | renderFollowUps() {
45 | const rows = [];
46 | if (this.props.followUps && this.props.followUps.size > 0) {
47 | this.props.followUps.forEach((followUp, key) => {
48 | if (followUp.completed) {
49 | return;
50 | }
51 | rows.push(
52 | ,
57 | );
58 | });
59 | }
60 | if (rows.length === 0) {
61 | rows.push(
62 | ,
66 | );
67 | }
68 | return rows.slice(0, 5);
69 | }
70 |
71 | render() {
72 | const meetingButtonStyle = {
73 | margin: 0,
74 | top: 76,
75 | right: 20,
76 | bottom: 'auto',
77 | left: 'auto',
78 | position: 'fixed',
79 | zIndex: 1,
80 | };
81 | const followupButtonStyle = {
82 | margin: 0,
83 | top: 150,
84 | right: 20,
85 | bottom: 'auto',
86 | left: 'auto',
87 | position: 'fixed',
88 | zIndex: 1,
89 | };
90 | const directButtonStyle = {
91 | margin: 0,
92 | top: 225,
93 | right: 20,
94 | bottom: 'auto',
95 | left: 'auto',
96 | position: 'fixed',
97 | zIndex: 1,
98 | };
99 |
100 | return (
101 |
102 |
103 | Recent Meetings
104 | {this.renderMeetings()}
105 |
106 |
107 | Pending Follow Ups
108 | {this.renderFollowUps()}
109 |
110 | }
113 | backgroundColor={lightBlue500}
114 | >
115 |
116 |
117 | }
120 | backgroundColor={cyan500}
121 | >
122 |
123 |
124 | }
127 | >
128 |
129 |
130 |
131 | );
132 | }
133 | }
134 |
135 | const mapStateToProps = (state) => {
136 | return {
137 | followUps: state.followUps.list,
138 | meetings: state.meetings.list,
139 | loadingData: (state.meetings.loading || state.followUps.loading),
140 | };
141 | };
142 |
143 | export default connect(mapStateToProps)(
144 | ShowLoaderHOC('loadingData', true)(Dashboard),
145 | );
146 |
--------------------------------------------------------------------------------