├── app ├── helpers │ ├── .gitkeep │ ├── validation.js │ ├── queryParams.js │ ├── url.js │ └── constants.js ├── entry │ ├── styles.css │ └── index.js ├── pages │ ├── Static │ │ ├── styles.css │ │ ├── ComingSoon.js │ │ ├── PageNotFound.js │ │ ├── AboutUs.jsx │ │ └── CodeOfConduct.js │ ├── Onboarding │ │ ├── index.js │ │ ├── styles.css │ │ ├── Social │ │ │ └── Social.js │ │ ├── EmailSettings │ │ │ └── EmailSettings.js │ │ └── Work │ │ │ └── Work.js │ ├── Account │ │ ├── index.js │ │ ├── styles.css │ │ ├── AccountFormContainer.jsx │ │ ├── ResetPassword.js │ │ ├── Login.js │ │ ├── ConfirmResetPassword.js │ │ └── Register.js │ ├── Page │ │ └── Page.js │ ├── EditProfile │ │ ├── About │ │ │ └── styles.css │ │ ├── EmailSettings │ │ │ ├── styles.css │ │ │ └── EmailSettings.js │ │ ├── Account │ │ │ └── styles.css │ │ ├── styles.css │ │ ├── FormComponents │ │ │ ├── ImageUpload │ │ │ │ ├── styles.css │ │ │ │ └── ImageUpload.js │ │ │ └── TopicSelector │ │ │ │ └── TopicSelector.js │ │ ├── SideBar.js │ │ ├── Talks │ │ │ ├── style.css │ │ │ └── Talks.js │ │ └── EditProfile.js │ ├── Authenticate │ │ └── Authenticate.js │ ├── Speaker │ │ ├── components │ │ │ ├── SpeakerInfo.js │ │ │ ├── Topics.js │ │ │ ├── FeaturedTalks.js │ │ │ ├── SpeakerCard.js │ │ │ └── MessageSpeakerForm.js │ │ ├── styles.css │ │ └── Speaker.js │ ├── index.js │ ├── Organization │ │ └── Organization.js │ └── Home │ │ ├── components │ │ ├── SpeakerList.js │ │ ├── SpeakerCard.js │ │ ├── MobileSearch.js │ │ ├── MobileFilters.js │ │ └── Filters.js │ │ ├── styles.css │ │ └── Home.js ├── assets │ ├── og-img.png │ └── Tribalscale.svg ├── config │ ├── Main │ │ ├── styles.css │ │ └── MainContainer.js │ └── routes.js ├── common │ ├── Footer │ │ ├── Footer.js │ │ ├── MiniFooter.js │ │ ├── styles.css │ │ └── FullFooter.js │ ├── FormField.js │ ├── Navigation │ │ ├── ButtonMenu.js │ │ ├── styles.css │ │ ├── MenuDropdown.js │ │ ├── SearchField.js │ │ └── Navigation.js │ ├── styles.css │ ├── Notification │ │ └── Notification.jsx │ ├── StyledButton.js │ └── Banner.js ├── redux │ ├── store.js │ ├── modules │ │ ├── notification.js │ │ ├── authentication.js │ │ ├── action_template.js │ │ ├── location.js │ │ ├── subscriptionGroup.js │ │ ├── contactForm.js │ │ ├── topic.js │ │ ├── speaker.js │ │ ├── profile.js │ │ ├── __template__.js │ │ └── featuredTalk.js │ └── reducers.js └── sharedStyles │ ├── cssVariables.js │ └── styles.css ├── Procfile ├── .gitignore ├── prettier.config.js ├── .babelrc ├── static.json ├── app.json ├── ISSUE_TEMPLATE.md ├── CONTRIBUTING.md ├── package.json ├── CONDUCT.md ├── webpack.config.babel.js └── README.md /app/helpers/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/entry/styles.css: -------------------------------------------------------------------------------- 1 | .container { 2 | width: 100%; 3 | padding: 30px; 4 | } 5 | -------------------------------------------------------------------------------- /app/pages/Static/styles.css: -------------------------------------------------------------------------------- 1 | .header { 2 | padding: 2rem; 3 | text-align: center; 4 | } -------------------------------------------------------------------------------- /app/assets/og-img.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/womenandcolor/women-and-color-frontend/HEAD/app/assets/og-img.png -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | # The default procfile from the heroku static buildpack 2 | # @see https://github.com/heroku/heroku-buildpack-static 3 | web: bin/boot 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # dependencies 2 | node_modules/ 3 | dist/ 4 | 5 | # IDE 6 | .idea 7 | 8 | # misc 9 | .DS_Store 10 | yarn-error.log 11 | npm-debug.log 12 | -------------------------------------------------------------------------------- /prettier.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | bracketSpacing: true, 3 | singleQuote: true, 4 | jsxBracketSameLine: false, 5 | trailingComma: 'es5', 6 | printWidth: 80, 7 | arrowParens: 'avoid', 8 | }; 9 | -------------------------------------------------------------------------------- /app/pages/Onboarding/index.js: -------------------------------------------------------------------------------- 1 | export Profile from './Profile/Profile'; 2 | export Social from './Social/Social'; 3 | export Work from './Work/Work'; 4 | export EmailSettings from './EmailSettings/EmailSettings'; 5 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | presets: [ 3 | 'react', 4 | 'es2015', 5 | 'stage-0' 6 | ], 7 | env: { 8 | start: { 9 | presets: [ 10 | "react-hmre" 11 | ] 12 | } 13 | } 14 | } 15 | 16 | -------------------------------------------------------------------------------- /app/entry/index.js: -------------------------------------------------------------------------------- 1 | import "babel-polyfill"; 2 | 3 | import ReactDOM from 'react-dom' 4 | import routes from 'appConfig/routes' 5 | 6 | 7 | ReactDOM.render( 8 | routes, 9 | document.getElementById('app') 10 | ) 11 | -------------------------------------------------------------------------------- /app/helpers/validation.js: -------------------------------------------------------------------------------- 1 | import { isNil, not, pipe, isEmpty, allPass } from 'ramda'; 2 | 3 | export const isNotNil = pipe(isNil, not); 4 | export const isNotEmpty = pipe(isEmpty, not); 5 | export const hasValue = allPass([isNotNil, isNotEmpty]); 6 | -------------------------------------------------------------------------------- /static.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": "dist/", 3 | "routes": { 4 | "/**": "index.html" 5 | }, 6 | "https_only": true, 7 | "headers": { 8 | "/**": { 9 | "Strict-Transport-Security": "max-age=7776000" 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /app/pages/Account/index.js: -------------------------------------------------------------------------------- 1 | export Register from './Register' 2 | export Login from './Login' 3 | export ResetPassword from './ResetPassword' 4 | export ConfirmResetPassword from './ConfirmResetPassword' 5 | export AccountFormContainer from './AccountFormContainer' -------------------------------------------------------------------------------- /app/config/Main/styles.css: -------------------------------------------------------------------------------- 1 | .container { 2 | box-sizing: border-box; /* so that a child with width:100% will not overflow */ 3 | min-height: 100vh; 4 | display: flex; 5 | flex-direction: column; 6 | } 7 | 8 | .innerContainer { 9 | padding-top: var(--navbar-height); 10 | flex-grow: 1; 11 | } 12 | -------------------------------------------------------------------------------- /app/pages/Page/Page.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | 3 | 4 | class Page extends Component { 5 | render () { 6 | return ( 7 |
Custom Page
8 | ) 9 | } 10 | } 11 | 12 | class PageContainer extends Component { 13 | render () { 14 | return ( 15 | 16 | ) 17 | } 18 | } 19 | 20 | export default PageContainer 21 | -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "women-and-color-frontend", 3 | "description": "A website for finding talented women and people of color to speak at your event", 4 | "scripts": { 5 | }, 6 | "env": { 7 | }, 8 | "formation": { 9 | }, 10 | "addons": [ 11 | 12 | ], 13 | "buildpacks": [ 14 | { 15 | "url": "https://github.com/mars/create-react-app-buildpack.git" 16 | } 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /app/pages/EditProfile/About/styles.css: -------------------------------------------------------------------------------- 1 | /* about */ 2 | 3 | .header { 4 | font-size: 2rem; 5 | color: var(--color-grey-dark); 6 | font-weight: var(--font-weight-light); 7 | margin: 0; 8 | } 9 | 10 | .section { 11 | padding: 0 1rem 2rem 1rem; 12 | border-bottom: 1px solid var(--color-grey-light); 13 | margin-bottom: 2rem; 14 | } 15 | 16 | .sectionBorderless { 17 | padding: 0 1rem 2rem 1rem; 18 | } 19 | -------------------------------------------------------------------------------- /app/pages/EditProfile/EmailSettings/styles.css: -------------------------------------------------------------------------------- 1 | /* email settings */ 2 | 3 | .header { 4 | font-size: 2rem; 5 | color: var(--color-grey-dark); 6 | font-weight: var(--font-weight-light); 7 | margin: 0; 8 | } 9 | 10 | .section { 11 | padding: 0 1rem 2rem 1rem; 12 | border-bottom: 1px solid var(--color-grey-light); 13 | margin-bottom: 2rem; 14 | } 15 | 16 | .sectionBorderless { 17 | padding: 0 1rem 2rem 1rem; 18 | } 19 | -------------------------------------------------------------------------------- /app/pages/Static/ComingSoon.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Grid from 'material-ui/Grid'; 3 | import css from './styles.css' 4 | 5 | const ComingSoon = () => { 6 | return( 7 | 8 | 9 |
10 |

Coming soon!

11 |
12 |
13 |
14 | ) 15 | } 16 | 17 | export default ComingSoon; -------------------------------------------------------------------------------- /app/pages/Authenticate/Authenticate.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | 3 | class Authenticate extends Component { 4 | render () { 5 | return ( 6 |
7 |

Authentication

8 |
9 | ) 10 | } 11 | }; 12 | 13 | class AuthenticateContainer extends Component { 14 | render () { 15 | return ( 16 | 17 | ) 18 | } 19 | }; 20 | 21 | export default AuthenticateContainer 22 | -------------------------------------------------------------------------------- /app/pages/EditProfile/Account/styles.css: -------------------------------------------------------------------------------- 1 | .header { 2 | font-size: 2rem; 3 | color: var(--color-grey-dark); 4 | font-weight: var(--font-weight-light); 5 | margin: 0; 6 | } 7 | 8 | .changePasswordLink { 9 | text-decoration: none; 10 | } 11 | 12 | .section { 13 | padding: 0 1rem 2rem 1rem; 14 | border-bottom: 1px solid var(--color-grey-light); 15 | margin-bottom: 2rem; 16 | } 17 | 18 | .sectionBorderless { 19 | padding: 0 1rem 2rem 1rem; 20 | } 21 | -------------------------------------------------------------------------------- /app/pages/Speaker/components/SpeakerInfo.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react'; 2 | 3 | // App 4 | import css from '../styles.css'; 5 | 6 | const SpeakerInfo = ({ speaker }) => { 7 | const title = `About ${speaker.first_name}`; 8 | return ( 9 |
10 |

{title}

11 |

{speaker.description}

12 |
13 | ); 14 | }; 15 | 16 | export default SpeakerInfo; 17 | -------------------------------------------------------------------------------- /app/pages/EditProfile/styles.css: -------------------------------------------------------------------------------- 1 | /* side bar */ 2 | 3 | .sidebarTitle { 4 | color: var(--color-grey-dark); 5 | font-size: 0.9rem; 6 | font-weight: bold; 7 | border-bottom: 1px solid var(--color-grey-light); 8 | margin-bottom: 0.5rem; 9 | } 10 | 11 | .sidebarObjectSelected { 12 | color: #283ca7 !important; 13 | background-color: #e5e8f4 !important; /* oh I need to find a better way than this */ 14 | } 15 | 16 | /* main content */ 17 | .editProfileContainer { 18 | padding-top: 2rem; 19 | } 20 | -------------------------------------------------------------------------------- /ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## Design 2 | ![Image of section](http://placekitten.com/g/600/400) 3 | [Link to Invision](https://projects.invisionapp.com/share/YJCPM4TQ6#/screens/245103731) 4 | 5 | ## Description 6 | Provide a short description of what is needed to complete this. 7 | 8 | ## Work Needed 9 | - [ ] [Link Backend Issue](https://github.com/CivicTechTO/women-and-color-frontend/issues/000) 10 | - [ ] Works on mobile 11 | - [ ] Works on desktop 12 | - [ ] Wrote some tests 13 | - [ ] (Some Functionality that is expected) 14 | -------------------------------------------------------------------------------- /app/common/Footer/Footer.js: -------------------------------------------------------------------------------- 1 | // NPM 2 | import React from 'react'; 3 | 4 | // APP 5 | import FullFooter from './FullFooter' 6 | import MiniFooter from './MiniFooter' 7 | 8 | const ACCOUNT_PAGES = [ 9 | '/login', 10 | '/register', 11 | '/reset-password', 12 | '/get-started/profile', 13 | '/get-started/work', 14 | '/get-started/social', 15 | ] 16 | 17 | const Footer = props => { 18 | if (ACCOUNT_PAGES.indexOf(props.location.pathname) > -1) { 19 | return 20 | } 21 | 22 | return 23 | } 24 | 25 | 26 | export default Footer; 27 | -------------------------------------------------------------------------------- /app/pages/EditProfile/FormComponents/ImageUpload/styles.css: -------------------------------------------------------------------------------- 1 | .fileInput { 2 | display: none; 3 | } 4 | 5 | .photo { 6 | display: flex; 7 | justify-content: center; 8 | overflow: hidden; 9 | margin-bottom: 1rem; 10 | flex-direction: column; 11 | object-fit: cover; 12 | } 13 | 14 | .photo img { 15 | height: 10rem; 16 | width: 10rem; 17 | border-radius: 50%; 18 | margin-bottom: 0.5rem; 19 | object-fit: cover; 20 | } 21 | 22 | .imageError { 23 | font-size: 0.8rem; 24 | color: red; 25 | } 26 | 27 | .imageUpload { 28 | margin-top: 1rem; 29 | margin-bottom: 1rem; 30 | } -------------------------------------------------------------------------------- /app/pages/Account/styles.css: -------------------------------------------------------------------------------- 1 | .registrationForm { 2 | margin-top: 2rem; 3 | padding: 30px; 4 | border: 1px solid var(--color-grey-light); 5 | background-color: var(--color-inverted-light); 6 | border-radius: 10px; 7 | } 8 | 9 | .actions { 10 | padding-top: 2rem; 11 | } 12 | 13 | .formControl { 14 | margin: 2em; 15 | } 16 | 17 | .loginLink { 18 | text-align: center; 19 | } 20 | 21 | .buttonLabel { 22 | color: var(--color-inverted-light); 23 | } 24 | 25 | .title { 26 | composes: h1 from 'sharedStyles/styles.css'; 27 | } 28 | 29 | .loginRegisterPrompt { 30 | text-align: center; 31 | } -------------------------------------------------------------------------------- /app/common/FormField.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react' 2 | import { FormControl } from 'material-ui/Form'; 3 | import { withStyles } from 'material-ui/styles'; 4 | 5 | const styles = (theme) => ({ 6 | common: { 7 | marginTop: '1em', 8 | marginBottom: '1em' 9 | } 10 | }) 11 | 12 | const FormField = (props) => { 13 | const classes = props.classes; 14 | return( 15 | 16 | {props.children} 17 | 18 | ) 19 | } 20 | 21 | export default withStyles(styles)(FormField); 22 | -------------------------------------------------------------------------------- /app/pages/Onboarding/styles.css: -------------------------------------------------------------------------------- 1 | .registrationForm { 2 | padding: 30px; 3 | margin: 2rem auto; 4 | border: 1px solid var(--color-grey-light); 5 | background-color: var(--color-inverted-light); 6 | border-radius: 10px; 7 | max-width: 500px; 8 | box-sizing: border-box; 9 | } 10 | 11 | .registrationFormHeader { 12 | font-size: 2.5rem; 13 | font-weight: var(--font-weight-regular); 14 | line-height: 1.2; 15 | margin-top: 0; 16 | margin-bottom: 0.5rem; 17 | } 18 | 19 | .formControl { 20 | margin: 2em; 21 | } 22 | 23 | .loginLink { 24 | text-align: center; 25 | } 26 | 27 | .buttonLabel { 28 | color: var(--color-inverted-light); 29 | } -------------------------------------------------------------------------------- /app/common/Navigation/ButtonMenu.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import StyledButton from 'appCommon/StyledButton'; 3 | 4 | const ButtonMenu = props => { 5 | return ( 6 |
7 | { 8 | props.authed && Log out 9 | } 10 | {props.menuItems.map(item => { 11 | const link = `/#${item.slug}`; 12 | return ( 13 | 14 | {item.title} 15 | 16 | ); 17 | })} 18 |
19 | ); 20 | }; 21 | 22 | export default ButtonMenu; 23 | -------------------------------------------------------------------------------- /app/pages/index.js: -------------------------------------------------------------------------------- 1 | export Authenticate from './Authenticate/Authenticate'; 2 | export Home from './Home/Home'; 3 | export Page from './Page/Page'; 4 | export * from './Account/'; 5 | export * from './Onboarding'; 6 | export Speaker from './Speaker/Speaker'; 7 | export Organization from './Organization/Organization'; 8 | export EditProfile from './EditProfile/EditProfile'; 9 | export ComingSoon from './Static/ComingSoon'; 10 | export AboutUs from './Static/AboutUs'; 11 | export Privacy from './Static/Privacy'; 12 | export Terms from './Static/Terms'; 13 | export CodeOfConduct from './Static/CodeOfConduct'; 14 | export PageNotFound from './Static/PageNotFound'; 15 | -------------------------------------------------------------------------------- /app/redux/store.js: -------------------------------------------------------------------------------- 1 | // NPM 2 | import { createStore, applyMiddleware, compose } from "redux"; 3 | import thunk from 'redux-thunk'; 4 | import createHashHistory from 'history/createHashHistory' 5 | import { routerMiddleware } from 'react-router-redux' 6 | 7 | // App 8 | import appReducers from '../redux/reducers'; 9 | 10 | export const history = createHashHistory() 11 | const initialState = { 12 | authentication: { 13 | isLoggedIn: false 14 | } 15 | } 16 | 17 | const store = createStore( 18 | appReducers, 19 | initialState, 20 | compose( 21 | applyMiddleware( 22 | thunk, 23 | routerMiddleware(history), 24 | ), 25 | window.devToolsExtension ? window.devToolsExtension() : f => f 26 | ) 27 | ); 28 | 29 | export default store; 30 | -------------------------------------------------------------------------------- /app/redux/modules/notification.js: -------------------------------------------------------------------------------- 1 | const MODULE_NAME = 'NOTIFICATION'; 2 | 3 | const SHOW_NOTIFICATION = `${MODULE_NAME}/SHOW_NOTIFICATION`; 4 | const HIDE_NOTIFICATION = `${MODULE_NAME}/HIDE_NOTIFICATION`; 5 | 6 | export function showNotification(message) { 7 | return { type: SHOW_NOTIFICATION, message } 8 | } 9 | 10 | export function hideNotification() { 11 | return { type: HIDE_NOTIFICATION } 12 | } 13 | 14 | export const reducer = (state={}, action) => { 15 | switch (action.type) { 16 | case SHOW_NOTIFICATION: 17 | return { 18 | ...state, 19 | message: action.message 20 | } 21 | case HIDE_NOTIFICATION: 22 | return { 23 | ...state, 24 | message: null 25 | } 26 | default: 27 | return state 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /app/pages/Static/PageNotFound.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Grid from 'material-ui/Grid'; 3 | import { Link } from 'react-router-dom'; 4 | import { Helmet } from 'react-helmet'; 5 | 6 | import css from './styles.css' 7 | 8 | const PageNotFound = () => { 9 | return( 10 |
11 | 12 | Page Not Found 13 | 14 | 15 | 16 | 17 |
18 |

There's nothing here :(

19 |

The URL was not found. Let's go back to the home page.

20 |
21 |
22 |
23 |
24 | ) 25 | } 26 | 27 | export default PageNotFound; -------------------------------------------------------------------------------- /app/sharedStyles/cssVariables.js: -------------------------------------------------------------------------------- 1 | const globalCssVars = { 2 | '--background-color-light': '#fafafa', 3 | '--border-radius': '4px', 4 | '--color-primary': '#283ca7', 5 | '--color-secondary': '#E5E8F4', 6 | '--color-grey-dark': '#000000', 7 | '--color-grey': '#757575', 8 | '--color-grey-light': '#e0e0e0', 9 | '--color-grey-pale': '#f5f5f5', 10 | '--color-inverted-light': '#ffffff', 11 | '--font-family-sans-serif': '"Open Sans", sans-serif', 12 | '--font-family-serif': '"Georgia", serif', 13 | '--font-size-base': '16px', 14 | '--font-weight-light': '300', 15 | '--font-weight-regular': '400', 16 | '--font-weight-semibold': '600', 17 | '--font-weight-bold': '700', 18 | '--line-height-base': '24px', 19 | '--padding-horizontal': '16px', 20 | '--padding-vertical': '8px', 21 | '--navbar-height': '64px', 22 | } 23 | 24 | export default globalCssVars; -------------------------------------------------------------------------------- /app/pages/Account/AccountFormContainer.jsx: -------------------------------------------------------------------------------- 1 | // Project 2 | import React, { Component } from 'react'; 3 | import Grid from 'material-ui/Grid'; 4 | import Card from 'material-ui/Card'; 5 | import { withStyles } from 'material-ui/styles'; 6 | 7 | import css from './styles.css'; 8 | 9 | const styles = { 10 | root: { 11 | borderRadius: '8px', 12 | marginTop: '2rem', 13 | padding: '30px', 14 | border: '1px solid var(--color-grey-light)', 15 | backgroundColor: 'var(--color-inverted-light)', 16 | } 17 | }; 18 | 19 | const AccountFormContainer = props => { 20 | return ( 21 | 22 | 23 | 24 | {props.children} 25 | 26 | 27 | 28 | ); 29 | }; 30 | 31 | export default withStyles(styles)(AccountFormContainer); 32 | -------------------------------------------------------------------------------- /app/redux/reducers.js: -------------------------------------------------------------------------------- 1 | // NPM 2 | import { combineReducers } from "redux"; 3 | 4 | // App 5 | import { reducer as user } from './modules/user'; 6 | import { reducer as profile } from './modules/profile'; 7 | import { reducer as location } from './modules/location'; 8 | import { reducer as topic } from './modules/topic'; 9 | import { reducer as authentication } from './modules/authentication'; 10 | import { reducer as notification } from './modules/notification'; 11 | import { reducer as speaker } from './modules/speaker'; 12 | import { reducer as contactForm } from './modules/contactForm' 13 | import { reducer as featuredTalk } from './modules/featuredTalk'; 14 | import { reducer as subscriptionGroup } from './modules/subscriptionGroup'; 15 | 16 | export default combineReducers({ 17 | user, 18 | profile, 19 | location, 20 | topic, 21 | authentication, 22 | notification, 23 | speaker, 24 | contactForm, 25 | featuredTalk, 26 | subscriptionGroup, 27 | }); 28 | -------------------------------------------------------------------------------- /app/pages/EditProfile/SideBar.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Route } from 'react-router-dom'; 3 | import List, { ListItem, ListItemText } from 'material-ui/List'; 4 | import css from './styles.css'; 5 | 6 | const SideBarLink = ({ href, text, isActive }) => ( 7 | 13 | 14 | 15 | ); 16 | 17 | const SideBar = ({ baseUrl, subroutes, activeSubroute }) => ( 18 |
19 |

MENU

