├── 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 |
13 |
14 |
15 |
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 |
16 | 17 | 18 | 19 | 20 |
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 |
19 |
20 |
21 |
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 | avatar 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 |
41 | 48 | 53 | 56 |
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 |
42 |
43 | 48 |
49 | 54 | 55 | 59 |
60 | 61 |
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 |
40 |
41 | 48 |
49 | 54 | 55 | 59 |
60 | 61 |
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 | meeting screenshot 19 | meeting edit screenshot 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 | directs team screenshot 49 |
50 |
51 |
52 |
53 | followUp edit screenshot 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