20 | 21 | {subroutes.map(subroute => ( 22 | 28 | ))} 29 | 30 |
31 | ); 32 | 33 | export default SideBar; 34 | -------------------------------------------------------------------------------- /app/helpers/queryParams.js: -------------------------------------------------------------------------------- 1 | import { map, compact, uniqBy } from 'lodash'; 2 | import { VALID_PARAMS } from './constants'; 3 | 4 | export const generateQueryString = (opts = { params: {}, display: false }) => { 5 | const apiOnlyParams = ['limit', 'offset']; 6 | const queryParams = map(opts.params, (v, k) => { 7 | if (!!v) { 8 | if (opts.display && (k === 'limit' || k === 'offset' || k === 'append')) { 9 | return null; 10 | } 11 | return `${k}=${v}`; 12 | } 13 | }); 14 | const compacted = compact(queryParams); 15 | const queryString = compacted.join('&'); 16 | 17 | return queryString; 18 | }; 19 | 20 | export const parseQueryString = (queryString, params = {}) => { 21 | VALID_PARAMS.map(paramKey => { 22 | const regex = new RegExp(`${paramKey}=(.+?)(?=&|$)`); 23 | const match = regex.exec(queryString); 24 | if (match) { 25 | const val = match[1]; 26 | params[paramKey] = val; 27 | } 28 | }); 29 | 30 | return params; 31 | }; 32 | -------------------------------------------------------------------------------- /app/redux/modules/authentication.js: -------------------------------------------------------------------------------- 1 | const MODULE_NAME = 'AUTHENTICATION'; 2 | 3 | const USER_LOGIN_SUCCESS = `${MODULE_NAME}/USER_LOGIN_SUCCESS`; 4 | const USER_LOGIN_FAILURE = `${MODULE_NAME}/USER_LOGIN_FAILURE`; 5 | const USER_LOGGED_OUT = `${MODULE_NAME}/USER_LOGGED_OUT`; 6 | 7 | // AUTHENTICATION ------------------------ 8 | export function userLoginSuccess() { 9 | return { type: USER_LOGIN_SUCCESS } 10 | } 11 | 12 | export function userLoginFailure() { 13 | return { type: USER_LOGIN_FAILURE } 14 | } 15 | 16 | export function userLoggedOut() { 17 | return { type: USER_LOGGED_OUT } 18 | } 19 | 20 | 21 | export const reducer = (state={}, action) => { 22 | switch (action.type) { 23 | case USER_LOGIN_SUCCESS: 24 | return { ...state, isLoggedIn: true } 25 | 26 | case USER_LOGIN_FAILURE: 27 | return { ...state, isLoggedIn: false, error: action.error } 28 | 29 | case USER_LOGGED_OUT: 30 | return { ...state, isLoggedIn: false } 31 | 32 | default: 33 | return state 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /app/common/Footer/MiniFooter.js: -------------------------------------------------------------------------------- 1 | // NPM 2 | import React from 'react'; 3 | import Grid from 'material-ui/Grid'; 4 | 5 | // APP 6 | import css from './styles.css'; 7 | 8 | const MiniFooter = () => { 9 | return ( 10 | 26 | ) 27 | } 28 | 29 | 30 | export default MiniFooter; 31 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guidelines 2 | 3 | ## Heroku Review Apps 4 | 5 | [**Review Apps**][review-apps] are a Heroku feature that run the code from GitHub 6 | pull requests in a complete, disposable app. 7 | 8 | We are using review apps, and have enabled _automatic review app creation_ for each pull request. This automation only works when a pull request is made from a feature branch in the main upstream repo, and will not work from a personal fork. (This is a security feature.) 9 | 10 | So if you have push access on the upstream repo, this is a great reason to work from there. When you create a pull request, Heroku will drop a link within the comments, linking to your newly deployed preview app. The app will be updated with each new commit, and will be destroyed when the pull request is merged or closed. 11 | 12 | For an example of the automatic review app process in progress, see [this issue][review-app-example]. 13 | 14 | 15 | [review-apps]: https://devcenter.heroku.com/articles/github-integration-review-apps 16 | [review-app-example]: https://github.com/CivicTechTO/women-and-color-frontend/pull/3 17 | -------------------------------------------------------------------------------- /app/common/styles.css: -------------------------------------------------------------------------------- 1 | .searchButton { 2 | height: 100%; 3 | } 4 | 5 | .banner { 6 | background-color: var(--color-primary); 7 | padding-top: 6rem; 8 | padding-bottom: 6rem; 9 | margin-bottom: 2rem; 10 | background: url(https://s3.ca-central-1.amazonaws.com/womenandcolor/background-image.jpg) no-repeat center center fixed; 11 | background-size: cover; 12 | } 13 | 14 | .headline { 15 | font-size: 2rem; 16 | color: var(--color-inverted-light); 17 | margin-bottom: 0; 18 | margin-top: 0; 19 | font-weight: 100; 20 | line-height: 1.4; 21 | } 22 | 23 | .highlight { 24 | font-weight: 600; 25 | } 26 | 27 | .searchForm { 28 | background-color: var(--color-inverted-light); 29 | padding-top: 1rem; 30 | padding-bottom: 1rem; 31 | margin-top: 2rem; 32 | } 33 | 34 | .searchIcon { 35 | margin-right: 1rem; 36 | } 37 | 38 | @media (max-width: 1059px) { 39 | .banner { 40 | margin-bottom: 0; 41 | } 42 | } 43 | 44 | @media (max-width: 599px) { 45 | .banner { 46 | padding-top: 2rem; 47 | padding-bottom: 2rem; 48 | margin-bottom: 0; 49 | } 50 | 51 | .headline { 52 | font-size: 1.4rem; 53 | } 54 | } -------------------------------------------------------------------------------- /app/pages/EditProfile/Talks/style.css: -------------------------------------------------------------------------------- 1 | /* talks */ 2 | 3 | .header { 4 | font-size: 2rem; 5 | color: var(--color-grey-dark); 6 | font-weight: var(--font-weight-light); 7 | margin: 0; 8 | } 9 | 10 | .sectionBorderless { 11 | padding: 0 1rem 2rem 1rem; 12 | } 13 | 14 | .talkContainer { 15 | display: flex; 16 | justify-content: center; 17 | flex-direction: column; 18 | } 19 | 20 | .talk { 21 | border-radius: 8px; 22 | } 23 | 24 | .talk .formControl { 25 | margin: 0 0 1rem 0; 26 | } 27 | 28 | .talk .talkLabel { 29 | font-size: 1rem; 30 | margin-top: 0; 31 | } 32 | 33 | .addNewTalk { 34 | width: 100%; 35 | } 36 | 37 | .section { 38 | padding: 0 1rem 2rem 1rem; 39 | border-bottom: 1px solid var(--color-grey-light); 40 | margin-bottom: 2rem; 41 | } 42 | 43 | .sectionBorderless { 44 | padding: 0 1rem 2rem 1rem; 45 | } 46 | 47 | .talkCardImage { 48 | height: 12rem; 49 | border-radius: 2px 2px 0 0; 50 | background-color: var(--color-secondary); 51 | display: flex; 52 | align-items: center; 53 | justify-content: center; 54 | } 55 | 56 | .fileInput { 57 | display: none; 58 | } 59 | -------------------------------------------------------------------------------- /app/redux/modules/action_template.js: -------------------------------------------------------------------------------- 1 | export const GetRequest = (action) => `${action}/GET_REQUEST`; 2 | export const GetSuccess = (action) => `${action}/GET_SUCCESS`; 3 | export const GetError = (action) => `${action}/GET_ERROR`; 4 | 5 | export const GetDetailRequest = (action) => `${action}/GET_DETAIL_REQUEST`; 6 | export const GetDetailSuccess = (action) => `${action}/GET_DETAIL_SUCCESS`; 7 | export const GetDetailError = (action) => `${action}/GET_DETAIL_ERROR`; 8 | 9 | export const PostRequest = (action) => `${action}/POST_REQUEST`; 10 | export const PostSuccess = (action) => `${action}/POST_SUCCESS`; 11 | export const PostError = (action) => `${action}/POST_ERROR`; 12 | 13 | export const PutRequest = (action) => `${action}/PUT_REQUEST`; 14 | export const PutSuccess = (action) => `${action}/PUT_SUCCESS`; 15 | export const PutError = (action) => `${action}/PUT_ERROR`; 16 | 17 | export const DeleteRequest = (action) => `${action}/DELETE_REQUEST`; 18 | export const DeleteSuccess = (action) => `${action}/DELETE_SUCCESS`; 19 | export const DeleteError = (action) => `${action}/DELETE_ERROR`; 20 | 21 | export const OnChange = (action) => `${action}/ON_CHANGE`; 22 | -------------------------------------------------------------------------------- /app/helpers/url.js: -------------------------------------------------------------------------------- 1 | import { pipe, replace, join, filter, map, trim, always, ifElse } from 'ramda'; 2 | 3 | import { hasValue, isNil } from './validation'; 4 | 5 | // Anything that is not a-z A-Z 6 | const omittedNameCharacters = /[^a-zA-Z]/g; 7 | 8 | const whitespaceCharacters = /[\s]/g; 9 | 10 | const returnEmptyString = always(''); 11 | 12 | // Remove omitted charaters, replace spaces with dashes, 13 | // return empty string if empty / null / undefined 14 | const sanitizeName = ifElse( 15 | hasValue, 16 | pipe( 17 | trim, 18 | replace(omittedNameCharacters, ''), 19 | replace(whitespaceCharacters, '-') 20 | ), 21 | returnEmptyString 22 | ); 23 | 24 | export const speakerToNamePath = ({ first_name, last_name }) => 25 | pipe(map(sanitizeName), filter(hasValue), join('-'))([first_name, last_name]); 26 | 27 | export const speakerToProfilePath = ({ 28 | basePath = '', 29 | id, 30 | first_name, 31 | last_name, 32 | }) => { 33 | const namePath = speakerToNamePath({ first_name, last_name }); 34 | return `${basePath}/speaker/${id}/${namePath}`; 35 | }; 36 | 37 | 38 | export const ensureAbsoluteUrl = (url) => { 39 | return url.startsWith('http') ? url : `http://${url}`; 40 | } -------------------------------------------------------------------------------- /app/common/Notification/Notification.jsx: -------------------------------------------------------------------------------- 1 | // NPM 2 | import React, { Component } from 'react' 3 | import { connect } from 'react-redux' 4 | import Snackbar from 'material-ui/Snackbar'; 5 | 6 | import { 7 | showNotification, 8 | hideNotification 9 | } from 'appRedux/modules/notification'; 10 | 11 | const Notification = props => { 12 | return( 13 | {props.notification}} 22 | /> 23 | ) 24 | } 25 | 26 | function mapStateToProps(state) { 27 | return { 28 | notification: state.notification.message, 29 | } 30 | } 31 | 32 | function mapDispatchToProps(dispatch, props) { 33 | return { 34 | showNotification: (message) => { 35 | dispatch(showNotification(message)) 36 | }, 37 | hideNotification: () => { 38 | dispatch(hideNotification()) 39 | }, 40 | } 41 | } 42 | 43 | export default connect( 44 | mapStateToProps, 45 | mapDispatchToProps 46 | )(Notification); 47 | -------------------------------------------------------------------------------- /app/pages/Organization/Organization.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | // import { connect } from 'react-redux'; 3 | // import ReactLoading from 'react-loading'; 4 | import Grid from 'material-ui/Grid'; 5 | import { Helmet } from "react-helmet"; 6 | 7 | // App 8 | import Banner from 'appCommon/Banner'; 9 | 10 | 11 | const Organization = props => { 12 | const { speaker } = props; 13 | return ( 14 | 15 | 16 | 17 | 18 | 19 | 20 |

ORGANIZATION DETAILS GO HERE

21 |
22 |
23 |
24 | ); 25 | }; 26 | 27 | class OrganizationContainer extends Component { 28 | constructor(props) { 29 | super(props); 30 | this.state = {}; 31 | } 32 | 33 | componentDidMount() { 34 | const { match: { params: { id, orgName } } } = this.props; 35 | } 36 | 37 | render() { 38 | const { speaker } = this.props; 39 | return( 40 |
41 | 42 | 43 |
44 | ) 45 | } 46 | } 47 | 48 | export default (OrganizationContainer); 49 | -------------------------------------------------------------------------------- /app/helpers/constants.js: -------------------------------------------------------------------------------- 1 | export const registrationFlow = { 2 | registration: { 3 | next: '/get-started/profile', 4 | }, 5 | 6 | profile: { 7 | next: '/get-started/work', 8 | }, 9 | 10 | work: { 11 | next: '/get-started/social', 12 | }, 13 | 14 | social: { 15 | next: '/get-started/email-settings', 16 | }, 17 | 18 | email_settings: { 19 | next: '/', 20 | }, 21 | 22 | }; 23 | 24 | export const pronounDict = { 25 | she: 'She, her, hers', 26 | he: 'He, him, his', 27 | they: 'They, them, their', 28 | }; 29 | 30 | export const BASE_URL_PATH = process.env.REACT_APP_API_URL ? process.env.REACT_APP_API_URL : 'http://localhost:8000' 31 | 32 | export const IDENTITIES = [ 33 | { label: 'All speakers', value: { woman: null, poc: null } }, 34 | { label: 'Women', value: { woman: true, poc: null } }, 35 | { label: 'Women of color', value: { woman: true, poc: true } }, 36 | { label: 'People of color', value: { poc: true, woman: null } }, 37 | ]; 38 | 39 | export const MAXIMUM_IMAGE_SIZE = 2 * 1024 * 1024; //less than 2MB in bytes 40 | export const DEFAULT_SPEAKER_LIMIT = 20; 41 | export const VALID_PARAMS = [ 42 | 'location', 43 | 'poc', 44 | 'woman', 45 | 'q', 46 | 'limit', 47 | 'offset', 48 | ]; 49 | -------------------------------------------------------------------------------- /app/common/Navigation/styles.css: -------------------------------------------------------------------------------- 1 | .container { 2 | width: 100%; 3 | color: #4a90e2; 4 | font-size: 18px; 5 | } 6 | 7 | .navbar { 8 | width: '100%'; 9 | background-color: var(--color-inverted-light); 10 | color: var(--color-grey); 11 | } 12 | 13 | .navContainer { 14 | display: flex; 15 | flex-direction: row; 16 | justify-content: space-between; 17 | align-items: center; 18 | width: 100%; 19 | max-width: 1100px; 20 | margin: 0px auto; 21 | } 22 | 23 | .navContainer ul { 24 | display: flex; 25 | flex-direction: row; 26 | padding: 0; 27 | } 28 | 29 | .navContainer li { 30 | list-style-type: none; 31 | padding: 0 10px; 32 | line-height: 2.8rem; 33 | } 34 | 35 | .link { 36 | color: inherit; 37 | text-decoration: none; 38 | } 39 | 40 | .link:hover { 41 | color: #1877E6; 42 | } 43 | 44 | .speakerButton { 45 | composes: button-primary from 'sharedStyles/styles.css'; 46 | } 47 | 48 | .searchForSpeaker { 49 | composes: input from 'sharedStyles/styles.css'; 50 | } 51 | 52 | .hamburgerIcon { 53 | composes: button from 'sharedStyles/styles.css'; 54 | font-size: 1.4rem; 55 | } 56 | 57 | .animatedSearchForm { 58 | transition: all 0.4s ease-in-out; 59 | width: 0; 60 | position: absolute; 61 | background: #FFF; 62 | } 63 | 64 | .expand { 65 | width: auto; 66 | left: 10px; 67 | right: 50px; 68 | } 69 | 70 | -------------------------------------------------------------------------------- /app/common/StyledButton.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react' 2 | import Button from 'material-ui/Button'; 3 | import { withStyles } from 'material-ui/styles'; 4 | 5 | const styles = (theme) => ({ 6 | root: { 7 | fontFamily: theme.typography.fontFamily, 8 | fontSize: '16px', 9 | fontWeight: '400', 10 | lineHeight: '24px', 11 | textTransform: 'none', 12 | padding: '8px 16px', 13 | borderRadius: '4px', 14 | whiteSpace: 'nowrap', 15 | marginLeft: '2px', 16 | marginRight: '2px', 17 | }, 18 | flatPrimary: { 19 | color: theme.palette.primary.contrastText, 20 | backgroundColor: theme.palette.primary.main, 21 | 22 | '&:hover': { 23 | opacity: 0.6, 24 | backgroundColor: theme.palette.primary.main, 25 | } 26 | }, 27 | 28 | flatSecondary: { 29 | color: theme.palette.primary.main, 30 | backgroundColor: theme.palette.primary.contrastText, 31 | boxShadow: `0 0 0 1px ${theme.palette.primary.main} inset`, 32 | 33 | '&:hover': { 34 | opacity: 0.6, 35 | backgroundColor: theme.palette.primary.contrastText, 36 | color: theme.palette.primary.main, 37 | } 38 | }, 39 | }) 40 | 41 | const StyledButton = (props) => { 42 | return( 43 | 46 | ) 47 | } 48 | 49 | export default withStyles(styles)(StyledButton); 50 | -------------------------------------------------------------------------------- /app/pages/Home/components/SpeakerList.js: -------------------------------------------------------------------------------- 1 | // NPM 2 | import React, { PropTypes } from 'react'; 3 | import Grid from 'material-ui/Grid'; 4 | import ReactLoading from 'react-loading'; 5 | 6 | // APP 7 | import SpeakerCard from './SpeakerCard'; 8 | import StyledButton from 'appCommon/StyledButton'; 9 | import css from '../styles.css'; 10 | 11 | const SpeakerList = ({ speakers, endOfResults, loadMoreSpeakers, isLoading }) => { 12 | const noResults = speakers.length === 0; 13 | if (isLoading && noResults) { 14 | console.log('initial load') 15 | return ; 16 | } 17 | if (!isLoading && noResults) { 18 | return
No results
; 19 | } 20 | 21 | return ( 22 | 23 | 24 | {speakers.map((speaker, index) => ( 25 | 26 | ))} 27 | 28 | {!endOfResults && ( 29 | 30 | 31 | 32 | {isLoading ? 'Loading...' : 'Load more speakers'} 33 | 34 | 35 | 36 | )} 37 | 38 | ); 39 | }; 40 | 41 | export default SpeakerList; 42 | -------------------------------------------------------------------------------- /app/pages/Speaker/components/Topics.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react'; 2 | import { connect } from 'react-redux'; 3 | import { withRouter } from 'react-router-dom'; 4 | import { updateSearchParams } from 'appRedux/modules/speaker'; 5 | 6 | import { topicLinks } from '../styles.css'; 7 | 8 | const Topics = ({ history, topics, updateSearchParams, limit }) => { 9 | const onTopicClick = topic => event => { 10 | event.preventDefault(); 11 | const home = '/'; 12 | if (history.location.pathname !== home) { 13 | history.push(home) 14 | } 15 | 16 | updateSearchParams({ 17 | q: topic, 18 | offset: 0, 19 | limit: 20, 20 | append: false, 21 | }); 22 | }; 23 | 24 | const topicsList = limit ? topics.slice(0, limit - 1) : topics; 25 | 26 | return ( 27 |
28 | { 29 | topicsList.map(topic => ( 30 | 36 | {topic.topic} 37 | 38 | )) 39 | .reduce((prev, curr) => [prev, ', ', curr]) 40 | } 41 |
42 | ) 43 | } 44 | 45 | const mapDispatchToProps = dispatch => { 46 | return { 47 | updateSearchParams: params => { 48 | dispatch(updateSearchParams(params)); 49 | }, 50 | }; 51 | }; 52 | 53 | export default connect(null, mapDispatchToProps)(withRouter(Topics)); -------------------------------------------------------------------------------- /app/pages/Speaker/components/FeaturedTalks.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from "react"; 2 | import Card, { CardContent, CardMedia } from "material-ui/Card"; 3 | import Carousel from 'nuka-carousel'; 4 | 5 | // App 6 | import { ensureAbsoluteUrl } from 'appHelpers/url'; 7 | import css from "../styles.css"; 8 | 9 | const FeaturedTalk = props => { 10 | const { talk } = props; 11 | return ( 12 |
13 | 14 | 15 | 16 |

{props.talk.event_name}

17 | 18 | {props.talk.talk_title} 19 | 20 |
21 |
22 |
23 | ); 24 | }; 25 | 26 | const FeaturedTalks = props => { 27 | const slidesToShow = screen.width >= 960 ? 2 : 1 28 | return ( 29 |
30 |

Featured Talks and Links

31 |
32 | 39 | {props.talks.map(talk => )} 40 | 41 |
42 |
43 | ); 44 | }; 45 | 46 | export default FeaturedTalks; 47 | -------------------------------------------------------------------------------- /app/config/Main/MainContainer.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { withRouter } from 'react-router'; 3 | import { MuiThemeProvider, createMuiTheme } from 'material-ui/styles'; 4 | import CssBaseline from 'material-ui/CssBaseline'; 5 | import indigo from 'material-ui/colors/indigo'; 6 | import grey from 'material-ui/colors/grey'; 7 | import pink from 'material-ui/colors/pink'; 8 | 9 | import Notification from 'appCommon/Notification/Notification' 10 | import Navigation from 'appCommon/Navigation/Navigation' 11 | import Footer from 'appCommon/Footer/Footer' 12 | import { container, innerContainer } from './styles.css'; 13 | 14 | 15 | const theme = createMuiTheme({ 16 | palette: { 17 | primary: { 18 | light: '#E5E8F4', 19 | main: '#283CA7', 20 | dark: '#001777', 21 | contrastText: '#fff', 22 | }, 23 | secondary: { 24 | light: '#f5f5f5', 25 | main: '#e0e0e0', 26 | dark: '#757575', 27 | contrastText: '#000000', 28 | }, 29 | error: pink, 30 | background: { 31 | paper: '#FFF', 32 | default: '#FFF', 33 | }, 34 | }, 35 | typography: { 36 | fontFamily: '"Open Sans", "Helvetica", "Arial", sans-serif', 37 | }, 38 | }); 39 | 40 | const MainContainer = props => ( 41 |
42 | 43 |
44 | 45 | 46 |
47 | 48 |
{props.children}
49 |
50 |
51 |
52 |
53 |
54 | ) 55 | 56 | export default withRouter(MainContainer) 57 | -------------------------------------------------------------------------------- /app/common/Footer/styles.css: -------------------------------------------------------------------------------- 1 | .footer { 2 | border-top: 1px solid var(--color-grey-light); 3 | margin-top: 2rem; 4 | } 5 | 6 | .footer a { 7 | color: var(--color-inverted-light); 8 | text-decoration: none; 9 | white-space: nowrap; 10 | } 11 | 12 | .footer a:hover, .footer a:focus { 13 | text-decoration: underline; 14 | } 15 | 16 | .footerRow { 17 | padding-top: 1rem; 18 | padding-bottom: 1rem; 19 | color: var(--color-inverted-light); 20 | } 21 | 22 | .footerRow a { 23 | color: var(--color-inverted-light); 24 | text-decoration: none; 25 | margin-right: 1.2rem; 26 | } 27 | 28 | .footerRow a:last-of-type { 29 | margin-right: 0; 30 | } 31 | 32 | .footerRow a:hover, .footerRow a:focus { 33 | text-decoration: underline; 34 | } 35 | 36 | .backgroundPrimary { 37 | background-color: var(--color-primary); 38 | } 39 | 40 | .backgroundPrimaryDark { 41 | background-color: #21328c; 42 | } 43 | 44 | .backgroundPrimary a { 45 | font-weight: 700; 46 | } 47 | 48 | .backgroundSecondary { 49 | background-color: var(--color-secondary); 50 | color: var(--color-primary); 51 | } 52 | 53 | .backgroundGrey { 54 | background-color: #f5f5f5; 55 | color: var(--color-grey-dark); 56 | } 57 | 58 | .backgroundGrey a { 59 | color: var(--color-grey-dark); 60 | } 61 | 62 | .partnerLogos { 63 | padding-top: 3rem; 64 | padding-bottom: 3rem; 65 | } 66 | 67 | .partnerLogos a { 68 | margin: 2rem; 69 | } 70 | 71 | .alignRight { 72 | text-align: right; 73 | } 74 | 75 | .verticalOnMobile { 76 | display: flex; 77 | } 78 | 79 | @media (max-width: 599px) { 80 | 81 | .partnerLogos a { 82 | margin: 1rem; 83 | } 84 | 85 | .verticalOnMobile { 86 | flex-direction: column; 87 | align-items: center; 88 | } 89 | 90 | .footerRow .verticalOnMobile a { 91 | margin-right: 0; 92 | } 93 | 94 | .alignCenterOnMobile { 95 | text-align: center; 96 | } 97 | } -------------------------------------------------------------------------------- /app/redux/modules/location.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | // App 3 | import { 4 | GetRequest, GetSuccess, GetError 5 | } from './action_template'; 6 | import { BASE_URL_PATH } from 'appHelpers/constants'; 7 | 8 | const MODULE_NAME = 'locations'; 9 | const ENDPOINT_URL = `${BASE_URL_PATH}/api/v1/${MODULE_NAME}/`; 10 | 11 | // Actions 12 | export function getRequest() { 13 | return { 14 | type: GetRequest(MODULE_NAME) 15 | } 16 | } 17 | 18 | export function getSuccess(data) { 19 | return { 20 | type: GetSuccess(MODULE_NAME), 21 | data 22 | } 23 | } 24 | 25 | export function getError() { 26 | return { 27 | type: GetError(MODULE_NAME) 28 | } 29 | } 30 | 31 | // Async Actions 32 | export function get(opts={}) { 33 | return dispatch => { 34 | dispatch(getRequest()); 35 | const active = opts.active ? 'active' : '' 36 | axios.get(`${ENDPOINT_URL}${active}`) 37 | .then(res => { 38 | dispatch(getSuccess(res.data)); 39 | }) 40 | .catch(err => { 41 | dispatch(getError(err)); 42 | console.log(err); 43 | }) 44 | } 45 | } 46 | 47 | const initialState = { 48 | isInitialized: false, 49 | isLoading: false, 50 | isRequesting: false, 51 | locations: [] 52 | } 53 | 54 | export const reducer = (state=initialState, action) => { 55 | switch (action.type) { 56 | case GetRequest(MODULE_NAME): { 57 | return { 58 | ...state, 59 | isLoading: true, 60 | isRequesting: true 61 | } 62 | } 63 | 64 | case GetSuccess(MODULE_NAME): { 65 | return { 66 | ...state, 67 | isInitialized: true, 68 | isLoading: false, 69 | isRequesting: false, 70 | locations: action.data 71 | } 72 | } 73 | 74 | case GetError(MODULE_NAME): { 75 | return { 76 | ...state, 77 | isRequesting: false, 78 | isLoading: false, 79 | error: action.error 80 | } 81 | } 82 | 83 | default: 84 | return state 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "women-color", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "engines": { 7 | "node": ">=8.9.1" 8 | }, 9 | "scripts": { 10 | "start": "webpack-dev-server --env.NODE_ENV=development", 11 | "build": "webpack -p --config ./webpack.config.babel.js --progress --env.NODE_ENV=production", 12 | "test": "react-scripts test --env=jsdom" 13 | }, 14 | "keywords": [], 15 | "author": "", 16 | "license": "ISC", 17 | "dependencies": { 18 | "@fortawesome/fontawesome": "^1.1.6", 19 | "@fortawesome/fontawesome-free-brands": "^5.0.11", 20 | "@fortawesome/react-fontawesome": "^0.0.19", 21 | "@material-ui/icons": "^1.0.0-beta.43", 22 | "axios": "^0.17.1", 23 | "babel-polyfill": "^6.26.0", 24 | "downshift": "^1.31.11", 25 | "keycode": "^2.2.0", 26 | "lodash": "^4.17.4", 27 | "material-ui": "^1.0.0-beta.37", 28 | "nuka-carousel": "^4.2.1", 29 | "prop-types": "^15.6.0", 30 | "react": "^15.6.1", 31 | "react-dom": "^15.6.1", 32 | "react-helmet": "^5.2.0", 33 | "react-loading": "^1.0.3", 34 | "react-redux": "^5.0.6", 35 | "react-router-dom": "^4.1.2", 36 | "react-router-redux": "^4.0.8", 37 | "react-scripts": "^1.0.17", 38 | "redux": "^3.7.2", 39 | "redux-thunk": "^2.2.0" 40 | }, 41 | "devDependencies": { 42 | "autoprefixer": "^8.5.1", 43 | "babel-core": "^6.25.0", 44 | "babel-loader": "^7.1.1", 45 | "babel-preset-es2015": "^6.24.1", 46 | "babel-preset-es2025": "0.0.0", 47 | "babel-preset-react": "^6.24.1", 48 | "babel-preset-react-hmre": "^1.1.1", 49 | "babel-preset-stage-0": "^6.24.1", 50 | "css-loader": "^0.28.4", 51 | "html-webpack-plugin": "^2.30.1", 52 | "postcss-css-variables": "^0.8.1", 53 | "postcss-loader": "^2.1.5", 54 | "prettier": "1.10.2", 55 | "redux-devtools": "^3.4.0", 56 | "style-loader": "^0.18.2", 57 | "svg-react-loader": "^0.4.5", 58 | "webpack": "^3.8.1", 59 | "webpack-dev-server": "^2.6.1" 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /app/common/Navigation/MenuDropdown.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Link } from 'react-router-dom'; 3 | import Menu, { MenuItem } from 'material-ui/Menu'; 4 | import IconButton from 'material-ui/IconButton'; 5 | import MenuIcon from '@material-ui/icons/Menu'; 6 | 7 | class MenuDropdown extends React.Component { 8 | state = { 9 | anchorEl: null, 10 | }; 11 | 12 | handleMenu = event => { 13 | this.setState({ anchorEl: event.currentTarget }); 14 | }; 15 | 16 | handleClose = () => { 17 | this.setState({ anchorEl: null }); 18 | }; 19 | 20 | render() { 21 | const { anchorEl } = this.state; 22 | const open = Boolean(anchorEl); 23 | 24 | return ( 25 |
26 | 31 | 32 | 33 | 39 | {this.props.menuItems.map(item => { 40 | if (item.slug.startsWith('/accounts/')) { 41 | return ( 42 | (window.location.href = item.slug)} 46 | > 47 | {item.title} 48 | 49 | ); 50 | } else { 51 | return ( 52 | 59 | {item.title} 60 | 61 | ); 62 | } 63 | })} 64 | 65 |
66 | ); 67 | } 68 | } 69 | 70 | export default MenuDropdown; 71 | -------------------------------------------------------------------------------- /app/pages/EditProfile/EditProfile.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Route, Switch, Redirect } from 'react-router-dom'; 3 | import Grid from 'material-ui/Grid'; 4 | import { Helmet } from "react-helmet"; 5 | 6 | // App 7 | import SideBar from './SideBar'; 8 | import About from './About/About'; 9 | import Account from './Account/Account'; 10 | import Talks from './Talks/Talks'; 11 | import EmailSettings from './EmailSettings/EmailSettings'; 12 | import css from './styles.css'; 13 | 14 | const subroutes = [ 15 | { 16 | id: 0, 17 | path: 'about', 18 | text: 'about', 19 | component: About, 20 | }, 21 | { 22 | id: 1, 23 | path: 'talks', 24 | text: 'talks', 25 | component: Talks, 26 | }, 27 | { 28 | id: 2, 29 | path: 'account', 30 | text: 'account', 31 | component: Account, 32 | }, 33 | { 34 | id: 3, 35 | path: 'email-settings', 36 | text: 'communication', 37 | component: EmailSettings, 38 | }, 39 | ]; 40 | 41 | const _renderRoute = ({ component: Component, baseUrl, path, key }) => ( 42 | ( 46 | 47 | 48 | 53 | 54 | 55 | 56 | 57 | 58 | )} 59 | /> 60 | ); 61 | 62 | const EditProfile = ({ match }) => ( 63 |
64 | 65 | Edit Profile 66 | 67 | 68 | 69 | 70 | 71 | 76 | {subroutes.map(subroute => 77 | _renderRoute({ 78 | key: subroute.id, 79 | component: subroute.component, 80 | baseUrl: match.url, 81 | path: `${match.path}/${subroute.path}`, 82 | }) 83 | )} 84 | 85 | 86 | 87 |
88 | ); 89 | 90 | export default EditProfile; 91 | -------------------------------------------------------------------------------- /app/sharedStyles/styles.css: -------------------------------------------------------------------------------- 1 | @import url("https://fonts.googleapis.com/css?family=Open+Sans:300,400,600,700"); 2 | @import url("https://fonts.googleapis.com/icon?family=Material+Icons"); 3 | 4 | /* Styles */ 5 | html, 6 | body { 7 | margin: 0; 8 | padding: 0; 9 | height: 100%; 10 | width: 100%; 11 | color: var(--color-grey); 12 | font-family: var(--font-family-sans-serif); 13 | font-size: var(--font-size-base); 14 | line-height: var(--line-height-base); 15 | } 16 | 17 | .centeredContainer { 18 | display: flex; 19 | justify-content: center; 20 | align-items: center; 21 | flex-direction: column; 22 | } 23 | 24 | .h1 { 25 | color: #000000; 26 | font-size: 2.3rem; 27 | line-height: 3rem; 28 | font-weight: var(--font-weight-light); 29 | } 30 | 31 | .h2 { 32 | text-align: center; 33 | font-weight: var(--font-weight-light); 34 | font-size: 1.1rem; 35 | line-height: 1.7rem; 36 | } 37 | 38 | .error { 39 | color: #ff7777; 40 | text-align: center; 41 | } 42 | 43 | .button { 44 | line-height: 1.5rem; 45 | padding: 1rem 2rem; 46 | display: inline-block; 47 | text-decoration: none; 48 | border-radius: var(--border-radius); 49 | background-color: transparent; 50 | border: 1px solid transparent; 51 | } 52 | 53 | .button-primary { 54 | composes: button; 55 | background-color: var(--color-primary); 56 | color: #ffffff; 57 | } 58 | 59 | .input { 60 | line-height: 2.8rem; 61 | -webkit-appearance: none; 62 | color: var(--color-grey); 63 | border: 1px solid var(--color-grey-light); 64 | border-radius: var(--border-radius); 65 | padding: 0; 66 | font-size: 16px; 67 | } 68 | 69 | .searchForm { 70 | border: 1px solid var(--color-grey-light); 71 | border-radius: 4px; 72 | padding: 6px; 73 | padding-right: 20px; 74 | padding-left: 20px; 75 | display: flex; 76 | align-items: center; 77 | } 78 | 79 | .profilePhoto { 80 | background: #e5e8f5; 81 | border-radius: 50%; 82 | overflow: hidden; 83 | display: flex; 84 | justify-content: center; 85 | flex-shrink: 0; 86 | width: 100%; 87 | padding-top: 100%; 88 | position: relative; 89 | } 90 | 91 | .profilePhoto img { 92 | height: 100%; 93 | width: 100%; 94 | position: absolute; 95 | top: 0; 96 | bottom: 0; 97 | left: 0; 98 | object-fit: cover; 99 | } 100 | 101 | a { 102 | color: var(--color-grey); 103 | } 104 | 105 | @media (max-width: 380px){ 106 | .hideOnMobile { 107 | display: none !important; 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /app/pages/Static/AboutUs.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Grid from 'material-ui/Grid'; 3 | import { withStyles } from 'material-ui/styles'; 4 | import { Helmet } from 'react-helmet'; 5 | 6 | import css from './styles.css' 7 | 8 | const styles = theme => ({ 9 | header: { 10 | backgroundColor: theme.palette.secondary.light, 11 | paddingTop: '4rem', 12 | paddingBottom: '4rem', 13 | textAlign: 'center', 14 | }, 15 | title: { 16 | color: theme.palette.secondary.contrastText, 17 | fontSize: '1.8rem', 18 | lineHeight: '2.4rem', 19 | fontWeight: '300' 20 | }, 21 | strong: { 22 | fontWeight: '600', 23 | color: theme.palette.secondary.contrastText, 24 | }, 25 | body: { 26 | paddingTop: '2rem', 27 | paddingBottom: '2rem', 28 | textAlign: 'center', 29 | fontSize: '1.2rem', 30 | lineHeight: '1.8rem', 31 | fontWeight: '300', 32 | color: theme.palette.secondary.contrastText, 33 | } 34 | }) 35 | 36 | const AboutUs = (props) => { 37 | return( 38 |
39 | 40 | About Us 41 | 42 | 43 | 44 | 45 | 46 | 47 |

We are frustrated with the lack of gender and racial representation at tech-related events.

48 |
49 |
50 | 51 | 52 | 53 |

Women and Color is an online community of subject matter experts who identify as women and/or people of color.

54 |

Located in cities across Canada and the United States, each of our subject matter experts is available for speaking opportunities at tech-related events.

55 |

Interested in helping us build a more inclusive tech ecosystem? Send us an email at hello@womenandcolor.com.

56 |

Women and Color is built with passion in Toronto, ON, and is a federally incorporated not-for-profit in Canada.

57 |
58 |
59 |
60 |
61 |
62 | ) 63 | } 64 | 65 | export default withStyles(styles)(AboutUs); 66 | -------------------------------------------------------------------------------- /app/pages/Home/components/SpeakerCard.js: -------------------------------------------------------------------------------- 1 | // NPM 2 | import React, { PropTypes } from 'react'; 3 | import Hidden from 'material-ui/Hidden'; 4 | import Grid from 'material-ui/Grid'; 5 | import Chip from 'material-ui/Chip'; 6 | import { Link } from 'react-router-dom'; 7 | 8 | // App 9 | import { speakerToProfilePath } from 'appHelpers/url'; 10 | import StyledButton from 'appCommon/StyledButton'; 11 | import Topics from 'appPages/Speaker/components/Topics'; 12 | import { updateSearchParams } from 'appRedux/modules/speaker'; 13 | 14 | import css from '../styles.css'; 15 | import { profilePhoto } from 'appSharedStyles/styles.css' 16 | 17 | function buildTitle(position, organization) { 18 | let separator; 19 | if (position && organization) { 20 | separator = ` at `; 21 | } else { 22 | separator = ', '; 23 | } 24 | 25 | return ( 26 |

27 | {position || 'Independent'} 28 | {separator} 29 | 30 | {organization || 'No affiliation'} 31 | 32 |

33 | ); 34 | } 35 | 36 | const SpeakerCard = ({ speaker, classes }) => { 37 | const name = !!speaker.display_name ? speaker.display_name : speaker.email; 38 | const title = buildTitle(speaker.position, speaker.organization); 39 | const speakerProfilePath = speakerToProfilePath({ 40 | ...speaker, 41 | }); 42 | return ( 43 | 44 | 45 | 46 |
47 | 48 | {name} 49 | 50 |
51 |
52 | 53 | 54 |

{name}

55 | 56 | {title} 57 | { (speaker.topics.length > 0) && 58 | 59 | 60 | 61 | } 62 |
63 | 64 | 65 | 71 | View profile 72 | 73 | 74 | 75 |
76 |
77 | ); 78 | }; 79 | 80 | 81 | export default SpeakerCard; 82 | -------------------------------------------------------------------------------- /app/pages/Speaker/styles.css: -------------------------------------------------------------------------------- 1 | /* SpeakerCard */ 2 | 3 | .speakerCard { 4 | margin-bottom: 1rem; 5 | display: flex; 6 | flex-direction: column; 7 | align-items: center; 8 | } 9 | 10 | .speakerCardInfo { 11 | text-align: center; 12 | width: 100%; 13 | } 14 | 15 | .speakerCardName { 16 | font-size: 24px; 17 | font-weight: 600; 18 | line-height: 32px; 19 | margin-top: 0; 20 | margin-bottom: 0; 21 | color: var(--color-grey-dark); 22 | } 23 | 24 | .speakerCardTitle { 25 | font-size: 16px; 26 | line-height: 24px; 27 | margin-top: 0; 28 | margin-bottom: 0; 29 | } 30 | 31 | .speakerCardOrganization { 32 | margin-top: 0; 33 | margin-bottom: 1rem; 34 | } 35 | 36 | .speakerCardTags { 37 | font-size: 14px; 38 | line-height: 21px; 39 | margin-top: 0; 40 | margin-bottom: 0; 41 | } 42 | 43 | .topicLinks a { 44 | text-decoration: none; 45 | font-size: 0.9rem; 46 | line-height: 0.9; 47 | } 48 | 49 | .topicLinks a:hover, .topicLinks a:focus { 50 | text-decoration: underline; 51 | } 52 | 53 | .socialLinks a { 54 | color: var(--color-grey-dark); 55 | text-decoration: none; 56 | } 57 | 58 | .socialLinks a:hover, .socialLinks a:focus { 59 | text-decoration: underline; 60 | } 61 | 62 | /* Speaker Info */ 63 | 64 | .speakerInfo { 65 | margin-bottom: 4rem; 66 | } 67 | 68 | .sectionHeader { 69 | font-size: 2rem; 70 | color: var(--color-grey-dark); 71 | font-weight: var(--font-weight-light); 72 | margin-top: 0.5rem; 73 | } 74 | 75 | .sectionSubHeader { 76 | color: var(--color-grey-dark); 77 | text-transform: uppercase; 78 | border-bottom: 1px solid var(--color-grey-light); 79 | font-weight: var(--font-weight-semibold); 80 | margin-bottom: 0; 81 | } 82 | 83 | .speakerInfoDescription { 84 | color: var(--color-grey-dark); 85 | margin-bottom: 0; 86 | } 87 | 88 | /* FeaturedTalks */ 89 | 90 | .talksWrapper { 91 | display: flex; 92 | flex-wrap: wrap; 93 | justify-content: space-between; 94 | } 95 | 96 | .talkCardContainer { 97 | padding: 1rem 0.5rem; 98 | } 99 | 100 | .talkCard { 101 | max-height: 400px; 102 | overflow: hidden; 103 | } 104 | 105 | @media (min-width: 576px) { 106 | .talkCard { 107 | flex: 0 1 calc(50% - 1rem); 108 | } 109 | } 110 | 111 | .talkCardImage { 112 | height: 14rem; 113 | border-radius: 2px 2px 0 0; 114 | background-color: var(--color-secondary); 115 | } 116 | 117 | .talkCardHeader { 118 | font-size: 1rem; 119 | color: var(--color-grey-dark); 120 | font-weight: var(--font-weight-semibold); 121 | margin-top: 0; 122 | } 123 | 124 | .talkCardLink { 125 | /* todo color, text-decoration, etc */ 126 | } 127 | 128 | /* MessageSpeakerForm */ 129 | -------------------------------------------------------------------------------- /app/config/routes.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Router, Switch, Route, Redirect } from 'react-router-dom'; 3 | import { Provider, connect } from 'react-redux'; 4 | 5 | // App 6 | import { 7 | Home, 8 | Page, 9 | Register, 10 | Login, 11 | ResetPassword, 12 | ConfirmResetPassword, 13 | Speaker, 14 | Organization, 15 | Profile, 16 | Work, 17 | Social, 18 | EmailSettings, 19 | EditProfile, 20 | AboutUs, 21 | ComingSoon, 22 | Privacy, 23 | Terms, 24 | CodeOfConduct, 25 | PageNotFound, 26 | } from 'pages'; 27 | import MainContainer from './Main/MainContainer'; 28 | 29 | import store, { history } from '../redux/store'; 30 | 31 | function mapStateToProps(state) { 32 | return { 33 | user: state.user, 34 | }; 35 | } 36 | 37 | const ProtectedRoute = connect(mapStateToProps, null)(({ component: Component, user, ...rest }) => { 38 | return ( 39 | 42 | user.isAuthenticated ? ( 43 | 44 | ) : ( 45 | 51 | ) 52 | } 53 | /> 54 | ); 55 | }) 56 | 57 | 58 | const routes = ( 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | ) 88 | 89 | export default routes; 90 | -------------------------------------------------------------------------------- /app/common/Navigation/SearchField.js: -------------------------------------------------------------------------------- 1 | // NPM 2 | import React, { PropTypes, Component } from 'react' 3 | import {withRouter} from 'react-router-dom' 4 | import IconButton from 'material-ui/IconButton'; 5 | import SearchIcon from '@material-ui/icons/Search'; 6 | import TextField from 'material-ui/TextField'; 7 | import { connect } from 'react-redux' 8 | 9 | // APP 10 | import StyledButton from 'appCommon/StyledButton'; 11 | import { updateSearchParams } from 'appRedux/modules/speaker'; 12 | import css from './styles.css'; 13 | import { searchForm, hideOnMobile } from '../../sharedStyles/styles.css'; 14 | 15 | const styles = { 16 | form: { 17 | paddingLeft: '2px' 18 | }, 19 | searchButton: { 20 | height: '100%' 21 | } 22 | } 23 | 24 | class SearchField extends Component { 25 | constructor(props) { 26 | super(props); 27 | this.state = { query: this.props.q || '' } 28 | } 29 | 30 | searchProfiles = (event) => { 31 | event.preventDefault(); 32 | const query = this.state.query; 33 | const home = '/' 34 | if (this.props.history.location.pathname !== home) { 35 | this.props.history.push(home) 36 | } 37 | this.props.updateSearchParams({ 38 | q: query, 39 | offset: 0, 40 | limit: 20, 41 | append: false 42 | }) 43 | } 44 | 45 | onChange = (event) => { 46 | const query = event.target.value; 47 | this.setState({ query }); 48 | if (!query) { 49 | const home = '/' 50 | if (this.props.history.location.pathname !== home) { 51 | this.props.history.push(home) 52 | } 53 | this.props.updateSearchParams({ 54 | q: null, 55 | offset: 0, 56 | limit: 20, 57 | append: false 58 | }) 59 | } 60 | } 61 | 62 | render() { 63 | return ( 64 |
65 |
66 | 67 | 68 | 69 | 77 | 78 |
79 | ) 80 | } 81 | } 82 | 83 | function mapDispatchToProps(dispatch) { 84 | return { 85 | updateSearchParams: (params) => { 86 | dispatch(updateSearchParams(params)) 87 | } 88 | } 89 | } 90 | 91 | function mapStateToProps(state) { 92 | return { 93 | q: state.speaker.searchParams.q 94 | } 95 | } 96 | 97 | export default connect( 98 | mapStateToProps, 99 | mapDispatchToProps 100 | )(withRouter(SearchField)); 101 | -------------------------------------------------------------------------------- /app/redux/modules/subscriptionGroup.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | // App 3 | import { 4 | GetRequest, GetSuccess, GetError, PostRequest, PostSuccess, PostError 5 | } from './action_template'; 6 | import { BASE_URL_PATH } from 'appHelpers/constants'; 7 | 8 | const MODULE_NAME = 'subscription_groups'; 9 | const ENDPOINT_URL = `${BASE_URL_PATH}/api/v1/${MODULE_NAME}/`; 10 | 11 | // Actions 12 | export function getRequest() { 13 | return { 14 | type: GetRequest(MODULE_NAME) 15 | } 16 | } 17 | 18 | export function getSuccess(data) { 19 | return { 20 | type: GetSuccess(MODULE_NAME), 21 | data 22 | } 23 | } 24 | 25 | export function getError() { 26 | return { 27 | type: GetError(MODULE_NAME) 28 | } 29 | } 30 | 31 | export function postRequest() { 32 | return { 33 | type: PostRequest(MODULE_NAME) 34 | } 35 | } 36 | 37 | export function postSuccess(data) { 38 | return { 39 | type: PostSuccess(MODULE_NAME), 40 | data 41 | } 42 | } 43 | 44 | export function postError(error) { 45 | return { 46 | type: PostError(MODULE_NAME), 47 | error 48 | } 49 | } 50 | 51 | // Async Actions 52 | export function get() { 53 | return dispatch => { 54 | dispatch(getRequest()); 55 | 56 | axios.get(ENDPOINT_URL) 57 | .then(res => { 58 | dispatch(getSuccess(res.data)); 59 | }) 60 | .catch(err => { 61 | dispatch(getError(err)); 62 | console.log(err); 63 | }) 64 | } 65 | } 66 | 67 | 68 | const initialState = { 69 | isInitialized: false, 70 | isLoading: false, 71 | isRequesting: false, 72 | groups: [] 73 | } 74 | 75 | export const reducer = (state=initialState, action) => { 76 | switch (action.type) { 77 | case GetRequest(MODULE_NAME): { 78 | return { 79 | ...state, 80 | isLoading: true, 81 | isRequesting: true 82 | } 83 | } 84 | 85 | case GetSuccess(MODULE_NAME): { 86 | return { 87 | ...state, 88 | isInitialized: true, 89 | isLoading: false, 90 | isRequesting: false, 91 | groups: action.data 92 | } 93 | } 94 | 95 | case GetError(MODULE_NAME): { 96 | return { 97 | ...state, 98 | isRequesting: false, 99 | isLoading: false, 100 | error: action.error 101 | } 102 | } 103 | 104 | case PostRequest(MODULE_NAME): { 105 | return { 106 | ...state, 107 | isRequesting: true 108 | } 109 | } 110 | 111 | case PostSuccess(MODULE_NAME): { 112 | return { 113 | ...state, 114 | isRequesting: false, 115 | groups: [...state.groups].concat(action.data) 116 | } 117 | } 118 | 119 | case PostError(MODULE_NAME): { 120 | return { 121 | ...state, 122 | isRequesting: false, 123 | error: action.error 124 | } 125 | } 126 | 127 | default: 128 | return state 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /app/pages/EditProfile/FormComponents/ImageUpload/ImageUpload.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { connect } from 'react-redux'; 3 | import axios from 'axios'; 4 | import ReactLoading from 'react-loading'; 5 | 6 | import { onChange as onChangeProfile } from 'appRedux/modules/profile'; 7 | import { getApiToken } from 'appRedux/modules/user'; 8 | import StyledButton from 'appCommon/StyledButton'; 9 | import { BASE_URL_PATH, MAXIMUM_IMAGE_SIZE } from 'appHelpers/constants'; 10 | import css from './styles.css'; 11 | 12 | 13 | class ImageUpload extends Component { 14 | constructor(props) { 15 | super(props) 16 | this.state = { loading: false, imageError: false } 17 | } 18 | 19 | handleImageChange = event => { 20 | this.setState({ loading: true }) 21 | const file = event.currentTarget.files[0]; 22 | // image must not larger than 2MB 23 | if (file.size > MAXIMUM_IMAGE_SIZE) { 24 | this.setState({ 25 | imageError: true, 26 | loading: false 27 | }); 28 | return; 29 | } 30 | 31 | const data = new FormData(); 32 | data.append('file', file); 33 | data.append('profile', this.props.profile.id); 34 | const url = `${BASE_URL_PATH}/api/v1/images/`; 35 | const token = getApiToken() 36 | axios({ 37 | url, 38 | data, 39 | method: 'post', 40 | responseType: 'json', 41 | headers: { 42 | 'Authorization': `JWT ${token}` 43 | } 44 | }) 45 | .then(res => { 46 | this.props.onChangeProfile({ image: res.data.file }); 47 | this.setState({ loading: false }) 48 | console.log(res); 49 | }) 50 | .catch(err => { 51 | this.setState({ loading: false }) 52 | console.log(err); 53 | }); 54 | } 55 | 56 | render() { 57 | return( 58 |
59 |
60 | { this.state.loading ? 61 | () : 62 | () 63 | } 64 | {this.state.imageError ? ( 65 |
66 | ** The file size can not exceed 2MB. 67 |
68 | ) : null} 69 |
70 | 71 | 77 | Choose Image 78 | 79 |
80 | ) 81 | } 82 | } 83 | 84 | function mapStateToProps(state) { 85 | return { 86 | user: state.user, 87 | profile: state.profile 88 | }; 89 | } 90 | 91 | function mapDispatchToProps(dispatch) { 92 | return { 93 | onChangeProfile: attrs => { 94 | dispatch(onChangeProfile(attrs)); 95 | } 96 | }; 97 | } 98 | 99 | export default connect(mapStateToProps, mapDispatchToProps)(ImageUpload); -------------------------------------------------------------------------------- /app/pages/Home/components/MobileSearch.js: -------------------------------------------------------------------------------- 1 | // NPM 2 | import React, { PropTypes, Component } from 'react' 3 | import {withRouter} from 'react-router-dom' 4 | import IconButton from 'material-ui/IconButton'; 5 | import SearchIcon from '@material-ui/icons/Search'; 6 | import TextField from 'material-ui/TextField'; 7 | import { connect } from 'react-redux' 8 | import { withStyles } from 'material-ui/styles'; 9 | 10 | 11 | // APP 12 | import StyledButton from 'appCommon/StyledButton'; 13 | import { updateSearchParams } from 'appRedux/modules/speaker'; 14 | import css from '../styles.css'; 15 | import { searchForm, hideOnMobile } from '../../../sharedStyles/styles.css'; 16 | 17 | const styles = theme => ({ 18 | form: { 19 | height: '48px', 20 | display: 'flex', 21 | alignItems: 'center', 22 | paddingLeft: '2px', 23 | backgroundColor: theme.palette.primary.light, 24 | color: theme.palette.primary.main 25 | }, 26 | searchButton: { 27 | height: '100%', 28 | color: theme.palette.primary.main 29 | } 30 | }); 31 | 32 | class MobileSearch extends Component { 33 | constructor(props) { 34 | super(props); 35 | this.state = { query: this.props.q || '' } 36 | } 37 | 38 | searchProfiles = (event) => { 39 | event.preventDefault(); 40 | const query = this.state.query; 41 | const home = '/' 42 | if (this.props.history.location.pathname !== home) { 43 | this.props.history.push(home) 44 | } 45 | this.props.updateSearchParams({ 46 | q: query, 47 | offset: 0, 48 | limit: 20, 49 | append: false 50 | }) 51 | } 52 | 53 | onChange = (event) => { 54 | const query = event.target.value; 55 | this.setState({ query }); 56 | if (!query) { 57 | const home = '/' 58 | if (this.props.history.location.pathname !== home) { 59 | this.props.history.push(home) 60 | } 61 | this.props.updateSearchParams({ 62 | q: null, 63 | offset: 0, 64 | limit: 20, 65 | append: false 66 | }) 67 | } 68 | } 69 | 70 | render() { 71 | return ( 72 |
73 | 74 | 75 | 76 | 85 | 86 | ) 87 | } 88 | } 89 | 90 | function mapDispatchToProps(dispatch) { 91 | return { 92 | updateSearchParams: (params) => { 93 | dispatch(updateSearchParams(params)) 94 | } 95 | } 96 | } 97 | 98 | function mapStateToProps(state) { 99 | return { 100 | q: state.speaker.searchParams.q 101 | } 102 | } 103 | 104 | export default connect( 105 | mapStateToProps, 106 | mapDispatchToProps 107 | )(withRouter(withStyles(styles)(MobileSearch))); 108 | -------------------------------------------------------------------------------- /app/pages/Account/ResetPassword.js: -------------------------------------------------------------------------------- 1 | // Project 2 | import React, { Component } from 'react' 3 | import { connect } from 'react-redux' 4 | import TextField from 'material-ui/TextField'; 5 | import Button from 'material-ui/Button'; 6 | import Select from 'material-ui/Select'; 7 | import Grid from 'material-ui/Grid'; 8 | import Input, { InputLabel } from 'material-ui/Input'; 9 | import { MenuItem } from 'material-ui/Menu'; 10 | import { Link } from 'react-router-dom'; 11 | import Card from 'material-ui/Card'; 12 | import { Helmet } from "react-helmet"; 13 | 14 | // App 15 | import { 16 | onChange as onChangeUser, 17 | resetPassword as submitForm 18 | } from 'appRedux/modules/user'; 19 | import StyledButton from 'appCommon/StyledButton'; 20 | import FormField from 'appCommon/FormField'; 21 | import AccountFormContainer from './AccountFormContainer'; 22 | 23 | import css from './styles.css'; 24 | 25 | const ResetPassword = (props) => { 26 | const generateHandlerUser = (fieldName) => { 27 | return (event) => { props.handleUserInputChange(fieldName, event.currentTarget.value) } 28 | } 29 | 30 | return( 31 |
32 | 33 |
34 |

Reset your password

35 |

Forgot your password? Enter your e-mail address below, and we'll send you an e-mail allowing you to reset it.

36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | Submit 44 | 45 | 46 | 47 |
48 |
49 |
50 | ) 51 | } 52 | 53 | 54 | class ResetPasswordContainer extends Component { 55 | constructor(props) { 56 | super(props) 57 | } 58 | 59 | render() { 60 | return( 61 |
62 | 63 | Reset password 64 | 65 | 66 | { 68 | event.preventDefault(); 69 | this.props.submitForm(); 70 | }} 71 | handleUserInputChange={(field, value) => { 72 | this.props.onChangeUser({ [field]: value }) 73 | }} 74 | {...this.props} 75 | /> 76 |
77 | ) 78 | } 79 | } 80 | 81 | function mapStateToProps(state) { 82 | return { 83 | user: state.user, 84 | } 85 | } 86 | 87 | function mapDispatchToProps(dispatch, props) { 88 | return { 89 | onChangeUser: (attrs) => { 90 | dispatch(onChangeUser(attrs)) 91 | }, 92 | fetchLocations: () => { 93 | dispatch(fetchLocations()) 94 | }, 95 | submitForm: () => { 96 | dispatch(submitForm()); 97 | } 98 | } 99 | } 100 | 101 | export default connect( 102 | mapStateToProps, 103 | mapDispatchToProps 104 | )(ResetPasswordContainer); 105 | -------------------------------------------------------------------------------- /app/pages/Home/styles.css: -------------------------------------------------------------------------------- 1 | .container { 2 | composes: centeredContainer from 'sharedStyles/styles.css'; 3 | } 4 | 5 | .title { 6 | composes: h1 from 'sharedStyles/styles.css'; 7 | } 8 | 9 | .slogan { 10 | composes: h2 from 'sharedStyles/styles.css'; 11 | } 12 | 13 | // Sidebar 14 | 15 | .sidebar { 16 | height: 100%; 17 | width: 192px; 18 | } 19 | 20 | .sidebarTitles { 21 | height: 21px; 22 | width: 100%; /* 192px; */ 23 | color: #212121; 24 | font-family: 'Open Sans'; 25 | font-size: 14px; 26 | font-weight: bold; 27 | border-bottom: 1px solid #e0e0e0; 28 | margin-bottom: 8px; 29 | } 30 | 31 | // Content 32 | 33 | .contentContainer { 34 | padding-top: 2rem; 35 | } 36 | 37 | .contentTitles { 38 | color: #000000; 39 | background: #ffffff; 40 | font-family: 'Open Sans'; 41 | font-size: 32px; 42 | font-weight: 300; 43 | line-height: 40px; 44 | padding: 10px; 45 | } 46 | 47 | .contentCard { 48 | background-color: #ffffff; 49 | display: flex; 50 | border-bottom: 1px solid #e0e0e0; 51 | justify-content: space-between; 52 | } 53 | 54 | .contentCard > div { 55 | margin: 10px; 56 | } 57 | 58 | .info { 59 | flex-grow: 1; 60 | flex-shrink: 1; 61 | } 62 | 63 | .info a { 64 | text-decoration: none; 65 | } 66 | 67 | .info a:hover { 68 | text-decoration: underline; 69 | text-decoration-color: #000000; 70 | } 71 | 72 | .info h3 { 73 | overflow: hidden; 74 | flex-wrap: wrap; 75 | } 76 | 77 | .name { 78 | color: #000000; 79 | font-family: 'Open Sans'; 80 | font-size: 24px; 81 | font-weight: 600; 82 | line-height: 32px; 83 | margin-top: 0; 84 | margin-bottom: 0.5rem; 85 | white-space: nowrap; 86 | text-overflow: ellipsis; 87 | } 88 | 89 | .speakerTitle { 90 | color: #424242; 91 | font-family: 'Open Sans'; 92 | font-size: 16px; 93 | line-height: 24px; 94 | margin-top: 0; 95 | margin-bottom: 1rem; 96 | } 97 | 98 | .speakerPhoto { 99 | max-width: 112px; 100 | margin: auto; 101 | } 102 | 103 | .speakerTags { 104 | color: #757575; 105 | font-family: 'Open Sans'; 106 | font-size: 14px; 107 | line-height: 21px; 108 | margin-top: 0; 109 | margin-bottom: 1rem; 110 | } 111 | 112 | .speakersList { 113 | margin-bottom: 2rem !important; 114 | } 115 | 116 | .noResults { 117 | margin: 2rem 0; 118 | } 119 | 120 | .filterLabel { 121 | background-color: var(--color-grey-pale); 122 | color: var(--color-grey-dark); 123 | display: flex; 124 | align-items: center; 125 | height: 48px; 126 | } 127 | 128 | .filterOpen { 129 | background-color: var(--color-inverted-light) 130 | } 131 | 132 | .filterLabelContainer { 133 | border-bottom: 1px solid #e0e0e0; 134 | } 135 | 136 | .filtersContainer { 137 | padding-right: 1rem; 138 | } 139 | 140 | .mobileSearchInput input { 141 | color: var(--color-primary); 142 | font-weight: 400; 143 | padding-right: 1rem; 144 | } 145 | 146 | .mobileSearchInput input::placeholder { 147 | color: var(--color-primary); 148 | opacity: 1; 149 | } 150 | 151 | @media (max-width: 960px) { 152 | .separator, 153 | .organization { 154 | display: none; 155 | } 156 | 157 | .content { 158 | margin-top: 0; 159 | } 160 | } 161 | 162 | @media (max-width: 380px) { 163 | .contentTitles { 164 | padding: 1rem; 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /app/redux/modules/contactForm.js: -------------------------------------------------------------------------------- 1 | // NPM 2 | import { map } from 'lodash'; 3 | import axios from 'axios'; 4 | 5 | // App 6 | import { 7 | PostRequest, PostSuccess, PostError, 8 | OnChange 9 | } from './action_template'; 10 | import { showNotification } from './notification'; 11 | import { BASE_URL_PATH } from 'appHelpers/constants'; 12 | 13 | const MODULE_NAME = 'contact_form'; 14 | const ENDPOINT_URL = `${BASE_URL_PATH}/api/v1/${MODULE_NAME}/`; 15 | 16 | // Actions 17 | 18 | function postRequest() { 19 | return { 20 | type: PostRequest(MODULE_NAME) 21 | } 22 | } 23 | 24 | function postSuccess(data) { 25 | return { 26 | type: PostSuccess(MODULE_NAME), 27 | data 28 | } 29 | } 30 | 31 | function postError(error) { 32 | return { 33 | type: PostError(MODULE_NAME), 34 | error 35 | } 36 | } 37 | 38 | export function onChange(data) { 39 | return { 40 | type: OnChange(MODULE_NAME), 41 | data 42 | } 43 | } 44 | 45 | export function create() { 46 | return (dispatch, getState) => { 47 | dispatch(postRequest()); 48 | const { contactForm, speaker } = getState(); 49 | const data = { 50 | ...contactForm.form, 51 | profile: speaker.speaker.id 52 | } 53 | 54 | axios({ 55 | data, 56 | method: 'POST', 57 | url: ENDPOINT_URL, 58 | responseType: 'json' 59 | }).then(res => { 60 | console.log(res) 61 | if (res.errors) { 62 | return dispatch(showNotification(`${res.errors}`)); 63 | } 64 | dispatch(showNotification(`Your message will be sent to ${speaker.speaker.first_name}.`)); 65 | dispatch(postSuccess(res.data)); 66 | }).catch(err => { 67 | console.log(err) 68 | if (err.response && err.response.data) { 69 | const errorList = map(err.response.data, (v, k) => { 70 | return `${k}: ${v}`; 71 | }); 72 | dispatch( 73 | showNotification( 74 | `There was an error submitting your form. ${errorList.join( 75 | ' \n ' 76 | )}` 77 | ) 78 | ); 79 | } else { 80 | dispatch(postError(err)); 81 | } 82 | }); 83 | } 84 | } 85 | 86 | const initialState = { 87 | isInitialized: false, 88 | isLoading: false, 89 | isRequesting: false, 90 | form: { 91 | full_name: '', 92 | email: '', 93 | event_name: '', 94 | venue_name: '', 95 | event_date: '', 96 | event_time: '', 97 | speaker_compensation: 0, 98 | code_of_conduct: 0, 99 | comments: '', 100 | } 101 | } 102 | 103 | export const reducer = (state=initialState, action) => { 104 | switch (action.type) { 105 | case PostRequest(MODULE_NAME): { 106 | return { 107 | ...state, 108 | isRequesting: true 109 | } 110 | } 111 | 112 | case PostSuccess(MODULE_NAME): { 113 | return initialState 114 | } 115 | 116 | case PostError(MODULE_NAME): { 117 | return { 118 | ...state, 119 | isRequesting: false, 120 | error: action.error 121 | } 122 | } 123 | 124 | case OnChange(MODULE_NAME): { 125 | return { 126 | ...state, 127 | form: { 128 | ...state.form, 129 | ...action.data 130 | } 131 | } 132 | } 133 | 134 | default: 135 | return state 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, gender identity and expression, level of experience, 9 | nationality, personal appearance, race, religion, or sexual identity and 10 | orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at `info@womenandcolor.com`. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | -------------------------------------------------------------------------------- /app/redux/modules/topic.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | // App 3 | import { 4 | GetRequest, GetSuccess, GetError, PostRequest, PostSuccess, PostError 5 | } from './action_template'; 6 | import { BASE_URL_PATH } from 'appHelpers/constants'; 7 | import { getApiToken } from 'appRedux/modules/user'; 8 | 9 | const MODULE_NAME = 'topics'; 10 | const ENDPOINT_URL = `${BASE_URL_PATH}/api/v1/${MODULE_NAME}/`; 11 | 12 | // Actions 13 | export function getRequest() { 14 | return { 15 | type: GetRequest(MODULE_NAME) 16 | } 17 | } 18 | 19 | export function getSuccess(data) { 20 | return { 21 | type: GetSuccess(MODULE_NAME), 22 | data 23 | } 24 | } 25 | 26 | export function getError() { 27 | return { 28 | type: GetError(MODULE_NAME) 29 | } 30 | } 31 | 32 | export function postRequest() { 33 | return { 34 | type: PostRequest(MODULE_NAME) 35 | } 36 | } 37 | 38 | export function postSuccess(data) { 39 | return { 40 | type: PostSuccess(MODULE_NAME), 41 | data 42 | } 43 | } 44 | 45 | export function postError(error) { 46 | return { 47 | type: PostError(MODULE_NAME), 48 | error 49 | } 50 | } 51 | 52 | // Async Actions 53 | export function get() { 54 | return dispatch => { 55 | dispatch(getRequest()); 56 | 57 | axios.get(ENDPOINT_URL) 58 | .then(res => { 59 | dispatch(getSuccess(res.data)); 60 | }) 61 | .catch(err => { 62 | dispatch(getError(err)); 63 | console.log(err); 64 | }) 65 | } 66 | } 67 | 68 | export function create(topic) { 69 | return (dispatch, getState) => { 70 | dispatch(postRequest()); 71 | const token = getApiToken(); 72 | const authHeader = token ? `JWT ${token}` : null; 73 | 74 | axios({ 75 | method: 'POST', 76 | url: `${ENDPOINT_URL}`, 77 | data: { topic }, 78 | responseType: 'json', 79 | headers: { 80 | 'Authorization': authHeader 81 | } 82 | }).then(res => { 83 | console.log('topic post success', res.data) 84 | dispatch(postSuccess(res.data)); 85 | return res.data 86 | }).catch(err => { 87 | console.log(err); 88 | dispatch(postError(err)); 89 | }); 90 | } 91 | } 92 | const initialState = { 93 | isInitialized: false, 94 | isLoading: false, 95 | isRequesting: false, 96 | topics: [] 97 | } 98 | 99 | export const reducer = (state=initialState, action) => { 100 | switch (action.type) { 101 | case GetRequest(MODULE_NAME): { 102 | return { 103 | ...state, 104 | isLoading: true, 105 | isRequesting: true 106 | } 107 | } 108 | 109 | case GetSuccess(MODULE_NAME): { 110 | return { 111 | ...state, 112 | isInitialized: true, 113 | isLoading: false, 114 | isRequesting: false, 115 | topics: action.data 116 | } 117 | } 118 | 119 | case GetError(MODULE_NAME): { 120 | return { 121 | ...state, 122 | isRequesting: false, 123 | isLoading: false, 124 | error: action.error 125 | } 126 | } 127 | 128 | case PostRequest(MODULE_NAME): { 129 | return { 130 | ...state, 131 | isRequesting: true 132 | } 133 | } 134 | 135 | case PostSuccess(MODULE_NAME): { 136 | return { 137 | ...state, 138 | isRequesting: false, 139 | topics: [...state.topics].concat(action.data) 140 | } 141 | } 142 | 143 | case PostError(MODULE_NAME): { 144 | return { 145 | ...state, 146 | isRequesting: false, 147 | error: action.error 148 | } 149 | } 150 | 151 | default: 152 | return state 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /app/pages/EditProfile/Talks/Talks.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { connect } from 'react-redux'; 3 | import { bindActionCreators } from 'redux'; 4 | import Grid from 'material-ui/Grid'; 5 | import Input from 'material-ui/Input'; 6 | import { FormHelperText } from 'material-ui/Form'; 7 | import FormField from 'appCommon/FormField'; 8 | import Card, { CardActions, CardContent, CardMedia } from 'material-ui/Card'; 9 | import { onChange as onChangeUser } from 'appRedux/modules/user'; 10 | import { 11 | update as updateTalk, 12 | create as createTalk, 13 | destroy as destroyTalk, 14 | } from 'appRedux/modules/featuredTalk'; 15 | 16 | import css from './style.css'; 17 | import StyledButton from 'appCommon/StyledButton'; 18 | import EditTalk from './EditTalk'; 19 | 20 | const emptyTalk = { 21 | event_name: '', 22 | talk_title: '', 23 | url: '', 24 | image: '', 25 | }; 26 | 27 | class TalksContainer extends Component { 28 | state = { 29 | talks: this.props.profile.featured_talks || [], 30 | }; 31 | 32 | componentWillReceiveProps(nextProps) { 33 | if ( 34 | nextProps.profile.featured_talks !== this.props.profile.featured_talks 35 | ) { 36 | this.setState({ talks: nextProps.profile.featured_talks }); 37 | } 38 | } 39 | 40 | saveTalk = talkData => { 41 | if (talkData.id) { 42 | this.props.updateTalk({ ...talkData, profile: this.props.profile.id }); 43 | } else { 44 | this.props.createTalk({ ...talkData, profile: this.props.profile.id }); 45 | } 46 | }; 47 | 48 | destroyTalk = talkData => { 49 | this.props.destroyTalk(talkData); 50 | }; 51 | 52 | addEmptyTalk = () => { 53 | this.setState({ talks: this.state.talks.concat(emptyTalk) }); 54 | }; 55 | 56 | render() { 57 | const { talks } = this.state; 58 | 59 | return ( 60 |
61 |
62 |

Edit your featured talks

63 |
64 | 65 |
66 | {talks.map((talk, index) => ( 67 | 75 | ))} 76 |
77 | 78 | {talks.length < 6 && ( 79 |
80 | = 7} 82 | color="secondary" 83 | className={css.addNewTalk} 84 | onClick={this.addEmptyTalk} 85 | > 86 | Add new talk 87 | 88 |
89 | )} 90 |
91 | ); 92 | } 93 | } 94 | 95 | function mapStateToProps(state) { 96 | return { 97 | user: state.user, 98 | profile: state.profile, 99 | notification: state.notification.message, 100 | }; 101 | } 102 | 103 | function mapDispatchToProps(dispatch) { 104 | return { 105 | showNotification: message => { 106 | dispatch(showNotification(message)); 107 | }, 108 | hideNotification: () => { 109 | dispatch(hideNotification()); 110 | }, 111 | createTalk: data => { 112 | dispatch(createTalk(data)); 113 | }, 114 | updateTalk: data => { 115 | dispatch(updateTalk(data)); 116 | }, 117 | destroyTalk: data => { 118 | dispatch(destroyTalk(data)); 119 | }, 120 | }; 121 | } 122 | 123 | export default connect(mapStateToProps, mapDispatchToProps)(TalksContainer); 124 | -------------------------------------------------------------------------------- /app/pages/Account/Login.js: -------------------------------------------------------------------------------- 1 | // Project 2 | import React, { Component } from 'react' 3 | import { connect } from 'react-redux' 4 | import TextField from 'material-ui/TextField'; 5 | import Button from 'material-ui/Button'; 6 | import Select from 'material-ui/Select'; 7 | import Grid from 'material-ui/Grid'; 8 | import Input, { InputLabel } from 'material-ui/Input'; 9 | import { MenuItem } from 'material-ui/Menu'; 10 | import { Link } from 'react-router-dom' 11 | import Card from 'material-ui/Card'; 12 | import { Helmet } from "react-helmet"; 13 | import { push } from 'react-router-redux'; 14 | 15 | // App 16 | import { 17 | onChange as onChangeUser, 18 | login as submitForm 19 | } from 'appRedux/modules/user'; 20 | import StyledButton from 'appCommon/StyledButton'; 21 | import FormField from 'appCommon/FormField'; 22 | import AccountFormContainer from './AccountFormContainer'; 23 | 24 | import css from './styles.css'; 25 | 26 | const Login = (props) => { 27 | const generateHandlerUser = (fieldName) => { 28 | return (event) => { props.handleUserInputChange(fieldName, event.currentTarget.value) } 29 | } 30 | 31 | return( 32 |
33 | 34 |
35 |

Log in

36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | Forgot your password? 48 | 49 | 50 | Submit 51 | 52 | 53 | 54 |
55 |
56 | 57 | 58 |

If you have not created an account yet, then please sign up first.

59 |
60 |
61 |
62 | ) 63 | } 64 | 65 | 66 | class LoginContainer extends Component { 67 | constructor(props) { 68 | super(props) 69 | } 70 | 71 | render() { 72 | return( 73 |
74 | 75 | Log in 76 | 77 | 78 | { 80 | event.preventDefault(); 81 | this.props.submitForm(this.props.user); 82 | }} 83 | handleUserInputChange={(field, value) => { 84 | this.props.onChangeUser({ [field]: value }) 85 | }} 86 | {...this.props} 87 | /> 88 |
89 | ) 90 | } 91 | } 92 | 93 | function mapStateToProps(state) { 94 | return { 95 | user: state.user, 96 | } 97 | } 98 | 99 | function mapDispatchToProps(dispatch, props) { 100 | return { 101 | onChangeUser: (attrs) => { 102 | dispatch(onChangeUser(attrs)) 103 | }, 104 | fetchLocations: () => { 105 | dispatch(fetchLocations()) 106 | }, 107 | submitForm: (user) => { 108 | dispatch(submitForm(user)); 109 | }, 110 | goToProfile: () => { 111 | dispatch(push('/')); 112 | } 113 | } 114 | } 115 | 116 | export default connect( 117 | mapStateToProps, 118 | mapDispatchToProps 119 | )(LoginContainer); 120 | -------------------------------------------------------------------------------- /app/pages/Speaker/Speaker.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { connect } from 'react-redux'; 3 | import { getSpeaker } from 'appRedux/modules/speaker'; 4 | import Grid from 'material-ui/Grid'; 5 | import ReactLoading from 'react-loading'; 6 | import { Helmet } from "react-helmet"; 7 | 8 | // App 9 | import SpeakerCard from './components/SpeakerCard'; 10 | import SpeakerInfo from './components/SpeakerInfo'; 11 | import FeaturedTalks from './components/FeaturedTalks'; 12 | import MessageSpeakerForm from './components/MessageSpeakerForm'; 13 | import Banner from 'appCommon/Banner'; 14 | 15 | const Speaker = props => { 16 | const { speaker } = props; 17 | return ( 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | {speaker.description && } 29 | {(!!speaker.featured_talks.length) && } 30 | 31 | 32 | 33 | 34 | 35 | ); 36 | }; 37 | 38 | class SpeakerContainer extends Component { 39 | constructor(props) { 40 | super(props); 41 | this.state = {}; 42 | } 43 | 44 | componentDidMount() { 45 | const { match: { params: { id, fullName } } } = this.props; 46 | this.props.getSpeaker(id, fullName); 47 | } 48 | 49 | componentWillReceiveProps(nextProps) { 50 | const { match: { params: { id: currentId } } } = this.props; 51 | const { match: { params: { id: nextId } } } = nextProps; 52 | if (!!nextId && nextId !== currentId) { 53 | this.props.getSpeaker(nextId); 54 | } 55 | } 56 | 57 | generateTitle = speaker => { 58 | if (speaker) { 59 | const firstName = speaker.first_name || "Speaker"; 60 | const lastName = speaker.last_name || "Profile"; 61 | const position = speaker.position || "undisclosed position"; 62 | const organization = speaker.organization || "undisclosed organization"; 63 | 64 | 65 | return `${firstName} ${lastName}, ${position} at ${organization}` 66 | } 67 | 68 | return 'Speaker Profile'; 69 | } 70 | 71 | generateDescription = speaker => { 72 | if (speaker) { 73 | const threeTopics = speaker.topics.slice(0,2).map(topic => topic.topic).join(', ') 74 | return `${speaker.first_name} ${speaker.last_name} is available for speaking opportunities at tech-related events on ${threeTopics} and more.` 75 | } 76 | 77 | return 'Find talented diverse speakers for tech-related events' 78 | } 79 | 80 | render() { 81 | const { speaker } = this.props; 82 | return( 83 |
84 | 85 | {this.generateTitle(speaker)} 86 | 87 | 88 | { 89 | this.props.speaker ? ( 90 | 91 | ) : ( 92 | 93 | ) 94 | } 95 |
96 | ) 97 | } 98 | } 99 | 100 | function mapStateToProps(state) { 101 | return { 102 | speaker: state.speaker.speaker, 103 | notification: state.notification.message, 104 | }; 105 | } 106 | 107 | function mapDispatchToProps(dispatch, props) { 108 | return { 109 | getSpeaker: (id, fullName) => { 110 | dispatch(getSpeaker(id, fullName)); 111 | }, 112 | }; 113 | } 114 | 115 | export default connect(mapStateToProps, mapDispatchToProps)(SpeakerContainer); 116 | -------------------------------------------------------------------------------- /app/pages/Onboarding/Social/Social.js: -------------------------------------------------------------------------------- 1 | // NPM 2 | import React, { Component } from 'react' 3 | import { connect } from 'react-redux' 4 | import TextField from 'material-ui/TextField'; 5 | import Button from 'material-ui/Button'; 6 | import Select from 'material-ui/Select'; 7 | import Input, { InputLabel } from 'material-ui/Input'; 8 | import { MenuItem } from 'material-ui/Menu'; 9 | import Radio, { RadioGroup } from 'material-ui/Radio'; 10 | import { FormLabel, FormControlLabel } from 'material-ui/Form'; 11 | import { Link } from 'react-router-dom'; 12 | import { Helmet } from "react-helmet"; 13 | 14 | // App 15 | import { 16 | update as updateProfile, 17 | onChange as onChangeProfile 18 | } from 'appRedux/modules/profile'; 19 | import StyledButton from 'appCommon/StyledButton'; 20 | import FormField from 'appCommon/FormField'; 21 | import css from '../styles.css' 22 | 23 | 24 | const CURRENT_PAGE = 'social'; 25 | 26 | const Social = (props) => { 27 | 28 | const generateHandler = (fieldName) => { 29 | return (event) => { props.handleProfileInputChange(fieldName, event.currentTarget.value) } 30 | } 31 | 32 | return( 33 |
34 |
35 |

Be a little social

36 | 37 | 38 | 43 | 44 | 45 | 46 | 51 | 52 | 53 | 54 | 59 | 60 | 61 |
62 | 63 | Save and continue 64 | 65 |
66 |
67 | 68 |
69 | ) 70 | } 71 | 72 | class SocialContainer extends Component { 73 | constructor(props) { 74 | super(props) 75 | this.state = {} 76 | props.onChangeProfile({ current_page: CURRENT_PAGE }); 77 | } 78 | 79 | render() { 80 | const props = this.props; 81 | 82 | return( 83 |
84 | 85 | Get started - Social 86 | 87 | 88 | { 90 | event.preventDefault(); 91 | props.updateProfile(); 92 | }} 93 | handleProfileInputChange={(field, value) => { 94 | props.onChangeProfile({ [field]: value }) 95 | }} 96 | {...this.props} 97 | /> 98 |
99 | ) 100 | } 101 | } 102 | 103 | function mapStateToProps(state) { 104 | return { 105 | user: state.user, 106 | profile: state.profile, 107 | } 108 | } 109 | 110 | function mapDispatchToProps(dispatch, props) { 111 | return { 112 | onChangeProfile: (attrs) => { 113 | dispatch(onChangeProfile(attrs)) 114 | }, 115 | updateProfile: () => { 116 | dispatch(updateProfile()); 117 | }, 118 | } 119 | } 120 | 121 | export default connect( 122 | mapStateToProps, 123 | mapDispatchToProps 124 | )(SocialContainer); 125 | -------------------------------------------------------------------------------- /app/pages/Account/ConfirmResetPassword.js: -------------------------------------------------------------------------------- 1 | // Project 2 | import React, { Component } from 'react' 3 | import { connect } from 'react-redux' 4 | import TextField from 'material-ui/TextField'; 5 | import Button from 'material-ui/Button'; 6 | import Select from 'material-ui/Select'; 7 | import Grid from 'material-ui/Grid'; 8 | import Input, { InputLabel } from 'material-ui/Input'; 9 | import { MenuItem } from 'material-ui/Menu'; 10 | import { Link } from 'react-router-dom'; 11 | import Card from 'material-ui/Card'; 12 | import { Helmet } from "react-helmet"; 13 | 14 | // App 15 | import { 16 | onChange as onChangeUser, 17 | confirmResetPassword as submitForm 18 | } from 'appRedux/modules/user'; 19 | import StyledButton from 'appCommon/StyledButton'; 20 | import FormField from 'appCommon/FormField'; 21 | import AccountFormContainer from './AccountFormContainer'; 22 | 23 | import css from './styles.css'; 24 | 25 | const ConfirmResetPassword = (props) => { 26 | const generateHandler = (fieldName) => { 27 | return (event) => { props.handleUserInputChange(fieldName, event.currentTarget.value) } 28 | } 29 | 30 | return( 31 |
32 | 33 |
34 |

Enter your new password

35 | 36 | 37 | 44 | 45 | 46 | 47 | 54 | 55 | 56 | 57 | 58 | Submit 59 | 60 | 61 | 62 |
63 |
64 |
65 | ) 66 | } 67 | 68 | 69 | class ConfirmResetPasswordContainer extends Component { 70 | constructor(props) { 71 | super(props) 72 | this.state = {} 73 | } 74 | 75 | componentDidMount() { 76 | const { match: { params: { uid, token } } } = this.props; 77 | this.setState({ uid, token }) 78 | } 79 | 80 | render() { 81 | return( 82 |
83 | 84 | Set a new password 85 | 86 | 87 | { 89 | event.preventDefault(); 90 | this.props.submitForm(this.state.uid, this.state.token); 91 | }} 92 | handleUserInputChange={(field, value) => { 93 | this.props.onChangeUser({ [field]: value }) 94 | }} 95 | {...this.props} 96 | /> 97 |
98 | ) 99 | } 100 | } 101 | 102 | function mapStateToProps(state) { 103 | return { 104 | user: state.user, 105 | } 106 | } 107 | 108 | function mapDispatchToProps(dispatch, props) { 109 | return { 110 | onChangeUser: (attrs) => { 111 | dispatch(onChangeUser(attrs)) 112 | }, 113 | fetchLocations: () => { 114 | dispatch(fetchLocations()) 115 | }, 116 | submitForm: (uid, token) => { 117 | dispatch(submitForm(uid, token)); 118 | } 119 | } 120 | } 121 | 122 | export default connect( 123 | mapStateToProps, 124 | mapDispatchToProps 125 | )(ConfirmResetPasswordContainer); 126 | -------------------------------------------------------------------------------- /app/common/Footer/FullFooter.js: -------------------------------------------------------------------------------- 1 | // NPM 2 | import React from 'react'; 3 | import Grid from 'material-ui/Grid'; 4 | import FontAwesomeIcon from '@fortawesome/react-fontawesome'; 5 | import faGithub from '@fortawesome/fontawesome-free-brands/faGithub'; 6 | import faTwitter from '@fortawesome/fontawesome-free-brands/faTwitter'; 7 | import faInstagram from '@fortawesome/fontawesome-free-brands/faInstagram'; 8 | import { Link } from 'react-router-dom'; 9 | 10 | // APP 11 | import css from './styles.css'; 12 | import Tribalscale from 'svg-react-loader?name=Tribalscale!../../assets/Tribalscale.svg'; 13 | import Wealthsimple from 'svg-react-loader?name=Wealthsimple!../../assets/Wealthsimple.svg'; 14 | 15 | const FullFooter = () => { 16 | return ( 17 |
18 | 24 | 25 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 48 | 49 | 50 | 51 | About us 52 | Stay in touch 53 | 54 | 60 | 64 | 65 | 66 | 67 | 68 | 69 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 86 | 87 | 88 | 89 | Terms of Service 90 | Privacy Policy 91 | Code of Conduct 92 | 93 | 99 | © 2016 - 2018 Women and Color 100 | 101 | 102 | 103 | 104 |
105 | ); 106 | }; 107 | 108 | export default FullFooter; 109 | -------------------------------------------------------------------------------- /app/pages/Account/Register.js: -------------------------------------------------------------------------------- 1 | // Project 2 | import React, { Component } from 'react' 3 | import { connect } from 'react-redux' 4 | import TextField from 'material-ui/TextField'; 5 | import Button from 'material-ui/Button'; 6 | import Select from 'material-ui/Select'; 7 | import Grid from 'material-ui/Grid'; 8 | import Input, { InputLabel } from 'material-ui/Input'; 9 | import { MenuItem } from 'material-ui/Menu'; 10 | import { Link } from 'react-router-dom' 11 | import { Helmet } from "react-helmet"; 12 | 13 | // App 14 | import { 15 | onChange as onChangeUser, 16 | create as submitForm 17 | } from 'appRedux/modules/user'; 18 | import StyledButton from 'appCommon/StyledButton'; 19 | import FormField from 'appCommon/FormField'; 20 | import AccountFormContainer from './AccountFormContainer'; 21 | 22 | import css from './styles.css'; 23 | 24 | const CURRENT_PAGE = 'registration'; 25 | 26 | const Register = (props) => { 27 | const generateHandlerUser = (fieldName) => { 28 | return (event) => { props.handleUserInputChange(fieldName, event.currentTarget.value) } 29 | } 30 | 31 | return( 32 |
33 | 34 |
35 |

Sign up

36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | Create profile 52 | 53 | 54 | 55 |
56 |
57 | 58 | 59 |

Already have an account? Then please sign in.

60 |
61 |
62 |
63 | ) 64 | } 65 | 66 | 67 | class RegisterContainer extends Component { 68 | constructor(props) { 69 | super(props) 70 | this.state = {} 71 | this.props.onChangeUser({ page: CURRENT_PAGE }); 72 | } 73 | 74 | render() { 75 | return( 76 |
77 | 78 | Register 79 | 80 | 81 | { 83 | event.preventDefault(); 84 | this.props.submitForm(this.props.user, CURRENT_PAGE); 85 | }} 86 | handleUserInputChange={(field, value) => { 87 | this.props.onChangeUser({ [field]: value }) 88 | }} 89 | {...this.props} 90 | /> 91 |
92 | ) 93 | } 94 | } 95 | 96 | function mapStateToProps(state) { 97 | return { 98 | user: state.user, 99 | notification: state.notification.message 100 | } 101 | } 102 | 103 | function mapDispatchToProps(dispatch, props) { 104 | return { 105 | onChangeUser: (attrs) => { 106 | dispatch(onChangeUser(attrs)) 107 | }, 108 | fetchLocations: () => { 109 | dispatch(fetchLocations()) 110 | }, 111 | submitForm: (user, page) => { 112 | dispatch(submitForm(user, page)); 113 | } 114 | } 115 | } 116 | 117 | export default connect( 118 | mapStateToProps, 119 | mapDispatchToProps 120 | )(RegisterContainer); 121 | -------------------------------------------------------------------------------- /webpack.config.babel.js: -------------------------------------------------------------------------------- 1 | import webpack from 'webpack'; 2 | import path from 'path'; 3 | import HtmlWebpackPlugin from 'html-webpack-plugin'; 4 | import cssvariables from 'postcss-css-variables'; 5 | import globalCssVars from './app/sharedStyles/cssVariables.js' 6 | import autoprefixer from 'autoprefixer'; 7 | 8 | module.exports = env => { 9 | const HtmlWebpackPluginConfig = new HtmlWebpackPlugin({ 10 | template: __dirname + '/app/index.html', 11 | filename: 'index.html', 12 | inject: 'body', 13 | }); 14 | 15 | const PATHS = { 16 | app: path.join(__dirname, 'app/', 'entry/', 'index.js'), 17 | build: path.join(__dirname, 'dist'), 18 | }; 19 | 20 | const NODE_ENV = env.NODE_ENV 21 | 22 | const isProduction = NODE_ENV === 'production'; 23 | process.env.BABEL_ENV = NODE_ENV; 24 | 25 | const envPlugin = new webpack.DefinePlugin({ 26 | 'process.env': { 27 | NODE_ENV: JSON.stringify(NODE_ENV), 28 | REACT_APP_API_URL: JSON.stringify(process.env.REACT_APP_API_URL) 29 | }, 30 | }); 31 | 32 | const baseEntry = { 33 | app: PATHS.app 34 | }; 35 | 36 | const developEntry = {}; 37 | Object.keys(baseEntry).forEach(function(key) { 38 | var entryPoint = baseEntry[key]; 39 | developEntry[key] = [ 40 | 'webpack-dev-server/client?http://0.0.0.0:8080', 41 | 'webpack/hot/only-dev-server', 42 | entryPoint, 43 | ]; 44 | }); 45 | 46 | const base = { 47 | entry: baseEntry, 48 | output: { 49 | path: PATHS.build, 50 | filename: '[name].js', 51 | }, 52 | module: { 53 | loaders: [ 54 | { test: /\.js$|\.jsx$/, exclude: /node_modules/, loader: 'babel-loader' }, 55 | { 56 | test: /\.css$/, 57 | exclude: /node_modules/, 58 | use: [ 59 | 'style-loader', 60 | { 61 | loader: 'css-loader', 62 | options: { 63 | sourceMap: true, 64 | modules: true, 65 | localIdentName: '[name]__[local]___[hash:base64:5]', 66 | }, 67 | }, 68 | { 69 | loader: 'postcss-loader', 70 | options: { 71 | ident: 'postcss', 72 | plugins: () => [ 73 | cssvariables({ 74 | preserve: true, 75 | variables: globalCssVars 76 | }), 77 | autoprefixer({ 78 | browsers: ['last 2 versions', 'IE 11'], 79 | }) 80 | ], 81 | }, 82 | }, 83 | ], 84 | }, 85 | ], 86 | }, 87 | resolve: { 88 | modules: [path.resolve('./app'), path.resolve('./node_modules')], 89 | alias: { 90 | appCommon: path.resolve(__dirname, 'app/common/'), 91 | appPages: path.resolve(__dirname, 'app/pages/'), 92 | appConfig: path.resolve(__dirname, 'app/config/'), 93 | appRedux: path.resolve(__dirname, 'app/redux/'), 94 | appHelpers: path.resolve(__dirname, 'app/helpers/'), 95 | appAssets: path.resolve(__dirname, 'app/assets/'), 96 | appSharedStyles: path.resolve(__dirname, 'app/sharedStyles/'), 97 | }, 98 | extensions: ['.js', '.jsx'] 99 | }, 100 | }; 101 | 102 | const developmentConfig = { 103 | entry: developEntry, 104 | devtool: 'cheap-module-inline-source-map', 105 | devServer: { 106 | contentBase: PATHS.build, 107 | hot: true, 108 | inline: true, 109 | }, 110 | output: { 111 | path: PATHS.build, 112 | filename: '[name].js', 113 | publicPath: 'http://localhost:8080/', 114 | }, 115 | plugins: [HtmlWebpackPluginConfig, envPlugin, new webpack.HotModuleReplacementPlugin()], 116 | }; 117 | 118 | const productionConfig = { 119 | devtool: 'cheap-module-source-map', 120 | plugins: [HtmlWebpackPluginConfig, envPlugin], 121 | }; 122 | 123 | const envConfig = env.NODE_ENV==='production' ? productionConfig : developmentConfig 124 | 125 | return { 126 | ...base, 127 | ...envConfig 128 | } 129 | } -------------------------------------------------------------------------------- /app/redux/modules/speaker.js: -------------------------------------------------------------------------------- 1 | // NPM 2 | import { map, compact, uniqBy } from 'lodash'; 3 | import { equals } from 'ramda'; 4 | import { push } from 'react-router-redux'; 5 | import axios from 'axios'; 6 | 7 | // App 8 | import { 9 | BASE_URL_PATH, 10 | IDENTITIES, 11 | DEFAULT_SPEAKER_LIMIT, 12 | } from 'appHelpers/constants'; 13 | import { generateQueryString } from 'appHelpers/queryParams'; 14 | import { speakerToNamePath, speakerToProfilePath } from 'appHelpers/url'; 15 | import { showNotification } from './notification'; 16 | import { getApiToken } from './user' 17 | 18 | const MODULE_NAME = 'SPEAKER'; 19 | 20 | const GET_SPEAKER = `${MODULE_NAME}/GET_SPEAKER`; 21 | const UPDATE_SPEAKER = `${MODULE_NAME}/UPDATE_SPEAKER`; 22 | const UPDATE_SPEAKERS = `${MODULE_NAME}/UPDATE_SPEAKERS`; 23 | const UPDATE_SPEAKERS_START = `${MODULE_NAME}/UPDATE_SPEAKERS_START`; 24 | const UPDATE_SEARCH_PARAMS = `${MODULE_NAME}/UPDATE_SEARCH_PARAMS`; 25 | const UPDATE_SELECTION = `${MODULE_NAME}/UPDATE_SELECTION`; 26 | 27 | // Sync Action 28 | export function updateSpeakersStart(append) { 29 | return { type: UPDATE_SPEAKERS_START, append }; 30 | } 31 | 32 | export function updateSpeakers(results, append) { 33 | return { type: UPDATE_SPEAKERS, results, append }; 34 | } 35 | 36 | export function updateSpeaker(result) { 37 | return { type: UPDATE_SPEAKER, result }; 38 | } 39 | 40 | export function updateSearchParams(params) { 41 | return { type: UPDATE_SEARCH_PARAMS, params }; 42 | } 43 | 44 | // Async Actions 45 | export function fetchSpeakers(params = {}) { 46 | const queryStringforApi = generateQueryString({ params, display: false }); 47 | const queryStringforDisplay = generateQueryString({ params, display: true }); 48 | 49 | return dispatch => { 50 | dispatch(updateSpeakersStart(params.append)); 51 | 52 | axios 53 | .get(`${BASE_URL_PATH}/api/v1/profiles?${queryStringforApi}`) 54 | .then(res => { 55 | dispatch(push(`?${queryStringforDisplay}`)) 56 | dispatch(updateSpeakers(res.data, params.append)); 57 | }) 58 | .catch(err => console.log(err)); 59 | }; 60 | } 61 | 62 | export function getSpeaker(id, fullName = '') { 63 | return dispatch => { 64 | axios 65 | .get(`${BASE_URL_PATH}/api/v1/profiles/${id}`) 66 | .then(res => { 67 | console.log('res', res) 68 | dispatch(updateSpeaker(res.data)); 69 | if (!equals(fullName, speakerToNamePath(res.data))) { 70 | const speakerProfilePath = speakerToProfilePath(res.data); 71 | dispatch(push(speakerProfilePath)); 72 | } 73 | }) 74 | .catch(err => { 75 | console.log('err', err) 76 | dispatch(showNotification('This profile is not available.')) 77 | dispatch(push('/')) 78 | }); 79 | }; 80 | } 81 | 82 | // Reducer 83 | const INITIAL_STATE = { 84 | results: [], 85 | endOfResults: false, 86 | searchParams: { offset: 0, limit: DEFAULT_SPEAKER_LIMIT }, 87 | selectedLocation: null, 88 | selectedIdentity: IDENTITIES[0].label, 89 | speaker: null, 90 | isLoading: false, 91 | }; 92 | 93 | export const reducer = (state = INITIAL_STATE, action) => { 94 | switch (action.type) { 95 | case UPDATE_SPEAKERS_START: 96 | return { 97 | ...state, 98 | isLoading: true, 99 | }; 100 | case UPDATE_SPEAKERS: 101 | if (action.append) { 102 | return { 103 | ...state, 104 | results: uniqBy(state.results.concat(action.results), 'id'), 105 | endOfResults: action.results.length < DEFAULT_SPEAKER_LIMIT, 106 | isLoading: false, 107 | }; 108 | } 109 | return { 110 | ...state, 111 | results: uniqBy(action.results, 'id'), 112 | endOfResults: action.results.length < DEFAULT_SPEAKER_LIMIT, 113 | isLoading: false, 114 | }; 115 | case UPDATE_SPEAKER: 116 | return { 117 | ...state, 118 | speaker: action.result, 119 | }; 120 | case UPDATE_SEARCH_PARAMS: 121 | return { 122 | ...state, 123 | searchParams: { 124 | ...state.searchParams, 125 | ...action.params, 126 | }, 127 | }; 128 | default: 129 | return state; 130 | } 131 | }; 132 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Women and Color: Frontend Web UI 2 | 3 | [**Women and Color**][site-live] is an online community of talented women and people of color available for speaking opportunities at tech-related events. 4 | 5 | ## Table of Contents 6 | 7 | * [Get Involved](#get-involved) 8 | * [Architecture](#architecture) 9 | * [Local Development](#local-development) 10 | * [Deployment](#deployment) 11 | * [License & Copyright](#license--copyright) 12 | 13 | ## Get Involved 14 | 15 | 1. Review our [Contributor Guidelines][contributing] and [Code of 16 | Conduct][conduct] 17 | 2. Jump into our Slack chat channel, `#womenandcolor` 18 | * Anyone can [request an invite][slack-invite] to the **CivicTech 19 | Toronto** slack team. 20 | * Ping one of our project members (@heymosef, @sharonk) and 21 | say hey! 22 | 3. Check out our [Roadmap][roadmap] and [Task Tracker][task-tracker] to 23 | see what we're working on. 24 | 4. Join us at a [weekly CivicTech Toronto hacknight][meetup]. 25 | * Join the meetup and RSVP to see the location. 26 | * We're there most every week, but you might want to jump into chat 27 | first, and ask us to make sure! 28 | 29 | ## Architecture 30 | 31 | This code repository is the visual _frontend_ part of the website. It's built in React using Material UI and CSS modules. 32 | 33 | The other [`CivicTechTO/women-and-color-backend`][code-backend] code 34 | repository powers the backend API, storing and retreiving data from the 35 | database that underpins the website. 36 | 37 | ### Technology Used 38 | 39 | * ReactJS 40 | * Heroku 41 | 42 | ## Local Development 43 | 44 | #### Dependencies 45 | 46 | * Node.JS 8.x (Recommended install is [via `nvm`][node-install]) 47 | 48 | #### Setting up and running the app 49 | 50 | ``` 51 | npm install --global yarn 52 | ``` 53 | 54 | Now (and whenever you pull new package changes), do this: 55 | 56 | ``` 57 | yarn install 58 | yarn start 59 | ``` 60 | 61 | That's it! Your local development site is now available at: 62 | [http://localhost:8080/](http://localhost:8080/) 63 | 64 | 65 | #### Integrating with the API 66 | In order to use the API functionality, the backend code needs to be set up locally as well. The set up steps are: 67 | - Go to the [backend repo][code-backend] to clone the repo 68 | - Follow the setup instructions there 69 | - Use the development site [http://localhost:8000/](http://localhost:8000/) 70 | 71 | ## Deployment 72 | 73 | [**Heroku**][heroku] is a platform for easily deploying applications. 74 | 75 | A [**buildpack**][buildpack] provides framework and runtime support for apps running on 76 | platforms like Heroku. 77 | 78 | * We auto-deploy the `staging` branch to our staging website on Heroku: 79 | [`womenandcolor-staging.herokuapp.com`][site-staging]. (So merging a 80 | pull request also auto-deploys!) 81 | * We use the 82 | [`create-react-app-buildpack`](https://github.com/mars/create-react-app-buildpack) 83 | to handle many deployment settings. 84 | 85 | Please see [CONTRIBUTING.md][contributing] for important details, including our use of: 86 | 87 | * **Heroku Review Apps** 88 | 89 | ## License & Copyright 90 | 91 | Copyright (C) 2017 Women and Color 92 | This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, version 3.0. 93 | 94 | This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. 95 | 96 | See the [`LICENSE`](/LICENSE) file for details. 97 | 98 | 99 | [site-live]: https://www.womenandcolor.com/ 100 | [site-staging]: https://womenandcolor-staging.herokuapp.com/ 101 | [contributing]: CONTRIBUTING.md 102 | [conduct]: CONDUCT.md 103 | [code-backend]: https://github.com/CivicTechTO/women-and-color-backend 104 | [heroku]: https://github.com/CivicTechTO/women-and-color-backend 105 | [buildpack]: https://docs.cloudfoundry.org/buildpacks/ 106 | [license]: LICENSE 107 | [node-install]: https://nodejs.org/en/download/package-manager/#nvm 108 | [slack-invite]: https://civictechto-slack-invite.herokuapp.com 109 | [meetup]: https://www.meetup.com/Civic-Tech-Toronto/ 110 | [task-tracker]: https://trello.com/b/DwTxOhMB 111 | [roadmap]: https://trello.com/b/OB0S6wZq 112 | -------------------------------------------------------------------------------- /app/pages/EditProfile/EmailSettings/EmailSettings.js: -------------------------------------------------------------------------------- 1 | // NPM 2 | import React, { Component } from 'react'; 3 | import { connect } from 'react-redux'; 4 | import Grid from 'material-ui/Grid'; 5 | import Checkbox from 'material-ui/Checkbox'; 6 | import {FormControlLabel} from 'material-ui/Form'; 7 | import ReactLoading from 'react-loading'; 8 | import { find, remove } from 'lodash'; 9 | 10 | // App 11 | import { update as updateProfile, onChange as onChangeProfile } from 'appRedux/modules/profile'; 12 | import { get as getSubscriptionGroups } from 'appRedux/modules/subscriptionGroup' 13 | import StyledButton from 'appCommon/StyledButton'; 14 | import FormField from 'appCommon/FormField'; 15 | 16 | import css from './styles.css'; 17 | 18 | const EmailSettings = props => { 19 | if (!props.user.id) { 20 | return
User is not found
; 21 | } 22 | 23 | const group_options = props.subscription_groups || []; 24 | 25 | const handleSubscriptionGroups = (e) => { 26 | const group = find(props.subscription_groups, g => g.group_id === e.target.value) 27 | let selectedGroups = [...props.profile.subscription_groups]; 28 | 29 | if (e.target.checked) { 30 | selectedGroups.push(group); 31 | } else { 32 | remove(selectedGroups, g => g.group_id === group.group_id); 33 | } 34 | 35 | props.handleProfileInputChange('subscription_groups', selectedGroups); 36 | } 37 | 38 | 39 | return ( 40 |
41 |
42 |

Edit your communication settings

43 |
44 | 45 |
46 | 47 | 48 | 49 | 50 | 51 | { 52 | group_options.map(group => { 53 | const checked = find(props.profile.subscription_groups, g => g.group_id === group.group_id) 54 | return ( 55 | 64 | } 65 | label={group.label} 66 | /> 67 | ) 68 | }) 69 | } 70 | 71 | 72 | 73 | 74 | Save 75 | 76 | 77 | 78 | 79 |
80 |
81 | ); 82 | }; 83 | 84 | class EmailSettingsContainer extends React.Component { 85 | componentDidMount() { 86 | this.props.getSubscriptionGroups(); 87 | } 88 | 89 | 90 | render() { 91 | const { props } = this; 92 | if (!props.user.isInitialized || props.user.isLoading) { 93 | return ; 94 | } 95 | 96 | return ( 97 | { 99 | event.preventDefault(); 100 | props.updateProfile(); 101 | }} 102 | handleProfileInputChange={(field, value) => { 103 | props.onChangeProfile({ [field]: value }); 104 | }} 105 | {...props} 106 | /> 107 | ); 108 | } 109 | 110 | } 111 | 112 | function mapStateToProps(state) { 113 | return { 114 | user: state.user, 115 | profile: state.profile, 116 | subscription_groups: state.subscriptionGroup.groups, 117 | }; 118 | } 119 | 120 | function mapDispatchToProps(dispatch) { 121 | return { 122 | onChangeProfile: attrs => { 123 | dispatch(onChangeProfile(attrs)); 124 | }, 125 | updateProfile: () => { 126 | dispatch(updateProfile()); 127 | }, 128 | getSubscriptionGroups: () => { 129 | dispatch(getSubscriptionGroups()); 130 | } 131 | }; 132 | } 133 | 134 | export default connect(mapStateToProps, mapDispatchToProps)(EmailSettingsContainer); 135 | -------------------------------------------------------------------------------- /app/pages/Speaker/components/SpeakerCard.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react'; 2 | import Card, { CardContent } from 'material-ui/Card'; 3 | import List, { ListItem, ListItemText } from 'material-ui/List'; 4 | import Grid from 'material-ui/Grid'; 5 | 6 | import { withStyles } from 'material-ui/styles'; 7 | 8 | // App 9 | import css from '../styles.css'; 10 | import { profilePhoto } from 'appSharedStyles/styles.css'; 11 | import { pronounDict } from 'appHelpers/constants'; 12 | import { ensureAbsoluteUrl } from 'appHelpers/url'; 13 | import Topics from './Topics'; 14 | 15 | const styles = (theme) => ({ 16 | city: { 17 | backgroundColor: theme.palette.secondary.light, 18 | textAlign: 'center', 19 | borderTop: `1px solid ${theme.palette.secondary.main}`, 20 | }, 21 | pronouns: { 22 | backgroundColor: theme.palette.primary.light, 23 | textAlign: 'center', 24 | borderTop: `1px solid ${theme.palette.secondary.main}`, 25 | }, 26 | socials: { 27 | backgroundColor: theme.palette.primary.contrastText, 28 | textAlign: 'center', 29 | justifyContent: 'space-around', 30 | borderTop: `1px solid ${theme.palette.secondary.main}`, 31 | }, 32 | card: { 33 | borderRadius: '8px', 34 | border: `1px solid ${theme.palette.secondary.main}`, 35 | }, 36 | photo: { 37 | maxWidth: '128px', 38 | marginBottom: '1rem', 39 | marginTop: '1rem', 40 | }, 41 | listItemText: { 42 | padding: '0', 43 | }, 44 | }); 45 | 46 | const SpeakerCard = ({ speaker, classes }) => { 47 | const hasSocial = speaker.linkedin || speaker.twitter || speaker.website; 48 | 49 | return ( 50 | 51 | 52 | 53 | 54 | 55 | 56 |
57 | {speaker.display_name} 58 |
59 |
60 |
61 |
62 |
{speaker.display_name}
63 |

{speaker.position}

64 |

65 | {speaker.organization} 66 |

67 | {speaker.topics.length > 0 && 68 | 69 | } 70 |
71 |
72 | 73 | {speaker.pronouns && ( 74 | 75 | 79 | 80 | )} 81 | 82 | {speaker.city && ( 83 | 84 | 88 | 89 | )} 90 | {hasSocial && ( 91 | 92 | {speaker.twitter && ( 93 | 100 | Twitter 101 | 102 | )} 103 | {speaker.linkedin && ( 104 | 105 | LinkedIn 106 | 107 | )} 108 | {speaker.website && ( 109 | 110 | Website 111 | 112 | )} 113 | 114 | )} 115 | 116 |
117 |
118 |
119 | ); 120 | }; 121 | 122 | export default withStyles(styles)(SpeakerCard) 123 | -------------------------------------------------------------------------------- /app/common/Navigation/Navigation.js: -------------------------------------------------------------------------------- 1 | // NPM 2 | import React, { PropTypes, Component } from 'react'; 3 | import { withStyles } from 'material-ui/styles'; 4 | import AppBar from 'material-ui/AppBar'; 5 | import Toolbar from 'material-ui/Toolbar'; 6 | import Grid from 'material-ui/Grid'; 7 | import Hidden from 'material-ui/Hidden'; 8 | import { connect } from 'react-redux'; 9 | 10 | // App 11 | import { link } from './styles.css'; 12 | import SearchField from './SearchField'; 13 | import MenuDropdown from './MenuDropdown'; 14 | import ButtonMenu from './ButtonMenu'; 15 | import Logo from 'svg-react-loader?name=Logo!../../assets/logo_women_and_color.svg'; 16 | import { logout, validateToken } from 'appRedux/modules/user' 17 | 18 | const styles = theme => ({ 19 | root: { 20 | width: '100%', 21 | backgroundColor: theme.palette.primary.contrastText, 22 | color: theme.palette.secondary.dark, 23 | } 24 | }); 25 | 26 | const loggedOutMenuItems = { 27 | default: [ 28 | { title: 'Log in', slug: '/login', color: 'secondary' }, 29 | { title: 'Be a speaker', slug: '/register', color: 'primary' }, 30 | ], 31 | }; 32 | 33 | const loggedInMenuItems = profileId => { 34 | return { 35 | default: [ 36 | { title: 'Edit profile', slug: '/profile', color: 'primary' }, 37 | ], 38 | '/profile/about': [ 39 | { 40 | title: 'View profile', 41 | slug: `/speaker/${profileId}`, 42 | color: 'primary', 43 | }, 44 | ], 45 | '/profile/talks': [ 46 | { 47 | title: 'View profile', 48 | slug: `/speaker/${profileId}`, 49 | color: 'primary', 50 | }, 51 | ], 52 | '/profile/account': [ 53 | { 54 | title: 'View profile', 55 | slug: `/speaker/${profileId}`, 56 | color: 'primary', 57 | }, 58 | ], 59 | }; 60 | }; 61 | 62 | 63 | class Navigation extends Component { 64 | componentWillMount() { 65 | this.props.validateToken() 66 | } 67 | 68 | menuItemsList = (location, authed, profile) => { 69 | const menuItemsObj = authed 70 | ? loggedInMenuItems(profile.id) 71 | : loggedOutMenuItems; 72 | 73 | if (location && menuItemsObj[location.pathname]) { 74 | return menuItemsObj[location.pathname]; 75 | } 76 | 77 | return menuItemsObj.default; 78 | }; 79 | 80 | render() { 81 | const { 82 | classes, 83 | updateSearchParams, 84 | location, 85 | user, 86 | profile, 87 | logout, 88 | } = this.props; 89 | 90 | const menuItems = this.menuItemsList(location, user.isAuthenticated, profile); 91 | 92 | return ( 93 |
94 | 95 | 96 | 97 | 98 | 99 | 100 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 |
123 | ); 124 | } 125 | }; 126 | 127 | Navigation.propTypes = { 128 | classes: PropTypes.object.isRequired, 129 | }; 130 | 131 | function mapStateToProps(state) { 132 | return { 133 | user: state.user, 134 | profile: state.profile, 135 | }; 136 | } 137 | 138 | function mapDispatchToProps(dispatch) { 139 | return { 140 | logout: () => { 141 | dispatch(logout()) 142 | }, 143 | validateToken: () => { 144 | dispatch(validateToken()) 145 | } 146 | } 147 | } 148 | 149 | export default connect(mapStateToProps, mapDispatchToProps)(withStyles(styles)(Navigation)); 150 | -------------------------------------------------------------------------------- /app/pages/Onboarding/EmailSettings/EmailSettings.js: -------------------------------------------------------------------------------- 1 | // NPM 2 | import React, { Component } from 'react'; 3 | import { connect } from 'react-redux'; 4 | import Grid from 'material-ui/Grid'; 5 | import Checkbox from 'material-ui/Checkbox'; 6 | import { FormLabel, FormControlLabel, FormHelperText } from 'material-ui/Form'; 7 | import { Link } from 'react-router-dom'; 8 | import { push } from 'react-router-redux'; 9 | import { Helmet } from 'react-helmet'; 10 | import { find, remove } from 'lodash'; 11 | 12 | // App 13 | import { 14 | update as updateProfile, 15 | onChange as onChangeProfile, 16 | } from 'appRedux/modules/profile'; 17 | import { get as getSubscriptionGroups } from 'appRedux/modules/subscriptionGroup' 18 | import { showNotification } from 'appRedux/modules/notification'; 19 | import StyledButton from 'appCommon/StyledButton'; 20 | import FormField from 'appCommon/FormField'; 21 | import css from '../styles.css'; 22 | 23 | const CURRENT_PAGE = 'email_settings'; 24 | 25 | const EmailSettings = props => { 26 | const group_options = props.subscription_groups || []; 27 | 28 | const handleSubscriptionGroups = (e) => { 29 | const group = find(props.subscription_groups, g => g.group_id === e.target.value) 30 | let selectedGroups = [...props.profile.subscription_groups]; 31 | 32 | if (e.target.checked) { 33 | selectedGroups.push(group); 34 | } else { 35 | remove(selectedGroups, g => g.group_id === group.group_id); 36 | } 37 | 38 | props.handleProfileInputChange('subscription_groups', selectedGroups); 39 | } 40 | 41 | return ( 42 |
43 |
44 |

Stay in touch

45 | 46 | 47 | 48 | { 49 | group_options.map(group => { 50 | const checked = find(props.profile.subscription_groups, g => g.group_id === group.group_id) 51 | return ( 52 | 61 | } 62 | label={group.label} 63 | /> 64 | ) 65 | }) 66 | } 67 | 68 | 69 | 70 | 71 | Save 72 | 73 | 74 |
75 |
76 | ); 77 | }; 78 | 79 | class EmailSettingsContainer extends Component { 80 | constructor(props) { 81 | super(props); 82 | this.state = {}; 83 | props.onChangeProfile({ current_page: null }); 84 | } 85 | 86 | componentDidMount() { 87 | this.props.getSubscriptionGroups(); 88 | } 89 | 90 | render() { 91 | return ( 92 |
93 | 94 | Get started - Communication 95 | 99 | 100 | { 102 | event.preventDefault(); 103 | this.props.updateProfile(); 104 | }} 105 | handleProfileInputChange={(field, value) => { 106 | this.props.onChangeProfile({ [field]: value }); 107 | }} 108 | {...this.props} 109 | /> 110 |
111 | ); 112 | } 113 | } 114 | 115 | function mapStateToProps(state) { 116 | return { 117 | user: state.user, 118 | profile: state.profile, 119 | subscription_groups: state.subscriptionGroup.groups, 120 | }; 121 | } 122 | 123 | function mapDispatchToProps(dispatch, props) { 124 | return { 125 | onChangeProfile: attrs => { 126 | dispatch(onChangeProfile(attrs)); 127 | }, 128 | updateProfile: () => { 129 | dispatch(updateProfile()).then(() => { 130 | dispatch(push('/profile')) 131 | }); 132 | }, 133 | showNotification: message => { 134 | dispatch(showNotification(message)); 135 | }, 136 | getSubscriptionGroups: () => { 137 | dispatch(getSubscriptionGroups()); 138 | } 139 | }; 140 | } 141 | 142 | export default connect(mapStateToProps, mapDispatchToProps)(EmailSettingsContainer); 143 | -------------------------------------------------------------------------------- /app/redux/modules/profile.js: -------------------------------------------------------------------------------- 1 | // NPM 2 | import { push } from 'react-router-redux'; 3 | import { omitBy, isNil } from 'lodash'; 4 | 5 | // App 6 | import { 7 | GetRequest, GetSuccess, GetError, 8 | PutRequest, PutSuccess, PutError, 9 | OnChange 10 | } from './action_template'; 11 | import { registrationFlow, BASE_URL_PATH } from 'appHelpers/constants'; 12 | import axios from 'axios'; 13 | import { showNotification } from './notification'; 14 | import { getApiToken } from './user' 15 | 16 | const MODULE_NAME = 'profiles'; 17 | const ENDPOINT_URL = `${BASE_URL_PATH}/api/v1/${MODULE_NAME}/`; 18 | 19 | // Actions 20 | function getRequest() { 21 | return { 22 | type: GetRequest(MODULE_NAME) 23 | } 24 | } 25 | 26 | export function getSuccess(data) { 27 | return { 28 | type: GetSuccess(MODULE_NAME), 29 | data 30 | } 31 | } 32 | 33 | function getError(error) { 34 | return { 35 | type: GetError(MODULE_NAME), 36 | error 37 | } 38 | } 39 | 40 | function putRequest() { 41 | return { 42 | type: PutRequest(MODULE_NAME) 43 | } 44 | } 45 | 46 | function putSuccess(data) { 47 | return { 48 | type: PutSuccess(MODULE_NAME), 49 | data 50 | } 51 | } 52 | 53 | function putError(error) { 54 | return { 55 | type: PutError(MODULE_NAME), 56 | error 57 | } 58 | } 59 | 60 | export function onChange(data) { 61 | return { 62 | type: OnChange(MODULE_NAME), 63 | data 64 | } 65 | } 66 | 67 | export function logoutSuccess() { 68 | return { 69 | type: 'LOGOUT_SUCCESS' 70 | } 71 | } 72 | 73 | export function update() { 74 | return (dispatch, getState) => { 75 | dispatch(putRequest()); 76 | const { profile } = getState(); 77 | const token = getApiToken(); 78 | const authHeader = token ? `JWT ${token}` : null; 79 | const page = profile.current_page; 80 | profile.page = page; 81 | 82 | return axios({ 83 | method: 'PUT', 84 | url: `${ENDPOINT_URL}${profile.id}/`, 85 | data: profile, 86 | responseType: 'json', 87 | headers: { 88 | 'Authorization': authHeader 89 | } 90 | }).then(res => { 91 | dispatch(putSuccess(res.data)); 92 | dispatch(showNotification('Your profile has been updated.')); 93 | if (res.data.page) dispatch(push(registrationFlow[res.data.page].next)); 94 | }).catch(err => { 95 | console.log(err); 96 | if (err.response.status === 401) { 97 | dispatch(showNotification('This action is unauthorized. Please make sure you are logged in.')) 98 | } else if (err.response.data) { 99 | dispatch(showNotification(`There was an error: ${err.response.data.detail}`)) 100 | } else { 101 | dispatch(showNotification('There was an error updating your profile.')) 102 | } 103 | dispatch(putError(err)); 104 | }); 105 | } 106 | } 107 | 108 | const initialState = { 109 | isInitialized: false, 110 | isLoading: false, 111 | isRequesting: false, 112 | woman: true, 113 | poc: true, 114 | pronouns: 'they', 115 | location: 1, 116 | error: null, 117 | topics: [], 118 | } 119 | 120 | export const reducer = (state=initialState, action) => { 121 | switch (action.type) { 122 | case GetRequest(MODULE_NAME): { 123 | return { 124 | ...state, 125 | isLoading: true, 126 | isRequesting: true 127 | } 128 | } 129 | 130 | case GetSuccess(MODULE_NAME): { 131 | const profileData = omitBy(action.data, isNil); 132 | 133 | return { 134 | ...state, 135 | isInitialized: true, 136 | isLoading: false, 137 | isRequesting: false, 138 | ...profileData 139 | } 140 | } 141 | 142 | case GetError(MODULE_NAME): { 143 | return { 144 | ...state, 145 | isRequesting: false, 146 | isLoading: false, 147 | error: action.error 148 | } 149 | } 150 | 151 | case PutRequest(MODULE_NAME): { 152 | return { 153 | ...state, 154 | isRequesting: true 155 | } 156 | } 157 | 158 | case PutSuccess(MODULE_NAME): { 159 | return { 160 | ...state, 161 | isRequesting: false, 162 | ...action.data 163 | } 164 | } 165 | 166 | case PutError(MODULE_NAME): { 167 | return { 168 | ...state, 169 | isRequesting: false, 170 | error: action.error 171 | } 172 | } 173 | 174 | case OnChange(MODULE_NAME): { 175 | return { 176 | ...state, 177 | ...action.data 178 | } 179 | } 180 | 181 | case 'LOGOUT_SUCCESS': { 182 | return { 183 | ...initialState 184 | } 185 | } 186 | 187 | default: 188 | return state 189 | } 190 | } 191 | -------------------------------------------------------------------------------- /app/common/Banner.js: -------------------------------------------------------------------------------- 1 | // NPM 2 | import React, { PropTypes, Component } from 'react' 3 | import {withRouter} from 'react-router-dom' 4 | import IconButton from 'material-ui/IconButton'; 5 | import SearchIcon from '@material-ui/icons/Search'; 6 | import TextField from 'material-ui/TextField'; 7 | import Typography from 'material-ui/Typography'; 8 | import Grid from 'material-ui/Grid'; 9 | import Hidden from 'material-ui/Hidden'; 10 | import { connect } from 'react-redux'; 11 | 12 | // APP 13 | import StyledButton from 'appCommon/StyledButton'; 14 | import { updateSearchParams } from 'appRedux/modules/speaker'; 15 | import { searchForm } from '../sharedStyles/styles.css'; 16 | import css from './styles.css'; 17 | 18 | const styles = theme => ({ 19 | searchButton: { 20 | height: '100%' 21 | }, 22 | banner: { 23 | backgroundColor: theme.palette.primary.main, 24 | paddingTop: '6rem', 25 | paddingBottom: '6rem', 26 | marginBottom: '2rem', 27 | background: "url('https://s3.ca-central-1.amazonaws.com/womenandcolor/background-image.jpg') no-repeat center center fixed", 28 | backgroundSize: 'cover', 29 | }, 30 | headline: { 31 | fontSize: '2rem', 32 | color: theme.palette.primary.contrastText, 33 | marginBottom: '2rem', 34 | fontWeight: '100' 35 | }, 36 | highlight: { 37 | fontWeight: '600' 38 | }, 39 | searchForm: { 40 | backgroundColor: theme.palette.primary.contrastText, 41 | paddingTop: '1rem', 42 | paddingBottom: '1rem' 43 | }, 44 | textField: { 45 | marginRight: '1rem' 46 | }, 47 | searchIcon: { 48 | marginRight: '1rem' 49 | } 50 | }); 51 | 52 | class Banner extends Component { 53 | constructor(props) { 54 | super(props); 55 | this.state = { query: this.props.q || '' } 56 | } 57 | 58 | componentWillReceiveProps(newProps) { 59 | if (this.props.q !== newProps.q) { 60 | const query = newProps.q || ''; 61 | this.setState({ query }) 62 | } 63 | } 64 | 65 | searchProfiles = (event) => { 66 | event.preventDefault(); 67 | const home = '/' 68 | if (this.props.history.location.pathname !== home) { 69 | this.props.history.push(home) 70 | } 71 | const query = this.state.query; 72 | this.props.updateSearchParams({ 73 | q: query, 74 | offset: 0, 75 | limit: 20, 76 | append: false 77 | }) 78 | } 79 | 80 | onChange = (event) => { 81 | const query = event.target.value; 82 | this.setState({ query }); 83 | if (!query) { 84 | const home = '/' 85 | if (this.props.history.location.pathname !== home) { 86 | this.props.history.push(home) 87 | } 88 | this.props.updateSearchParams({ 89 | q: null, 90 | offset: 0, 91 | limit: 20, 92 | append: false 93 | }) 94 | } 95 | } 96 | 97 | render() { 98 | return ( 99 | 100 | 101 |

102 | Find talented women and people of color available for speaking opportunities at tech-related events. 103 |

104 | 105 |
106 | 107 | 108 | 109 | 117 |
118 | 119 | 120 | 121 | 122 | 123 | Find Speakers 124 | 125 | 126 |
127 | 128 |
129 |
130 |
131 | ) 132 | } 133 | } 134 | 135 | function mapDispatchToProps(dispatch) { 136 | return { 137 | updateSearchParams: (params) => { 138 | dispatch(updateSearchParams(params)) 139 | } 140 | } 141 | } 142 | 143 | function mapStateToProps(state) { 144 | return { 145 | q: state.speaker.searchParams.q 146 | } 147 | } 148 | 149 | export default connect( 150 | mapStateToProps, 151 | mapDispatchToProps 152 | )(withRouter(Banner)); 153 | -------------------------------------------------------------------------------- /app/pages/Home/Home.js: -------------------------------------------------------------------------------- 1 | // NPM 2 | import React, { PropTypes, Component } from 'react'; 3 | import { connect } from 'react-redux'; 4 | import Grid from 'material-ui/Grid'; 5 | import { find } from 'lodash'; 6 | import { Helmet } from "react-helmet"; 7 | 8 | // APP 9 | import SpeakerList from './components/SpeakerList'; 10 | import Filters from './components/Filters'; 11 | import MobileFilters from './components/MobileFilters'; 12 | import MobileSearch from './components/MobileSearch'; 13 | import StyledButton from 'appCommon/StyledButton'; 14 | import Banner from 'appCommon/Banner'; 15 | import { fetchSpeakers, updateSearchParams } from 'appRedux/modules/speaker'; 16 | import { get as getLocations } from 'appRedux/modules/location'; 17 | import { DEFAULT_SPEAKER_LIMIT } from 'appHelpers/constants'; 18 | 19 | import css from './styles.css'; 20 | 21 | const searchParamsToSpeakerIdentity = ({ poc, woman }) => { 22 | if (!poc && !woman) { 23 | return 'All speakers'; 24 | } else { 25 | const genderIdentityString = woman ? 'Women' : 'People'; 26 | const pocIdentityString = poc ? 'of color' : ''; 27 | return `${genderIdentityString} ${pocIdentityString}`; 28 | } 29 | }; 30 | 31 | const Home = ({ 32 | searchParams, 33 | locations, 34 | speakers, 35 | endOfResults, 36 | loadMoreSpeakers, 37 | isLoading, 38 | }) => { 39 | const searchQuery = searchParams.q ? `'${searchParams.q}'` : 'all topics'; 40 | const locationObj = find(locations, { id: parseInt(searchParams.location) }) 41 | const location = locationObj 42 | ? locationObj.city 43 | : 'all cities'; 44 | const speakerIdentity = searchParamsToSpeakerIdentity(searchParams); 45 | 46 | return ( 47 | 48 | 49 | 50 | 51 | 52 | 53 | 56 | 60 | 61 | 62 | 63 |
64 | {`${speakerIdentity} in ${location} for ${searchQuery}`} 65 |
66 |
67 |
68 | 74 |
75 |
76 |
77 |
78 | ); 79 | }; 80 | 81 | class HomeContainer extends Component { 82 | constructor(props) { 83 | super(props); 84 | this.state = {}; 85 | this.props.getLocations({ active: true }); 86 | this.props.fetchSpeakers(this.props.searchParams); 87 | } 88 | 89 | componentWillReceiveProps(nextProps) { 90 | if (this.props.searchParams != nextProps.searchParams) { 91 | this.props.fetchSpeakers(nextProps.searchParams); 92 | } 93 | } 94 | 95 | loadMoreSpeakers = () => { 96 | this.props.updateSearchParams({ 97 | limit: DEFAULT_SPEAKER_LIMIT, 98 | offset: this.props.searchParams.offset + DEFAULT_SPEAKER_LIMIT, 99 | append: true, 100 | }); 101 | }; 102 | 103 | render() { 104 | return( 105 |
106 | 107 | Women and Color 108 | 109 | 110 | 111 |
112 | ) 113 | } 114 | } 115 | 116 | const mapStateToProps = state => { 117 | return { 118 | user: state.user, 119 | speakers: state.speaker.results, 120 | locations: state.location.locations, 121 | searchParams: state.speaker.searchParams, 122 | endOfResults: state.speaker.endOfResults, 123 | isLoading: state.speaker.isLoading, 124 | }; 125 | }; 126 | 127 | const mapDispatchToProps = dispatch => { 128 | return { 129 | fetchSpeakers: params => { 130 | dispatch(fetchSpeakers(params)); 131 | }, 132 | updateSearchParams: params => { 133 | dispatch(updateSearchParams(params)); 134 | }, 135 | getLocations: (opts) => { 136 | dispatch(getLocations(opts)); 137 | }, 138 | }; 139 | }; 140 | 141 | export default connect(mapStateToProps, mapDispatchToProps)(HomeContainer); 142 | -------------------------------------------------------------------------------- /app/pages/Onboarding/Work/Work.js: -------------------------------------------------------------------------------- 1 | // NPM 2 | import React, { Component } from 'react'; 3 | import { connect } from 'react-redux'; 4 | import Grid from 'material-ui/Grid'; 5 | import TextField from 'material-ui/TextField'; 6 | import Button from 'material-ui/Button'; 7 | import Select from 'material-ui/Select'; 8 | import Input, { InputLabel } from 'material-ui/Input'; 9 | import { MenuItem } from 'material-ui/Menu'; 10 | import Radio, { RadioGroup } from 'material-ui/Radio'; 11 | import { FormLabel, FormControlLabel, FormHelperText } from 'material-ui/Form'; 12 | import { Link } from 'react-router-dom'; 13 | import { Helmet } from 'react-helmet'; 14 | 15 | // App 16 | import { 17 | update as updateProfile, 18 | onChange as onChangeProfile, 19 | } from 'appRedux/modules/profile'; 20 | import { 21 | get as getTopics, 22 | create as createTopic, 23 | } from 'appRedux/modules/topic'; 24 | import { showNotification } from 'appRedux/modules/notification'; 25 | import StyledButton from 'appCommon/StyledButton'; 26 | import FormField from 'appCommon/FormField'; 27 | import TopicSelector from 'appPages/EditProfile/FormComponents/TopicSelector/TopicSelector'; 28 | import css from '../styles.css'; 29 | 30 | const CURRENT_PAGE = 'work'; 31 | 32 | const Work = props => { 33 | const generateHandler = fieldName => { 34 | return event => { 35 | props.handleProfileInputChange(fieldName, event.currentTarget.value); 36 | }; 37 | }; 38 | 39 | const handleTopicsChange = topics => { 40 | props.handleProfileInputChange('topics', topics); 41 | }; 42 | 43 | return ( 44 |
45 |
46 |

Let's talk about work

47 | 48 | 49 | 50 | 51 | 52 | 53 | 57 | 58 | 59 | 60 | Speaking Topics 61 | 67 | 68 | {`Topics: ${props.profile.topics.length || '0'} of 10`} 69 | 70 | 71 | 72 |
73 | 74 | 75 | Save and continue 76 | 77 | 78 |
79 |
80 |
81 | ); 82 | }; 83 | 84 | class WorkContainer extends Component { 85 | constructor(props) { 86 | super(props); 87 | this.state = {}; 88 | props.getTopics(); 89 | props.onChangeProfile({ current_page: CURRENT_PAGE }); 90 | } 91 | 92 | render() { 93 | return ( 94 |
95 | 96 | Get started - Work 97 | 101 | 102 | { 104 | event.preventDefault(); 105 | if (this.props.profile.topics.length < 1) { 106 | return this.props.showNotification( 107 | 'Please enter at least one topic.' 108 | ); 109 | } 110 | this.props.updateProfile(); 111 | }} 112 | handleProfileInputChange={(field, value) => { 113 | this.props.onChangeProfile({ [field]: value }); 114 | }} 115 | {...this.props} 116 | /> 117 |
118 | ); 119 | } 120 | } 121 | 122 | function mapStateToProps(state) { 123 | return { 124 | user: state.user, 125 | profile: state.profile, 126 | topics: state.topic.topics, 127 | }; 128 | } 129 | 130 | function mapDispatchToProps(dispatch, props) { 131 | return { 132 | onChangeProfile: attrs => { 133 | dispatch(onChangeProfile(attrs)); 134 | }, 135 | updateProfile: () => { 136 | dispatch(updateProfile()); 137 | }, 138 | getTopics: () => { 139 | dispatch(getTopics()); 140 | }, 141 | createTopic: topic => { 142 | dispatch(createTopic(topic)); 143 | }, 144 | showNotification: message => { 145 | dispatch(showNotification(message)); 146 | }, 147 | }; 148 | } 149 | 150 | export default connect(mapStateToProps, mapDispatchToProps)(WorkContainer); 151 | -------------------------------------------------------------------------------- /app/redux/modules/__template__.js: -------------------------------------------------------------------------------- 1 | // NPM 2 | import axios from 'axios'; 3 | 4 | 5 | // App 6 | import { 7 | GetRequest, GetSuccess, GetError, 8 | PostRequest, PostSuccess, PostError, 9 | PutRequest, PutSuccess, PutError 10 | OnChange 11 | } from './action_template'; 12 | import { BASE_URL_PATH } from 'appHelpers/constants'; 13 | 14 | const MODULE_NAME = 'REPLACE_ME'; 15 | const ENDPOINT_URL = `${BASE_URL_PATH}/api/v1/${MODULE_NAME}/`; 16 | 17 | // Actions 18 | function getRequest() { 19 | return { 20 | type: GetRequest(MODULE_NAME) 21 | } 22 | } 23 | 24 | export function getSuccess(data) { 25 | return { 26 | type: GetSuccess(MODULE_NAME), 27 | data 28 | } 29 | } 30 | 31 | function getError(error) { 32 | return { 33 | type: GetError(MODULE_NAME), 34 | error 35 | } 36 | } 37 | 38 | function postRequest() { 39 | return { 40 | type: PutRequest(MODULE_NAME) 41 | } 42 | } 43 | 44 | function postSuccess(data) { 45 | return { 46 | type: PutSuccess(MODULE_NAME), 47 | data 48 | } 49 | } 50 | 51 | function postError(error) { 52 | return { 53 | type: PutError(MODULE_NAME), 54 | error 55 | } 56 | } 57 | 58 | function putRequest() { 59 | return { 60 | type: PutRequest(MODULE_NAME) 61 | } 62 | } 63 | 64 | function putSuccess(data) { 65 | return { 66 | type: PutSuccess(MODULE_NAME), 67 | data 68 | } 69 | } 70 | 71 | function putError(error) { 72 | return { 73 | type: PutError(MODULE_NAME), 74 | error 75 | } 76 | } 77 | 78 | export function onChange(data) { 79 | return { 80 | type: OnChange(MODULE_NAME), 81 | data 82 | } 83 | } 84 | 85 | export function get() { 86 | return (dispatch, getState) => { 87 | dispatch(getRequest()); 88 | axios({ 89 | method: 'GET', 90 | url: `${ENDPOINT_URL}/`, 91 | responseType: 'json' 92 | }).then(res => { 93 | dispatch(getSuccess(res.data)); 94 | }).catch(err => { 95 | console.log(err); 96 | dispatch(getError(err)); 97 | }); 98 | } 99 | } 100 | 101 | export function create() { 102 | return (dispatch, getState) => { 103 | dispatch(postRequest()); 104 | const { __state__ } = getState(); 105 | 106 | axios({ 107 | method: 'POST', 108 | url: `${ENDPOINT_URL}/`, 109 | data: __state__, 110 | responseType: 'json' 111 | }).then(res => { 112 | dispatch(postSuccess(res.data)); 113 | }).catch(err => { 114 | console.log(err); 115 | dispatch(postError(err)); 116 | }); 117 | } 118 | } 119 | 120 | export function update() { 121 | return (dispatch, getState) => { 122 | dispatch(putRequest()); 123 | const { __state__ } = getState(); 124 | 125 | axios({ 126 | method: 'PUT', 127 | url: `${ENDPOINT_URL}${__state__.id}/`, 128 | data: __state__, 129 | responseType: 'json' 130 | }).then(res => { 131 | dispatch(putSuccess(res.data)); 132 | }).catch(err => { 133 | console.log(err); 134 | dispatch(putError(err)); 135 | }); 136 | } 137 | } 138 | 139 | const initialState = { 140 | isInitialized: false, 141 | isLoading: false, 142 | isRequesting: false 143 | } 144 | 145 | export const reducer = (state=initialState, action) => { 146 | switch (action.type) { 147 | case GetRequest(MODULE_NAME): { 148 | return { 149 | ...state, 150 | isLoading: true, 151 | isRequesting: true 152 | } 153 | } 154 | 155 | case GetSuccess(MODULE_NAME): { 156 | return { 157 | ...state, 158 | isInitialized: true, 159 | isLoading: false, 160 | isRequesting: false, 161 | ...action.data 162 | } 163 | } 164 | 165 | case GetError(MODULE_NAME): { 166 | return { 167 | ...state, 168 | isRequesting: false, 169 | isLoading: false, 170 | error: action.error 171 | } 172 | } 173 | 174 | case PostRequest(MODULE_NAME): { 175 | return { 176 | ...state, 177 | isRequesting: true 178 | } 179 | } 180 | 181 | case PostSuccess(MODULE_NAME): { 182 | return { 183 | ...state, 184 | isRequesting: false, 185 | ...action.data 186 | } 187 | } 188 | 189 | case PostError(MODULE_NAME): { 190 | return { 191 | ...state, 192 | isRequesting: false, 193 | error: action.error 194 | } 195 | } 196 | 197 | case PutRequest(MODULE_NAME): { 198 | return { 199 | ...state, 200 | isRequesting: true 201 | } 202 | } 203 | 204 | case PutSuccess(MODULE_NAME): { 205 | return { 206 | ...state, 207 | isRequesting: false, 208 | ...action.data 209 | } 210 | } 211 | 212 | case PutError(MODULE_NAME): { 213 | return { 214 | ...state, 215 | isRequesting: false, 216 | error: action.error 217 | } 218 | } 219 | 220 | case OnChange(MODULE_NAME): { 221 | return { 222 | ...state, 223 | ...action.data 224 | } 225 | } 226 | 227 | default: 228 | return state 229 | } 230 | } 231 | -------------------------------------------------------------------------------- /app/pages/Home/components/MobileFilters.js: -------------------------------------------------------------------------------- 1 | // NPM 2 | import React, { PropTypes, Component } from 'react'; 3 | import { connect } from 'react-redux'; 4 | import { withStyles } from 'material-ui/styles'; 5 | import List, { ListItem, ListItemText } from 'material-ui/List'; 6 | import Collapse from 'material-ui/transitions/Collapse'; 7 | import ExpandLess from '@material-ui/icons/ExpandLess'; 8 | import ExpandMore from '@material-ui/icons/ExpandMore'; 9 | import FilterList from '@material-ui/icons/FilterList'; 10 | import Close from '@material-ui/icons/Close'; 11 | import IconButton from 'material-ui/IconButton'; 12 | import Hidden from 'material-ui/Hidden'; 13 | import Modal from 'material-ui/Modal'; 14 | import Paper from 'material-ui/Paper'; 15 | import Grid from 'material-ui/Grid'; 16 | import { map } from 'lodash'; 17 | 18 | // APP 19 | import css from '../styles.css'; 20 | import { updateSearchParams, updateSelection } from 'appRedux/modules/speaker'; 21 | import { IDENTITIES, DEFAULT_SPEAKER_LIMIT } from 'appHelpers/constants'; 22 | import Filters from './Filters' 23 | import StyledButton from 'appCommon/StyledButton'; 24 | 25 | const styles = { 26 | closeButton: { 27 | textAlign: 'right', 28 | }, 29 | paper: { 30 | width: '100%', 31 | padding: '1rem' 32 | }, 33 | }; 34 | 35 | class MobileFilters extends Component { 36 | constructor(props) { 37 | super(props); 38 | this.state = { expand: {}, openModal: false }; 39 | } 40 | 41 | createLocationDict = (locations, dict) => { 42 | locations.map(location => { 43 | const country = location.country.toLowerCase(); 44 | if (!dict[country]) { 45 | dict[country] = []; 46 | } 47 | dict[country].push(location); 48 | }); 49 | 50 | return dict; 51 | }; 52 | 53 | toggleCountry = country => { 54 | this.setState({ 55 | expand: { 56 | [country]: !this.state.expand[country], 57 | }, 58 | }); 59 | }; 60 | 61 | handleOpen = () => { 62 | this.setState({ openModal: true }); 63 | }; 64 | 65 | handleClose = () => { 66 | this.setState({ openModal: false }); 67 | }; 68 | 69 | handleSelectCity = location => { 70 | this.props.updateSearchParams({ 71 | location: location, 72 | offset: 0, 73 | limit: DEFAULT_SPEAKER_LIMIT, 74 | append: false, 75 | }); 76 | this.props.updateSelection({ selectedLocation: location.id }); 77 | }; 78 | 79 | handleSelectIdentity = identity => { 80 | this.props.updateSearchParams({ 81 | ...identity.value, 82 | offset: 0, 83 | limit: DEFAULT_SPEAKER_LIMIT, 84 | append: false, 85 | }); 86 | this.props.updateSelection({ selectedIdentity: identity.label }); 87 | }; 88 | 89 | render() { 90 | const locations = this.createLocationDict(this.props.locations, {}); 91 | 92 | return ( 93 |
94 |
95 | 96 | 97 | 98 | Filter results 99 |
100 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | Filter results 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | Update results 125 | 126 | 127 | 128 | 129 | 130 |
131 | ); 132 | } 133 | } 134 | 135 | const mapStateToProps = state => { 136 | return { 137 | selectedLocation: state.speaker.selectedLocation, 138 | selectedIdentity: state.speaker.selectedIdentity, 139 | }; 140 | }; 141 | 142 | const mapDispatchToProps = dispatch => { 143 | return { 144 | updateSearchParams: params => { 145 | dispatch(updateSearchParams(params)); 146 | }, 147 | updateSelection: selected => { 148 | dispatch(updateSelection(selected)); 149 | }, 150 | fetchSpeakers: params => { 151 | dispatch(fetchSpeakers(params)); 152 | }, 153 | }; 154 | }; 155 | 156 | export default connect(mapStateToProps, mapDispatchToProps)( 157 | withStyles(styles)(MobileFilters) 158 | ); 159 | -------------------------------------------------------------------------------- /app/redux/modules/featuredTalk.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | 3 | // App 4 | import { 5 | GetRequest, GetSuccess, GetError, 6 | PostRequest, PostSuccess, PostError, 7 | PutRequest, PutSuccess, PutError, 8 | OnChange 9 | } from './action_template'; 10 | import { get as getUser, getApiToken } from './user'; 11 | import { showNotification } from './notification'; 12 | import { BASE_URL_PATH } from 'appHelpers/constants'; 13 | 14 | const MODULE_NAME = 'featured_talks'; 15 | const ENDPOINT_URL = `${BASE_URL_PATH}/api/v1/${MODULE_NAME}/`; 16 | 17 | // Actions 18 | function getRequest() { 19 | return { 20 | type: GetRequest(MODULE_NAME) 21 | } 22 | } 23 | 24 | export function getSuccess(data) { 25 | return { 26 | type: GetSuccess(MODULE_NAME), 27 | data 28 | } 29 | } 30 | 31 | function getError(error) { 32 | return { 33 | type: GetError(MODULE_NAME), 34 | error 35 | } 36 | } 37 | 38 | function postRequest() { 39 | return { 40 | type: PutRequest(MODULE_NAME) 41 | } 42 | } 43 | 44 | function postSuccess(data) { 45 | return { 46 | type: PutSuccess(MODULE_NAME), 47 | data 48 | } 49 | } 50 | 51 | function postError(error) { 52 | return { 53 | type: PutError(MODULE_NAME), 54 | error 55 | } 56 | } 57 | 58 | function putRequest() { 59 | return { 60 | type: PutRequest(MODULE_NAME) 61 | } 62 | } 63 | 64 | function putSuccess(data) { 65 | return { 66 | type: PutSuccess(MODULE_NAME), 67 | data 68 | } 69 | } 70 | 71 | function putError(error) { 72 | return { 73 | type: PutError(MODULE_NAME), 74 | error 75 | } 76 | } 77 | 78 | export function onChange(data) { 79 | return { 80 | type: OnChange(MODULE_NAME), 81 | data 82 | } 83 | } 84 | 85 | export function get() { 86 | return (dispatch, getState) => { 87 | dispatch(getRequest()); 88 | axios({ 89 | method: 'GET', 90 | url: `${ENDPOINT_URL}/`, 91 | responseType: 'json' 92 | }).then(res => { 93 | dispatch(getSuccess(res.data)); 94 | }).catch(err => { 95 | console.log(err); 96 | dispatch(getError(err)); 97 | }); 98 | } 99 | } 100 | 101 | export function create(data) { 102 | return (dispatch, getState) => { 103 | dispatch(postRequest()); 104 | const token = getApiToken() 105 | const authHeader = token ? `JWT ${token}` : null; 106 | 107 | axios({ 108 | method: 'POST', 109 | url: `${ENDPOINT_URL}`, 110 | data: data, 111 | responseType: 'json', 112 | headers: { 113 | 'Authorization': authHeader 114 | } 115 | }).then(res => { 116 | dispatch(postSuccess(res.data)); 117 | showNotification('Your talk has been created.') 118 | dispatch(getUser()) 119 | }).catch(err => { 120 | console.log(err); 121 | dispatch(postError(err)); 122 | showNotification('There was an error saving your talk.') 123 | }); 124 | } 125 | } 126 | 127 | export function update(data) { 128 | return (dispatch, getState) => { 129 | dispatch(putRequest()); 130 | const token = getApiToken() 131 | const authHeader = token ? `JWT ${token}` : null; 132 | 133 | axios({ 134 | method: 'PUT', 135 | url: `${ENDPOINT_URL}${data.id}/`, 136 | data: data, 137 | responseType: 'json', 138 | headers: { 139 | 'Authorization': authHeader 140 | } 141 | }).then(res => { 142 | dispatch(putSuccess(res.data)); 143 | showNotification('Your talk has been updated.') 144 | dispatch(getUser()) 145 | }).catch(err => { 146 | console.log(err); 147 | dispatch(putError(err)); 148 | showNotification('There was an error saving your talk.') 149 | }); 150 | } 151 | }; 152 | 153 | export function destroy(data) { 154 | return (dispatch, getState) => { 155 | const token = getApiToken() 156 | const authHeader = token ? `JWT ${token}` : null; 157 | 158 | axios({ 159 | method: 'DELETE', 160 | url: `${ENDPOINT_URL}${data.id}/`, 161 | data: data, 162 | responseType: 'json', 163 | headers: { 164 | 'Authorization': authHeader 165 | } 166 | }).then(res => { 167 | dispatch(getUser()) 168 | showNotification('Your talk has been deleted.') 169 | }).catch(err => { 170 | console.log(err); 171 | showNotification('There was an error deleting your talk.') 172 | }); 173 | } 174 | } 175 | 176 | const initialState = { 177 | isInitialized: false, 178 | isLoading: false, 179 | isRequesting: false 180 | } 181 | 182 | export const reducer = (state=initialState, action) => { 183 | switch (action.type) { 184 | case GetRequest(MODULE_NAME): { 185 | return { 186 | ...state, 187 | isLoading: true, 188 | isRequesting: true 189 | } 190 | } 191 | 192 | case GetSuccess(MODULE_NAME): { 193 | return { 194 | ...state, 195 | isInitialized: true, 196 | isLoading: false, 197 | isRequesting: false, 198 | ...action.data 199 | } 200 | } 201 | 202 | case GetError(MODULE_NAME): { 203 | return { 204 | ...state, 205 | isRequesting: false, 206 | isLoading: false, 207 | error: action.error 208 | } 209 | } 210 | 211 | case PostRequest(MODULE_NAME): { 212 | return { 213 | ...state, 214 | isRequesting: true 215 | } 216 | } 217 | 218 | case PostSuccess(MODULE_NAME): { 219 | return { 220 | ...state, 221 | isRequesting: false, 222 | ...action.data 223 | } 224 | } 225 | 226 | case PostError(MODULE_NAME): { 227 | return { 228 | ...state, 229 | isRequesting: false, 230 | error: action.error 231 | } 232 | } 233 | 234 | case PutRequest(MODULE_NAME): { 235 | return { 236 | ...state, 237 | isRequesting: true 238 | } 239 | } 240 | 241 | case PutSuccess(MODULE_NAME): { 242 | return { 243 | ...state, 244 | isRequesting: false, 245 | ...action.data 246 | } 247 | } 248 | 249 | case PutError(MODULE_NAME): { 250 | return { 251 | ...state, 252 | isRequesting: false, 253 | error: action.error 254 | } 255 | } 256 | 257 | case OnChange(MODULE_NAME): { 258 | return { 259 | ...state, 260 | ...action.data 261 | } 262 | } 263 | 264 | default: 265 | return state 266 | } 267 | } 268 | -------------------------------------------------------------------------------- /app/assets/Tribalscale.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Logo/Tribalscale 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 27 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /app/pages/EditProfile/FormComponents/TopicSelector/TopicSelector.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import keycode from 'keycode'; 4 | import Downshift from 'downshift'; 5 | import { withStyles } from 'material-ui/styles'; 6 | import TextField from 'material-ui/TextField'; 7 | import Paper from 'material-ui/Paper'; 8 | import { MenuItem } from 'material-ui/Menu'; 9 | import { ListItemText } from 'material-ui/List'; 10 | import Chip from 'material-ui/Chip'; 11 | 12 | import { find } from 'lodash'; 13 | 14 | const styles = theme => ({ 15 | container: { 16 | flexGrow: 1, 17 | position: 'relative', 18 | marginTop: '1rem', 19 | marginBottom: '1rem', 20 | }, 21 | paper: { 22 | position: 'absolute', 23 | zIndex: 1, 24 | marginTop: theme.spacing.unit, 25 | left: 0, 26 | right: 0, 27 | }, 28 | chip: { 29 | margin: `${theme.spacing.unit / 2}px ${theme.spacing.unit / 4}px`, 30 | }, 31 | inputRoot: { 32 | flexWrap: 'wrap', 33 | }, 34 | }); 35 | 36 | const renderInput = inputProps => { 37 | const { InputProps, classes, ref, ...other } = inputProps; 38 | 39 | return ( 40 | 50 | ); 51 | }; 52 | 53 | const renderSuggestion = ({ 54 | suggestion, 55 | index, 56 | itemProps, 57 | highlightedIndex, 58 | selectedItem, 59 | }) => { 60 | const isHighlighted = highlightedIndex === index; 61 | const isSelected = (selectedItem || '').indexOf(suggestion.topic) > -1; 62 | 63 | return ( 64 | 74 | 75 | 76 | ); 77 | }; 78 | 79 | renderSuggestion.propTypes = { 80 | highlightedIndex: PropTypes.number, 81 | index: PropTypes.number, 82 | itemProps: PropTypes.object, 83 | selectedItem: PropTypes.string, 84 | suggestion: PropTypes.shape({ topic: PropTypes.string }).isRequired, 85 | }; 86 | 87 | const getSuggestions = (inputValue, topics, selectedTopics) => { 88 | let count = 0; 89 | 90 | return topics.filter(suggestion => { 91 | const keep = 92 | (!inputValue || 93 | (suggestion.topic && 94 | suggestion.topic.toLowerCase().includes(inputValue.toLowerCase()))) && 95 | count < 5; 96 | 97 | if (keep) { 98 | count += 1; 99 | } 100 | 101 | return keep; 102 | }); 103 | }; 104 | 105 | class TopicSelector extends React.Component { 106 | state = { 107 | inputValue: '', 108 | }; 109 | 110 | handleKeyDown = event => { 111 | const { inputValue } = this.state; 112 | const { selectedTopics, handleChange, createTopic, topics } = this.props; 113 | if ( 114 | selectedTopics.length && 115 | !inputValue.length && 116 | keycode(event) === 'backspace' 117 | ) { 118 | const updatedTopics = selectedTopics.slice(0, selectedTopics.length - 1); 119 | handleChange(updatedTopics); 120 | } 121 | 122 | if (keycode(event) === 'enter' && inputValue.length > 1) { 123 | const topicList = topics.map(topic => topic.topic); 124 | if (topicList.includes(inputValue)) return; // prevent creating same topic name with different ids 125 | createTopic(inputValue); 126 | } 127 | }; 128 | 129 | handleInputChange = event => { 130 | this.setState({ inputValue: event.target.value }); 131 | }; 132 | 133 | handleChange = item => { 134 | let { selectedTopics, handleChange } = this.props; 135 | 136 | if (!find(selectedTopics, ['topic', item.topic])) { 137 | selectedTopics = [...selectedTopics, item]; 138 | } 139 | 140 | handleChange(selectedTopics); 141 | this.setState({ inputValue: '' }); 142 | }; 143 | 144 | handleDelete = item => () => { 145 | const selectedTopics = [...this.props.selectedTopics]; 146 | selectedTopics.splice(selectedTopics.indexOf(item), 1); 147 | 148 | this.props.handleChange(selectedTopics); 149 | }; 150 | 151 | render() { 152 | const { classes, selectedTopics, topics } = this.props; 153 | const { inputValue } = this.state; 154 | 155 | return ( 156 | (i == null ? '' : i.topic)} 161 | > 162 | {({ 163 | getInputProps, 164 | getItemProps, 165 | isOpen, 166 | inputValue: inputValue2, 167 | selectedItem: selectedItem2, 168 | highlightedIndex, 169 | }) => ( 170 |
171 | {renderInput({ 172 | disabled: (selectedTopics.length >= 10), 173 | fullWidth: true, 174 | classes, 175 | InputProps: getInputProps({ 176 | startAdornment: selectedTopics.map(item => ( 177 | 184 | )), 185 | onChange: this.handleInputChange, 186 | onKeyDown: this.handleKeyDown, 187 | placeholder: 'Select topics', 188 | id: 'integration-downshift-multiple', 189 | }), 190 | })} 191 | {isOpen ? ( 192 | 193 | {getSuggestions(inputValue2, topics, selectedTopics).map( 194 | (suggestion, index) => 195 | renderSuggestion({ 196 | suggestion, 197 | index, 198 | itemProps: getItemProps({ item: suggestion }), 199 | highlightedIndex, 200 | selectedItem: selectedItem2, 201 | }) 202 | )} 203 | 204 | ) : null} 205 |
206 | )} 207 |
208 | ); 209 | } 210 | } 211 | 212 | TopicSelector.propTypes = { 213 | classes: PropTypes.object.isRequired, 214 | }; 215 | 216 | export default withStyles(styles)(TopicSelector); 217 | -------------------------------------------------------------------------------- /app/pages/Home/components/Filters.js: -------------------------------------------------------------------------------- 1 | // NPM 2 | import React, { PropTypes, Component } from 'react'; 3 | import { connect } from 'react-redux'; 4 | import { withStyles } from 'material-ui/styles'; 5 | import List, { ListItem, ListItemText } from 'material-ui/List'; 6 | import Collapse from 'material-ui/transitions/Collapse'; 7 | import ExpandLess from '@material-ui/icons/ExpandLess'; 8 | import ExpandMore from '@material-ui/icons/ExpandMore'; 9 | import { map, find } from 'lodash'; 10 | 11 | // APP 12 | import css from '../styles.css'; 13 | import { updateSearchParams } from 'appRedux/modules/speaker'; 14 | import { IDENTITIES, DEFAULT_SPEAKER_LIMIT } from 'appHelpers/constants'; 15 | 16 | const styles = theme => ({ 17 | primary: { 18 | fontWeight: '600', 19 | }, 20 | selectedItem: { 21 | backgroundColor: theme.palette.primary.light 22 | }, 23 | }); 24 | 25 | 26 | class Filters extends Component { 27 | constructor(props) { 28 | super(props); 29 | this.state = { expand: {}, showFilters: false }; 30 | } 31 | 32 | componentWillReceiveProps(newProps) { 33 | if (newProps.selectedLocation && this.props.selectedLocation !== newProps.selectedLocation) { 34 | const country = newProps.selectedLocation.country.toLowerCase(); 35 | this.setState({ 36 | expand: { 37 | [country]: true 38 | } 39 | }) 40 | } 41 | } 42 | 43 | createLocationDict = (locations, dict) => { 44 | locations.map(location => { 45 | const country = location.country.toLowerCase(); 46 | if (!dict[country]) { 47 | dict[country] = []; 48 | } 49 | dict[country].push(location); 50 | }); 51 | 52 | return dict; 53 | }; 54 | 55 | toggleCountry = country => { 56 | this.setState({ 57 | expand: { 58 | [country]: !this.state.expand[country], 59 | }, 60 | }); 61 | }; 62 | 63 | toggleFilters = () => { 64 | this.setState({ showFilters: !this.state.showFilters }) 65 | } 66 | 67 | handleSelectCity = location => { 68 | const locationVal = location ? location.id : location; 69 | this.props.updateSearchParams({ 70 | location: locationVal, 71 | offset: 0, 72 | limit: DEFAULT_SPEAKER_LIMIT, 73 | append: false, 74 | }); 75 | }; 76 | 77 | handleSelectIdentity = identity => { 78 | this.props.updateSearchParams({ 79 | ...identity.value, 80 | offset: 0, 81 | limit: DEFAULT_SPEAKER_LIMIT, 82 | append: false, 83 | }); 84 | }; 85 | 86 | render() { 87 | const locations = this.createLocationDict(this.props.locations, {}); 88 | const { classes } = this.props; 89 | 90 | return ( 91 |
92 |

CITY

93 | 94 | this.handleSelectCity(null)} 96 | button 97 | className={!this.props.searchParams.location ? classes.selectedItem : ''} 98 | > 99 | 103 | 104 | {map(locations, (cities, country) => { 105 | return ( 106 |
107 | this.toggleCountry(country)} button> 108 | 112 | {this.state.expand[country] ? : } 113 | 114 | 119 | 120 | {cities.map((location, index) => { 121 | const selected = 122 | location === this.props.selectedLocation; 123 | 124 | const handleClick = () => this.handleSelectCity(location); 125 | 126 | return ( 127 | 133 | 134 | 135 | ); 136 | })} 137 | 138 | 139 |
140 | ); 141 | })} 142 |
143 | 144 |
FILTER
145 | 146 | {IDENTITIES.map((identity, index) => { 147 | const searchParams = this.props.searchParams; 148 | let selected = true; 149 | 150 | switch (identity.label) { 151 | case 'All speakers': 152 | selected = !searchParams['poc'] && !searchParams['woman']; 153 | break; 154 | case 'Women': 155 | selected = !searchParams['poc'] && !!searchParams['woman']; 156 | break; 157 | case 'People of color': 158 | selected = !!searchParams['poc'] && !searchParams['woman']; 159 | break; 160 | case 'Women of color': 161 | selected = !!searchParams['poc'] && !!searchParams['woman']; 162 | break; 163 | default: 164 | selected = false; 165 | break; 166 | } 167 | 168 | const handleClick = () => this.handleSelectIdentity(identity); 169 | 170 | return ( 171 | 177 | 178 | 179 | ); 180 | })} 181 | 182 |
183 | ); 184 | } 185 | } 186 | 187 | const mapStateToProps = state => { 188 | return { 189 | searchParams: state.speaker.searchParams, 190 | }; 191 | }; 192 | 193 | const mapDispatchToProps = dispatch => { 194 | return { 195 | updateSearchParams: params => { 196 | dispatch(updateSearchParams(params)); 197 | }, 198 | fetchSpeakers: params => { 199 | dispatch(fetchSpeakers(params)); 200 | }, 201 | }; 202 | }; 203 | 204 | export default connect(mapStateToProps, mapDispatchToProps)( 205 | withStyles(styles)(Filters) 206 | ); 207 | -------------------------------------------------------------------------------- /app/pages/Speaker/components/MessageSpeakerForm.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { connect } from 'react-redux'; 3 | import axios from 'axios'; 4 | import TextField from 'material-ui/TextField'; 5 | import MenuItem from 'material-ui/Menu/MenuItem'; 6 | import Input, { InputLabel } from 'material-ui/Input'; 7 | import Grid from 'material-ui/Grid'; 8 | 9 | // App 10 | import StyledButton from 'appCommon/StyledButton'; 11 | import FormField from 'appCommon/FormField'; 12 | import css from '../styles.css'; 13 | import { BASE_URL_PATH } from 'appHelpers/constants'; 14 | 15 | import { create, onChange } from 'appRedux/modules/contactForm'; 16 | import { hideNotification } from 'appRedux/modules/notification'; 17 | 18 | const MessageSpeakerForm = ({ speaker, onInputChange, onSubmit, form }) => { 19 | const generateHandler = fieldName => event => 20 | onInputChange(fieldName, event.target.value); 21 | const title = `Message ${speaker.first_name}`; 22 | return ( 23 |
24 |

{title}

25 |
26 |
27 |

Your Info

28 | 29 | 30 | 31 | 37 | 38 | 39 | 40 | 41 | 48 | 49 | 50 | 51 |
52 |
53 |

Event Info

54 | 55 | 56 | 57 | 62 | 63 | 64 | 65 | 66 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 86 | 87 | 88 | 89 | 90 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 111 | 112 | No 113 | 114 | 115 | Yes 116 | 117 | 118 | 119 | 120 | 121 | 122 | 128 | 129 | None 130 | 131 | 132 | Publicly Accessible 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 149 | 150 | 151 | 152 |
153 | 154 | Send Message 155 | 156 |
157 |
158 | ); 159 | }; 160 | 161 | const MessageSpeakerFormContainer = props => { 162 | const handleInputChange = (fieldname, value) => { 163 | props.onChange({ [fieldname]: value }); 164 | }; 165 | 166 | const submitForm = event => { 167 | event.preventDefault(); 168 | props.submitForm(); 169 | }; 170 | 171 | return ( 172 | 178 | ); 179 | }; 180 | 181 | function mapDispatchToProps(dispatch, props) { 182 | return { 183 | submitForm: () => { 184 | dispatch(create()); 185 | }, 186 | onChange: data => { 187 | dispatch(onChange(data)); 188 | } 189 | }; 190 | } 191 | 192 | function mapStateToProps(state) { 193 | return { 194 | form: state.contactForm.form, 195 | speaker: state.speaker.speaker, 196 | }; 197 | } 198 | 199 | export default connect(mapStateToProps, mapDispatchToProps)( 200 | MessageSpeakerFormContainer 201 | ); 202 | -------------------------------------------------------------------------------- /app/pages/Static/CodeOfConduct.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Grid from 'material-ui/Grid'; 3 | import { Link } from 'react-router-dom'; 4 | import { Helmet } from "react-helmet"; 5 | 6 | import css from './styles.css' 7 | 8 | const CodeOfConduct = () => { 9 | return( 10 |
11 | 12 | Code of Conduct 13 | 14 | 15 | 16 | 17 |
18 |

Code of Conduct

19 |
20 |

1. Purpose

21 |

A primary goal of Women and Color is to be an inclusive community with the largest number of contributors, representing many various and diverse backgrounds as possible. As such, we are committed to providing a nonsectarian, friendly, safe and welcoming environment for all, regardless of gender, sexual orientation, ability, ethnicity, or socioeconomic status.

22 |

This code of conduct outlines our expectations for all those who use Women and Color, as well as the consequences for unacceptable behavior.

23 |

We invite all those who use Women and Color to help us create safe and positive experiences for everyone.

24 |

2. Community Expectations

25 |

The following behaviors are expected and requested of all community members:

26 |
    27 |
  • Active participation in an authentic way. In doing so, you contribute to the health and longevity of this community.
  • 28 |
  • Exercise consideration and respect in your speech and actions.
  • 29 |
  • Attempt collaboration before conflict.
  • 30 |
  • Refrain from demeaning, discriminatory, or harassing behavior and speech.
  • 31 |
  • Be mindful of your surroundings and of your fellow community members. Alert Women and Color if you notice a dangerous situation, someone in distress, or violations of this Code of Conduct, even if they seem inconsequential.
  • 32 |
  • Be understanding of differences. With diversity, we find strength.
  • 33 |
  • Participants who engage in a working relationship will treat each other respectfully.
  • 34 |
35 |

3. Unacceptable Behavior

36 |

The following are examples of behaviors that are considered harassment and are unacceptable within our community:

37 |
    38 |
  • Violence, threats of violence or violent language directed against another person or entity;
  • 39 |
  • Sexist, racist, homophobic, transphobic, ableist or otherwise discriminatory jokes or language;
  • 40 |
  • Posting or displaying sexually explicit or violent material;
  • 41 |
  • Posting or threatening to post other people’s personally identifying information (“doxing”);
  • 42 |
  • Personal insults, particularly those related to gender, sexual orientation, race, religion, or disability;
  • 43 |
  • Advocating for, or encouraging, any of the above behavior.
  • 44 |
45 |

4. Consequences of Unacceptable Behavior

46 |

Unacceptable behavior from any community member, including sponsors and those with decision-making authority, will not be tolerated. Anyone asked to stop unacceptable behavior is expected to comply immediately and is expected to adhere to Community Guidelines in all future interactions with our community. Failure to do so may result in a temporary ban or permanent expulsion, without further notice.

47 |

5. Reporting Guidelines

48 |

If you are subject to or witness unacceptable behavior, or have any other concerns, please notify a community organizer as soon as possible at hello@womenandcolor. Read our full Reporting Guide below.

49 |

6. Scope

50 |

We expect all community members (including, but not limited to, paid contributors, volunteer contributors, sponsors, or other guests) to abide by this Code of Conduct in all community interactions, both online and in-person.

51 |

This Code of Conduct and its related policies and procedures also applies to unacceptable behavior occurring outside the scope of community activities when such behavior has the potential to adversely affect the safety and well-being of community members.

52 |

Women and Color reserves the right to remove any profiles that violate our terms of use.

53 |

7. Contact Info

54 |

hello@womenandcolor.com

55 |

8. License & Attribution

56 |

This Code of Conduct is distributed under a Creative Commons Attribution-ShareAlike license.

57 |

Portions of this Code of Conduct derived from the Django Code of Conduct and the Geek Feminism Anti-Harassment Policy.

58 |

9. Women and Color’s Code of Conduct - Reporting Guide

59 |

If you believe someone is violating the code of conduct we ask that you report it to the Women and Color team by emailing hello@womenandcolor.com. We will make every reasonable effort to keep your report confidential.

60 |

In the event it is determined we need to address the matter publicly, we will attempt to maintain the confidentiality of all parties involved unless those individuals instruct us otherwise.

61 |

If you are unsure whether the incident is a violation, we encourage you to still report it! We strive to provide a safe environment and rather have a few extra reports where we decide to take no action, rather than miss a report of an actual violation. We do not look negatively on you if we find the incident is not a violation. Utilizing these reports, we can improve on our Code of Conduct and continue to provide the Women and Color Community with the best possible environment.

62 |

When making a report, please include the following:

63 |
    64 |
  • Contact Information;
  • 65 |
  • Names (real, nicknames, or pseudonyms) of all individuals involved. If there were other witnesses besides you, please try to include them as well;
  • 66 |
  • Specific details surrounding the incident, including when and where the incident occurred;
  • 67 |
  • If there is a publicly available record (e.g. a mailing list archive or a public IRC logger) please include a link.
  • 68 |
  • Any relevant information related to the incident;
  • 69 |
  • If you believe this incident is ongoing.
  • 70 |
  • Any other information you believe we should have.
  • 71 |
72 |

What happens after you file a report?

73 |

You will receive an email from Women and Color with an acknowledgment receipt. Once we have a complete account of the events we will make a decision as to how to respond.

74 |

Once we’ve determined our final action, we’ll contact the original reporter to let them know what action (if any) we’ll be taking.

75 |
76 |
77 |
78 | ) 79 | } 80 | 81 | export default CodeOfConduct; --------------------------------------------------------------------------------