├── .babelrc ├── .eslintrc ├── .gitignore ├── 200.html ├── README.md ├── _gulpfile.js ├── circle.yml ├── gulpfile.babel.js ├── images ├── cyclejs_logo.svg ├── daytimeicon_0.png ├── daytimeicon_1.png ├── daytimeicon_2.png ├── daytimeicon_3.png ├── daytimeicon_4.png ├── daytimeicon_5.png ├── daytimeicon_6.png ├── pitch │ ├── heartIcon.svg │ ├── icon-checklist.svg │ ├── icon-clock.svg │ ├── icon-direct.svg │ ├── icon-first.svg │ ├── icon-flag.svg │ ├── icon-mountains.svg │ ├── icon-piggy.svg │ ├── icon-sailboat.svg │ └── sparklerHeader-2048.jpg └── sn-logo-32.png ├── index.html ├── package.json ├── src ├── components │ ├── AppBar │ │ └── index.js │ ├── AppFrame │ │ └── index.js │ ├── AppMenu │ │ ├── index.js │ │ └── styles.scss │ ├── ApplyQuickNavMenu.js │ ├── BlankComponent.js │ ├── ComingSoon.js │ ├── CreateOrganizerInvite.js │ ├── DropAndCrop │ │ ├── Cropper.js │ │ ├── Dropper.js │ │ └── index.js │ ├── EnvBanner │ │ ├── index.js │ │ └── styles.scss │ ├── Header.js │ ├── HeaderLogo.js │ ├── OrganizerInviteForm.js │ ├── ProfileForm.js │ ├── QuickNav │ │ ├── index.js │ │ └── styles.scss │ ├── SetImage.js │ ├── SideNav │ │ └── index.js │ ├── SoloFrame.js │ ├── SwitchedComponent.js │ ├── TabBar │ │ ├── index.js │ │ └── styles.scss │ ├── TextareaListItemFactory.js │ ├── Title │ │ ├── index.js │ │ └── styles.scss │ ├── assignment │ │ ├── AssignmentsFetcher.js │ │ └── index.js │ ├── commitment │ │ ├── CommitmentList.js │ │ └── index.js │ ├── cyclic-surface-material │ │ ├── index.js │ │ └── scss │ │ │ ├── _imports │ │ │ ├── _colors.scss │ │ │ ├── _importMaster.scss │ │ │ ├── _reset.scss │ │ │ └── _variables.scss │ │ │ ├── alerts.scss │ │ │ ├── animations.scss │ │ │ ├── buttons.scss │ │ │ ├── cards_tiles.scss │ │ │ ├── code.scss │ │ │ ├── collapsible.scss │ │ │ ├── colors.scss │ │ │ ├── footer.scss │ │ │ ├── form.scss │ │ │ ├── general.scss │ │ │ ├── grid.scss │ │ │ ├── header.scss │ │ │ ├── lightbox.scss │ │ │ ├── links.scss │ │ │ ├── lists.scss │ │ │ ├── media.scss │ │ │ ├── modals.scss │ │ │ ├── nav.scss │ │ │ ├── surface_styles.scss │ │ │ ├── tables.scss │ │ │ ├── tabs.scss │ │ │ ├── tooltip.scss │ │ │ ├── type.scss │ │ │ └── utility.scss │ ├── engagement │ │ ├── EngagementButtons.js │ │ ├── EngagementItem.js │ │ ├── EngagementNav.js │ │ ├── EngagementPriorityList.js │ │ └── index.js │ ├── index.js │ ├── opp │ │ ├── AddCommitmentGet.js │ │ ├── AddCommitmentGive.js │ │ ├── CreateOppHeader.js │ │ ├── CreateOppListItem.js │ │ ├── GetItems.js │ │ ├── GiveItems.js │ │ ├── OppForm.js │ │ ├── OppListNavigating.js │ │ ├── OppNav.js │ │ ├── codeIcons.js │ │ ├── codePopups.js │ │ ├── codeTitles.js │ │ └── index.js │ ├── organizer │ │ ├── OrganizerInviteItem.js │ │ ├── OrganizerItem.js │ │ └── index.js │ ├── profile │ │ ├── ProfileAvatar │ │ │ └── index.js │ │ ├── ProfileFetcher.js │ │ ├── ProfileSidenav.js │ │ └── index.js │ ├── project │ │ ├── ProjectAvatar.js │ │ ├── ProjectForm.js │ │ ├── ProjectItem.js │ │ ├── ProjectNav.js │ │ ├── ProjectQuickNavMenu.js │ │ └── index.js │ ├── redirects.js │ ├── remote │ │ └── index.js │ ├── sdm │ │ ├── Avatar │ │ │ ├── index.js │ │ │ └── styles.scss │ │ ├── Button.js │ │ ├── Card │ │ │ ├── index.js │ │ │ └── styles.scss │ │ ├── CheckboxControl.js │ │ ├── Dialog │ │ │ └── index.js │ │ ├── Fab.js │ │ ├── InputControl.js │ │ ├── List.js │ │ ├── ListItem │ │ │ ├── ListItem.js │ │ │ ├── ListItemCheckbox.js │ │ │ ├── ListItemClickable.js │ │ │ ├── ListItemCollapsible.js │ │ │ ├── ListItemCollapsibleTextArea.js │ │ │ ├── ListItemCollapsibleWithMenu.js │ │ │ ├── ListItemHeader.js │ │ │ ├── ListItemNavigating.js │ │ │ ├── ListItemTextArea.js │ │ │ ├── ListItemToggle.js │ │ │ ├── ListItemWithDialog.js │ │ │ ├── ListItemWithMenu.js │ │ │ ├── index.js │ │ │ └── styles.scss │ │ ├── Menu │ │ │ ├── index.js │ │ │ └── styles.scss │ │ ├── SelectControl.js │ │ ├── TextAreaControl │ │ │ ├── index.js │ │ │ └── styles.scss │ │ ├── ToggleControl.js │ │ ├── Toolbar.js │ │ ├── id.js │ │ ├── index.js │ │ └── styles.scss │ ├── shift │ │ └── index.js │ ├── team │ │ ├── CreateTeamHeader.js │ │ ├── CreateTeamListItem.js │ │ ├── TeamAvatar.js │ │ ├── TeamFetcher.js │ │ ├── TeamForm.js │ │ ├── TeamIcon.js │ │ ├── TeamItemNavigating.js │ │ ├── TeamListNavigating.js │ │ └── index.js │ └── ui │ │ ├── ActionButton.js │ │ ├── DescriptionListItem.js │ │ ├── Form.js │ │ ├── LoginButtons.js │ │ ├── MenuItemPopup.js │ │ ├── Modal.js │ │ ├── QuotingListItem.js │ │ ├── RoutedComponent.js │ │ ├── StepListItem.js │ │ ├── SubtitleListItem.js │ │ ├── TabbedPage.js │ │ ├── TitleListItem.js │ │ ├── ToDoListItem.js │ │ ├── index.js │ │ └── styles.scss ├── drivers │ ├── bugsnag.js │ └── isMobile.js ├── helpers │ ├── buttons │ │ ├── index.js │ │ └── styles.scss │ ├── fabMenu.js │ ├── frame.js │ ├── index.js │ ├── landing.js │ ├── layout.js │ ├── listHeader.js │ ├── listItem │ │ └── index.js │ ├── listItemDisabled.js │ ├── menu.js │ ├── menuItem.js │ ├── modal.js │ ├── projectForm.js │ ├── quickNavMenu.js │ ├── sideNav.js │ ├── tabs │ │ ├── index.js │ │ └── styles.scss │ └── text │ │ ├── index.js │ │ └── styles.scss ├── main.js ├── remote.js ├── root │ ├── Admin │ │ ├── Profiles.js │ │ ├── Projects.js │ │ └── index.js │ ├── Apply │ │ ├── Opp.js │ │ ├── Overview.js │ │ └── index.js │ ├── Confirm │ │ └── index.js │ ├── Dash │ │ ├── Being.js │ │ ├── Doing.js │ │ └── index.js │ ├── Engagement │ │ ├── Application │ │ │ ├── AnswerQuestion.js │ │ │ ├── ChooseTeams.js │ │ │ ├── Step1.js │ │ │ ├── Step2.js │ │ │ └── index.js │ │ ├── Confirmation │ │ │ ├── Accountability.js │ │ │ ├── MakePayment.js │ │ │ ├── Nonrefundable.js │ │ │ ├── Step1.js │ │ │ ├── Step2.js │ │ │ └── index.js │ │ ├── Glance │ │ │ ├── Commitments.js │ │ │ ├── Priority.js │ │ │ └── index.js │ │ ├── OldApplication │ │ │ ├── AnswerQuestion.js │ │ │ ├── ChooseTeams.js │ │ │ ├── NextSteps.js │ │ │ └── index.js │ │ ├── Priority │ │ │ ├── CardApplicationNextSteps.js │ │ │ ├── CardConfirmNow.js │ │ │ ├── CardEnergyExchange.js │ │ │ ├── CardPickMoreShifts.js │ │ │ ├── CardUpcomingShifts.js │ │ │ ├── CardWhois.js │ │ │ └── index.js │ │ ├── Schedule │ │ │ ├── Priority.js │ │ │ └── index.js │ │ └── index.js │ ├── Landing │ │ ├── index.js │ │ └── styles.scss │ ├── Opp │ │ ├── Confirmed │ │ │ ├── AssignmentItem.js │ │ │ ├── Item.js │ │ │ └── index.js │ │ ├── Engaged │ │ │ ├── Detail.js │ │ │ ├── FilteredView.js │ │ │ └── index.js │ │ ├── FetchEngagements.js │ │ ├── Glance │ │ │ ├── Priority.js │ │ │ └── index.js │ │ ├── Manage │ │ │ ├── Applying.js │ │ │ ├── Describe.js │ │ │ ├── Exchange.js │ │ │ └── index.js │ │ ├── OppNav.js │ │ ├── RecruitmentLinkItem.js │ │ └── index.js │ ├── Organize │ │ └── index.js │ ├── Project │ │ ├── Glance │ │ │ ├── Checkin.js │ │ │ ├── Priority.js │ │ │ └── index.js │ │ ├── Manage │ │ │ ├── Describe.js │ │ │ ├── Staff.js │ │ │ └── index.js │ │ └── index.js │ ├── SideNav.js │ ├── SideNav.scss │ ├── Team │ │ ├── Glance │ │ │ ├── Priority.js │ │ │ └── index.js │ │ ├── Manage │ │ │ ├── Applying.js │ │ │ ├── Describe.js │ │ │ └── index.js │ │ ├── Members │ │ │ ├── Applied.js │ │ │ ├── Detail.js │ │ │ ├── FilteredView.js │ │ │ └── index.js │ │ ├── Schedule │ │ │ ├── AssignmentItem.js │ │ │ ├── Overview.js │ │ │ ├── ShiftForm.js │ │ │ ├── Shifts.js │ │ │ └── index.js │ │ └── index.js │ ├── index.js │ └── styles.scss └── util.js └── webpack.config.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": [ "transform-class-properties" ], 3 | "presets": ["es2015", "stage-0", "react"] 4 | } -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "eslint-config-cycle", 3 | "parser": "babel-eslint", 4 | "env": { 5 | "browser": true, 6 | "node": true 7 | }, 8 | "rules": { 9 | "quotes": [2, "single"], 10 | "no-class/no-class": 0, 11 | "no-unused-vars": [1, {"varsIgnorePattern": "React"}], 12 | "max-params": [1, 5] 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.local.* 2 | 3 | # Logs 4 | logs 5 | *.log 6 | npm-debug.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | 13 | # Directory for instrumented libs generated by jscoverage/JSCover 14 | lib-cov 15 | 16 | # Coverage directory used by tools like istanbul 17 | coverage 18 | 19 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 20 | .grunt 21 | 22 | # node-waf configuration 23 | .lock-wscript 24 | 25 | # Compiled binary addons (http://nodejs.org/api/addons.html) 26 | build/Release 27 | 28 | # Dependency directory 29 | node_modules 30 | 31 | # Optional npm cache directory 32 | .npm 33 | 34 | # Optional REPL history 35 | .node_repl_history 36 | 37 | # dist 38 | dist -------------------------------------------------------------------------------- /200.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Sparks.Network 8 | 9 | 10 | 19 | 22 | 23 | 24 | 25 | 26 | 27 |
28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /circle.yml: -------------------------------------------------------------------------------- 1 | machine: 2 | node: 3 | version: 4.2.2 4 | 5 | notify: 6 | webhooks: 7 | - url: https://hooks.slack.com/services/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 8 | 9 | deployment: 10 | production: 11 | branch: master 12 | commands: 13 | - BUILD_ENV=production BUILD_FIREBASE_HOST=http://sparks-production.firebaseio.com npm run build 14 | - surge ./dist sparks.network 15 | staging: 16 | branch: release 17 | commands: 18 | - BUILD_ENV=staging BUILD_FIREBASE_HOST=http://sparks-staging.firebaseio.com npm run build 19 | - surge ./dist staging.sparks.network 20 | -------------------------------------------------------------------------------- /gulpfile.babel.js: -------------------------------------------------------------------------------- 1 | import gulp from 'gulp' 2 | import webpack from 'webpack' 3 | import webpackStream from 'webpack-stream' 4 | import copy from 'gulp-copy' 5 | import del from 'del' 6 | import sequence from 'run-sequence' 7 | import gutil from 'gulp-util' 8 | import surge from 'gulp-surge' 9 | 10 | import WebpackDevServer from 'webpack-dev-server' 11 | 12 | import WEBPACK_CONFIG from './webpack.config' 13 | 14 | import minimist from 'minimist' 15 | 16 | const args = minimist(process.argv.slice(2)) 17 | 18 | const path = { 19 | ENTRY: './src/main.js', 20 | DEST: 'dist/', 21 | INDEX: './200.html', 22 | } 23 | 24 | const dev = { 25 | PORT: 8080, 26 | HOST: 'localhost', 27 | } 28 | 29 | gulp.task('default', ['build']) 30 | 31 | gulp.task('build', cb => sequence('clean', 'build:webpack', 'build:dist', cb)) 32 | 33 | gulp.task('clean', () => del([path.DEST])) 34 | 35 | gulp.task('build:dist', () => 36 | gulp.src(path.INDEX) 37 | .pipe(copy(path.DEST)) 38 | ) 39 | 40 | gulp.task('build:webpack', () => 41 | gulp.src(path.ENTRY) 42 | .pipe(webpackStream(WEBPACK_CONFIG)) 43 | .pipe(gulp.dest(path.DEST)) 44 | ) 45 | 46 | gulp.task('serve', cb => { 47 | const log = msg => gutil.log('[webpack-dev-server]', msg) 48 | 49 | const config = Object.create(WEBPACK_CONFIG) 50 | // config.devtool = 'cheap-module-eval-source-map' 51 | config.devtool = 'eval' 52 | config.debug = true 53 | 54 | log('Creating dev server...') 55 | const server = new WebpackDevServer(webpack(config), { 56 | publicPath: '/', 57 | inline: true, 58 | historyApiFallback: true, 59 | stats: {colors: true} 60 | }) 61 | 62 | log('...starting to listen...') 63 | server.listen(dev.PORT, dev.HOST, err => { 64 | if (err) { throw new gutil.PluginError('webpack-dev-server', err) } 65 | log(`http://${dev.HOST}:${dev.PORT}/webpack-dev-server/index.html`) 66 | }) 67 | }) 68 | 69 | gulp.task('deploy', ['build'], cb => { 70 | const log = msg => gutil.log('[surge]', msg) 71 | const domain = args.domain 72 | const project = path.DEST 73 | 74 | log('Starting surge deployment of ' + project + ' to ' + domain + ' ...') 75 | return surge({project, domain}) 76 | }) 77 | -------------------------------------------------------------------------------- /images/cyclejs_logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 8 | 9 | 10 | 11 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /images/daytimeicon_0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sdebaun/sparks-cyclejs/648af959914a455c0f9313c42e446d5edaf9b9e4/images/daytimeicon_0.png -------------------------------------------------------------------------------- /images/daytimeicon_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sdebaun/sparks-cyclejs/648af959914a455c0f9313c42e446d5edaf9b9e4/images/daytimeicon_1.png -------------------------------------------------------------------------------- /images/daytimeicon_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sdebaun/sparks-cyclejs/648af959914a455c0f9313c42e446d5edaf9b9e4/images/daytimeicon_2.png -------------------------------------------------------------------------------- /images/daytimeicon_3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sdebaun/sparks-cyclejs/648af959914a455c0f9313c42e446d5edaf9b9e4/images/daytimeicon_3.png -------------------------------------------------------------------------------- /images/daytimeicon_4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sdebaun/sparks-cyclejs/648af959914a455c0f9313c42e446d5edaf9b9e4/images/daytimeicon_4.png -------------------------------------------------------------------------------- /images/daytimeicon_5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sdebaun/sparks-cyclejs/648af959914a455c0f9313c42e446d5edaf9b9e4/images/daytimeicon_5.png -------------------------------------------------------------------------------- /images/daytimeicon_6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sdebaun/sparks-cyclejs/648af959914a455c0f9313c42e446d5edaf9b9e4/images/daytimeicon_6.png -------------------------------------------------------------------------------- /images/pitch/heartIcon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /images/pitch/icon-direct.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 9 | 10 | -------------------------------------------------------------------------------- /images/pitch/sparklerHeader-2048.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sdebaun/sparks-cyclejs/648af959914a455c0f9313c42e446d5edaf9b9e4/images/pitch/sparklerHeader-2048.jpg -------------------------------------------------------------------------------- /images/sn-logo-32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sdebaun/sparks-cyclejs/648af959914a455c0f9313c42e446d5edaf9b9e4/images/sn-logo-32.png -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Sparks.Network 8 | 9 | 10 | 11 | 12 | 13 |
14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /src/components/AppBar/index.js: -------------------------------------------------------------------------------- 1 | import {Toolbar} from 'components/sdm' 2 | 3 | import AppMenu from 'components/AppMenu' 4 | import HeaderLogo from 'components/HeaderLogo' 5 | 6 | import {sidenavButton} from 'components/Title' 7 | 8 | const AppBar = sources => { 9 | const appMenu = AppMenu(sources) 10 | const headerLogo = HeaderLogo(sources) 11 | 12 | return { 13 | auth$: appMenu.auth$, 14 | route$: appMenu.route$, 15 | ...Toolbar({ 16 | ...sources, 17 | leftItemDOM$: sources.isMobile$.map(m => m ? sidenavButton : null), 18 | titleDOM$: headerLogo.DOM, 19 | rightItemDOM$: appMenu.DOM, 20 | }), 21 | } 22 | } 23 | 24 | export {AppBar} 25 | -------------------------------------------------------------------------------- /src/components/AppFrame/index.js: -------------------------------------------------------------------------------- 1 | import {Observable as $} from 'rx' 2 | 3 | import {AppBar} from 'components/AppBar' 4 | import SideNav from 'components/SideNav' 5 | 6 | import {mobileFrame, desktopFrame} from 'helpers' 7 | import {mergeOrFlatMapLatest} from 'util' 8 | 9 | import {div} from 'helpers' 10 | 11 | export default sources => { 12 | const appBar = AppBar(sources) 13 | 14 | const navButton$ = sources.DOM.select('.nav-button').events('click') 15 | 16 | const sideNav = SideNav({ 17 | contentDOM: sources.navDOM, 18 | isOpen$: navButton$.map(true).startWith(false), 19 | ...sources, 20 | }) 21 | 22 | const children = [appBar, sideNav] 23 | 24 | const auth$ = mergeOrFlatMapLatest('auth$', ...children) 25 | const route$ = mergeOrFlatMapLatest('route$', ...children) 26 | 27 | const layoutParams = { 28 | sideNav: sideNav.DOM, 29 | appBar: appBar.DOM, 30 | header: sources.headerDOM || $.of(div('',[])), 31 | page: sources.pageDOM, 32 | } 33 | 34 | const DOM = sources.isMobile$.map(isMobile => 35 | isMobile ? mobileFrame(layoutParams) : desktopFrame(layoutParams) 36 | ) 37 | 38 | return { 39 | DOM, 40 | auth$, 41 | route$, 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/components/AppMenu/styles.scss: -------------------------------------------------------------------------------- 1 | .app-menu { 2 | i { 3 | color: #FFF; 4 | } 5 | .menu-contents * { 6 | color: #000; 7 | } 8 | } -------------------------------------------------------------------------------- /src/components/ApplyQuickNavMenu.js: -------------------------------------------------------------------------------- 1 | // TODO: tlc 2 | 3 | import {Observable} from 'rx' 4 | import combineLatestObj from 'rx-combine-latest-obj' 5 | 6 | import quickNavMenu from 'helpers/quickNavMenu' 7 | 8 | import {rows} from 'util' 9 | // import {log} from 'util' 10 | 11 | // const _navActions$ = ({DOM, projectKey$}) => 12 | // Observable.merge( 13 | // projectKey$.sample(DOM.select('.project').events('click')) 14 | // .map(projectKey => '/apply/' + projectKey), 15 | // DOM.select('.opp').events('click') 16 | // .map(e => e.ownerTarget.dataset.link) 17 | // ) 18 | 19 | const _openActions$ = ({DOM}) => Observable.merge( 20 | DOM.select('.apply-menu-button').events('click').map(true), 21 | DOM.select('.close-menu').events('click').map(false), 22 | ) 23 | 24 | const _oppItems = (oppRows, createHref) => [ 25 | // oppRows.length && {divider: true}, 26 | ...oppRows.map(({name,$key}) => ( 27 | {className: 'opp.navLink', label: name, link: createHref('/opp/' + $key)}, 28 | )), 29 | ] 30 | 31 | const _menuItems = (project, opps, createHref) => [ 32 | ..._oppItems(rows(opps), createHref), 33 | ].filter(r => !!r) 34 | 35 | const _render = ({isOpen, project, opps, createHref}) => 36 | quickNavMenu({ 37 | isOpen, 38 | className: 'apply-menu-button', // necessary with isolate? 39 | label: 'Check out one of these ways you can get involved...', 40 | menu: {rightAlign: false}, 41 | items: _menuItems(project,opps,createHref), 42 | color: '#000', 43 | }) 44 | 45 | const _navActions = ({DOM}) => 46 | DOM.select('.navLink').events('click') 47 | .map(e => e.ownerTarget.dataset.link) 48 | 49 | export default sources => { 50 | const route$ = _navActions(sources) 51 | 52 | const isOpen$ = _openActions$(sources) 53 | .merge(route$.map(false)) 54 | .startWith(false) 55 | 56 | const viewState = { 57 | isOpen$, 58 | project$: sources.project$, 59 | opps$: sources.opps$, 60 | auth$: sources.auth$, 61 | userProfile$: sources.userProfile$, 62 | createHref: Observable.just(sources.router.createHref), 63 | } 64 | 65 | const DOM = combineLatestObj(viewState).map(_render) 66 | 67 | return { 68 | DOM, 69 | route$, 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/components/BlankComponent.js: -------------------------------------------------------------------------------- 1 | import {Observable as $} from 'rx' 2 | import {div} from 'cycle-snabbdom' 3 | 4 | const BlankComponent = () => { 5 | return {DOM: $.just(div())} 6 | } 7 | 8 | export {BlankComponent} 9 | -------------------------------------------------------------------------------- /src/components/ComingSoon.js: -------------------------------------------------------------------------------- 1 | import {Observable} from 'rx' 2 | import {TitleListItem} from 'components/ui' 3 | 4 | export default name => sources => TitleListItem({...sources, 5 | title$: Observable.of('Coming Soon: ' + name), 6 | }) 7 | -------------------------------------------------------------------------------- /src/components/CreateOrganizerInvite.js: -------------------------------------------------------------------------------- 1 | import {Observable} from 'rx' 2 | const {just} = Observable 3 | 4 | // import isolate from '@cycle/isolate' 5 | 6 | import {Organizers} from 'remote' 7 | 8 | import {ListItemWithDialog} from 'components/sdm' 9 | import {OrganizerInviteForm} from 'components/OrganizerInviteForm' 10 | 11 | const CreateOrganizerListItem = sources => { 12 | const form = OrganizerInviteForm(sources) 13 | 14 | const listItem = ListItemWithDialog({...sources, 15 | iconName$: just('person_add'), 16 | title$: just('Invite another Organizer to help you run the project.'), 17 | dialogTitleDOM$: just('Invite Organizer'), 18 | dialogContentDOM$: form.DOM, 19 | }) 20 | 21 | const queue$ = form.item$ 22 | .sample(listItem.submit$) 23 | .zip(sources.projectKey$, (opp,projectKey) => ({projectKey, ...opp})) 24 | .map(Organizers.create) 25 | 26 | return { 27 | DOM: listItem.DOM, 28 | queue$, 29 | } 30 | } 31 | 32 | export default CreateOrganizerListItem 33 | -------------------------------------------------------------------------------- /src/components/DropAndCrop/Cropper.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom' 3 | import ReactCropper from 'react-cropper' 4 | import {Observable, BehaviorSubject} from 'rx' 5 | const {just, combineLatest} = Observable 6 | 7 | import {reactComponent} from 'helpers' 8 | 9 | // stupid react component requires ref (and thus a class) 10 | // to get dataurl from canvas 11 | class Cropper extends React.Component { 12 | crop = () => this.props.onCrop( 13 | this.refs.cropper.getCroppedCanvas().toDataURL() 14 | ) 15 | 16 | render() { 17 | return 22 | } 23 | } 24 | 25 | export default (sources) => { 26 | const cropped$ = new BehaviorSubject(null) 27 | 28 | const DOM1 = combineLatest( 29 | sources.image$ || just(null), 30 | sources.aspectRatio$ || just(300 / 120), 31 | (src, aspectRatio) => 32 | reactComponent(Cropper, { 33 | src, 34 | aspectRatio, 35 | onCrop: e => cropped$.onNext(e), 36 | }, 'update') 37 | ) 38 | 39 | // const DOM2 = sources.image$ 40 | // .map(src => reactComponent(Cropper, { 41 | // src, 42 | // onCrop: e => cropped$.onNext(e), 43 | // aspectRatio: 300 / 120, // HAX 44 | // }, 'update')) 45 | 46 | return { 47 | cropped$, 48 | DOM: DOM1, 49 | // DOM: DOM2, 50 | // has to be attached on 'update', the default, breaks if 'insert' 51 | // DOM: sources.image$ 52 | // .map(src => reactComponent(Cropper, { 53 | // src, 54 | // onCrop: e => cropped$.onNext(e), 55 | // aspectRatio: 300 / 120, // HAX 56 | // }, 'update')), 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/components/DropAndCrop/Dropper.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom' 3 | import ReactDropzone from 'react-dropzone' 4 | import {BehaviorSubject} from 'rx' 5 | import {reactComponent} from 'helpers' 6 | 7 | const divStyle = { 8 | padding: '1em', 9 | display: 'flex', 10 | justifyContent: 'center', 11 | alignItems: 'center', 12 | border: '3px dashed #666', 13 | borderRadius: '1em', 14 | } 15 | 16 | const Dropper = ({dropped$}) => 17 | dropped$.onNext(e[0].preview)} 19 | style={{maxWidth: 800, margin: '0 auto'}}> 20 |
21 | Drop an Image or click to upload 22 |
23 |
24 | 25 | export default () => { 26 | const dropped$ = new BehaviorSubject(null) 27 | 28 | return { 29 | dropped$, // has to be attached on 'insert', breaks if changed to 'update' 30 | DOM: dropped$.map( 31 | () => reactComponent(Dropper, {dropped$}, 'insert') 32 | ), 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/components/DropAndCrop/index.js: -------------------------------------------------------------------------------- 1 | import Dropper from './Dropper' 2 | import Cropper from './Cropper' 3 | // import {Observable} from 'rx' 4 | import combineLatestObj from 'rx-combine-latest-obj' 5 | import {col} from 'helpers' 6 | import {img, h5} from 'cycle-snabbdom' 7 | import {Row, Button} from 'snabbdom-material' 8 | 9 | const imageStyle = { 10 | border: 'solid 2px black', 11 | height: '128px', 12 | width: '128px', 13 | } 14 | 15 | const _render = ({dropper, dropped, cropper, cropped}) => 16 | col( 17 | dropped ? cropper : dropper, 18 | Row({style: {textAlign: 'center'}}, [ 19 | col( 20 | h5({style: {marginTop: '0.5em', textAlign: 'center'}}, [ 21 | cropped ? 'Image Preview' : null, 22 | ]), 23 | cropped && img({attrs: {src: cropped}, style: imageStyle}), 24 | ), 25 | cropped && Button({className: 'save-image', onClick: true}, [ 26 | 'Click me when finished!', 27 | ]), 28 | ]) 29 | ) 30 | 31 | export default (sources) => { 32 | const save$ = sources.DOM.select('.save-image').events('click') 33 | const {DOM: dropper, dropped$} = Dropper(sources) 34 | const {DOM: cropper, cropped$} = Cropper({...sources, 35 | image$: dropped$, 36 | }) 37 | 38 | const dataUrl$ = cropped$.sample(save$) 39 | 40 | const viewState = { 41 | dropper, dropped$, 42 | cropper, cropped$, 43 | } 44 | 45 | const DOM = combineLatestObj(viewState).map(_render) 46 | 47 | return { 48 | DOM, 49 | dataUrl$, 50 | cropped$, 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/components/EnvBanner/index.js: -------------------------------------------------------------------------------- 1 | require('./styles.scss') 2 | import {div} from 'cycle-snabbdom' 3 | 4 | const EnvBanner = () => 5 | process.env.BUILD_ENV === 'production' ? 6 | null : 7 | div('.env-banner', [process.env.BUILD_ENV]) 8 | 9 | export default EnvBanner 10 | -------------------------------------------------------------------------------- /src/components/EnvBanner/styles.scss: -------------------------------------------------------------------------------- 1 | 2 | .env-banner { 3 | background: rgb(255, 255, 200); 4 | border: 1px solid orange; 5 | color: orange; 6 | height: 30px; 7 | left: 50%; 8 | margin: 0 0 0 -75px; 9 | position: fixed; 10 | text-align: center; 11 | top: 0px; 12 | width: 150px; 13 | z-index: 2; 14 | } 15 | -------------------------------------------------------------------------------- /src/components/Header.js: -------------------------------------------------------------------------------- 1 | import {div} from 'cycle-snabbdom' 2 | 3 | export default sources => ({ 4 | DOM: sources.isMobile$ 5 | .map(isMobile => 6 | div( 7 | {}, 8 | [isMobile ? sources.titleDOM : sources.tabsDOM] 9 | ) 10 | ), 11 | }) 12 | 13 | -------------------------------------------------------------------------------- /src/components/HeaderLogo.js: -------------------------------------------------------------------------------- 1 | import {Observable} from 'rx' 2 | import {a, img} from 'cycle-snabbdom' 3 | 4 | const src = require('images/sn-logo-32.png') 5 | 6 | export default () => ({ 7 | DOM: Observable.just( 8 | a({props: {href: '/'}}, [ 9 | img({ 10 | style: {height: '24px', float: 'left'}, 11 | attrs: {src: '/' + src}, 12 | }), 13 | ]) 14 | ), 15 | }) 16 | -------------------------------------------------------------------------------- /src/components/OrganizerInviteForm.js: -------------------------------------------------------------------------------- 1 | import {Observable} from 'rx' 2 | const {just} = Observable 3 | 4 | import {Form} from 'components/ui/Form' 5 | import {InputControl, SelectControl} from 'components/sdm' 6 | 7 | const EmailInput = sources => 8 | InputControl({label$: just('Send to Email'), ...sources}) 9 | 10 | const authorityOptions = [ 11 | {value: 'manager', label: 'Manager'}, 12 | {value: 'owner', label: 'Owner'}, 13 | ] 14 | 15 | const AuthoritySelect = sources => SelectControl({...sources, 16 | label$: just('What kind of Organizer?'), 17 | options$: just(authorityOptions), 18 | }) 19 | 20 | const OrganizerInviteForm = sources => Form({ 21 | ...sources, 22 | Controls$: just([ 23 | {field: 'inviteEmail', Control: EmailInput}, 24 | {field: 'authority', Control: AuthoritySelect}, 25 | ]), 26 | }) 27 | 28 | export {OrganizerInviteForm} 29 | -------------------------------------------------------------------------------- /src/components/ProfileForm.js: -------------------------------------------------------------------------------- 1 | import {Observable} from 'rx' 2 | const {just} = Observable 3 | 4 | import {Form} from 'components/ui/Form' 5 | import {InputControl} from 'components/sdm' 6 | 7 | import {importantTip} from 'helpers' 8 | 9 | const InfoBlock = () => ({ 10 | DOM: just( 11 | importantTip(` 12 | Your email and phone number will only be shared 13 | with organizers that you work with. 14 | `), 15 | ), 16 | }) 17 | 18 | const FullNameInput = sources => 19 | InputControl({label$: just('Your Full Name'), ...sources}) 20 | 21 | const EmailInput = sources => 22 | InputControl({label$: just('Your Email Address'), ...sources}) 23 | 24 | const PhoneInput = sources => 25 | InputControl({label$: just('Your Phone Number'), ...sources}) 26 | 27 | const ProfileForm = sources => Form({...sources, 28 | Controls$: just([ 29 | {field: 'fullName', Control: FullNameInput}, 30 | {Control: InfoBlock}, 31 | {field: 'email', Control: EmailInput}, 32 | {field: 'phone', Control: PhoneInput}, 33 | ]), 34 | }) 35 | 36 | export {ProfileForm} 37 | -------------------------------------------------------------------------------- /src/components/QuickNav/index.js: -------------------------------------------------------------------------------- 1 | require('./styles.scss') 2 | 3 | import {Observable} from 'rx' 4 | const {just, combineLatest} = Observable 5 | 6 | import {icon, div} from 'helpers' 7 | 8 | import { 9 | Menu, 10 | } from 'components/sdm' 11 | 12 | const HeaderClickable = sources => ({ 13 | click$: sources.DOM.select('.nav').events('click'), 14 | DOM: sources.label$.map(l => div('.nav',[l])), 15 | }) 16 | 17 | const QuickNav = sources => { 18 | const item = HeaderClickable({...sources, 19 | label$: sources.label$.map(name => 20 | div({},[name, icon('caret-down')]) 21 | ), 22 | }) 23 | 24 | const isOpen$ = item.click$.map(true).startWith(false) 25 | 26 | const children$ = sources.menuItems$ || just([]) 27 | 28 | const menu = Menu({ 29 | ...sources, 30 | isOpen$, 31 | children$, 32 | }) 33 | 34 | const DOM = combineLatest( 35 | item.DOM, menu.DOM, 36 | (...doms) => div('.quick-nav',doms) 37 | ) 38 | 39 | return { 40 | DOM, 41 | } 42 | } 43 | 44 | export {QuickNav} 45 | -------------------------------------------------------------------------------- /src/components/QuickNav/styles.scss: -------------------------------------------------------------------------------- 1 | .quick-nav { 2 | cursor: pointer; 3 | 4 | .nav { 5 | font-size: 16px; 6 | font-weight: normal; 7 | i { 8 | margin-left: 12px; 9 | } 10 | } 11 | } 12 | 13 | .title-block .quick-nav { 14 | .nav { 15 | color: #EEE; 16 | } 17 | 18 | .menu * { 19 | color: black; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/components/SetImage.js: -------------------------------------------------------------------------------- 1 | import {Observable} from 'rx' 2 | const {just} = Observable 3 | 4 | import DropAndCrop from 'components/DropAndCrop' 5 | 6 | import { 7 | ListItemCollapsible, 8 | } from 'components/sdm' 9 | 10 | const ChooseItem = sources => ListItemCollapsible({...sources, 11 | title$: sources.inputDataUrl$.map(v => 12 | v ? 'Change your picture.' : 'Choose a picture to use.' 13 | ), 14 | iconName$: just('add_a_photo'), 15 | }) 16 | 17 | export default sources => { 18 | const dropAndCrop = DropAndCrop(sources) 19 | 20 | const choose = ChooseItem({...sources, 21 | isOpen$: dropAndCrop.dataUrl$.map(false), 22 | contentDOM$: dropAndCrop.DOM, 23 | }) 24 | 25 | return { 26 | DOM: choose.DOM, 27 | dataUrl$: dropAndCrop.dataUrl$, 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/components/SideNav/index.js: -------------------------------------------------------------------------------- 1 | import {sideNav} from 'helpers' 2 | import combineLatestObj from 'rx-combine-latest-obj' 3 | 4 | export default sources => { 5 | const close$ = sources.DOM.select('.close-sideNav').events('click').map(false) 6 | 7 | const DOM = combineLatestObj({ 8 | isMobile: sources.isMobile$, 9 | isOpen: sources.isOpen$.merge(close$), 10 | content: sources.contentDOM, 11 | }).map(sideNav) 12 | 13 | return { 14 | DOM, 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/components/SoloFrame.js: -------------------------------------------------------------------------------- 1 | import {AppBar} from 'components/AppBar' 2 | 3 | import {mobileFrame, desktopFrame} from 'helpers' 4 | 5 | export default sources => { 6 | const appBar = AppBar(sources) 7 | 8 | const auth$ = appBar.auth$ 9 | const route$ = appBar.route$.share() 10 | 11 | const layoutParams = { 12 | appBar: appBar.DOM, 13 | header: sources.headerDOM, 14 | page: sources.pageDOM, 15 | } 16 | 17 | const DOM = sources.isMobile$.map(isMobile => 18 | isMobile ? mobileFrame(layoutParams) : desktopFrame(layoutParams) 19 | ) 20 | 21 | return {DOM, auth$, route$} 22 | } 23 | -------------------------------------------------------------------------------- /src/components/SwitchedComponent.js: -------------------------------------------------------------------------------- 1 | import {pluckLatest, pluckLatestOrNever} from 'util' 2 | import isolate from '@cycle/isolate' 3 | 4 | const SwitchedComponent = sources => { 5 | const comp$ = sources.Component$ 6 | .distinctUntilChanged() 7 | .map(C => isolate(C)(sources)) 8 | .shareReplay(1) 9 | 10 | return { 11 | pluck: key => pluckLatestOrNever(key, comp$), 12 | DOM: pluckLatest('DOM', comp$), 13 | ...['auth$', 'queue$', 'route$'].reduce((a,k) => 14 | (a[k] = pluckLatestOrNever(k,comp$)) && a, {} 15 | ), 16 | } 17 | } 18 | 19 | export {SwitchedComponent} 20 | -------------------------------------------------------------------------------- /src/components/TabBar/index.js: -------------------------------------------------------------------------------- 1 | import {Observable} from 'rx' 2 | const {combineLatest} = Observable 3 | 4 | import {div,h} from 'cycle-snabbdom' 5 | 6 | import {material} from 'util' 7 | 8 | import './styles.scss' 9 | 10 | import {controlsFromRows, combineDOMsToDiv, mergeOrFlatMapLatest} from 'util' 11 | // import {log} from 'util' 12 | 13 | const _view = ({label}) => 14 | div({class: {'tab-label-content': true}},[ 15 | h('label',{attrs: {for: label}, style: { 16 | color: material.primaryFontColor}, 17 | },[label]), 18 | ]) 19 | 20 | const Tab = sources => { 21 | const click$ = sources.DOM.select('div').events('click') 22 | const path$ = sources.item$.pluck('path') 23 | .map(p => sources.router.createHref(p)) 24 | 25 | return { 26 | DOM: sources.item$.map(_view), 27 | route$: click$.withLatestFrom(path$, (c,p) => p), 28 | } 29 | } 30 | 31 | const isBetter = (cur, next, best) => 32 | cur.includes(next) && next.length > best.length 33 | 34 | const bestMatchIdx = (curPath, paths) => 35 | paths.reduce((bestIdx,nextPath,i) => 36 | isBetter(curPath,nextPath,paths[bestIdx]) ? i : bestIdx, 37 | 0 38 | ) 39 | 40 | const dist = (curPath, tabs, createHref) => 41 | bestMatchIdx(curPath, tabs.map(t => createHref(t.path))) * 100 / tabs.length 42 | 43 | const Slide = sources => { 44 | const DOM = combineLatest( 45 | sources.tabs$, 46 | sources.router.observable.pluck('pathname'), 47 | (t,p) => 48 | div({ 49 | class: {slide: true}, 50 | style: { 51 | width: `${100 / t.length}%`, 52 | left: `${dist(p,t,sources.router.createHref)}%`, 53 | }, 54 | },['']), 55 | ) 56 | return { 57 | DOM, 58 | } 59 | } 60 | 61 | const TabBar = sources => { 62 | const sl = Slide(sources) 63 | const tctrls$ = sources.tabs$.map(tabs => 64 | controlsFromRows(sources, tabs.map((t,i) => ({$key: `${i}`, ...t})), Tab) 65 | ).shareReplay(1) 66 | 67 | return { 68 | DOM: tctrls$.map(c => combineDOMsToDiv('.tab-wrap', ...c, sl)).switch(), 69 | route$: tctrls$.map(c => mergeOrFlatMapLatest('route$', ...c)).switch(), 70 | } 71 | } 72 | 73 | export {TabBar} 74 | -------------------------------------------------------------------------------- /src/components/Title/styles.scss: -------------------------------------------------------------------------------- 1 | .title-block.profile { 2 | cursor: pointer; 3 | } 4 | 5 | .title-block { 6 | z-index: 0; 7 | height: 120px; 8 | background-size: cover; 9 | display: flex; 10 | flex-direction: column; 11 | justify-content: flex-end; 12 | min-width: 300px; 13 | flex: 0 0 120px; 14 | 15 | * { 16 | color: white; 17 | } 18 | 19 | &.profile { 20 | .left { 21 | margin-bottom: -32px; 22 | } 23 | } 24 | 25 | .content { 26 | padding: 12px 12px; 27 | z-index: 10; 28 | .bottom { 29 | display: flex; 30 | justify-content: space-between; 31 | align-items: center; 32 | .left { 33 | margin-right: 12px; 34 | } 35 | .main { 36 | flex: 1; 37 | .title { 38 | font-size: 18px; 39 | font-weight: bold; 40 | } 41 | } 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/components/assignment/AssignmentsFetcher.js: -------------------------------------------------------------------------------- 1 | import {Observable as $} from 'rx' 2 | 3 | import { 4 | Assignments, 5 | } from 'components/remote' 6 | 7 | export const AssignmentsFetcher = sources => ({ 8 | assignments$: sources.profileKey$ 9 | .flatMapLatest(k => k ? 10 | Assignments.query.byProfile(sources)(k) : 11 | $.just(null) 12 | ), 13 | }) 14 | -------------------------------------------------------------------------------- /src/components/assignment/index.js: -------------------------------------------------------------------------------- 1 | export {AssignmentsFetcher} from './AssignmentsFetcher' 2 | -------------------------------------------------------------------------------- /src/components/commitment/CommitmentList.js: -------------------------------------------------------------------------------- 1 | import {Observable} from 'rx' 2 | const {just, combineLatest} = Observable 3 | // import combineLatestObj from 'rx-combine-latest-obj' 4 | 5 | import isolate from '@cycle/isolate' 6 | 7 | import {Commitments} from 'components/remote' 8 | 9 | import { 10 | List, 11 | ListItem, 12 | ListItemWithMenu, 13 | MenuItem, 14 | } from 'components/sdm' 15 | 16 | import codeIcons from 'components/opp/codeIcons' 17 | import codeTitles from 'components/opp/codeTitles' 18 | import {div} from 'cycle-snabbdom' 19 | 20 | const Delete = sources => MenuItem({...sources, 21 | iconName$: just('remove'), 22 | title$: just('Remove'), 23 | }) 24 | 25 | const Edit = sources => isolate(MenuItem, 'edit')({ 26 | ...sources, 27 | iconName$: just('pencil'), 28 | title$: just('Edit'), 29 | }) 30 | 31 | const CommitmentItem = sources => { 32 | const item$ = sources.item$ 33 | 34 | const deleteItem = isolate(Delete,'delete')(sources) 35 | const editItem = Edit(sources) 36 | 37 | const listItem = ListItemWithMenu({...sources, 38 | iconName$: item$.map(({code}) => codeIcons[code]), 39 | title$: item$.map(({code, ...vals}) => codeTitles[code](vals)), 40 | menuItems$: just([deleteItem.DOM, editItem.DOM]), 41 | }) 42 | 43 | const edit$ = editItem.click$ 44 | .flatMapLatest(item$) 45 | 46 | const queue$ = deleteItem.click$ 47 | .flatMapLatest(item$) 48 | .pluck('$key') 49 | .map(Commitments.action.remove) 50 | 51 | const DOM = combineLatest( 52 | listItem.DOM, 53 | (...doms) => div({}, doms) 54 | ) 55 | 56 | return { 57 | DOM, 58 | queue$, 59 | edit$, 60 | } 61 | } 62 | 63 | const CommitmentItemPassive = sources => ListItem({...sources, 64 | iconName$: sources.item$.map(({code}) => codeIcons[code]), 65 | title$: sources.item$.map(({code, ...vals}) => codeTitles[code](vals)), 66 | }) 67 | 68 | const CommitmentList = sources => List({...sources, 69 | Control$: just(CommitmentItem), 70 | }) 71 | 72 | export { 73 | CommitmentItem, 74 | CommitmentList, 75 | CommitmentItemPassive, 76 | } 77 | -------------------------------------------------------------------------------- /src/components/commitment/index.js: -------------------------------------------------------------------------------- 1 | export { 2 | CommitmentItem, 3 | CommitmentList, 4 | CommitmentItemPassive, 5 | } from './CommitmentList' 6 | -------------------------------------------------------------------------------- /src/components/cyclic-surface-material/index.js: -------------------------------------------------------------------------------- 1 | import {Observable} from 'rx' 2 | const {just} = Observable 3 | 4 | import combineLatestObj from 'rx-combine-latest-obj' 5 | import {h} from 'cycle-snabbdom' 6 | 7 | require('./scss/surface_styles.scss') 8 | 9 | let _id = 0 10 | const newId = () => _id += 1 11 | 12 | const RaisedButton = sources => { 13 | const id = newId() 14 | 15 | const viewState = { 16 | label$: sources.label$ || just('Button'), 17 | classNames$: sources.classNames$ || just([]), 18 | } 19 | 20 | const click$ = sources.DOM.select('.id' + id).events('click') 21 | 22 | const DOM = combineLatestObj(viewState) 23 | .map(({label, classNames}) => 24 | h(['button', 'btn--raised', 'id' + id, ...classNames].join('.'), [label]) 25 | ) 26 | 27 | return { 28 | DOM, 29 | click$, 30 | } 31 | } 32 | 33 | export {RaisedButton} 34 | -------------------------------------------------------------------------------- /src/components/cyclic-surface-material/scss/_imports/_colors.scss: -------------------------------------------------------------------------------- 1 | //Greens 2 | $turqoise: #1abc9c; 3 | $green-sea: #16a085; 4 | $emerald: #2ecc71; 5 | $nephritis: #27ae60; 6 | $green: #4caf50; 7 | $light-green: #8bc34a; 8 | $lime: #cddc39; 9 | //Blues 10 | $river: #3498db; 11 | $belize: #2980b9; 12 | $asphalt: #34495e; 13 | $midnight-blue: #2c3e50; 14 | $blue: #2196f3; 15 | $light-blue: #03a9f4; 16 | $cyan: #00bcd4; 17 | $teal: #009688; 18 | //Reds 19 | $alizarin: #e74c3c; 20 | $pomegranate: #c0392b; 21 | $red: #f44336; 22 | //Oranges 23 | $carrot: #e67e22; 24 | $pumpkin: #d35400; 25 | $dull-orange: #f39c12; 26 | $orange: #ff9800; 27 | $blood-orange: #ff5722; 28 | $amber: #ffc107; 29 | //Yellows 30 | $sunflower: #f1c40f; 31 | $yellow: #ffeb3b; 32 | //Purples + Pinks 33 | $amethyst: #9b59b6; 34 | $plum: #8e44ad; 35 | $purple: #9c27b0; 36 | $deep-purple: #673ab7; 37 | $pink: #e91e63; 38 | $indigo: #3f51b5; 39 | //Browns 40 | $brown: #795548; 41 | //Greys 42 | $grey: #9e9e9e; 43 | $gun-metal: #607d8b; 44 | $asbestos: #7f8c8d; 45 | $concrete: #95a5a6; 46 | $silver: #bdc3c7; 47 | //Whites 48 | $clouds: #dde4e6; 49 | $paper: #efefef; 50 | //Blacks 51 | $black: #212121; -------------------------------------------------------------------------------- /src/components/cyclic-surface-material/scss/_imports/_importMaster.scss: -------------------------------------------------------------------------------- 1 | @import './_colors'; 2 | @import './_reset'; 3 | @import './_variables'; -------------------------------------------------------------------------------- /src/components/cyclic-surface-material/scss/_imports/_reset.scss: -------------------------------------------------------------------------------- 1 | html, body { 2 | width: 100%; 3 | } 4 | 5 | body { 6 | display: flex; 7 | flex-wrap: wrap; 8 | } 9 | 10 | html, body, div, span, applet, object, iframe, 11 | h1, h2, h3, h4, h5, h6, p, blockquote, pre, 12 | a, abbr, acronym, address, big, cite, code, 13 | del, dfn, em, img, ins, kbd, q, s, samp, 14 | small, strike, strong, sub, sup, tt, var, 15 | b, u, i, center, 16 | dl, dt, dd, ol, ul, li, 17 | fieldset, form, label, legend, 18 | table, caption, tbody, tfoot, thead, tr, th, td, 19 | article, aside, canvas, details, embed, 20 | figure, figcaption, footer, header, hgroup, 21 | menu, nav, output, ruby, section, summary, 22 | time, mark, audio, video { 23 | margin: 0; 24 | padding: 0; 25 | border: 0; 26 | font-size: 100%; 27 | font: inherit; 28 | vertical-align: baseline; 29 | box-sizing: border-box; 30 | } 31 | /* HTML5 display-role reset for older browsers */ 32 | article, aside, details, figcaption, figure, 33 | footer, header, hgroup, menu, nav, section { 34 | display: block; 35 | } 36 | body { 37 | line-height: 1; 38 | } 39 | ol, ul { 40 | list-style: none; 41 | } 42 | blockquote, q { 43 | quotes: none; 44 | } 45 | blockquote:before, blockquote:after, 46 | q:before, q:after { 47 | content: ''; 48 | content: none; 49 | } 50 | table { 51 | border-collapse: collapse; 52 | border-spacing: 0; 53 | } 54 | button { 55 | border: none; 56 | cursor: pointer; 57 | } -------------------------------------------------------------------------------- /src/components/cyclic-surface-material/scss/_imports/_variables.scss: -------------------------------------------------------------------------------- 1 | @import './colors'; 2 | 3 | // Type 4 | $serif: 'Gentium Book Basic', serif; 5 | $sans: Roboto, sans-serif; 6 | $font-small: 12px; 7 | $font-med: 16px; 8 | $font-big: 22px; 9 | $font-huge: 40px; 10 | $line-height: 130%; 11 | 12 | // Colors 13 | $primary: $turqoise; 14 | $secondary: lighten($turqoise, 10%); 15 | $accent: $yellow; 16 | 17 | $body-background: $paper; 18 | 19 | // Grid 20 | $grid-columns: 12; 21 | 22 | // Decorative 23 | $box-shadow-card: 0 2px 5px 0 rgba(0, 0, 0, 0.12), 0 2px 10px 0 rgba(0, 0, 0, 0.09); 24 | $box-shadow-float: 0 2px 5px 0 rgba(0, 0, 0, 0.16), 0 2px 10px 0 rgba(0, 0, 0, 0.12); 25 | $box-shadow-float-hover: 0 5px 11px 0 rgba(0, 0, 0, 0.18), 0 4px 15px 0 rgba(0, 0, 0, 0.15); 26 | $box-shadow-raised: 0 2px 5px 0 rgba(0, 0, 0, 0.16), 0 2px 10px 0 rgba(0, 0, 0, 0.12); 27 | $box-shadow-raised-hover: 0 5px 11px 0 rgba(0, 0, 0, 0.18), 0 4px 15px 0 rgba(0, 0, 0, 0.15); 28 | 29 | $cubic: cubic-bezier(.64,.09,.08,1); 30 | 31 | $border-radius-small: 3px; 32 | $border-radius-med: 6px; 33 | $border-radius-big: 10px; 34 | 35 | // Spacing 36 | $space-big: 40px; 37 | $space-med: 20px; 38 | $space-small: 10px; 39 | 40 | // Media Queries 41 | $media-med: 1200px; 42 | $media-small: 900px; 43 | $media-tiny: 520px; 44 | 45 | 46 | -------------------------------------------------------------------------------- /src/components/cyclic-surface-material/scss/alerts.scss: -------------------------------------------------------------------------------- 1 | @import './_imports/_variables'; 2 | 3 | .alert-wrap { 4 | position: relative; 5 | } 6 | 7 | .alert { 8 | padding-bottom: 50px; 9 | } 10 | 11 | #alert-check { 12 | display: none; 13 | &:checked { 14 | ~ div, ~ label { 15 | display: none; 16 | } 17 | } 18 | + label { 19 | position: absolute; 20 | right: 16px; 21 | bottom: 10px; 22 | cursor: pointer; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/components/cyclic-surface-material/scss/animations.scss: -------------------------------------------------------------------------------- 1 | .fade-in-from-top { 2 | opacity: 0; 3 | transform: translateY(-6px); 4 | animation: fadeInVert 0.5s ease-out forwards; 5 | -webkit-animation: fadeInVert 0.5s ease-out forwards; 6 | } 7 | 8 | .fade-in-from-bottom { 9 | opacity: 0; 10 | transform: translateY(6px); 11 | animation: fadeInVert 0.5s ease-out forwards; 12 | -webkit-animation: fadeInVert 0.5s ease-out forwards; 13 | } 14 | 15 | @keyframes fadeInVert { 16 | to { 17 | opacity: 1; 18 | transform: translateY(0); 19 | } 20 | } 21 | 22 | @-webkit-keyframes fadeInVert { 23 | to { 24 | opacity: 1; 25 | transform: translateY(0); 26 | } 27 | } 28 | 29 | .fade-in-from-left { 30 | opacity: 0; 31 | transform: translateX(-6px); 32 | animation: fadeInHoriz 0.5s ease-out forwards; 33 | -webkit-animation: fadeInHoriz 0.5s ease-out forwards; 34 | } 35 | 36 | .fade-in-from-right { 37 | opacity: 0; 38 | transform: translateX(6px); 39 | animation: fadeInHoriz 0.5s ease-out forwards; 40 | -webkit-animation: fadeInHoriz 0.5s ease-out forwards; 41 | } 42 | 43 | @keyframes fadeInHoriz { 44 | to { 45 | opacity: 1; 46 | transform: translateX(0); 47 | } 48 | } 49 | 50 | @-webkit-keyframes fadeInHoriz { 51 | to { 52 | opacity: 1; 53 | transform: translateX(0); 54 | } 55 | } 56 | 57 | @for $i from 1 through 10 { 58 | .anim-delay--#{$i*5} { 59 | animation-delay: #{$i*500}ms; 60 | -webkit-animation-delay: #{$i*500}ms; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/components/cyclic-surface-material/scss/cards_tiles.scss: -------------------------------------------------------------------------------- 1 | @import './_imports/_variables'; 2 | @import './_imports/_colors'; 3 | 4 | .card { 5 | box-shadow: $box-shadow-card; 6 | border-radius: $border-radius-small; 7 | } 8 | 9 | .tile, .card { 10 | padding: $space-med; 11 | background: white; 12 | } -------------------------------------------------------------------------------- /src/components/cyclic-surface-material/scss/code.scss: -------------------------------------------------------------------------------- 1 | @import './_imports/_colors'; 2 | @import './_imports/_variables'; 3 | 4 | // https://highlightjs.org/ 5 | 6 | code { 7 | font-family: monospace; 8 | font-size: $font-med; 9 | padding-left: 40px; 10 | padding-right: 40px; 11 | min-width: 500px; 12 | max-width: 800px; 13 | @media screen and (max-width: $media-small) { 14 | width: 400px; 15 | } 16 | @media screen and (max-width: $media-tiny) { 17 | width: 100%; 18 | min-width: 100%; 19 | max-width: 100%; 20 | } 21 | } -------------------------------------------------------------------------------- /src/components/cyclic-surface-material/scss/collapsible.scss: -------------------------------------------------------------------------------- 1 | @import './_imports/_variables'; 2 | 3 | [id*="collapsible-"] { 4 | display: none; 5 | &:checked { 6 | ~ [class*="collapsible-"][class$="area"] { // Collapsible content 7 | transform: scaleY(1); 8 | height: auto; 9 | padding: $space-small*1.5 $space-med; 10 | margin-bottom: $space-med; 11 | } 12 | + label { 13 | &:before { 14 | margin-top: 6px; 15 | transform: rotate(-45deg) translateX(1px); 16 | } 17 | &:after { 18 | margin-top: 5px; 19 | transform: rotate(45deg) translate(4px, -3px); 20 | } 21 | } 22 | } 23 | } 24 | 25 | label[for*="collapsible-"] { // Label 26 | width: 100%; 27 | cursor: pointer; 28 | display: flex; 29 | position: relative; 30 | padding: $space-small*1.5 24px; 31 | border-bottom: solid 1px lighten($grey, 30%); 32 | color: lighten($black, 15%); 33 | border-radius: 3px; 34 | &:before, &:after { 35 | content: ''; 36 | position: absolute; 37 | right: $space-med; 38 | width: 2px; 39 | height: 8px; 40 | background: $grey; 41 | transition: all 0.3s ease; 42 | } 43 | &:before { 44 | margin-top: 2px; 45 | transform: rotate(50deg); 46 | } 47 | &:after { 48 | margin-top: 6px; 49 | transform: rotate(-50deg); 50 | 51 | } 52 | } 53 | 54 | [class*="collapsible-"][class$="area"] { // Collapsible content 55 | transform: scaleY(0); 56 | transform-origin: 0 0; 57 | height: 0; 58 | will-change: height, transform; 59 | transition: all 0.3s ease; 60 | padding-left: $space-med; 61 | } -------------------------------------------------------------------------------- /src/components/cyclic-surface-material/scss/footer.scss: -------------------------------------------------------------------------------- 1 | @import './_imports/_variables'; 2 | @import './_imports/_colors'; 3 | 4 | footer { 5 | display: block; 6 | width: 100%; 7 | background: $secondary; 8 | padding-top: $space-med; 9 | padding-bottom: $space-med; 10 | } -------------------------------------------------------------------------------- /src/components/cyclic-surface-material/scss/general.scss: -------------------------------------------------------------------------------- 1 | @import './_imports/_variables'; 2 | 3 | body { 4 | background: $body-background; 5 | line-height: $line-height; 6 | } -------------------------------------------------------------------------------- /src/components/cyclic-surface-material/scss/header.scss: -------------------------------------------------------------------------------- 1 | @import './_imports/_variables'; 2 | 3 | header { 4 | width: 100%; 5 | padding-top: $space-small; 6 | padding-bottom: $space-small; 7 | background: $primary; 8 | margin-bottom: $space-big*1.75; 9 | h1, h2, h3 { 10 | color: white; 11 | } 12 | @media screen and (max-width: $media-med) { 13 | margin-bottom: $space-big*0.85; 14 | padding-top: $space-med; 15 | } 16 | @media screen and (max-width: $media-small) { 17 | padding-top: $space-big; 18 | padding-bottom: $space-small*0.5; 19 | margin-bottom: $space-small*0.85; 20 | } 21 | } -------------------------------------------------------------------------------- /src/components/cyclic-surface-material/scss/lightbox.scss: -------------------------------------------------------------------------------- 1 | @import './_imports/_variables'; 2 | 3 | label[for*="lightbox-"] { 4 | cursor: pointer; 5 | width: 100%; 6 | transition: none; 7 | } 8 | 9 | input[id*="lightbox-"] { 10 | display: none; 11 | } 12 | 13 | input[id*="lightbox-"]:checked { 14 | display: block; 15 | position: absolute; 16 | top: 50%; 17 | left: 50%; 18 | &:before { 19 | content: ''; 20 | position: fixed; 21 | z-index: 9; 22 | top: 0; 23 | left: 0; 24 | width: 100%; 25 | height: 100%; 26 | background-color: rgba(0,0,0,0.4); 27 | } 28 | + label { 29 | top: 50%; 30 | left: 50%; 31 | transform: translate(-50%, -50%); 32 | width: 50vw; 33 | position: fixed; 34 | z-index: 10; 35 | } 36 | } -------------------------------------------------------------------------------- /src/components/cyclic-surface-material/scss/links.scss: -------------------------------------------------------------------------------- 1 | @import './_imports/_variables'; 2 | 3 | a { 4 | background: linear-gradient(to top, rgba($secondary, 0.8) 50%, rgba(255,255,255,0) 50%); 5 | background-size: 100% 200%; 6 | background-position: 0 10%; 7 | background-repeat: no-repeat; 8 | text-decoration: none; 9 | color: inherit; 10 | transition: background-position 0.3s $cubic; 11 | will-change: background-position; 12 | &:active { 13 | color: inherit; 14 | } 15 | &:hover { 16 | background-position: 0 100%; 17 | } 18 | } -------------------------------------------------------------------------------- /src/components/cyclic-surface-material/scss/lists.scss: -------------------------------------------------------------------------------- 1 | @import './_imports/_variables'; 2 | @import './_imports/_colors'; 3 | 4 | ul, ol { 5 | margin-left: $space-med; 6 | margin-bottom: $space-med; 7 | li { 8 | margin-top: $space-small*1; 9 | } 10 | } 11 | 12 | ol { 13 | list-style-type: decimal; 14 | white-space: nowrap; 15 | } -------------------------------------------------------------------------------- /src/components/cyclic-surface-material/scss/media.scss: -------------------------------------------------------------------------------- 1 | img, audio, video { 2 | width: 100%; 3 | } -------------------------------------------------------------------------------- /src/components/cyclic-surface-material/scss/modals.scss: -------------------------------------------------------------------------------- 1 | @import './_imports/_variables'; 2 | @import './_imports/_colors'; 3 | 4 | input[id*="modal-"] { 5 | display: none; 6 | &:checked { 7 | + label { 8 | outline: none; 9 | background-size: 1000%; 10 | transition: all 1s $cubic; 11 | &:before { 12 | content: ''; 13 | position: fixed; 14 | top: 0; 15 | left: 0; 16 | width: 100%; 17 | height: 100%; 18 | background: rgba(0,0,0,0.4); 19 | transition: all 0.3s $cubic; 20 | z-index: 9; 21 | } 22 | } 23 | ~ .modal-content { 24 | transition: opacity 0.3s $cubic; 25 | opacity: 1; 26 | display: block; 27 | height: auto; 28 | width: auto; 29 | padding: $space-big; 30 | left: 50%; 31 | top: 50%; 32 | transform: translate(-50%, -50%); 33 | z-index: 10; 34 | * { 35 | height: auto; 36 | width: auto; 37 | } 38 | } 39 | } 40 | } 41 | 42 | .modal-trigger { 43 | white-space: pre; 44 | cursor: pointer; 45 | @extend .btn--raised; 46 | transition: all 0.3s $cubic; 47 | padding: $space-small $space-med; 48 | background-size: 1%; 49 | background-repeat: no-repeat; 50 | background-position: 50% 50%; 51 | &:after { 52 | white-space: nowrap; 53 | padding: $space-small; 54 | cursor: pointer; 55 | transition: all 0.2s $cubic; 56 | @extend .btn--raised; 57 | background-image: none; 58 | } 59 | } 60 | 61 | .modal-content { 62 | position: fixed; 63 | opacity: 0; 64 | height: 0; 65 | background: white; 66 | border-radius: $border-radius-small; 67 | * { 68 | width: 0; 69 | height: 0; 70 | } 71 | } -------------------------------------------------------------------------------- /src/components/cyclic-surface-material/scss/surface_styles.scss: -------------------------------------------------------------------------------- 1 | @import './_imports/_reset'; 2 | @import './grid'; 3 | @import './cards_tiles'; 4 | @import './type'; 5 | @import './general'; 6 | @import './buttons'; 7 | @import './links'; 8 | @import './tables'; 9 | @import './header'; 10 | @import './nav'; 11 | @import './lists'; 12 | @import './footer'; 13 | @import './modals'; 14 | @import './tooltip'; 15 | @import './tabs'; 16 | @import './form'; 17 | @import './lightbox'; 18 | @import './utility'; 19 | @import './animations'; 20 | @import './media'; 21 | @import './alerts'; 22 | @import './collapsible'; 23 | @import './code'; 24 | @import './colors'; -------------------------------------------------------------------------------- /src/components/cyclic-surface-material/scss/tables.scss: -------------------------------------------------------------------------------- 1 | @import './_imports/_variables'; 2 | @import './_imports/_colors'; 3 | 4 | table { 5 | width: 100%; 6 | } 7 | 8 | .table-header { 9 | color: lighten($black, 15%); 10 | font-size: $font-med; 11 | line-height: $space-big*1.3; 12 | } 13 | 14 | tr { 15 | font-size: $font-med + 1px; 16 | line-height: $space-big*1.3; 17 | border-bottom: solid 1px lighten($grey, 30%); 18 | will-change: background; 19 | &:not(.table-header) { 20 | &:hover { 21 | background: lighten($grey, 30%); 22 | } 23 | } 24 | } 25 | 26 | td { 27 | &:first-child { 28 | padding-left: $space-big; 29 | @media screen and (max-width: $media-med) { 30 | padding-left: $space-big*0.8; 31 | } 32 | @media screen and (max-width: $media-small) { 33 | padding-left: $space-med; 34 | } 35 | } 36 | &:last-child { 37 | padding-right: $space-big; 38 | @media screen and (max-width: $media-med) { 39 | padding-right: $space-big*0.8; 40 | } 41 | @media screen and (max-width: $media-small) { 42 | padding-right: $space-med; 43 | } 44 | } 45 | 46 | } -------------------------------------------------------------------------------- /src/components/cyclic-surface-material/scss/tooltip.scss: -------------------------------------------------------------------------------- 1 | @import './_imports/_variables'; 2 | @import './_imports/_colors'; 3 | 4 | .tooltip { 5 | position: relative; 6 | &:hover { 7 | &:after { 8 | position: absolute; 9 | content: attr(data-text); 10 | background: lighten($grey, 10%); 11 | border-radius: $border-radius-small; 12 | padding: $space-small*0.8; 13 | bottom: -2.5em; 14 | left: 50%; 15 | transform: translateX(-50%); 16 | z-index: 2; 17 | } 18 | } 19 | } -------------------------------------------------------------------------------- /src/components/cyclic-surface-material/scss/type.scss: -------------------------------------------------------------------------------- 1 | @import './_imports/_variables'; 2 | 3 | @import url(http://fonts.googleapis.com/css?family=Roboto:400,300,500,700|Gentium+Book+Basic:400,700); 4 | 5 | div, p, h1, h2, h3, h4, h5, h6, a, input, label, header, aside, menu, body { 6 | font-family: $sans; 7 | color: $black; 8 | } 9 | 10 | h1, h2, h3, h4, h5, h6 { 11 | margin-bottom: 1em*0.8; 12 | line-height: 130%; 13 | } 14 | 15 | p { 16 | margin-bottom: $space-med; 17 | } 18 | 19 | h1 { 20 | font-size: $font-huge; 21 | } 22 | 23 | h2 { 24 | font-size: $font-huge*0.85; 25 | } 26 | 27 | h3 { 28 | font-size: $font-huge*0.7; 29 | } 30 | 31 | h4 { 32 | font-size: $font-huge*0.62; 33 | } 34 | 35 | h5 { 36 | font-size: $font-huge*0.52; 37 | } 38 | 39 | h6 { 40 | font-size: $font-huge*0.45; 41 | } 42 | 43 | p, a { 44 | font-size: $font-med; 45 | } 46 | 47 | .subtitle { 48 | font-style: italic; 49 | color: darken($grey, 5%); 50 | } -------------------------------------------------------------------------------- /src/components/cyclic-surface-material/scss/utility.scss: -------------------------------------------------------------------------------- 1 | .right { 2 | float: right; 3 | } 4 | 5 | .left { 6 | float: left; 7 | } 8 | 9 | .inline { 10 | display: inline; 11 | } 12 | 13 | .inline-block { 14 | display: inline-block; 15 | } 16 | 17 | .block { 18 | display: iblock; 19 | } 20 | 21 | .clear { 22 | clear: both; 23 | } 24 | 25 | .no-pad { 26 | padding: 0; 27 | } 28 | 29 | .no-margin-vertical { 30 | margin-top: 0; 31 | margin-bottom: 0; 32 | } 33 | 34 | .no-margin { 35 | margin: 0; 36 | } 37 | -------------------------------------------------------------------------------- /src/components/engagement/EngagementButtons.js: -------------------------------------------------------------------------------- 1 | import {Observable} from 'rx' 2 | const {just, combineLatest, merge} = Observable 3 | 4 | import {div} from 'cycle-snabbdom' 5 | import {FlatButton} from 'components/sdm' 6 | 7 | const view = (...children) => 8 | div({style: {textAlign: 'center'}}, children) 9 | 10 | export const EngagementButtons = (sources) => { 11 | const priority = FlatButton({ 12 | ...sources, 13 | label$: just('Priority'), 14 | classNames$: just(['green']), 15 | }) 16 | const accept = FlatButton({ 17 | ...sources, 18 | label$: just('Accept'), 19 | classNames$: just(['blue']), 20 | }) 21 | const decline = FlatButton({ 22 | ...sources, 23 | label$: just('Decline'), 24 | classNames$: just(['red']), 25 | }) 26 | const close = FlatButton({ 27 | ...sources, 28 | label$: just('Cancel'), 29 | classNames$: just(['accent']), 30 | }) 31 | 32 | const DOM = combineLatest( 33 | priority.DOM, accept.DOM, decline.DOM, close.DOM, 34 | view 35 | ) 36 | 37 | const value$ = merge( 38 | priority.click$.map(() => 'priority'), 39 | accept.click$.map(() => 'accept'), 40 | decline.click$.map(() => 'decline') 41 | ).shareReplay(1) 42 | 43 | return { 44 | DOM, 45 | value$, 46 | ok$: value$, 47 | cancel$: close.click$.share(), 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/components/engagement/EngagementItem.js: -------------------------------------------------------------------------------- 1 | import {Observable} from 'rx' 2 | const {combineLatest} = Observable 3 | 4 | import {ProjectItem} from 'components/project' 5 | import {Opps, Projects} from 'components/remote' 6 | 7 | const _label = ({isApplied, isAccepted, isConfirmed}) => 8 | isConfirmed && 'Confirmed' || 9 | isAccepted && 'Accepted' || 10 | isApplied && 'Applied' || 11 | 'Unknown' 12 | 13 | const _Fetch = sources => { 14 | const opp$ = sources.item$.pluck('oppKey') 15 | .tap(x => console.log('oppKey', x)) 16 | .flatMapLatest(Opps.query.one(sources)) 17 | const project$ = opp$.pluck('projectKey') 18 | .tap(x => console.log('projectKey', x)) 19 | .flatMapLatest(Projects.query.one(sources)) 20 | .combineLatest( 21 | opp$.pluck('projectKey'), 22 | (p, $key) => ({$key, ...p}) 23 | ) 24 | return { 25 | opp$, 26 | project$, 27 | } 28 | } 29 | 30 | const EngagementItem = sources => { 31 | const _sources = {...sources, ..._Fetch(sources)} 32 | return ProjectItem({..._sources, 33 | subtitle$: combineLatest( 34 | _sources.item$, _sources.opp$, 35 | (e,opp) => opp.name + ' | ' + _label(e) 36 | ), 37 | item$: _sources.project$, 38 | path$: _sources.item$.map(({$key}) => '/engaged/' + $key), 39 | }) 40 | } 41 | 42 | export {EngagementItem} 43 | -------------------------------------------------------------------------------- /src/components/engagement/EngagementNav.js: -------------------------------------------------------------------------------- 1 | import {Observable} from 'rx' 2 | const {just, merge, combineLatest} = Observable 3 | 4 | import isolate from '@cycle/isolate' 5 | 6 | import {div} from 'cycle-snabbdom' 7 | 8 | // import {log} from 'util' 9 | 10 | import {ListItemNavigating} from 'components/sdm' 11 | 12 | const EngagementNav = sources => { 13 | const glance = isolate(ListItemNavigating,'glance')({...sources, 14 | title$: just('At a Glance'), 15 | iconName$: just('home'), 16 | path$: just('/'), 17 | }) 18 | const app = isolate(ListItemNavigating,'app')({...sources, 19 | title$: just('Your Application!'), 20 | iconName$: just('event_note'), 21 | path$: just('/application'), 22 | }) 23 | 24 | const listDOM$ = combineLatest(glance.DOM, app.DOM, (...doms) => doms) 25 | 26 | // const teamListHeader = CreateTeamHeader(sources) 27 | // const oppListHeader = CreateOppHeader(sources) 28 | 29 | // const queue$ = Observable.merge( 30 | // teamListHeader.queue$, 31 | // oppListHeader.queue$, 32 | // ) 33 | 34 | const route$ = merge(glance.route$, app.route$) 35 | .map(sources.router.createHref) 36 | 37 | const DOM = combineLatest( 38 | sources.isMobile$, 39 | sources.titleDOM, 40 | listDOM$, 41 | (isMobile, titleDOM, listDOM) => 42 | div({}, [ 43 | isMobile ? null : titleDOM, 44 | div('.rowwrap', {style: {padding: '0px 15px'}}, listDOM), 45 | ]) 46 | ) 47 | 48 | return { 49 | DOM, 50 | route$, 51 | // queue$, 52 | } 53 | } 54 | 55 | export {EngagementNav} 56 | -------------------------------------------------------------------------------- /src/components/engagement/EngagementPriorityList.js: -------------------------------------------------------------------------------- 1 | // TODO: TLC 2 | 3 | import combineLatestObj from 'rx-combine-latest-obj' 4 | import listItem from 'helpers/listItem' 5 | import {div} from 'cycle-snabbdom' 6 | import {NavClicker} from 'components' 7 | 8 | const EngagementPriorityList = sources => { 9 | const viewState = { 10 | oppAnswer$: sources.engagement$.pluck('answer'), 11 | } 12 | 13 | const DOM = combineLatestObj(viewState) 14 | .map(({oppAnswer}) => 15 | div({},[ 16 | !oppAnswer ? listItem({title: 'Opp answer'}) : null, 17 | ]) 18 | ) 19 | 20 | const route$ = NavClicker(sources).route$ 21 | 22 | return { 23 | DOM, 24 | route$, 25 | } 26 | } 27 | 28 | export {EngagementPriorityList} 29 | -------------------------------------------------------------------------------- /src/components/engagement/index.js: -------------------------------------------------------------------------------- 1 | export {EngagementItem} from './EngagementItem' 2 | export {EngagementNav} from './EngagementNav' 3 | export {EngagementPriorityList} from './EngagementPriorityList' 4 | export {EngagementButtons} from './EngagementButtons' 5 | 6 | export const label = ({isApplied, isAccepted, isConfirmed}) => 7 | isConfirmed && 'Confirmed' || 8 | isAccepted && 'Accepted' || 9 | isApplied && 'Applied' || 10 | 'Unknown' 11 | -------------------------------------------------------------------------------- /src/components/index.js: -------------------------------------------------------------------------------- 1 | export const NavClicker = sources => ({ 2 | route$: sources.DOM.select('.nav').events('click') 3 | .map(e => e.ownerTarget.dataset.link), 4 | }) 5 | 6 | -------------------------------------------------------------------------------- /src/components/opp/AddCommitmentGive.js: -------------------------------------------------------------------------------- 1 | // TODO: convert makeMenuItemFormPopup 2 | 3 | import {Observable} from 'rx' 4 | const {just, merge, combineLatest} = Observable 5 | 6 | import {div} from 'helpers' 7 | import {Menu} from 'components/sdm' 8 | 9 | import {ListItemClickable} from 'components/sdm' 10 | import {GiveWaiver, GiveShifts, GivePayment, GiveDeposit} from './GiveItems' 11 | 12 | const SelectingItem = sources => ListItemClickable({...sources, 13 | title$: just('What do Volunteers GIVE?'), 14 | iconName$: just('plus'), 15 | classes$: just({header: true}), 16 | }) 17 | 18 | export const AddCommitmentGive = sources => { 19 | const selecting = SelectingItem(sources) 20 | const giveWaiver = GiveWaiver(sources) 21 | const giveDeposit = GiveDeposit(sources) 22 | const givePayment = GivePayment(sources) 23 | const giveShifts = GiveShifts(sources) 24 | 25 | const children = [ 26 | giveWaiver, 27 | giveShifts, 28 | givePayment, 29 | giveDeposit, 30 | ] 31 | 32 | const menuItemDOMs$ = just([div({}, 33 | children.map(c => c.itemDOM) 34 | )]) 35 | 36 | const modalDOMs$ = just( 37 | children.map(c => c.modalDOM) 38 | ) 39 | 40 | const submits$ = merge(...children.map(c => c.submit$)) 41 | 42 | const isOpen$ = sources.DOM.select('.clickable').events('click') 43 | .map(true) 44 | .merge(submits$.map(false)) 45 | .startWith(false) 46 | 47 | const dropdown = Menu({...sources, isOpen$, children$: menuItemDOMs$}) 48 | 49 | const DOM = combineLatest( 50 | modalDOMs$, 51 | selecting.DOM, 52 | dropdown.DOM, 53 | (modals, ...rest) => div({},[...rest, ...modals]) 54 | ) 55 | 56 | const commitment$ = merge( 57 | giveWaiver.item$.sample(giveWaiver.submit$) 58 | .map(c => ({...c, code: 'waiver'})), 59 | giveDeposit.item$.sample(giveDeposit.submit$) 60 | .map(c => ({...c, code: 'deposit'})), 61 | givePayment.item$.sample(givePayment.submit$) 62 | .map(c => ({...c, code: 'payment'})), 63 | giveShifts.item$.sample(giveShifts.submit$) 64 | .map(c => ({...c, code: 'shifts'})), 65 | ).map(c => ({...c, party: 'vol'})) 66 | 67 | return { 68 | DOM, 69 | isOpen$, 70 | commitment$, 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/components/opp/CreateOppHeader.js: -------------------------------------------------------------------------------- 1 | import {Observable} from 'rx' 2 | import combineLatestObj from 'rx-combine-latest-obj' 3 | 4 | // import isolate from '@cycle/isolate' 5 | 6 | import {Opps} from 'remote' 7 | 8 | import {OppForm} from 'components/opp' 9 | 10 | import {col} from 'helpers' 11 | import modal from 'helpers/modal' 12 | import listItem from 'helpers/listItem' 13 | 14 | // import {log} from 'util' 15 | 16 | const _openActions$ = ({DOM}) => Observable.merge( 17 | DOM.select('.open').events('click').map(true), 18 | DOM.select('.close').events('click').map(false), 19 | ) 20 | 21 | const _submitAction$ = ({DOM}) => 22 | DOM.select('.submit').events('click').map(true) 23 | 24 | const _render = ({isOpen, oppFormDOM}) => 25 | col( 26 | listItem({ 27 | iconName: 'power', 28 | iconBackgroundColor: 'yellow', 29 | title: 'Opportunities', 30 | className: 'open', 31 | clickable: true, 32 | header: true, 33 | }), 34 | modal({ 35 | isOpen, 36 | title: 'Create Opportunity', 37 | iconName: 'power', 38 | submitLabel: 'But of Course', 39 | closeLabel: 'Not the Now', 40 | content: oppFormDOM, 41 | }) 42 | ) 43 | 44 | const CreateOppHeader = sources => { 45 | const oppForm = OppForm(sources) 46 | 47 | const submit$ = _submitAction$(sources) 48 | 49 | const queue$ = oppForm.item$ 50 | .sample(submit$) 51 | .zip(sources.projectKey$, 52 | (opp,projectKey) => ({projectKey, ...opp}) 53 | ) 54 | .map(Opps.create) 55 | 56 | const isOpen$ = _openActions$(sources) 57 | .merge(submit$.map(false)) 58 | .startWith(false) 59 | 60 | const viewState = { 61 | isOpen$, 62 | project$: sources.project$, 63 | oppFormDOM$: oppForm.DOM, 64 | } 65 | 66 | const DOM = combineLatestObj(viewState).map(_render) 67 | 68 | return {DOM, queue$} 69 | } 70 | 71 | export {CreateOppHeader} 72 | -------------------------------------------------------------------------------- /src/components/opp/CreateOppListItem.js: -------------------------------------------------------------------------------- 1 | import {Observable} from 'rx' 2 | const {just} = Observable 3 | 4 | // import isolate from '@cycle/isolate' 5 | 6 | import {Opps} from 'remote' 7 | 8 | import {OppForm} from 'components/opp' 9 | import {ListItemWithDialog} from 'components/sdm' 10 | 11 | const CreateOppListItem = sources => { 12 | const form = OppForm(sources) 13 | 14 | const listItem = ListItemWithDialog({...sources, 15 | iconName$: just('power'), 16 | title$: just('Create an Opportunity to get volunteers.'), 17 | dialogTitleDOM$: just('Create an Opportunity'), 18 | dialogContentDOM$: form.DOM, 19 | }) 20 | 21 | const queue$ = form.item$ 22 | .sample(listItem.submit$) 23 | .zip(sources.projectKey$, (opp,projectKey) => ({projectKey, ...opp})) 24 | .map(Opps.create) 25 | 26 | return { 27 | DOM: listItem.DOM, 28 | queue$, 29 | } 30 | } 31 | 32 | export {CreateOppListItem} 33 | -------------------------------------------------------------------------------- /src/components/opp/OppForm.js: -------------------------------------------------------------------------------- 1 | import {Observable} from 'rx' 2 | const {just} = Observable 3 | 4 | import {Form} from 'components/ui/Form' 5 | import {InputControl} from 'components/sdm' 6 | 7 | const NameInput = sources => InputControl({...sources, 8 | label$: just('Name the Opportunity'), 9 | }) 10 | 11 | const OppForm = sources => Form({...sources, 12 | Controls$: just([{field: 'name', Control: NameInput}]), 13 | }) 14 | 15 | export {OppForm} 16 | -------------------------------------------------------------------------------- /src/components/opp/OppListNavigating.js: -------------------------------------------------------------------------------- 1 | import {Observable} from 'rx' 2 | const {just} = Observable 3 | 4 | import { 5 | ListItem, 6 | ListWithHeader, 7 | ListItemNavigating, 8 | } from 'components/sdm' 9 | 10 | const OppItemNavigating = sources => ListItemNavigating({...sources, 11 | title$: sources.item$.pluck('name'), 12 | path$: sources.path$ || sources.item$.map(({$key}) => '/opp/' + $key), 13 | }) 14 | 15 | const Header = () => ListItem({ 16 | classes$: just({header: true}), 17 | title$: just('opps'), 18 | }) 19 | 20 | const OppListNavigating = sources => ListWithHeader({...sources, 21 | headerDOM: Header(sources).DOM, 22 | Control$: just(OppItemNavigating), 23 | }) 24 | 25 | export {OppItemNavigating, OppListNavigating} 26 | -------------------------------------------------------------------------------- /src/components/opp/OppNav.js: -------------------------------------------------------------------------------- 1 | // import {Observable} from 'rx' 2 | // const {just, merge, combineLatest} = Observable 3 | 4 | // import isolate from '@cycle/isolate' 5 | 6 | // import {div} from 'cycle-snabbdom' 7 | 8 | // // import {log} from 'util' 9 | 10 | // import {ListItemNavigating} from 'components/sdm' 11 | 12 | // import {mergeSinks, combineLatestToDiv} from 'util' 13 | 14 | // const _Glance = sources => ListItemNavigating({...sources, 15 | // title$: just('At a Glance'), 16 | // iconName$: just('home'), 17 | // path$: just('/'), 18 | // }) 19 | 20 | // const _Manage = sources => ListItemNavigating({...sources, 21 | // title$: just('Manage'), 22 | // iconName$: just('settings'), 23 | // path$: just('/manage'), 24 | // }) 25 | 26 | // const _Confirmed = sources => ListItemNavigating({...sources, 27 | // title$: just('Confirmed'), 28 | // iconName$: just('people'), 29 | // path$: just('/confirmed'), 30 | // }) 31 | 32 | // const _Engaged = sources => ListItemNavigating({...sources, 33 | // title$: just('Engaged'), 34 | // iconName$: just('event_available'), 35 | // path$: just('/engaged'), 36 | // }) 37 | 38 | // const _List = sources => { 39 | // const childs = [ 40 | // isolate(_Glance,'glance')(sources), 41 | // isolate(_Manage,'manage')(sources), 42 | // isolate(_Confirmed,'confirmed')(sources), 43 | // isolate(_Engaged,'enaged')(sources), 44 | // ] 45 | 46 | // return { 47 | // DOM: combineLatestToDiv(...childs.map(c => c.DOM)), 48 | // route$: merge(...childs.map(c => c.route$)) 49 | // .map(sources.router.createHref), 50 | // } 51 | // } 52 | 53 | // const OppNav = sources => { 54 | // const l = _List(sources) 55 | 56 | // const DOM = combineLatest( 57 | // sources.isMobile$, sources.titleDOM, l.DOM, 58 | // (isMobile, title, list) => 59 | // div({}, [isMobile ? null : title, div('.rowwrap', [list])]) 60 | // ) 61 | 62 | // return { 63 | // DOM, 64 | // ...mergeSinks(l), 65 | // } 66 | // } 67 | 68 | // export {OppNav} 69 | -------------------------------------------------------------------------------- /src/components/opp/codeIcons.js: -------------------------------------------------------------------------------- 1 | // TODO: where should these live 2 | 3 | export default { 4 | help: 'heart', 5 | ticket: 'ticket', 6 | tracked: 'restaurant', 7 | schwag: 'gift', 8 | waiver: 'event_available', 9 | deposit: 'credit-card', 10 | payment: 'banknote', 11 | shifts: 'calendar2', 12 | } 13 | -------------------------------------------------------------------------------- /src/components/opp/codePopups.js: -------------------------------------------------------------------------------- 1 | import { 2 | GiveWaiver, 3 | GiveShifts, 4 | GivePayment, 5 | GiveDeposit, 6 | } from './GiveItems' 7 | 8 | import { 9 | GetHelp, 10 | GetTicket, 11 | GetTracked, 12 | GetSchwag, 13 | } from './GetItems' 14 | 15 | const augmentItem$ = (code, component) => sources => { 16 | const sinks = component(sources) 17 | return {...sinks, item$: sinks.item$.map(i => ({...i, code}))} 18 | } 19 | 20 | export default { 21 | waiver: augmentItem$('waiver', GiveWaiver), 22 | shifts: augmentItem$('shifts', GiveShifts), 23 | payment: augmentItem$('payment', GivePayment), 24 | deposit: augmentItem$('deposit', GiveDeposit), 25 | help: augmentItem$('help', GetHelp), 26 | ticket: augmentItem$('ticket', GetTicket), 27 | tracked: augmentItem$('tracked', GetTracked), 28 | schwag: augmentItem$('schwag', GetSchwag), 29 | } 30 | -------------------------------------------------------------------------------- /src/components/opp/codeTitles.js: -------------------------------------------------------------------------------- 1 | // TODO: where should these live 2 | 3 | export default { 4 | help: ({who}) => 'To help ' + who, 5 | ticket: ({ticketType}) => 'A ' + ticketType + ' ticket', 6 | tracked: ({count, description}) => count + ' ' + description, 7 | schwag: ({what}) => what, 8 | waiver: ({who}) => 'A waiver for ' + who, 9 | deposit: ({amount}) => 'Make a refundable deposit of ' + amount, 10 | payment: ({amount}) => 'A nonrefundable ' + amount, 11 | shifts: ({count}) => 'Work ' + count + ' shifts', 12 | } 13 | -------------------------------------------------------------------------------- /src/components/opp/index.js: -------------------------------------------------------------------------------- 1 | export {AddCommitmentGive} from './AddCommitmentGive' 2 | export {AddCommitmentGet} from './AddCommitmentGet' 3 | export {CreateOppListItem} from './CreateOppListItem' 4 | export {CreateOppHeader} from './CreateOppHeader' 5 | export {OppForm} from './OppForm' 6 | export {OppNav} from './OppNav' 7 | export {OppItemNavigating, OppListNavigating} from './OppListNavigating' 8 | -------------------------------------------------------------------------------- /src/components/organizer/OrganizerInviteItem.js: -------------------------------------------------------------------------------- 1 | import {Observable} from 'rx' 2 | const {just} = Observable 3 | // import combineLatestObj from 'rx-combine-latest-obj' 4 | 5 | import isolate from '@cycle/isolate' 6 | 7 | import {Organizers} from 'components/remote' 8 | 9 | import { 10 | ListItemWithMenu, 11 | ListItemClickable, 12 | ListItemNavigating, 13 | } from 'components/sdm' 14 | 15 | const Delete = sources => ListItemClickable({...sources, 16 | iconName$: just('remove'), 17 | title$: just('Remove'), 18 | }) 19 | 20 | const View = sources => ListItemNavigating({...sources, 21 | iconName$: just('remove'), 22 | title$: just('View Invite Page'), 23 | path$: sources.item$.map(({$key}) => '/organize/' + $key), 24 | }) 25 | 26 | const OrganizerInviteItem = sources => { 27 | const deleteItem = isolate(Delete,'delete')(sources) 28 | const viewItem = isolate(View,'view')(sources) 29 | 30 | const listItem = ListItemWithMenu({...sources, 31 | iconName$: just('mail_outline'), 32 | title$: sources.item$.map(({inviteEmail}) => inviteEmail), 33 | menuItems$: just([deleteItem.DOM, viewItem.DOM]), 34 | }) 35 | 36 | const queue$ = deleteItem.click$ 37 | .flatMapLatest(sources.item$) 38 | .pluck('$key') 39 | .map(Organizers.action.remove) 40 | 41 | return { 42 | DOM: listItem.DOM, 43 | route$: viewItem.route$, 44 | queue$, 45 | } 46 | } 47 | 48 | // const OrganizerInviteList = sources => List({...sources, 49 | // Control$: just(OrganizerInviteItem), 50 | // }) 51 | 52 | export {OrganizerInviteItem} 53 | -------------------------------------------------------------------------------- /src/components/organizer/OrganizerItem.js: -------------------------------------------------------------------------------- 1 | // import {ProjectItem} from 'components/project' 2 | 3 | // const _label = ({isApplied, isAccepted, isConfirmed}) => 4 | // isConfirmed && 'Confirmed' || 5 | // isAccepted && 'Accepted' || 6 | // isApplied && 'Applied' || 7 | // 'Unknown' 8 | 9 | // const OrganizerItem = sources => ProjectItem({...sources, 10 | // subtitle$: sources.item$.map(e => e.opp.name + ' | ' + _label(e)), 11 | // item$: sources.item$ 12 | // .map(e => ({$key: e.opp.projectKey, ...e.opp.project})), 13 | // path$: sources.item$.map(({$key}) => '/engaged/' + $key), 14 | // }) 15 | 16 | // export {OrganizerItem} 17 | -------------------------------------------------------------------------------- /src/components/organizer/index.js: -------------------------------------------------------------------------------- 1 | export {OrganizerInviteItem} from './OrganizerInviteItem' 2 | -------------------------------------------------------------------------------- /src/components/profile/ProfileAvatar/index.js: -------------------------------------------------------------------------------- 1 | import { 2 | Avatar, 3 | MediumAvatar, 4 | LargeAvatar, 5 | } from 'components/sdm' 6 | 7 | import {ProfileFetcher} from 'components/profile/ProfileFetcher' 8 | 9 | const PortraitFetcher = sources => ({ 10 | portraitUrl$: ProfileFetcher(sources).profile$ 11 | .map(p => p ? p.portraitUrl : null), 12 | }) 13 | 14 | export const ProfileAvatar = sources => Avatar({...sources, 15 | src$: PortraitFetcher(sources).portraitUrl$, 16 | }) 17 | 18 | export const MediumProfileAvatar = sources => MediumAvatar({...sources, 19 | src$: PortraitFetcher(sources).portraitUrl$, 20 | }) 21 | 22 | export const LargeProfileAvatar = sources => LargeAvatar({...sources, 23 | src$: PortraitFetcher(sources).portraitUrl$, 24 | }) 25 | -------------------------------------------------------------------------------- /src/components/profile/ProfileFetcher.js: -------------------------------------------------------------------------------- 1 | import {Observable as $} from 'rx' 2 | 3 | import { 4 | Profiles, 5 | } from 'components/remote' 6 | 7 | export const ProfileFetcher = sources => ({ 8 | profile$: sources.profileKey$ 9 | .flatMapLatest(k => k ? Profiles.query.one(sources)(k) : $.just(null)), 10 | }) 11 | -------------------------------------------------------------------------------- /src/components/profile/ProfileSidenav.js: -------------------------------------------------------------------------------- 1 | import {Observable as $} from 'rx' 2 | import {h} from 'cycle-snabbdom' 3 | 4 | import isolate from '@cycle/isolate' 5 | 6 | import {SidedrawerTitle} from 'components/Title' 7 | import {MediumProfileAvatar} from 'components/profile' 8 | 9 | import { 10 | ListItem, 11 | } from 'components/sdm' 12 | 13 | // const _Nav = sources => ({ 14 | // DOM: sources.isMobile$.map(m => m ? null : sources.titleDOM), 15 | // }) 16 | 17 | // const _Page = sources => TabbedPage({...sources, 18 | // tabs$: of([ 19 | // {path: '/', label: 'Doing'}, 20 | // {path: '/being', label: 'Being'}, 21 | // ]), 22 | // routes$: of({ 23 | // '/': Doing, 24 | // '/being': Being, 25 | // }), 26 | // }) 27 | 28 | import {combineDOMsToDiv} from 'util' 29 | 30 | const _Title = sources => SidedrawerTitle({...sources, 31 | titleDOM$: sources.userName$, 32 | subtitleDOM$: $.of('Welcome'), 33 | leftDOM$: MediumProfileAvatar({...sources, 34 | profileKey$: sources.userProfileKey$, 35 | }).DOM, 36 | classes$: $.of(['profile']), 37 | }) 38 | 39 | const _Welcome = sources => ListItem({...sources, 40 | title$: $.just('Welcome to the Sparks.Network!'), 41 | }) 42 | 43 | const _HelpLink = sources => isolate(ListItem)({...sources, 44 | title$: $.just(h('a',{ 45 | props: { 46 | href: 'mailto:help@sparks.network', 47 | }, 48 | },['Got questions?'])), 49 | }) 50 | 51 | export const ProfileSidenav = sources => { 52 | const t = _Title(sources) 53 | const wel = _Welcome(sources) 54 | const help = _HelpLink(sources) 55 | 56 | // const route$ = sources.DOM.select('') 57 | return { 58 | DOM: combineDOMsToDiv('', t, wel, help), 59 | route$: t.route$, 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/components/profile/index.js: -------------------------------------------------------------------------------- 1 | export * from './ProfileAvatar' 2 | export * from './ProfileSidenav' 3 | export * from './ProfileFetcher' 4 | -------------------------------------------------------------------------------- /src/components/project/ProjectAvatar.js: -------------------------------------------------------------------------------- 1 | import { 2 | Avatar, 3 | } from 'components/sdm' 4 | 5 | import { 6 | ProjectImages, 7 | } from 'components/remote' 8 | 9 | const sparkly = require('images/pitch/sparklerHeader-2048.jpg') 10 | 11 | const ProjectImageFetcher = sources => ({ 12 | projectImage$: sources.projectKey$ 13 | .flatMapLatest(ProjectImages.query.one(sources)), 14 | }) 15 | 16 | const Fetcher = sources => ({ 17 | dataUrl$: ProjectImageFetcher(sources).projectImage$ 18 | .map(p => p && p.dataUrl || `/${sparkly}`), 19 | }) 20 | 21 | export const ProjectAvatar = sources => Avatar({...sources, 22 | src$: Fetcher(sources).dataUrl$, 23 | }) 24 | 25 | -------------------------------------------------------------------------------- /src/components/project/ProjectForm.js: -------------------------------------------------------------------------------- 1 | // TODO: use Form() and Modal() and all that good stuff 2 | 3 | import {Observable} from 'rx' 4 | import combineLatestObj from 'rx-combine-latest-obj' 5 | 6 | import {projectForm} from 'helpers' 7 | 8 | export const ProjectForm = sources => { 9 | const submitClick$ = sources.DOM.select('.submit').events('click') 10 | 11 | const submitForm$ = sources.DOM.select('.project').events('submit') 12 | .doAction(ev => ev.preventDefault()) 13 | 14 | const cancelClick$ = sources.DOM.select('.cancel').events('click') 15 | 16 | const submit$ = Observable.merge(submitClick$, submitForm$) 17 | 18 | const name$ = sources.DOM.select('.name').events('input') 19 | .pluck('target', 'value') 20 | .startWith('') 21 | 22 | const clearFormData$ = cancelClick$ 23 | .map(() => ({})) 24 | 25 | const formData$ = combineLatestObj({name$}) 26 | 27 | const editProject$ = (sources.project$ || Observable.empty()) 28 | .merge(formData$) 29 | .merge(clearFormData$) 30 | .distinctUntilChanged() 31 | 32 | const project$ = editProject$ 33 | .sample(submit$) 34 | .filter(p => p !== {}) 35 | 36 | const DOM = editProject$.startWith({}).map(projectForm) 37 | 38 | return { 39 | DOM, 40 | project$, 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/components/project/ProjectItem.js: -------------------------------------------------------------------------------- 1 | import {ListItemNavigating} from 'components/sdm' 2 | 3 | import {ProjectImages} from 'components/remote' 4 | 5 | const sparkly = require('images/pitch/sparklerHeader-2048.jpg') 6 | 7 | const ProjectItem = sources => { 8 | const projectImage$ = sources.item$.pluck('$key') 9 | .flatMapLatest(ProjectImages.query.one(sources)) 10 | 11 | return ListItemNavigating({...sources, 12 | iconSrc$: projectImage$.map(i => i && i.dataUrl || '/' + sparkly), 13 | title$: sources.item$.pluck('name'), 14 | path$: sources.path$ || sources.item$.map(({$key}) => '/project/' + $key), 15 | }) 16 | } 17 | 18 | export {ProjectItem} 19 | -------------------------------------------------------------------------------- /src/components/project/ProjectQuickNavMenu.js: -------------------------------------------------------------------------------- 1 | import {Observable} from 'rx' 2 | const {just, merge} = Observable 3 | 4 | import isolate from '@cycle/isolate' 5 | 6 | import { 7 | List, 8 | ListItemNavigating, 9 | } from 'components/sdm' 10 | 11 | import {TeamItemNavigating} from 'components/team' 12 | import {OppItemNavigating} from 'components/opp' 13 | 14 | import {QuickNav} from 'components/QuickNav' 15 | 16 | const TEAMREGEX = /(team)\/(.+?)\// 17 | const OPPREGEX = /(opp)\/(.+?)\// 18 | 19 | const _TeamNav = sources => TeamItemNavigating({...sources, 20 | path$: sources.item$.combineLatest( 21 | sources.router.observable.pluck('pathname'), 22 | ({$key},path) => path.match(TEAMREGEX) ? 23 | path.replace(TEAMREGEX, `team/${$key}/`) : 24 | `/team/${$key}` 25 | ), 26 | }) 27 | 28 | const _OppNav = sources => OppItemNavigating({...sources, 29 | path$: sources.item$.combineLatest( 30 | sources.router.observable.pluck('pathname'), 31 | ({$key},path) => path.match(OPPREGEX) ? 32 | path.replace(OPPREGEX, `opp/${$key}/`) : 33 | `/opp/${$key}` 34 | ), 35 | }) 36 | 37 | const ProjectQuickNavMenu = sources => { 38 | const project = isolate(ListItemNavigating,'project')({...sources, 39 | title$: sources.project$.pluck('name'), 40 | path$: sources.projectKey$.map($key => '/project/' + $key), 41 | }) 42 | 43 | const teams = isolate(List,'teams')({...sources, 44 | Control$: just(_TeamNav), 45 | rows$: sources.teams$, 46 | }) 47 | 48 | const opps = isolate(List,'opps')({...sources, 49 | Control$: just(_OppNav), 50 | rows$: sources.opps$, 51 | }) 52 | 53 | const nav = QuickNav({...sources, 54 | label$: sources.project$.pluck('name'), 55 | menuItems$: just([project.DOM, opps.DOM, teams.DOM]), 56 | }) 57 | 58 | return { 59 | DOM: nav.DOM, 60 | route$: merge(opps.route$, project.route$, teams.route$), 61 | } 62 | } 63 | 64 | export {ProjectQuickNavMenu} 65 | -------------------------------------------------------------------------------- /src/components/project/index.js: -------------------------------------------------------------------------------- 1 | export {ProjectForm} from './ProjectForm' 2 | export {ProjectItem} from './ProjectItem' 3 | export {ProjectNav} from './ProjectNav' 4 | export {ProjectQuickNavMenu} from './ProjectQuickNavMenu' 5 | export * from './ProjectAvatar' 6 | -------------------------------------------------------------------------------- /src/components/redirects.js: -------------------------------------------------------------------------------- 1 | const LogoutRedirector = sources => ({ 2 | route$: sources.redirectLogout$, 3 | }) 4 | 5 | export {LogoutRedirector} 6 | -------------------------------------------------------------------------------- /src/components/sdm/Avatar/index.js: -------------------------------------------------------------------------------- 1 | require('./styles.scss') 2 | 3 | import {Observable} from 'rx' 4 | const {just, combineLatest} = Observable 5 | 6 | import {img} from 'cycle-snabbdom' 7 | 8 | const CLASSES = {avatar: true} 9 | const MEDIUM = {medium: true} 10 | const LARGE = {large: true} 11 | 12 | const Avatar = sources => ({ 13 | DOM: combineLatest( 14 | sources.classes$ || just({}), 15 | sources.src$, 16 | (classes, src) => img({class: {...CLASSES, ...classes}, attrs: {src}}) 17 | ), 18 | }) 19 | 20 | const MediumAvatar = ({classes$, ...sources}) => Avatar({...sources, 21 | classes$: classes$ ? classes$.map(c => ({...MEDIUM, ...c})) : just(MEDIUM), 22 | }) 23 | 24 | const LargeAvatar = ({classes$, ...sources}) => Avatar({...sources, 25 | classes$: classes$ ? classes$.map(c => ({...LARGE, ...c})) : just(LARGE), 26 | }) 27 | 28 | export { 29 | Avatar, 30 | MediumAvatar, 31 | LargeAvatar, 32 | } 33 | -------------------------------------------------------------------------------- /src/components/sdm/Avatar/styles.scss: -------------------------------------------------------------------------------- 1 | .avatar { 2 | width: 40px; 3 | height: 40px; 4 | border-radius: 20px; 5 | 6 | &.medium { 7 | width: 80px; 8 | height: 80px; 9 | border-radius: 40px; 10 | } 11 | 12 | &.large { 13 | width: 200px; 14 | height: 200px; 15 | border-radius: 100px; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/components/sdm/Button.js: -------------------------------------------------------------------------------- 1 | //placeholder for replacement w cyclic-surface-material 2 | import {Observable} from 'rx' 3 | const {just, combineLatest} = Observable 4 | 5 | // import isolate from '@cycle/isolate' 6 | 7 | import combineLatestObj from 'rx-combine-latest-obj' 8 | import {Button} from 'snabbdom-material' 9 | import {div} from 'helpers' 10 | import {span} from 'cycle-snabbdom' 11 | 12 | import newId from './id' 13 | 14 | const FlatButton = sources => { 15 | const id = newId() 16 | 17 | const viewState = { 18 | label$: sources.label$ || just('Button'), 19 | classNames$: sources.classNames$ || just([]), 20 | } 21 | 22 | const click$ = sources.DOM.select('.' + id).events('click') 23 | 24 | const DOM = combineLatestObj(viewState) 25 | .map(({label, classNames}) => 26 | Button({ 27 | onClick: true, 28 | flat: true, 29 | className: [id, ...classNames].join('.'), 30 | }, [ 31 | label, 32 | ]), 33 | ) 34 | 35 | return { 36 | DOM, 37 | click$, 38 | } 39 | } 40 | 41 | const RaisedButton = sources => { 42 | const id = newId() 43 | 44 | const viewState = { 45 | label$: sources.label$ || just('Button'), 46 | classNames$: sources.classNames$ || just([]), 47 | } 48 | 49 | const click$ = sources.DOM.select('.' + id).events('click') 50 | 51 | const DOM = combineLatestObj(viewState) 52 | .map(({label, classNames}) => span({},[ 53 | Button({ 54 | onClick: true, 55 | primary: true, 56 | className: [id, ...classNames].join('.'), 57 | }, [ 58 | label, 59 | ]), 60 | ])) 61 | 62 | return { 63 | DOM, 64 | click$, 65 | } 66 | } 67 | 68 | const OkAndCancel = sources => { 69 | const ok = RaisedButton({...sources, 70 | label$: sources.okLabel$ || just('OK'), 71 | }) 72 | const cancel = FlatButton({...sources, 73 | label$: sources.cancelLabel$ || just('Cancel'), 74 | }) 75 | 76 | return { 77 | DOM: combineLatest(ok.DOM, cancel.DOM, (...DOMs) => div({},DOMs)), 78 | ok$: ok.click$, 79 | cancel$: cancel.click$, 80 | } 81 | } 82 | 83 | export { 84 | RaisedButton, 85 | FlatButton, 86 | OkAndCancel, 87 | } 88 | -------------------------------------------------------------------------------- /src/components/sdm/Card/styles.scss: -------------------------------------------------------------------------------- 1 | .fullpage { 2 | display: flex; 3 | flex-flow: column; 4 | flex: 1 1 100%; 5 | 6 | & > div { 7 | display: flex; 8 | flex-flow: column; 9 | flex: 1 1 auto; 10 | min-height: 100%; 11 | } 12 | } 13 | 14 | .cardcontainer { 15 | height: 100%; 16 | background-color: #DDD; 17 | flex: 1 1 100%; 18 | padding-top: 1em; 19 | display: flex; 20 | flex-wrap: wrap; 21 | } 22 | 23 | .card { 24 | margin: 0 0 1em 0; 25 | min-height: 120px; 26 | display: flex; 27 | flex-flow: column; 28 | } 29 | 30 | .cardmedia { 31 | flex: 1 1 auto; 32 | cursor: pointer; 33 | padding: 0.5em; 34 | display: flex; 35 | flex-flow: column; 36 | justify-content: flex-end; 37 | background-size: cover; 38 | background-position: center; 39 | min-height: 120px; 40 | 41 | .title, .subtitle { 42 | color: white; 43 | } 44 | 45 | .title { 46 | font-size: 18px; 47 | font-weight: medium; 48 | xflex: 1 1 auto; 49 | } 50 | 51 | .subtitle { 52 | font-size: 16px; 53 | font-weight: normal; 54 | xflex: 1 1 0; 55 | } 56 | } 57 | 58 | .cardtitle { 59 | background-color: #FFEB3B; 60 | font-size: 1.8em; 61 | font-weight: bolder; 62 | color: white; 63 | padding: .5em; 64 | } 65 | 66 | .cardcontent { 67 | padding: 0.5em; 68 | } -------------------------------------------------------------------------------- /src/components/sdm/CheckboxControl.js: -------------------------------------------------------------------------------- 1 | import {icon} from 'helpers' 2 | 3 | const CheckboxControl = sources => ({ 4 | DOM: sources.value$.map(v => 5 | v ? 6 | icon('check_box','accent') : 7 | icon('check_box_outline_blank') 8 | ), 9 | }) 10 | 11 | export {CheckboxControl} 12 | -------------------------------------------------------------------------------- /src/components/sdm/Fab.js: -------------------------------------------------------------------------------- 1 | //placeholder for replacement w cyclic-surface-material 2 | import {Observable} from 'rx' 3 | const {just} = Observable 4 | 5 | import combineLatestObj from 'rx-combine-latest-obj' 6 | import {Appbar} from 'snabbdom-material' 7 | 8 | import newId from './id' 9 | 10 | const Fab = sources => { 11 | const id = newId() 12 | 13 | const viewState = { 14 | classNames$: sources.classNames$ || just([]), 15 | iconDOM$: sources.iconDOM$, 16 | } 17 | 18 | const click$ = sources.DOM.select('.' + id).events('click') 19 | 20 | const DOM = combineLatestObj(viewState) 21 | .map(({iconDOM, classNames}) => 22 | Appbar.Button({ 23 | onClick: true, 24 | primary: true, 25 | className: [id, ...classNames].join('.'), 26 | }, [ 27 | iconDOM, 28 | ]), 29 | ) 30 | 31 | return { 32 | DOM, 33 | click$, 34 | } 35 | } 36 | 37 | export {Fab} 38 | -------------------------------------------------------------------------------- /src/components/sdm/InputControl.js: -------------------------------------------------------------------------------- 1 | import {Observable} from 'rx' 2 | const {just} = Observable 3 | 4 | import combineLatestObj from 'rx-combine-latest-obj' 5 | 6 | import {div} from 'helpers' 7 | 8 | import {Input} from 'snabbdom-material' 9 | 10 | // import {log} from 'util' 11 | 12 | const InputControl = sources => { 13 | const input$ = sources.DOM.select('.input').events('input') 14 | 15 | const value$ = (sources.value$ || just(null)) 16 | .merge(input$.pluck('target','value')) 17 | // value$.subscribe(log('value$')) 18 | 19 | const viewState = { 20 | label$: sources.label$ || just(null), 21 | value$, 22 | classNames$: sources.classNames$ || just([]), 23 | } 24 | 25 | const DOM = combineLatestObj(viewState) 26 | .map(({label, value, classNames}) => 27 | div({},[ 28 | Input({label, value, className: ['input', ...classNames].join('.')}), 29 | ]) 30 | ) 31 | 32 | return { 33 | DOM, 34 | value$, 35 | } 36 | } 37 | 38 | export {InputControl} 39 | -------------------------------------------------------------------------------- /src/components/sdm/ListItem/ListItemCheckbox.js: -------------------------------------------------------------------------------- 1 | import {ListItemClickable} from './ListItemClickable' 2 | import {CheckboxControl} from 'components/sdm' 3 | 4 | export const ListItemCheckbox = sources => { 5 | const cb = CheckboxControl(sources) 6 | 7 | const item = ListItemClickable({...sources, 8 | leftDOM$: sources.leftDOM$ || cb.DOM, 9 | title$: sources.value$.flatMapLatest(v => 10 | sources.title$ || 11 | (v ? sources.titleTrue$ : sources.titleFalse$) 12 | ), 13 | rightDOM$: sources.rightDOM$ || sources.leftDOM$ && cb.DOM, 14 | }) 15 | 16 | const value$ = item.click$ 17 | .withLatestFrom(sources.value$) 18 | .map(click_and_val => !click_and_val[1]) 19 | 20 | return { 21 | DOM: item.DOM, 22 | value$, 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/components/sdm/ListItem/ListItemClickable.js: -------------------------------------------------------------------------------- 1 | import {Observable as $} from 'rx' 2 | import {ListItem} from './ListItem' 3 | 4 | export const ListItemClickable = sources => { 5 | const classes$ = (sources.classes$ || $.just({})) 6 | .map(c => ({clickable: true, ...c})) 7 | 8 | const click$ = sources.DOM.select('.list-item').events('click') 9 | 10 | return { 11 | DOM: ListItem({...sources, classes$}).DOM, 12 | click$, 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/components/sdm/ListItem/ListItemCollapsible.js: -------------------------------------------------------------------------------- 1 | import {Observable as $} from 'rx' 2 | import combineLatestObj from 'rx-combine-latest-obj' 3 | 4 | import {ListItemClickable} from './ListItemClickable' 5 | 6 | import {div} from 'cycle-snabbdom' 7 | 8 | export const ListItemCollapsible = sources => { 9 | const li = ListItemClickable(sources) 10 | 11 | const isOpen$ = $.merge( 12 | sources.isOpen$ || $.just(false), 13 | li.click$.map(-1), 14 | ) 15 | .scan((acc, next) => next === -1 ? !acc : next, false) 16 | .startWith(false) 17 | 18 | const viewState = { 19 | isOpen$: isOpen$, 20 | listItemDOM$: li.DOM, 21 | contentDOM$: sources.contentDOM$ || $.just(div({},['no contentDOM$'])), 22 | } 23 | 24 | const DOM = combineLatestObj(viewState) 25 | .map(({isOpen, listItemDOM, contentDOM}) => 26 | div({},[ 27 | listItemDOM, 28 | isOpen && div('.collapsible',[contentDOM]), 29 | ].filter(i => !!i)) 30 | ) 31 | 32 | return { 33 | DOM, 34 | } 35 | } 36 | 37 | // A "dumb" version of the ListItemCollapsible component 38 | // Does not manage any type of internal isOpen state, and 39 | // only responds to external sources. 40 | export const ListItemCollapsibleDumb = sources => { 41 | const li = ListItemClickable(sources) 42 | 43 | const isOpen$ = sources.isOpen$ || $.just(false) 44 | .startWith(false) 45 | 46 | const viewState = { 47 | isOpen$: isOpen$, 48 | listItemDOM$: li.DOM, 49 | contentDOM$: sources.contentDOM$ || $.just(div({},['no contentDOM$'])), 50 | } 51 | 52 | const DOM = combineLatestObj(viewState) 53 | .map(({isOpen, listItemDOM, contentDOM}) => 54 | div({},[ 55 | listItemDOM, 56 | isOpen && div('.collapsible',[contentDOM]), 57 | ].filter(i => !!i)) 58 | ) 59 | 60 | return { 61 | DOM, 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/components/sdm/ListItem/ListItemCollapsibleTextArea.js: -------------------------------------------------------------------------------- 1 | import {Observable as $} from 'rx' 2 | 3 | import {div} from 'cycle-snabbdom' 4 | 5 | import {ListItemCollapsible} from './ListItemCollapsible' 6 | 7 | import {TextAreaControl} from 'components/sdm' 8 | import {OkAndCancel} from 'components/sdm' 9 | 10 | export const ListItemCollapsibleTextArea = sources => { 11 | const ta = TextAreaControl(sources) 12 | const oac = OkAndCancel(sources) 13 | 14 | const value$ = ta.value$.sample($.merge(oac.ok$, ta.enter$)) 15 | 16 | const li = ListItemCollapsible({...sources, 17 | contentDOM$: $.combineLatest(ta.DOM, oac.DOM, (...doms) => div({},doms)), 18 | subtitle$: sources.value$.combineLatest( 19 | sources.subtitle$ || $.just(null), 20 | (v,st) => v || st 21 | ).merge(value$), 22 | isOpen$: $.merge( 23 | sources.isOpen$ || $.never(), 24 | ta.enter$.map(false), 25 | oac.ok$.map(false), 26 | oac.cancel$.map(false) 27 | ).share(), 28 | }) 29 | 30 | return { 31 | DOM: li.DOM, 32 | ok$: oac.ok$, 33 | value$, 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/components/sdm/ListItem/ListItemCollapsibleWithMenu.js: -------------------------------------------------------------------------------- 1 | import {Observable as $} from 'rx' 2 | 3 | import isolate from '@cycle/isolate' 4 | 5 | import {div} from 'cycle-snabbdom' 6 | import {icon} from 'helpers' 7 | 8 | import {ListItem} from './ListItem' 9 | 10 | import {Menu} from 'components/sdm' 11 | import {FlatButton} from 'components/sdm' 12 | 13 | import {combineDOMsToDiv} from 'util' 14 | 15 | // should replace the one in SDM 16 | // and have a ListItemClickableCollapsible or somesuch for pure collapsers 17 | const ListItemCollapsible = sources => { 18 | const isOpen$ = sources.isOpen$ || $.of(false) 19 | const contentDOM$ = sources.contentDOM$ || $.of(div({},['no contentDOM$'])) 20 | 21 | const li = ListItem({...sources, classes$: $.of({clickable: true})}) 22 | 23 | const DOM = $.combineLatest( 24 | isOpen$, li.DOM, contentDOM$, 25 | (isOpen, listItemDOM, contentDOM) => 26 | div('.clickable',[ 27 | listItemDOM, 28 | isOpen && div('.collapsible',[contentDOM]), 29 | ].filter(i => !!i)) 30 | ) 31 | 32 | return { 33 | DOM, 34 | } 35 | } 36 | 37 | const MenuButton = sources => { 38 | const btn = FlatButton({...sources, label$: $.of(icon('menu'))}) 39 | 40 | const isOpen$ = btn.click$.map(true).startWith(false) 41 | const children$ = sources.menuItems$ || $.just([]) 42 | 43 | const menu = Menu({ 44 | ...sources, 45 | isOpen$, 46 | children$, 47 | leftAlign$: $.of(false), 48 | }) 49 | 50 | return { 51 | DOM: combineDOMsToDiv('', btn, menu), 52 | } 53 | } 54 | 55 | export const ListItemCollapsibleWithMenu = sources => { 56 | const mb = isolate(MenuButton)(sources) 57 | 58 | const click$ = $.merge( 59 | sources.DOM.select('.left').events('click'), 60 | sources.DOM.select('.content').events('click'), 61 | ).map(-1) 62 | 63 | const isOpen$ = $.merge( 64 | sources.isOpen$ || $.just(false), 65 | click$, 66 | ) 67 | .scan((acc, next) => next === -1 ? !acc : next, false) 68 | .startWith(false) 69 | 70 | return ListItemCollapsible({...sources, 71 | isOpen$, 72 | rightDOM$: mb.DOM, 73 | }) 74 | } 75 | -------------------------------------------------------------------------------- /src/components/sdm/ListItem/ListItemHeader.js: -------------------------------------------------------------------------------- 1 | import {Observable as $} from 'rx' 2 | 3 | import {ListItem} from './ListItem' 4 | 5 | export const ListItemHeader = sources => 6 | ListItem({...sources, classes$: $.just({header: true})}) 7 | -------------------------------------------------------------------------------- /src/components/sdm/ListItem/ListItemNavigating.js: -------------------------------------------------------------------------------- 1 | import {Observable as $} from 'rx' 2 | import {ListItemClickable} from './ListItemClickable' 3 | 4 | export const ListItemNavigating = sources => { 5 | const item = ListItemClickable(sources) 6 | 7 | const route$ = item.click$ 8 | .withLatestFrom( 9 | sources.path$ || $.just('/'), 10 | (cl,p) => p, 11 | ) 12 | 13 | return { 14 | DOM: item.DOM, 15 | route$, 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/components/sdm/ListItem/ListItemTextArea.js: -------------------------------------------------------------------------------- 1 | import {Observable as $} from 'rx' 2 | 3 | import {div} from 'cycle-snabbdom' 4 | 5 | import {ListItem} from './ListItem' 6 | 7 | import {TextAreaControl} from 'components/sdm' 8 | import {OkAndCancel} from 'components/sdm' 9 | 10 | export const ListItemTextArea = sources => { 11 | const ta = TextAreaControl(sources) 12 | const oac = OkAndCancel(sources) 13 | const li = ListItem({...sources, 14 | title$: $.combineLatest(ta.DOM, oac.DOM, (...doms) => div({},doms)), 15 | }) 16 | 17 | return { 18 | DOM: li.DOM, 19 | value$: ta.value$.sample($.merge(oac.ok$, ta.enter$)), 20 | } 21 | } 22 | 23 | -------------------------------------------------------------------------------- /src/components/sdm/ListItem/ListItemToggle.js: -------------------------------------------------------------------------------- 1 | import {ListItemClickable} from './ListItemClickable' 2 | import {ToggleControl} from 'components/sdm' 3 | 4 | export const ListItemToggle = sources => { 5 | const toggle = ToggleControl(sources) 6 | 7 | const item = ListItemClickable({...sources, 8 | leftDOM$: toggle.DOM, 9 | title$: sources.value$.flatMapLatest(v => 10 | v ? sources.titleTrue$ : sources.titleFalse$ 11 | ), 12 | }) 13 | 14 | const value$ = sources.value$ 15 | .sample(item.click$) 16 | .map(x => !x) 17 | 18 | return { 19 | DOM: item.DOM, 20 | value$, 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/components/sdm/ListItem/ListItemWithDialog.js: -------------------------------------------------------------------------------- 1 | import {Observable as $} from 'rx' 2 | import combineLatestObj from 'rx-combine-latest-obj' 3 | 4 | import {ListItemClickable} from './ListItemClickable' 5 | 6 | import {div} from 'cycle-snabbdom' 7 | 8 | import {Dialog} from 'components/sdm' 9 | 10 | export const ListItemWithDialog = sources => { 11 | const _listItem = ListItemClickable(sources) 12 | 13 | const iconName$ = sources.iconUrl$ || 14 | sources.dialogIconName$ || 15 | sources.iconName$ 16 | 17 | const dialog = Dialog({...sources, 18 | isOpen$: _listItem.click$.map(true).merge(sources.isOpen$ || $.never()), 19 | titleDOM$: sources.dialogTitleDOM$, 20 | iconName$, 21 | contentDOM$: sources.dialogContentDOM$, 22 | }) 23 | 24 | const DOM = combineLatestObj({ 25 | listItemDOM$: _listItem.DOM, 26 | dialogDOM$: dialog.DOM, 27 | }).map(({ 28 | listItemDOM, 29 | dialogDOM, 30 | }) => 31 | div({},[listItemDOM, dialogDOM]) 32 | ) 33 | 34 | return { 35 | DOM, 36 | value$: dialog.value$, 37 | submit$: dialog.submit$, 38 | close$: dialog.close$, 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/components/sdm/ListItem/ListItemWithMenu.js: -------------------------------------------------------------------------------- 1 | import {Observable as $} from 'rx' 2 | import combineLatestObj from 'rx-combine-latest-obj' 3 | 4 | import {div} from 'cycle-snabbdom' 5 | 6 | import {ListItemClickable} from './ListItemClickable' 7 | import {Menu} from 'components/sdm' 8 | 9 | export const ListItemWithMenu = sources => { 10 | const item = ListItemClickable(sources) 11 | 12 | const isOpen$ = item.click$.map(true).startWith(false) 13 | 14 | const children$ = sources.menuItems$ || $.just([]) 15 | 16 | const menu = Menu({ 17 | ...sources, 18 | isOpen$, 19 | children$, 20 | }) 21 | 22 | const viewState = { 23 | itemDOM$: item.DOM, 24 | menuDOM$: menu.DOM, 25 | } 26 | 27 | const DOM = combineLatestObj(viewState) 28 | .map(({itemDOM, menuDOM}) => 29 | div({},[itemDOM, menuDOM]) 30 | ) 31 | 32 | return { 33 | DOM, 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/components/sdm/ListItem/index.js: -------------------------------------------------------------------------------- 1 | require('./styles.scss') 2 | 3 | export * from './ListItem' 4 | export * from './ListItemCheckbox' 5 | export * from './ListItemClickable' 6 | export * from './ListItemCollapsible' 7 | export * from './ListItemCollapsibleTextArea' 8 | export * from './ListItemCollapsibleWithMenu' 9 | export * from './ListItemHeader' 10 | export * from './ListItemNavigating' 11 | export * from './ListItemTextArea' 12 | export * from './ListItemToggle' 13 | export * from './ListItemWithDialog' 14 | export * from './ListItemWithMenu' 15 | 16 | -------------------------------------------------------------------------------- /src/components/sdm/ListItem/styles.scss: -------------------------------------------------------------------------------- 1 | .collapsible { 2 | padding: 0 8px 16px 64px; 3 | } 4 | 5 | a.list-item { 6 | text-decoration: none; 7 | color: inherit; 8 | } 9 | 10 | .list-item { 11 | min-height: 64px; 12 | display: flex; 13 | justify-content: space-between; 14 | align-items: center; 15 | 16 | &.blue { 17 | background-color: rgba(7, 193, 255, 1) !important; 18 | &::after { 19 | border-color: rgba(7, 193, 255, 1) transparent !important; 20 | } 21 | } 22 | 23 | &.green { 24 | background-color: #07FF33 !important; 25 | &::after { 26 | border-color: #07FF33 transparent !important; 27 | } 28 | } 29 | 30 | &.yellow { 31 | background-color: #FFC107 !important; 32 | &::after { 33 | border-color: #FFC107 transparent !important; 34 | } 35 | } 36 | 37 | &.rotate { 38 | transform: rotateY(180deg); 39 | 40 | .content { 41 | transform: rotateY(180deg); 42 | } 43 | 44 | .subtitle { 45 | float: right; 46 | } 47 | } 48 | 49 | &.header { 50 | background-color: #666; 51 | color: #FFF; 52 | text-transform: uppercase; 53 | font-size: 1.4em; 54 | font-weight: bold; 55 | 56 | &.clickable:hover { 57 | background-color: #000 !important; 58 | } 59 | } 60 | &.clickable { 61 | cursor: pointer; 62 | 63 | &:hover { 64 | background-color: #eee; 65 | } 66 | } 67 | &.disabled, &.disabled * { 68 | color: #CCC !important; 69 | } 70 | 71 | .left, .right { 72 | flex-basis: 64px; 73 | 74 | i { 75 | font-size: 32px; 76 | line-height: 40px !important; 77 | } 78 | 79 | &.left { 80 | padding: 0 0 0 16px; 81 | } 82 | 83 | &.right { 84 | padding: 0 16px 0 0; 85 | text-align: right; 86 | } 87 | } 88 | 89 | .content { 90 | flex: 1; 91 | line-height: 24px; 92 | 93 | padding: 0 16px; 94 | 95 | .title { 96 | font-size: 18px; 97 | 98 | &:xonly-child { 99 | margin-top: 20px; 100 | } 101 | 102 | } 103 | .subtitle { 104 | color: #666; 105 | } 106 | } 107 | 108 | .right { 109 | 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /src/components/sdm/Menu/styles.scss: -------------------------------------------------------------------------------- 1 | .menu { 2 | z-index: 1000; 3 | position: relative; 4 | height: 0; 5 | overflow: visible; 6 | 7 | .list-item { 8 | margin-left: 0px; 9 | margin-right: 0px; 10 | } 11 | 12 | .menu-contents { 13 | z-index: 1001; 14 | padding: 10px 0; 15 | background-color: #fff; 16 | color: #000; 17 | position: absolute; 18 | overflow-y: auto; 19 | scrollbar-width: 4px; 20 | top: -8px; 21 | opacity: 0; 22 | transition: opacity 0.3s; 23 | min-width: 340px; 24 | max-width: 400px; 25 | 26 | &.left { 27 | left: 0; 28 | right: auto; 29 | } 30 | 31 | &.right { 32 | right: 0; 33 | left: auto; 34 | } 35 | } 36 | } 37 | 38 | -------------------------------------------------------------------------------- /src/components/sdm/SelectControl.js: -------------------------------------------------------------------------------- 1 | import {Observable} from 'rx' 2 | const {just} = Observable 3 | 4 | import combineLatestObj from 'rx-combine-latest-obj' 5 | 6 | import {div} from 'helpers' 7 | 8 | import {Select} from 'snabbdom-material' 9 | 10 | // import {log} from 'util' 11 | 12 | const optionIndex = e => // because children is not a real js array 13 | [...e.ownerTarget.parentNode.children] 14 | .indexOf(e.ownerTarget || e.currentTarget || e.target) 15 | 16 | const SelectControl = sources => { 17 | const options$ = sources.options$.shareReplay(1) 18 | 19 | const selection$ = sources.DOM.select('.menu-item').events('click') 20 | .flatMapLatest(e => options$.map(options => options[optionIndex(e)])) 21 | .pluck('value') 22 | 23 | const value$ = (sources.value$ || just(null)) 24 | .merge(selection$) 25 | 26 | const openClick$ = sources.DOM.select('.input-group').events('click') 27 | const closeClick$ = sources.DOM.select('.mask').events('click') 28 | 29 | const isOpen$ = Observable.merge( 30 | openClick$.map(true), 31 | selection$.map(false), 32 | closeClick$.map(false), 33 | ).startWith(false) 34 | 35 | const viewState = { 36 | isOpen$, 37 | label$: sources.label$ || just(null), 38 | value$, 39 | options$, 40 | classNames$: sources.classNames$ || just([]), 41 | } 42 | 43 | const DOM = combineLatestObj(viewState) 44 | .map(({isOpen, label, value, options, classNames}) => 45 | div({},[ 46 | Select({ 47 | isOpen, label, value, options, 48 | className: ['input', ...classNames].join(' '), 49 | }), 50 | ]) 51 | ) 52 | 53 | return { 54 | DOM, 55 | value$, 56 | } 57 | } 58 | 59 | export {SelectControl} 60 | -------------------------------------------------------------------------------- /src/components/sdm/TextAreaControl/styles.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sdebaun/sparks-cyclejs/648af959914a455c0f9313c42e446d5edaf9b9e4/src/components/sdm/TextAreaControl/styles.scss -------------------------------------------------------------------------------- /src/components/sdm/ToggleControl.js: -------------------------------------------------------------------------------- 1 | import {div} from 'helpers' 2 | import {icon} from 'helpers' 3 | 4 | const ToggleControl = sources => ({ 5 | click$: sources.DOM.select('.toggle').events('click'), 6 | 7 | DOM: sources.value$.map(v => 8 | div({class: {toggle: true}},[ 9 | v ? 10 | icon('toggle-on','accent') : 11 | icon('toggle-off'), 12 | ]) 13 | ), 14 | }) 15 | 16 | export {ToggleControl} 17 | -------------------------------------------------------------------------------- /src/components/sdm/Toolbar.js: -------------------------------------------------------------------------------- 1 | import {Observable} from 'rx' 2 | const {just} = Observable 3 | import combineLatestObj from 'rx-combine-latest-obj' 4 | import {div} from 'cycle-snabbdom' 5 | 6 | import {Appbar} from 'snabbdom-material' 7 | 8 | import {material} from 'util' 9 | 10 | const LEFTSTYLE = {style: {display: 'block', width: '32px', float: 'none'}} 11 | const MIDSTYLE = {style: {display: 'block', flex: '100% 100%', float: 'none'}} 12 | const RIGHTSTYLE = {style: {flex: '25% 25%'}} 13 | 14 | const AccentToolbar = sources => ({ 15 | DOM: combineLatestObj({ 16 | leftItemDOM$: sources.leftItemDOM$ || just(null), 17 | titleDOM$: sources.titleDOM$ || sources.title$ || just('no title'), 18 | rightItemDOM$: sources.rightItemDOM$ || just(null), 19 | }).map(({ 20 | leftItemDOM, 21 | titleDOM, 22 | rightItemDOM, 23 | }) => 24 | Appbar({material}, [div({style: {display: 'flex'}}, [ 25 | leftItemDOM && div(LEFTSTYLE, [leftItemDOM]), 26 | Appbar.Title(MIDSTYLE, [titleDOM]), 27 | rightItemDOM && div(RIGHTSTYLE, [rightItemDOM]), 28 | ].filter(e => !!e))]) 29 | ), 30 | }) 31 | 32 | const Toolbar = sources => ({ 33 | DOM: combineLatestObj({ 34 | leftItemDOM$: sources.leftItemDOM$ || just(null), 35 | titleDOM$: sources.titleDOM$ || just('no title'), 36 | rightItemDOM$: sources.rightItemDOM$ || just(null), 37 | }).map(({ 38 | leftItemDOM, 39 | titleDOM, 40 | rightItemDOM, 41 | }) => 42 | Appbar({fixed: true, material}, [ 43 | leftItemDOM && div({style: {float: 'left'}}, [leftItemDOM]), 44 | Appbar.Title({style: {float: 'left'}}, [titleDOM]), 45 | rightItemDOM && div({style: {float: 'right'}}, [rightItemDOM]), 46 | ].filter(e => !!e)) 47 | ), 48 | }) 49 | 50 | export {Toolbar, AccentToolbar} 51 | -------------------------------------------------------------------------------- /src/components/sdm/id.js: -------------------------------------------------------------------------------- 1 | let _id = 0 2 | 3 | export default () => 'id' + (_id += 1) 4 | -------------------------------------------------------------------------------- /src/components/sdm/index.js: -------------------------------------------------------------------------------- 1 | require('./styles.scss') 2 | 3 | export * from './Card' 4 | export * from './List' 5 | export * from './ListItem' 6 | 7 | export * from './Toolbar' 8 | export * from './Menu' 9 | 10 | export * from './Avatar' 11 | 12 | export * from './Button' 13 | 14 | export {Fab} from './Fab' 15 | 16 | export {InputControl} from './InputControl' 17 | export {SelectControl} from './SelectControl' 18 | export {ToggleControl} from './ToggleControl' 19 | export {TextAreaControl} from './TextAreaControl' 20 | export {CheckboxControl} from './CheckboxControl' 21 | 22 | export * from './Dialog' 23 | 24 | -------------------------------------------------------------------------------- /src/components/sdm/styles.scss: -------------------------------------------------------------------------------- 1 | .accent { 2 | color: #FFC107; 3 | } 4 | 5 | button.accent { 6 | background-color: #FFC107 !important; 7 | } 8 | 9 | button.green { 10 | background-color: #07FF33 !important; 11 | } 12 | 13 | button.blue { 14 | background-color: #07C1FF !important; 15 | } 16 | 17 | button.red { 18 | background-color: #FF3307 !important; 19 | } 20 | 21 | .disabled { 22 | color: #999; 23 | } 24 | 25 | .center { 26 | text-align: center; 27 | } 28 | 29 | .narrow { 30 | max-width: 600px; 31 | margin-top: 1em; 32 | margin-left: auto; 33 | margin-right: auto; 34 | } 35 | 36 | h3 { 37 | font-weight: 900 !important; 38 | } 39 | -------------------------------------------------------------------------------- /src/components/team/CreateTeamHeader.js: -------------------------------------------------------------------------------- 1 | // literally one line differene wit CreateTeam 2 | 3 | import {Observable} from 'rx' 4 | import combineLatestObj from 'rx-combine-latest-obj' 5 | 6 | // import isolate from '@cycle/isolate' 7 | 8 | import {Teams} from 'remote' 9 | 10 | import {TeamForm} from './TeamForm' 11 | 12 | import {col} from 'helpers' 13 | import modal from 'helpers/modal' 14 | import listItem from 'helpers/listItem' 15 | 16 | // import {log} from 'util' 17 | 18 | const _openActions$ = ({DOM}) => Observable.merge( 19 | DOM.select('.open').events('click').map(true), 20 | DOM.select('.close').events('click').map(false), 21 | ) 22 | 23 | const _submitAction$ = ({DOM}) => 24 | DOM.select('.submit').events('click').map(true) 25 | 26 | const _render = ({isOpen, teamFormDOM}) => 27 | col( 28 | listItem({ 29 | iconName: 'group_add', 30 | iconBackgroundColor: 'yellow', 31 | title: 'Teams', 32 | className: 'open', 33 | clickable: true, 34 | header: true, // this is the line 35 | }), 36 | modal({ 37 | isOpen, 38 | title: 'Add a Team', 39 | iconName: 'group_add', 40 | submitLabel: 'Make It So', 41 | closeLabel: 'Hang On', 42 | content: teamFormDOM, 43 | }) 44 | ) 45 | 46 | const CreateTeamHeader = sources => { 47 | const teamForm = TeamForm(sources) 48 | 49 | const submit$ = _submitAction$(sources) 50 | 51 | const queue$ = teamForm.item$ 52 | .sample(submit$) 53 | .zip(sources.projectKey$, 54 | (team,projectKey) => ({projectKey, ...team}) 55 | ) 56 | .map(Teams.create) 57 | 58 | const isOpen$ = _openActions$(sources) 59 | .merge(submit$.map(false)) 60 | .startWith(false) 61 | 62 | const viewState = { 63 | isOpen$, 64 | project$: sources.project$, 65 | teamFormDOM$: teamForm.DOM, 66 | } 67 | 68 | const DOM = combineLatestObj(viewState).map(_render) 69 | 70 | return {DOM, queue$} 71 | } 72 | 73 | export {CreateTeamHeader} 74 | -------------------------------------------------------------------------------- /src/components/team/CreateTeamListItem.js: -------------------------------------------------------------------------------- 1 | import {Observable} from 'rx' 2 | const {just} = Observable 3 | 4 | import {Teams} from 'remote' 5 | 6 | import {TeamForm} from 'components/team' 7 | import {ListItemWithDialog} from 'components/sdm' 8 | 9 | const CreateTeamListItem = sources => { 10 | const form = TeamForm(sources) 11 | 12 | const listItem = ListItemWithDialog({...sources, 13 | iconName$: just('group_add'), 14 | title$: just('Build your first Team.'), 15 | dialogTitleDOM$: just('Create a Team'), 16 | dialogContentDOM$: form.DOM, 17 | }) 18 | 19 | const queue$ = form.item$ 20 | .sample(listItem.submit$) 21 | .zip(sources.projectKey$, (item,projectKey) => ({projectKey, ...item})) 22 | .map(Teams.create) 23 | 24 | return { 25 | DOM: listItem.DOM, 26 | queue$, 27 | } 28 | } 29 | 30 | export {CreateTeamListItem} 31 | -------------------------------------------------------------------------------- /src/components/team/TeamAvatar.js: -------------------------------------------------------------------------------- 1 | import { 2 | Avatar, 3 | MediumAvatar, 4 | LargeAvatar, 5 | } from 'components/sdm' 6 | 7 | import { 8 | TeamImages, 9 | } from 'components/remote' 10 | 11 | const sparkly = require('images/pitch/sparklerHeader-2048.jpg') 12 | 13 | const TeamImageFetcher = sources => ({ 14 | teamImage$: sources.teamKey$ 15 | .flatMapLatest(TeamImages.query.one(sources)), 16 | }) 17 | 18 | const Fetcher = sources => ({ 19 | dataUrl$: TeamImageFetcher(sources).teamImage$ 20 | .map(p => p && p.dataUrl || `/${sparkly}`), 21 | }) 22 | 23 | const TeamAvatar = sources => Avatar({...sources, 24 | src$: Fetcher(sources).dataUrl$, 25 | }) 26 | 27 | const MediumTeamAvatar = sources => MediumAvatar({...sources, 28 | src$: Fetcher(sources).dataUrl$, 29 | }) 30 | 31 | const LargeTeamAvatar = sources => LargeAvatar({...sources, 32 | src$: Fetcher(sources).dataUrl$, 33 | }) 34 | 35 | export { 36 | TeamAvatar, 37 | MediumTeamAvatar, 38 | LargeTeamAvatar, 39 | } 40 | -------------------------------------------------------------------------------- /src/components/team/TeamFetcher.js: -------------------------------------------------------------------------------- 1 | import {Observable as $} from 'rx' 2 | 3 | import { 4 | Teams, 5 | } from 'components/remote' 6 | 7 | export const TeamFetcher = sources => ({ 8 | team$: sources.teamKey$ 9 | .flatMapLatest(k => k ? Teams.query.one(sources)(k) : $.just(null)), 10 | }) 11 | -------------------------------------------------------------------------------- /src/components/team/TeamForm.js: -------------------------------------------------------------------------------- 1 | import {Observable} from 'rx' 2 | const {just} = Observable 3 | 4 | import {Form} from 'components/ui/Form' 5 | import {InputControl} from 'components/sdm' 6 | 7 | const NameInput = sources => InputControl({...sources, 8 | label$: just('Name the Team'), 9 | }) 10 | 11 | const TeamForm = sources => Form({...sources, 12 | Controls$: just([{field: 'name', Control: NameInput}]), 13 | }) 14 | 15 | export {TeamForm} 16 | -------------------------------------------------------------------------------- /src/components/team/TeamIcon.js: -------------------------------------------------------------------------------- 1 | import { 2 | TeamImages, 3 | } from 'components/remote' 4 | 5 | import {icon, iconSrc} from 'helpers' 6 | 7 | const TeamIcon = sources => ({ 8 | DOM: sources.teamKey$ 9 | .flatMapLatest(TeamImages.query.one(sources)) 10 | .map(i => i && i.dataUrl && iconSrc(i.dataUrl) || icon('power')), 11 | }) 12 | 13 | export {TeamIcon} 14 | -------------------------------------------------------------------------------- /src/components/team/TeamItemNavigating.js: -------------------------------------------------------------------------------- 1 | import {ListItemNavigating} from 'components/sdm' 2 | 3 | import {TeamImages} from 'components/remote' 4 | 5 | const sparkly = require('images/pitch/sparklerHeader-2048.jpg') 6 | 7 | const TeamItemNavigating = sources => { 8 | const image$ = sources.item$.pluck('$key') 9 | .flatMapLatest(TeamImages.query.one(sources)) 10 | 11 | return ListItemNavigating({...sources, 12 | iconSrc$: image$.map(i => i && i.dataUrl || '/' + sparkly), 13 | title$: sources.item$.pluck('name'), 14 | path$: sources.path$ || sources.item$.map(({$key}) => '/team/' + $key), 15 | }) 16 | } 17 | 18 | export {TeamItemNavigating} 19 | -------------------------------------------------------------------------------- /src/components/team/TeamListNavigating.js: -------------------------------------------------------------------------------- 1 | import {Observable} from 'rx' 2 | const {just} = Observable 3 | 4 | import { 5 | ListItem, 6 | ListWithHeader, 7 | } from 'components/sdm' 8 | 9 | import {TeamItemNavigating} from './TeamItemNavigating' 10 | 11 | const TeamHeader = () => ListItem({ 12 | classes$: just({header: true}), 13 | title$: just('teams'), 14 | }) 15 | 16 | const TeamListNavigating = sources => ListWithHeader({...sources, 17 | headerDOM: TeamHeader(sources).DOM, 18 | Control$: just(TeamItemNavigating), 19 | }) 20 | 21 | export {TeamListNavigating} 22 | -------------------------------------------------------------------------------- /src/components/team/index.js: -------------------------------------------------------------------------------- 1 | export {CreateTeamListItem} from './CreateTeamListItem' 2 | export {CreateTeamHeader} from './CreateTeamHeader' 3 | export {TeamForm} from './TeamForm' 4 | export {TeamItemNavigating} from './TeamItemNavigating' 5 | export {TeamListNavigating} from './TeamListNavigating' 6 | export {TeamIcon} from './TeamIcon' 7 | export {TeamFetcher} from './TeamFetcher' 8 | 9 | export { 10 | TeamAvatar, 11 | MediumTeamAvatar, 12 | LargeTeamAvatar, 13 | } from './TeamAvatar' 14 | -------------------------------------------------------------------------------- /src/components/ui/ActionButton.js: -------------------------------------------------------------------------------- 1 | import { 2 | RaisedButton, 3 | } from 'components/sdm' 4 | 5 | const ActionButton = sources => { 6 | const b = RaisedButton(sources) 7 | return { 8 | action$: b.click$.withLatestFrom( 9 | sources.params$, 10 | (_, params) => params 11 | ), 12 | DOM: b.DOM, 13 | } 14 | } 15 | 16 | export {ActionButton} 17 | -------------------------------------------------------------------------------- /src/components/ui/DescriptionListItem.js: -------------------------------------------------------------------------------- 1 | import {Observable} from 'rx' 2 | const {just, combineLatest} = Observable 3 | 4 | import {span} from 'cycle-snabbdom' 5 | 6 | import { 7 | ListItem, 8 | } from 'components/sdm' 9 | 10 | const DescriptionListItem = sources => ListItem({...sources, 11 | title$: combineLatest( 12 | sources.title$, 13 | sources.default$ || just('Empty'), 14 | (title,def) => title || span('.secondary',def) 15 | ), 16 | classes$: just({description: true}), 17 | }) 18 | 19 | export {DescriptionListItem} 20 | -------------------------------------------------------------------------------- /src/components/ui/Form.js: -------------------------------------------------------------------------------- 1 | import {Observable} from 'rx' 2 | const {just} = Observable 3 | 4 | import combineLatestObj from 'rx-combine-latest-obj' 5 | import isolate from '@cycle/isolate' 6 | 7 | import {div} from 'helpers' 8 | // import {log} from 'util' 9 | 10 | const pluckStartValue = (item$, field) => 11 | item$ && item$.map(i => i[field]) || just(null) 12 | 13 | const reduceControlsToObject = controls => 14 | controls.reduce((a, {field,control}) => 15 | field && (a[field] = control.value$) && a || a, {} 16 | ) 17 | 18 | // const _controlSources = (field,sources) => ({...sources, 19 | // value$: (sources.value$ || just({})) 20 | // .tap(x => console.log('form value$',x)) 21 | // .pluck(field), 22 | // }) 23 | const _controlSources = (field,sources) => ({...sources, 24 | value$: (sources.value$ || 25 | sources.item$ && pluckStartValue(sources.item$, field) || 26 | just({}) 27 | ) 28 | // .tap(x => console.log('form value$',x)) 29 | .merge(pluckStartValue(sources.item$, field)), 30 | }) 31 | 32 | const Form = sources => { 33 | // sources.Controls$ is an array of components 34 | 35 | // controls$ is array of the created components (sink collections technically) 36 | const controls$ = sources.Controls$.map(Controls => 37 | Controls.map(({field,Control}) => ({ 38 | field, 39 | control: isolate(Control,field)({...sources, 40 | value$: _controlSources(field, sources).value$, 41 | // .merge(pluckStartValue(sources.item$, field)) || 42 | // pluckStartValue(sources.item$, field), 43 | // value$: sources.value$ && sources.value$ 44 | // .merge(pluckStartValue(sources.item$, field)) || 45 | // pluckStartValue(sources.item$, field), 46 | }), 47 | })) 48 | ).shareReplay(1) // keeps it from being pwnd every time 49 | 50 | // item$ gets their values$ 51 | const item$ = controls$.flatMapLatest(controls => 52 | combineLatestObj(reduceControlsToObject(controls)) 53 | ) 54 | 55 | const DOM = controls$.map(controls => 56 | div({}, controls.map(({control}) => control.DOM)) 57 | ) 58 | 59 | return { 60 | DOM, 61 | item$, 62 | } 63 | } 64 | 65 | export {Form} 66 | -------------------------------------------------------------------------------- /src/components/ui/LoginButtons.js: -------------------------------------------------------------------------------- 1 | import {Observable} from 'rx' 2 | const {just, merge, combineLatest} = Observable 3 | 4 | import {PROVIDERS} from 'util' 5 | 6 | import {RaisedButton} from 'components/sdm' 7 | 8 | import {div} from 'helpers' 9 | 10 | const LoginButtons = sources => { 11 | const goog = RaisedButton({label$: just('Login with Google'), ...sources}) 12 | const fb = RaisedButton({label$: just('Login with Facebook'), ...sources}) 13 | 14 | const auth$ = merge( 15 | goog.click$.map(PROVIDERS.google), 16 | fb.click$.map(PROVIDERS.facebook), 17 | ) 18 | 19 | return { 20 | DOM: combineLatest(goog.DOM, fb.DOM, (...doms) => div('.logins',doms)), 21 | auth$, 22 | } 23 | } 24 | 25 | export {LoginButtons} 26 | -------------------------------------------------------------------------------- /src/components/ui/MenuItemPopup.js: -------------------------------------------------------------------------------- 1 | // TODO: exterminate 2 | 3 | import {Observable} from 'rx' 4 | const {just} = Observable 5 | import isolate from '@cycle/isolate' 6 | // import {log} from 'util' 7 | 8 | import {makeModal} from 'components/ui' 9 | import menuItem from 'helpers/menuItem' 10 | 11 | const makeMenuItemPopup = ({iconName, title, className}) => sources => { 12 | const isOpen$ = sources.DOM.select('.' + className).events('click') 13 | .map(true) 14 | // .merge(modalComponent.submit$.map(false)) 15 | // .merge(modalComponent.close$.map(false)) 16 | .startWith(false) 17 | 18 | const ModalComponent = makeModal({title, icon: iconName}) 19 | const modalComponent = isolate(ModalComponent)({ 20 | isOpen$, 21 | ...sources, 22 | }) 23 | 24 | const submit$ = modalComponent.submit$ 25 | 26 | const itemDOM = just( 27 | menuItem({iconName, title, className, clickable: true}), 28 | ) 29 | 30 | const modalDOM = modalComponent.DOM 31 | 32 | return { 33 | itemDOM, 34 | modalDOM, 35 | submit$, 36 | } 37 | } 38 | 39 | const makeMenuItemFormPopup = ({ 40 | FormControl, 41 | title = 'No Title', 42 | iconName, 43 | className, 44 | }) => sources => { 45 | const ItemControl = makeMenuItemPopup({title, iconName, className}) 46 | 47 | const form = isolate(FormControl)(sources) 48 | const control = ItemControl({contentDOM$: form.DOM, ...sources}) 49 | 50 | const item$ = form.item$ 51 | 52 | return { 53 | itemDOM: control.itemDOM, 54 | modalDOM: control.modalDOM, 55 | submit$: control.submit$.share(), 56 | item$, 57 | } 58 | } 59 | 60 | export { 61 | makeMenuItemPopup, 62 | makeMenuItemFormPopup, 63 | } 64 | -------------------------------------------------------------------------------- /src/components/ui/Modal.js: -------------------------------------------------------------------------------- 1 | // TODO: exterminate 2 | 3 | import {Observable} from 'rx' 4 | const {merge} = Observable 5 | 6 | import combineLatestObj from 'rx-combine-latest-obj' 7 | import modal from 'helpers/modal' 8 | 9 | const makeModal = ({ 10 | title, 11 | iconName, 12 | submitLabel = 'OK', 13 | closeLabel = 'CANCEL', 14 | }) => 15 | sources => { 16 | const _modalRender = ({isOpen, contentDOM}) => 17 | modal({ 18 | isOpen, 19 | title, 20 | iconName, 21 | submitLabel, 22 | closeLabel, 23 | content: contentDOM, 24 | }) 25 | 26 | const submit$ = sources.DOM.select('.submit').events('click') 27 | 28 | const close$ = sources.DOM.select('.close').events('click') 29 | 30 | const isOpen$ = merge( 31 | sources.isOpen$, 32 | submit$.map(false), 33 | close$.map(false), 34 | ) 35 | 36 | const viewState = { 37 | isOpen$, 38 | contentDOM$: sources.contentDOM$, 39 | } 40 | 41 | const DOM = combineLatestObj(viewState).map(_modalRender) 42 | 43 | return { 44 | DOM, 45 | submit$, 46 | close$, 47 | } 48 | } 49 | 50 | export {makeModal} 51 | -------------------------------------------------------------------------------- /src/components/ui/QuotingListItem.js: -------------------------------------------------------------------------------- 1 | import {Observable} from 'rx' 2 | const {just, combineLatest} = Observable 3 | 4 | import { 5 | Profiles, 6 | } from 'components/remote' 7 | 8 | import { 9 | ListItem, 10 | } from 'components/sdm' 11 | 12 | import {ProfileAvatar} from 'components/profile' 13 | 14 | import {div} from 'helpers' 15 | 16 | // import {log} from 'util' 17 | 18 | const QuotingListItem = sources => { 19 | const profile$ = sources.profileKey$ 20 | .flatMapLatest(Profiles.query.one(sources)) 21 | 22 | // const src$ = profile$.map(p => p && p.portraitUrl) 23 | 24 | const li = ListItem({...sources, 25 | classes$: just({quote: true}), 26 | }) // uses title$ 27 | const liq = ListItem({...sources, 28 | leftDOM$: ProfileAvatar(sources).DOM, 29 | title$: profile$.map(p => p && p.fullName), 30 | subtitle$: sources.subtitle$ || just('Organizer'), 31 | }) 32 | 33 | const DOM = combineLatest( 34 | li.DOM, 35 | liq.DOM, 36 | (...doms) => div({},doms) 37 | ) 38 | 39 | return { 40 | DOM, 41 | } 42 | } 43 | 44 | export {QuotingListItem} 45 | -------------------------------------------------------------------------------- /src/components/ui/RoutedComponent.js: -------------------------------------------------------------------------------- 1 | import {Observable} from 'rx' 2 | const {empty} = Observable 3 | import {div} from 'helpers' 4 | // import {log} from 'util' 5 | 6 | const pluckLatest = (k,s$) => s$.pluck(k).switch() 7 | 8 | const pluckLatestOrNever = (k,s$) => 9 | s$.map(c => c[k] || empty()).switch() 10 | 11 | export const RoutedComponent = sources => { 12 | const comp$ = sources.routes$ 13 | .map(routes => sources.router.define(routes)) 14 | .switch() 15 | .distinctUntilChanged(({path}) => path) 16 | .map(({path, value}) => { 17 | const c = value({...sources, router: sources.router.path(path)}) 18 | return { 19 | ...c, 20 | DOM: c.DOM && c.DOM.startWith(div('.loading',['Loading...'])), 21 | } 22 | }) 23 | .shareReplay(1) 24 | 25 | return { 26 | pluck: key => pluckLatestOrNever(key, comp$), 27 | DOM: pluckLatest('DOM', comp$), 28 | ...['auth$', 'queue$', 'route$'].reduce((a,k) => 29 | (a[k] = pluckLatestOrNever(k,comp$)) && a, {} 30 | ), 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/components/ui/StepListItem.js: -------------------------------------------------------------------------------- 1 | import {Observable as $} from 'rx' 2 | import {div, icon} from 'helpers' 3 | import isolate from '@cycle/isolate' 4 | 5 | import { 6 | ListItemCollapsible, 7 | ListItemCollapsibleDumb, 8 | } from 'components/sdm' 9 | 10 | const StepListItem = sources => { 11 | const isOpen$ = sources.isOpen$ || $.just(false) 12 | 13 | const leftDOM$ = isOpen$.map(isOpen => 14 | div({},[ 15 | isOpen ? 16 | icon('chevron-circle-right','accent') : 17 | icon('chevron-circle-right', 'disabled'), 18 | ]) 19 | ) 20 | 21 | return isolate(ListItemCollapsible)({...sources, 22 | classes$: $.just({'list-item-title': true}), 23 | leftDOM$, 24 | // contentDOM$: $.just(div('',['wat'])), 25 | isOpen$, 26 | // classes$: sources.isDone$.map(isDone => ({disabled: isDone})), 27 | }) 28 | } 29 | 30 | const StepListItemDumb = sources => { 31 | const isOpen$ = sources.isOpen$ || $.just(false) 32 | 33 | const leftDOM$ = isOpen$.map(isOpen => 34 | div({},[ 35 | isOpen ? 36 | icon('chevron-circle-right','accent') : 37 | icon('chevron-circle-right', 'disabled'), 38 | ]) 39 | ) 40 | 41 | return isolate(ListItemCollapsibleDumb)({...sources, 42 | classes$: $.just({'list-item-title': true}), 43 | leftDOM$, 44 | // contentDOM$: $.just(div('',['wat'])), 45 | isOpen$, 46 | // classes$: sources.isDone$.map(isDone => ({disabled: isDone})), 47 | }) 48 | } 49 | 50 | export {StepListItem, StepListItemDumb} 51 | -------------------------------------------------------------------------------- /src/components/ui/SubtitleListItem.js: -------------------------------------------------------------------------------- 1 | import {Observable} from 'rx' 2 | const {just} = Observable 3 | 4 | import { 5 | ListItem, 6 | } from 'components/sdm' 7 | 8 | const SubtitleListItem = sources => ListItem({...sources, 9 | classes$: just({'list-item-subtitle': true}), 10 | }) 11 | 12 | export {SubtitleListItem} 13 | -------------------------------------------------------------------------------- /src/components/ui/TabbedPage.js: -------------------------------------------------------------------------------- 1 | import {RoutedComponent} from './RoutedComponent' 2 | import {TabBar} from 'components/TabBar' 3 | import {mergeSinks} from 'util' 4 | 5 | export const TabbedPage = sources => { 6 | const routed = RoutedComponent(sources) 7 | const tbar = TabBar({...sources, tabs: sources.tabs$}) 8 | 9 | return { 10 | DOM: routed.DOM, 11 | tabBarDOM: tbar.DOM, 12 | ...mergeSinks(routed, tbar), 13 | } 14 | } 15 | 16 | -------------------------------------------------------------------------------- /src/components/ui/TitleListItem.js: -------------------------------------------------------------------------------- 1 | import {Observable} from 'rx' 2 | const {just} = Observable 3 | 4 | import { 5 | ListItem, 6 | } from 'components/sdm' 7 | 8 | const TitleListItem = sources => ListItem({...sources, 9 | classes$: just({'list-item-title': true}), 10 | }) 11 | 12 | export {TitleListItem} 13 | -------------------------------------------------------------------------------- /src/components/ui/ToDoListItem.js: -------------------------------------------------------------------------------- 1 | import {div, icon} from 'helpers' 2 | 3 | import { 4 | ListItemNavigating, 5 | } from 'components/sdm' 6 | 7 | const ToDoListItem = sources => { 8 | const leftDOM$ = sources.isDone$.map(isDone => 9 | div({},[ 10 | isDone ? 11 | icon('check_box','disabled') : 12 | icon('chevron-circle-right', 'accent'), 13 | ]) 14 | ) 15 | 16 | return ListItemNavigating({...sources, 17 | leftDOM$, 18 | classes$: sources.isDone$.map(isDone => ({disabled: isDone})), 19 | }) 20 | } 21 | 22 | export {ToDoListItem} 23 | -------------------------------------------------------------------------------- /src/components/ui/index.js: -------------------------------------------------------------------------------- 1 | require('./styles.scss') 2 | 3 | export {RoutedComponent} from './RoutedComponent' 4 | export {TabbedPage} from './TabbedPage' 5 | 6 | export {makeModal} from './Modal' 7 | 8 | export { 9 | makeMenuItemPopup, 10 | makeMenuItemFormPopup, 11 | } from './MenuItemPopup' 12 | 13 | export {Form} from './Form' 14 | 15 | export {LoginButtons} from './LoginButtons' 16 | 17 | export {DescriptionListItem} from './DescriptionListItem' 18 | 19 | export {QuotingListItem} from './QuotingListItem' 20 | 21 | export {TitleListItem} from './TitleListItem' 22 | 23 | export {ToDoListItem} from './ToDoListItem' 24 | export {StepListItem, StepListItemDumb} from './StepListItem' 25 | 26 | export {ActionButton} from './ActionButton' 27 | export {SubtitleListItem} from './SubtitleListItem' 28 | -------------------------------------------------------------------------------- /src/components/ui/styles.scss: -------------------------------------------------------------------------------- 1 | .quote { 2 | position:relative; 3 | padding:8px; 4 | margin:12px 12px 10px 12px; 5 | border:1px solid black; 6 | color:#333; 7 | background:#fff; 8 | /* css3 */ 9 | -webkit-border-radius:10px; 10 | -moz-border-radius:10px; 11 | border-radius:10px; 12 | } 13 | 14 | .quote:before { 15 | content:""; 16 | position:absolute; 17 | bottom:-13px; /* value = - border-top-width - border-bottom-width */ 18 | left:10px; /* controls horizontal position */ 19 | border-width:14px 14px 0; 20 | border-style:solid; 21 | border-color:black transparent; 22 | /* reduce the damage in FF3.0 */ 23 | display:block; 24 | width:0; 25 | } 26 | 27 | /* creates the smaller triangle */ 28 | .quote:after { 29 | content:""; 30 | position:absolute; 31 | bottom:-12px; /* value = - border-top-width - border-bottom-width */ 32 | left:11px; /* value = (:before left) + (:before border-left) - (:after border-left) */ 33 | border-width:13px 13px 0; 34 | border-style:solid; 35 | border-color:#fff transparent; 36 | /* reduce the damage in FF3.0 */ 37 | display:block; 38 | width:0; 39 | } 40 | 41 | .list-item-title .content .title { 42 | font-size: 24px; 43 | line-height: 42px; 44 | font-weight: bold; 45 | color: #666; 46 | } 47 | 48 | .list-item-subtitle { 49 | xmargin-top: 24px; 50 | border-top: 1px solid #999; 51 | .content .title { 52 | font-size: 20px; 53 | line-height: 36px; 54 | font-weight: bold; 55 | color: #777; 56 | } 57 | } 58 | 59 | .description { 60 | color: #666; 61 | } 62 | -------------------------------------------------------------------------------- /src/drivers/bugsnag.js: -------------------------------------------------------------------------------- 1 | import Bugsnag from 'Bugsnag' 2 | 3 | const makeBugsnagDriver = options => { 4 | if (Bugsnag._u) { 5 | return function nullDriver(input$) { 6 | input$.subscribe(() => {}) 7 | return {} 8 | } 9 | } 10 | 11 | Bugsnag.releaseStage = options.releaseStage 12 | 13 | const actions = { 14 | refresh: () => Bugsnag.refresh(), 15 | user: action => { Bugsnag.user = action.user }, 16 | notify: action => { 17 | if (action.user) { 18 | Bugsnag.user = action.user 19 | } 20 | if (action.metaData) { 21 | Bugsnag.metaData = action.metaData 22 | } 23 | 24 | Bugsnag.notify(action.error) 25 | }, 26 | } 27 | 28 | return function bugsnagDriver(input$) { 29 | input$.subscribe(payload => { 30 | if (payload.action) { 31 | actions[payload.action](payload) 32 | } else { 33 | actions.notify({error: payload}) 34 | } 35 | }) 36 | 37 | return Bugsnag 38 | } 39 | } 40 | 41 | export default makeBugsnagDriver 42 | -------------------------------------------------------------------------------- /src/drivers/isMobile.js: -------------------------------------------------------------------------------- 1 | import {Observable} from 'rx' 2 | import {events} from 'snabbdom-material' 3 | 4 | const isMobile$ = () => { 5 | let screenInfo$ = 6 | Observable.create(obs => { 7 | events.responsive.addListener(screenInfo => { 8 | obs.onNext(screenInfo) 9 | }) 10 | }).map(si => si.size <= 2).replay(null, 1) 11 | 12 | const disposable = screenInfo$.connect() 13 | 14 | screenInfo$.dispose = () => disposable.dispose() 15 | return screenInfo$ 16 | } 17 | 18 | export {isMobile$} 19 | -------------------------------------------------------------------------------- /src/helpers/buttons/index.js: -------------------------------------------------------------------------------- 1 | import {span} from 'cycle-snabbdom' 2 | import {Dialog} from 'snabbdom-material' 3 | 4 | const Button = Dialog.Button 5 | 6 | require('./styles.scss') 7 | 8 | export const submitAndCancel = (submitLabel, cancelLabel) => 9 | span({},[ 10 | Button({onClick: true, primary: true, className: 'submit'},[submitLabel]), 11 | Button({onClick: true, flat: true, className: 'cancel'},[cancelLabel]), 12 | ]) 13 | 14 | export const centeredSignup = () => 15 | span({class: {signup: true}},[ 16 | Button({onClick: true, primary: true, className: 'facebook'}, 17 | ['Sign up with Facebook'] 18 | ), 19 | Button({onClick: true, primary: true, className: 'google'}, 20 | ['Sign up with Google'] 21 | ), 22 | ]) 23 | 24 | export const bigButton = (label, className) => 25 | Button({onClick: true, primary: true, className},[label]) 26 | -------------------------------------------------------------------------------- /src/helpers/buttons/styles.scss: -------------------------------------------------------------------------------- 1 | .signup { 2 | margin: auto; // nope 3 | } -------------------------------------------------------------------------------- /src/helpers/fabMenu.js: -------------------------------------------------------------------------------- 1 | import {div} from 'cycle-snabbdom' 2 | import {Appbar, Menu} from 'snabbdom-material' 3 | const {Item, Separator} = Menu 4 | import {icon} from './index' 5 | import {menu} from './menu' 6 | 7 | // the goal of these kinds of functions 8 | // is to abstract away the details of dom-specific representation 9 | // and express that as reusable interface metaphors 10 | 11 | const _menuItems = items => 12 | items.map(({className, label, divider}) => 13 | divider ? Separator({}) : Item({className}, label) 14 | ) 15 | 16 | // there shouldn't be anything passed to this that is DOM-specific 17 | // exception for className: 18 | // because its used to select event streams from DOM driver 19 | // conceptually its something like 'actionTag' 20 | // but there is no purpose in deliberately obfuscating that :) 21 | export default ({isOpen, className, iconName, menu: {rightAlign}, items}) => 22 | div({},[ 23 | Appbar.Button({className}, [icon(iconName)]), 24 | menu({isOpen, rightAlign}, _menuItems(items)), 25 | ]) 26 | -------------------------------------------------------------------------------- /src/helpers/frame.js: -------------------------------------------------------------------------------- 1 | import {div} from 'cycle-snabbdom' 2 | import EnvBanner from 'components/EnvBanner' 3 | 4 | export const mobileFrame = ({sideNav, appBar, header, page}) => 5 | div({style: {display: 'block'}}, [ 6 | sideNav, 7 | appBar, 8 | div({style: {flex: '1 1 100%'}},[ 9 | header, 10 | div({}, [page]), 11 | ]), 12 | EnvBanner(), 13 | ]) 14 | 15 | const withSidenav = (sideNav, header, page) => 16 | div({style: {display: 'flex', flex: '1 1 100%'}}, [ 17 | sideNav ? div({style: {width: '300px'}}, [sideNav]) : null, 18 | div({style: {flex: '1 1 auto', display: 'flex', flexFlow: 'column'}}, [ 19 | header, 20 | div('.fullpage', [page]), 21 | ]), 22 | ]) 23 | 24 | const noSidenav = (header, page) => 25 | div({style: {display: 'flex', flex: '1 1 100%'}}, [ 26 | div({style: {flex: '1 1 auto'}},['']), 27 | div({style: {flex: '1 1 800px', display: 'flex', flexFlow: 'column'}}, [ 28 | header, 29 | div('.fullpage', [page]), 30 | ]), 31 | div({style: {flex: '1 1 auto'}},['']), 32 | ]) 33 | // div({style: navlessStyle}, [ 34 | // div({style: { 35 | // flex: '1 1 100%', 36 | // display: 'flex', 37 | // flexFlow: 'column', 38 | // alignItems: 'stretch' 39 | // }}, [ 40 | // header, 41 | // div('.fullpage', [page]), 42 | // ]) 43 | 44 | export const desktopFrame = ({sideNav, appBar, header, page}) => 45 | div({}, [ 46 | appBar, 47 | sideNav ? withSidenav(sideNav,header,page) : noSidenav(header,page), 48 | EnvBanner(), 49 | ]) 50 | -------------------------------------------------------------------------------- /src/helpers/layout.js: -------------------------------------------------------------------------------- 1 | import {div, h} from 'cycle-snabbdom' 2 | 3 | export const row = (style, ...els) => 4 | div({style: {display: 'flex', ...style}}, els) 5 | 6 | export const cell = (style, ...els) => 7 | div({style: {flex: '1', ...style}}, els) 8 | 9 | export const cellC = (_class, ...els) => 10 | div({style: {flex: '1'}, class: _class}, els) 11 | 12 | export const icon = (name, className) => 13 | h(`i.icon-${name}.${className}`,[]) 14 | -------------------------------------------------------------------------------- /src/helpers/listHeader.js: -------------------------------------------------------------------------------- 1 | import {h} from 'cycle-snabbdom' 2 | import {Col} from 'snabbdom-material' 3 | import {icon} from 'helpers' 4 | 5 | const style = clickable => ({ 6 | 'line-height': '64px', 7 | backgroundColor: '#666', 8 | color: '#FFF', 9 | textTransform: 'uppercase', 10 | fontSize: '1.4em', 11 | fontWeight: 'bold', 12 | display: 'block', 13 | cursor: clickable ? 'pointer' : '', 14 | }) 15 | 16 | export default ({title, className, iconName, clickable}) => 17 | h('div.row.' + className, {style: style(clickable)}, [ 18 | Col({type: 'xs-10'},[title]), 19 | iconName ? 20 | Col( 21 | {type: 'xs-1', style: {width: '48px', 'font-size': '32px'}}, 22 | [icon(iconName, 'white')] 23 | ) : null, 24 | ]) 25 | // h('div.row', {}, [h('div.row.' + className, {style}, [ 26 | // Col({type: 'xs-10'},[title]), 27 | // iconName ? 28 | // Col( 29 | // {type: 'xs-1', style: {width: '48px', 'font-size': '32px'}}, 30 | // [icon(iconName, 'white')] 31 | // ) : null, 32 | // ])]) 33 | // h('div.row.' + className, {style}, [ 34 | // Col({type: 'xs-10'},[title]), 35 | // iconName ? 36 | // Col( 37 | // {type: 'xs-1', style: {width: '48px', 'font-size': '32px'}}, 38 | // [icon(iconName, 'white')] 39 | // ) : null, 40 | // ]) 41 | -------------------------------------------------------------------------------- /src/helpers/listItemDisabled.js: -------------------------------------------------------------------------------- 1 | import listItem from 'helpers/listItem' 2 | 3 | export default params => listItem({ 4 | subtitle: 'Coming Soon!', 5 | ...params, 6 | }) 7 | -------------------------------------------------------------------------------- /src/helpers/menu.js: -------------------------------------------------------------------------------- 1 | import {Mask, getScreenSize} from 'snabbdom-material' 2 | import {div} from 'cycle-snabbdom' 3 | 4 | const insert = (vnode) => { 5 | const {height: screenHeight} = getScreenSize() 6 | const {top, bottom} = vnode.elm.getBoundingClientRect() 7 | const originalHeight = bottom - top 8 | const minHeight = 32 * 8 + 20 9 | 10 | let offsetTop = top < 6 ? Math.ceil((top - 16) / -32) * 32 : 0 11 | const offsetBottom = bottom > screenHeight - 6 ? 12 | Math.ceil((bottom - screenHeight + 16) / 32) * 32 : 0 13 | let height = bottom - top - offsetTop - offsetBottom 14 | if (height < minHeight) { 15 | height = minHeight > originalHeight ? originalHeight : minHeight 16 | if (top + offsetTop + height + 16 > screenHeight) { 17 | offsetTop -= top + offsetTop + height + 16 - screenHeight 18 | } 19 | } 20 | vnode.elm.style.top = `${vnode.elm.offsetTop + offsetTop}px` 21 | vnode.elm.style.height = `${height}px` 22 | vnode.elm.scrollTop += offsetTop 23 | } 24 | 25 | const containerStyle = { 26 | zIndex: '1000', 27 | position: 'relative', 28 | height: '0', 29 | overflow: 'visible', 30 | } 31 | 32 | const menuStyle = { 33 | zIndex: '1001', 34 | padding: '10px 0', 35 | backgroundColor: '#fff', 36 | color: '#000', 37 | position: 'absolute', 38 | overflowY: 'auto', 39 | scrollbar: 'width: 4px', 40 | top: '-8px', 41 | opacity: '0', 42 | transition: `opacity 0.3s`, 43 | delayed: { 44 | opacity: '1', 45 | }, 46 | remove: { 47 | opacity: '0', 48 | }, 49 | minWidth: '200px', 50 | maxWidth: '400px', 51 | } 52 | 53 | function menu(config, children) { 54 | const {isOpen, rightAlign, style: styles = {}} = config 55 | 56 | const style = Object.assign( 57 | menuStyle, 58 | styles, 59 | rightAlign ? {right: '0', left: 'auto'} : {left: '0', right: 'auto'} 60 | ) 61 | 62 | return div('.app-menu', {style: containerStyle}, [ 63 | Mask({className: 'close-menu', dark: false, isOpen}), 64 | isOpen ? div('.paper1', {hook: {insert}, style}, children) : null, 65 | ]) 66 | } 67 | 68 | export {menu} 69 | -------------------------------------------------------------------------------- /src/helpers/menuItem.js: -------------------------------------------------------------------------------- 1 | import {h,div} from 'cycle-snabbdom' 2 | import {Col} from 'snabbdom-material' 3 | import {icon} from 'helpers' 4 | 5 | // import {Menu} from 'snabbdom-material' 6 | // const Item = Menu.Item 7 | 8 | // import 'helpers/listItem/styles.scss' 9 | 10 | const fadeInOut = { 11 | opacity: 0, 12 | transition: 'opacity 100', 13 | delayed: { 14 | opacity: 1, 15 | }, 16 | remove: { 17 | opacity: 0, 18 | }, 19 | } 20 | 21 | const style = { 22 | lineHeight: '48px', 23 | // lineHeight: '64px', 24 | cursor: 'pointer', 25 | margin: '0', 26 | ...fadeInOut, 27 | } 28 | 29 | const iconCellStyle = { 30 | width: '48px', 31 | 'font-size': '24px', 32 | } 33 | 34 | const titleStyle = { 35 | fontSize: '1.3em', 36 | } 37 | 38 | const subtitleStyle = { 39 | color: '#666', 40 | } 41 | 42 | export default ({ 43 | iconName, title, subtitle, className, link, key, iconBackgroundColor, 44 | }) => 45 | // h('div.row.list-item.clickable.' + className, { 46 | h('div.row.list-item.' + className, { 47 | style, attrs: {'data-link': link, 'data-key': key}, 48 | }, [ 49 | iconName ? 50 | Col( 51 | {type: 'xs-1', style: iconCellStyle}, 52 | [icon(iconName, 'black', iconBackgroundColor)] 53 | ) : null, 54 | Col({type: 'xs-8'},[ 55 | div({style: titleStyle},[title]), 56 | div({style: subtitleStyle},[subtitle]), 57 | ]), 58 | ]) 59 | -------------------------------------------------------------------------------- /src/helpers/modal.js: -------------------------------------------------------------------------------- 1 | import {div, span} from 'cycle-snabbdom' 2 | import {Col} from 'snabbdom-material' 3 | import {icon} from 'helpers' 4 | 5 | import {Dialog} from 'snabbdom-material' 6 | 7 | const dialogStyle = { 8 | minWidth: '400px', 9 | } 10 | 11 | const titleStyle = { 12 | color: '#FFF', 13 | backgroundColor: '#F00', 14 | lineHeight: '64px', 15 | height: '64px', 16 | } 17 | 18 | const contentStyle = { 19 | padding: '0em 1em 1em 1em', 20 | } 21 | 22 | const titleRow = (iconName, title) => 23 | div({style: titleStyle}, [ 24 | Col( 25 | {type: 'xs-1', style: {width: '48px', 'font-size': '32px'}}, 26 | [icon(iconName)] 27 | ), 28 | Col({type: 'xs-8'},[title]), 29 | ]) 30 | 31 | const modal = ({title, iconName, content, submitLabel, closeLabel}) => 32 | Dialog({ 33 | isOpen: true, 34 | noPadding: true, 35 | style: dialogStyle, 36 | title: titleRow(iconName, title), 37 | footer: span({},[ 38 | Dialog.Button( 39 | {onClick: true, primary: true, className: 'submit'},[submitLabel] 40 | ), 41 | Dialog.Button( 42 | {onClick: true, flat: true, className: 'close'},[closeLabel] 43 | ), 44 | ]), 45 | },[ 46 | div({style: contentStyle}, [content]), 47 | ]) 48 | 49 | // div({},[ 50 | // Mask({isOpen: true, material, className: 'close'}), 51 | // dialog(props), 52 | // ]) 53 | 54 | export default ({isOpen, ...props}) => 55 | isOpen && modal(props) || div({},[]) 56 | 57 | -------------------------------------------------------------------------------- /src/helpers/projectForm.js: -------------------------------------------------------------------------------- 1 | import {div} from 'cycle-snabbdom' 2 | import {Form,Input,Button} from 'snabbdom-material' 3 | 4 | const projectForm = ({name}) => 5 | Form({className: 'project'}, [ 6 | Input({ 7 | className: 'name', 8 | label: 'New Project Name', 9 | value: name, 10 | }), 11 | // need onClick: true or snabbdom-material renders as disabled :/ 12 | name ? div({}, [ 13 | Button({className: 'submit', onClick: true, primary: true},['Create']), 14 | Button( 15 | {className: 'cancel', onClick: true, secondary: true, flat: true}, 16 | ['Cancel'] 17 | ), 18 | ]) : null, 19 | ]) 20 | 21 | export {projectForm} 22 | -------------------------------------------------------------------------------- /src/helpers/quickNavMenu.js: -------------------------------------------------------------------------------- 1 | import {h, div} from 'cycle-snabbdom' 2 | import {Menu, Button} from 'snabbdom-material' 3 | const {Separator} = Menu 4 | import {menu} from './menu' 5 | 6 | import menuItem from 'helpers/menuItem' 7 | 8 | // the goal of these kinds of functions 9 | // is to abstract away the details of dom-specific representation 10 | // and express that as reusable interface metaphors 11 | 12 | const svgDropDownIcon = color => 13 | h('svg', { 14 | attrs: { 15 | fill: color, 16 | height: 16, 17 | viewBox: '0 0 16 16', 18 | width: 16, 19 | }, 20 | }, [ 21 | h('path', {attrs: {d: 'M7 10l5 5 5-5z'}}), 22 | h('path', {attrs: {d: 'M0 0h24v24H0z', fill: 'none'}}), 23 | ]) 24 | 25 | const _menuItems = items => 26 | items.map(({className, label, key, link, divider}) => 27 | divider ? Separator({}) : menuItem({className, title: label, key, link}) 28 | ) 29 | // items.map(({className, label, divider}) => 30 | // divider ? Separator({}) : Item({className}, label) 31 | // ) 32 | 33 | export default ({ 34 | isOpen, 35 | className, 36 | label, 37 | menu: {rightAlign}, 38 | items, 39 | color = '#FFF', 40 | }) => 41 | div({},[ 42 | Button( 43 | {className, flat: true, onClick: true, 44 | style: {color, margin: 0, paddingLeft: '0.5em'}, 45 | }, 46 | [label, svgDropDownIcon(color)] 47 | ), 48 | menu({isOpen, rightAlign}, _menuItems(items)), 49 | ]) 50 | -------------------------------------------------------------------------------- /src/helpers/sideNav.js: -------------------------------------------------------------------------------- 1 | import {div, span} from 'cycle-snabbdom' 2 | import {Mask} from 'snabbdom-material' 3 | import {material} from 'util' 4 | 5 | const defaultStyles = { 6 | zIndex: '1001', 7 | position: 'fixed', 8 | top: '0', 9 | bottom: '0', 10 | overflow: 'auto', 11 | } 12 | 13 | function renderSideNav(config, children) { 14 | const {className = '', style: userStyle = {}} = config 15 | const classes = ['sidenav', 'paper2', className].filter(Boolean) 16 | const style = Object.assign(defaultStyles, userStyle, material.sidenav) 17 | return div({},[ 18 | Mask({isOpen: true, material, className: 'close-sideNav'}), 19 | div(`.${classes.join('.')}`, {style}, [ 20 | span({}, children), 21 | ]), 22 | ]) 23 | } 24 | 25 | export function sideNav({isMobile, isOpen, content}) { 26 | if (isMobile && isOpen) { 27 | return renderSideNav({}, [content]) 28 | } 29 | return isMobile ? span({}, []) : div({}, [content]) 30 | } 31 | -------------------------------------------------------------------------------- /src/helpers/tabs/index.js: -------------------------------------------------------------------------------- 1 | // import {div,h} from 'cycle-snabbdom' 2 | 3 | // import {material} from 'util' 4 | 5 | // import './styles.scss' 6 | 7 | // const tabs = (props,children) => 8 | // children && div({class: {'tab-wrap': true}, style: { 9 | // // 'background-color': material.primaryColor, 10 | // }}, 11 | // children.reduce((a,b) => a.concat(b)) 12 | // .concat([div({class: {slide: true}},'')]) 13 | // ) 14 | 15 | // const tab = ({id, link},children) => [ 16 | // h('input',{attrs: {type: 'radio', name: 'tabs', id}}), 17 | // div({class: {'tab-label-content': true}, attrs: {'data-link': link}},[ 18 | // h('label',{attrs: {for: id}, style: { 19 | // color: material.primaryFontColor}, 20 | // },children), 21 | // ]), 22 | // ] 23 | 24 | // export default tabs 25 | // export {tab} 26 | -------------------------------------------------------------------------------- /src/helpers/text/index.js: -------------------------------------------------------------------------------- 1 | import {div} from 'cycle-snabbdom' 2 | 3 | require('./styles.scss') 4 | 5 | export const textTweetSized = text => 6 | div({class: {text: true, tweetSized: true}}, [text]) 7 | // div({className: 'tweetSize'}, [text]) 8 | 9 | export const textQuote = text => 10 | div({class: {text: true, quote: true}}, [text]) 11 | 12 | // export const quoteText = text => 13 | // div({class: {text: true, quote: true}}, [text]) 14 | -------------------------------------------------------------------------------- /src/helpers/text/styles.scss: -------------------------------------------------------------------------------- 1 | .text { 2 | padding: 18px; 3 | margin: 0px 0px; 4 | 5 | &.tweetSized { 6 | font-size: 20px; 7 | line-height: 36px; 8 | } 9 | &.quote { 10 | font-size: 18px; 11 | margin: 18px 0px; 12 | color: #333; 13 | border-radius: 16px; 14 | border: 1px solid #AAA; 15 | } 16 | } 17 | 18 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | import {run} from '@cycle/core' 2 | 3 | // drivers 4 | import {makeDOMDriver} from 'cycle-snabbdom' 5 | import {makeRouterDriver, supportsHistory} from 'cyclic-router' 6 | import {createHistory, createHashHistory} from 'history' 7 | import Firebase from 'firebase' 8 | import {makeAuthDriver, makeFirebaseDriver, makeQueueDriver} from 'cyclic-fire' 9 | import {isMobile$} from 'drivers/isMobile' 10 | import makeBugsnagDriver from 'drivers/bugsnag' 11 | 12 | // app root function 13 | import Root from './root' 14 | 15 | const history = supportsHistory() ? 16 | createHistory() : createHashHistory() 17 | 18 | const fbRoot = new Firebase(__FIREBASE_HOST__) // eslint-disable-line 19 | 20 | const {sources, sinks} = run(Root, { 21 | isMobile$, 22 | DOM: makeDOMDriver('#root'), 23 | router: makeRouterDriver(history), 24 | firebase: makeFirebaseDriver(fbRoot), 25 | auth$: makeAuthDriver(fbRoot), 26 | queue$: makeQueueDriver(fbRoot.child('!queue')), 27 | bugsnag: makeBugsnagDriver({ 28 | releaseStage: process.env.BUILD_ENV || 'development', 29 | }), 30 | }) 31 | 32 | if (module.hot) { 33 | module.hot.accept() 34 | 35 | module.hot.dispose(() => { 36 | sinks.dispose() 37 | sources.dispose() 38 | }) 39 | } 40 | -------------------------------------------------------------------------------- /src/root/Admin/Profiles.js: -------------------------------------------------------------------------------- 1 | import ComingSoon from 'components/ComingSoon' 2 | 3 | export default ComingSoon('Admin/Dash') 4 | -------------------------------------------------------------------------------- /src/root/Admin/Projects.js: -------------------------------------------------------------------------------- 1 | import {Observable} from 'rx' 2 | const {just} = Observable 3 | 4 | import combineLatestObj from 'rx-combine-latest-obj' 5 | 6 | import {div} from 'cycle-snabbdom' 7 | 8 | import {Projects} from 'components/remote' 9 | import {List} from 'components/sdm' 10 | import {ProjectItem, ProjectForm} from 'components/project' 11 | 12 | // import {log} from 'util' 13 | 14 | export default sources => { 15 | const projects$ = Projects.query.all(sources)() 16 | 17 | const projectForm = ProjectForm(sources) 18 | const projectList = List({...sources, 19 | Control$: just(ProjectItem), 20 | rows$: projects$, 21 | }) 22 | 23 | const queue$ = projectForm.project$ 24 | .map(Projects.action.create) 25 | 26 | const route$ = Observable.merge( 27 | projectList.route$, 28 | Projects.redirect.create(sources).route$, 29 | ) 30 | 31 | const viewState = { 32 | listDOM$: projectList.DOM, 33 | formDOM$: projectForm.DOM, 34 | } 35 | 36 | const DOM = combineLatestObj(viewState) 37 | .map(({listDOM, formDOM}) => div({},[formDOM, listDOM])) 38 | 39 | return { 40 | DOM, 41 | queue$, 42 | route$, 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/root/Admin/index.js: -------------------------------------------------------------------------------- 1 | import {Observable} from 'rx' 2 | const {of} = Observable 3 | // import combineLatestObj from 'rx-combine-latest-obj' 4 | 5 | import AppFrame from 'components/AppFrame' 6 | import Title from 'components/Title' 7 | import Header from 'components/Header' 8 | 9 | import {mergeOrFlatMapLatest} from 'util' 10 | 11 | import ComingSoon from 'components/ComingSoon' 12 | 13 | import Projects from './Projects.js' 14 | 15 | import {TabbedPage} from 'components/ui' 16 | 17 | const _Nav = sources => ({ 18 | DOM: sources.isMobile$.map(m => m ? null : sources.titleDOM), 19 | }) 20 | 21 | const _Title = sources => Title({...sources, 22 | labelText$: of('Administration'), 23 | subLabelText$: of('At a Glance'), // eventually page$.something 24 | }) 25 | 26 | const _Page = sources => TabbedPage({...sources, 27 | tabs$: of([ 28 | {path: '/', label: 'Projects'}, 29 | {path: '/profiles', label: 'Profiles'}, 30 | {path: '/previously', label: 'Previously'}, 31 | {path: '/test', label: 'Test'}, 32 | ]), 33 | routes$: of({ 34 | '/': Projects, 35 | '/profiles': ComingSoon('Admin/Dash'), 36 | '/previously': ComingSoon('Admin/Previously'), 37 | '/test': ComingSoon('Admin/Test'), 38 | }), 39 | }) 40 | 41 | export default sources => { 42 | const page = _Page(sources) 43 | const title = _Title({...sources, tabsDOM$: page.tabBarDOM}) 44 | const nav = _Nav({...sources, titleDOM: title.DOM}) 45 | const header = Header({...sources, 46 | titleDOM: title.DOM, 47 | tabsDOM: page.tabBarDOM, 48 | }) 49 | 50 | const appFrame = AppFrame({ 51 | navDOM: nav.DOM, 52 | headerDOM: header.DOM, 53 | pageDOM: page.DOM, 54 | ...sources, 55 | }) 56 | 57 | const children = [appFrame, page, title, nav, header] 58 | 59 | const route$ = Observable.merge( 60 | mergeOrFlatMapLatest('route$', ...children), 61 | sources.redirectLogout$, 62 | ) 63 | 64 | return { 65 | DOM: appFrame.DOM, 66 | auth$: mergeOrFlatMapLatest('auth$', ...children), 67 | queue$: mergeOrFlatMapLatest('queue$', ...children), 68 | route$, 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/root/Apply/Overview.js: -------------------------------------------------------------------------------- 1 | import {Observable} from 'rx' 2 | const {just, combineLatest} = Observable 3 | 4 | import { 5 | List, 6 | ListItemNavigating, 7 | } from 'components/sdm' 8 | 9 | import { 10 | TitleListItem, 11 | } from 'components/ui' 12 | 13 | import {div, icon} from 'helpers' 14 | 15 | const _Title = sources => TitleListItem({...sources, 16 | title$: just('Check out these Opportunities!'), 17 | }) 18 | 19 | const _Item = sources => ListItemNavigating({...sources, 20 | title$: sources.item$.pluck('name'), 21 | subtitle$: sources.item$.pluck('description'), 22 | leftDOM$: just(icon('power','accent')), 23 | path$: sources.item$.pluck('$key') 24 | .map(k => '/opp/' + k) 25 | .map(sources.router.createHref), 26 | }) 27 | 28 | const _List = sources => List({...sources, 29 | rows$: sources.opps$, 30 | Control$: just(_Item), 31 | }) 32 | 33 | export default sources => { 34 | const t = _Title(sources) 35 | const l = _List(sources) 36 | const childs = [t,l] 37 | 38 | return { 39 | DOM: combineLatest(childs.map(c => c.DOM), (...doms) => div({},doms)), 40 | route$: l.route$.share(), 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/root/Apply/index.js: -------------------------------------------------------------------------------- 1 | import {Observable} from 'rx' 2 | const {of} = Observable 3 | 4 | import isolate from '@cycle/isolate' 5 | 6 | import SoloFrame from 'components/SoloFrame' 7 | import {ResponsiveTitle} from 'components/Title' 8 | 9 | import Opp from './Opp' 10 | import Overview from './Overview' 11 | 12 | import { 13 | DescriptionListItem, 14 | RoutedComponent, 15 | } from 'components/ui' 16 | 17 | import { 18 | Opps, 19 | ProjectImages, 20 | Projects, 21 | } from 'components/remote' 22 | 23 | // import {log} from 'util' 24 | import {combineLatestToDiv, mergeSinks} from 'util' 25 | 26 | const _Fetch = sources => { 27 | const project$ = sources.projectKey$ 28 | .flatMapLatest(Projects.query.one(sources)) 29 | 30 | const projectImage$ = sources.projectKey$ 31 | .flatMapLatest(ProjectImages.query.one(sources)) 32 | 33 | const opps$ = sources.projectKey$ 34 | .flatMapLatest(Opps.query.byProject(sources)) 35 | .map(opps => opps.filter(({isPublic}) => isPublic)) 36 | 37 | return { 38 | project$, 39 | projectImage$, 40 | opps$, 41 | } 42 | } 43 | 44 | const _Title = sources => ResponsiveTitle({...sources, 45 | titleDOM$: sources.project$.pluck('name'), 46 | subtitleDOM$: sources.opps$.map(o => o.length + ' Opportunities Available'), 47 | backgroundUrl$: sources.projectImage$.map(pi => pi && pi.dataUrl), 48 | }) 49 | 50 | const _Description = sources => DescriptionListItem({...sources, 51 | title$: sources.project$.pluck('description'), 52 | }) 53 | 54 | const _Page = sources => RoutedComponent({...sources, routes$: of({ 55 | '/': Overview, 56 | '/opp/:key': key => _sources => 57 | isolate(Opp)({oppKey$: Observable.just(key), ..._sources}), 58 | })}) 59 | 60 | export default sources => { 61 | const _sources = {...sources, ..._Fetch(sources)} 62 | 63 | const title = _Title(_sources) 64 | const desc = _Description(_sources) 65 | const page = _Page(_sources) 66 | 67 | const frame = SoloFrame({...sources, 68 | headerDOM: title.DOM, 69 | pageDOM: combineLatestToDiv(desc.DOM, page.DOM), 70 | }) 71 | 72 | return { 73 | DOM: frame.DOM, 74 | ...mergeSinks(frame, page), 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/root/Dash/index.js: -------------------------------------------------------------------------------- 1 | import {Observable} from 'rx' 2 | const {of} = Observable 3 | 4 | import AppFrame from 'components/AppFrame' 5 | import Header from 'components/Header' 6 | 7 | import {mergeSinks} from 'util' 8 | 9 | // import {ResponsiveTitle} from 'components/Title' 10 | // import {MediumProfileAvatar} from 'components/profile' 11 | 12 | import { 13 | TabbedPage, 14 | } from 'components/ui' 15 | 16 | import { 17 | LogoutRedirector, 18 | } from 'components/redirects' 19 | 20 | import Doing from './Doing' 21 | import Being from './Being' 22 | 23 | // import {ProfileSidenav} from 'components/profile' 24 | 25 | const _Page = sources => TabbedPage({...sources, 26 | tabs$: of([ 27 | {path: '/', label: 'Doing'}, 28 | {path: '/being', label: 'Being'}, 29 | ]), 30 | routes$: of({ 31 | '/': Doing, 32 | '/being': Being, 33 | }), 34 | }) 35 | 36 | export default sources => { 37 | const page = _Page(sources) 38 | // const title = _Title({...sources, tabsDOM$: page.tabBarDOM}) 39 | const header = Header({...sources, 40 | // titleDOM: of(null), 41 | tabsDOM: page.tabBarDOM, 42 | }) 43 | 44 | // const nav = ProfileSidenav(sources) 45 | 46 | const frame = AppFrame({...sources, 47 | // navDOM: nav.DOM, 48 | navDOM: sources.navDOM$, 49 | headerDOM: header.DOM, 50 | pageDOM: page.DOM, 51 | }) 52 | 53 | const redirect = LogoutRedirector(sources) 54 | 55 | return { 56 | DOM: frame.DOM, 57 | // ...mergeSinks(frame, page, nav, header, redirect), 58 | ...mergeSinks(frame, page, header, redirect), 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/root/Engagement/Application/Step1.js: -------------------------------------------------------------------------------- 1 | import {Observable as $} from 'rx' 2 | 3 | import { 4 | StepListItem, 5 | } from 'components/ui' 6 | 7 | import AnswerQuestion from './AnswerQuestion' 8 | 9 | export const Step1 = sources => { 10 | const aq = AnswerQuestion(sources) 11 | 12 | const li = StepListItem({...sources, 13 | title$: $.just('Step 1: Answer the Question'), 14 | contentDOM$: aq.DOM, 15 | isOpen$: sources.engagement$.map(({answer}) => !answer), 16 | }) 17 | 18 | return { 19 | ...li, 20 | queue$: aq.queue$, 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/root/Engagement/Application/Step2.js: -------------------------------------------------------------------------------- 1 | import {Observable as $} from 'rx' 2 | 3 | import { 4 | StepListItem, 5 | } from 'components/ui' 6 | 7 | import ChooseTeams from './ChooseTeams' 8 | 9 | export const Step2 = sources => { 10 | const pt = ChooseTeams(sources) 11 | 12 | const li = StepListItem({...sources, 13 | title$: $.just('Step 2: Pick Some Teams'), 14 | contentDOM$: pt.DOM, 15 | isOpen$: sources.engagement$.map(({answer}) => !!answer), 16 | }) 17 | 18 | return { 19 | ...li, 20 | queue$: pt.queue$, 21 | route$: pt.route$, 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/root/Engagement/Confirmation/Accountability.js: -------------------------------------------------------------------------------- 1 | import {Observable as $} from 'rx' 2 | import {combineDOMsToDiv} from 'util' 3 | import isolate from '@cycle/isolate' 4 | import {h, div} from 'cycle-snabbdom' 5 | import {startValue} from 'util' 6 | 7 | import { 8 | ListItemNewTarget, 9 | ListItemCheckbox, 10 | } from 'components/sdm' 11 | 12 | const formatAmount = s => 13 | '$' + s.toFixed(2) 14 | 15 | const Agree = sources => startValue(ListItemCheckbox,false)({...sources, 16 | title$: $.just('I Promise!'), 17 | classes$: $.just({total: true}), 18 | subtitle$: $.just(` 19 | I agree to let Sparks.Network charge my card or Paypal account 20 | on behalf of the organizer 21 | if I do not complete my shifts or cancel my commitment before the event. 22 | I understand that Sparks.Network will arbitrate any disputes I have with 23 | the organizer and agree to abide by their decision. 24 | `), 25 | rightDOM$: sources.amountDeposit$.map(amount => 26 | div('.money', [formatAmount(amount)]) 27 | ), 28 | }) 29 | 30 | const Title = sources => ListItemNewTarget({...sources, 31 | title$: $.just('Accountability Amount'), 32 | classes$: $.just({'list-item-subtitle': true}), 33 | subtitle$: $.just(div({},[ 34 | `You are NOT paying this right now!`, 35 | h('br'), 36 | h('a', 37 | 'Learn how our accountability system lets you secure your spot' 38 | ), 39 | ])), 40 | url$: $.just('http://blog.sparks.network/p/refunds-and-deposit-returns.html'), 41 | }) 42 | 43 | export default sources => { 44 | const t = Title(sources) 45 | const agree = isolate(Agree)(sources) 46 | 47 | return { 48 | DOM: combineDOMsToDiv('',t,agree), 49 | isAgreed$: agree.value$, 50 | } 51 | } 52 | 53 | -------------------------------------------------------------------------------- /src/root/Engagement/Confirmation/Step1.js: -------------------------------------------------------------------------------- 1 | import { 2 | StepListItem, 3 | } from 'components/ui' 4 | 5 | import ChooseShifts from '../Schedule/Priority' 6 | 7 | export default sources => { 8 | const content = ChooseShifts(sources) 9 | 10 | const li = StepListItem({...sources, 11 | title$: sources.neededAssignments$.map(n => n > 0 ? 12 | `Step 1: Choose ${n} More Preferred Shifts` : 13 | `Step 1: Preferred Shifts Selected` 14 | ), 15 | // title$: $.just('Step 1: Choose Your Shifts'), 16 | contentDOM$: content.DOM, 17 | isOpen$: sources.engagement$.map(({isAssigned}) => !isAssigned), 18 | }) 19 | 20 | return { 21 | ...li, 22 | queue$: content.queue$, 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/root/Engagement/Confirmation/index.js: -------------------------------------------------------------------------------- 1 | import {Observable as $} from 'rx' 2 | // const {just, merge, combineLatest} = Observable 3 | 4 | // import {log} from 'util' 5 | 6 | import {combineDOMsToDiv} from 'util' 7 | import {icon} from 'helpers' 8 | 9 | import { 10 | TitleListItem, 11 | } from 'components/ui' 12 | 13 | import { 14 | LargeCard, 15 | ListItemNavigating, 16 | } from 'components/sdm' 17 | 18 | import Step1 from './Step1' 19 | import Step2 from './Step2' 20 | 21 | const _Title = sources => TitleListItem({...sources, 22 | title$: sources.isConfirmed$.map(isConfirmed => 23 | isConfirmed ? 'You Are Confirmed' : 'Confirm Your Spot' 24 | ), 25 | }) 26 | 27 | const _AllDone = sources => ListItemNavigating({...sources, 28 | title$: $.just('You\'re confirmed!'), 29 | subtitle$: $.just('We will send you a message when the event is coming up.'), 30 | leftDOM$: $.of(icon('chevron-circle-right', 'accent')), 31 | path$: sources.engagementKey$.map(k => `/engaged/${k}`), 32 | isVisible$: sources.isConfirmed$, 33 | }) 34 | 35 | export default sources => { 36 | const t = _Title(sources) 37 | const s1 = Step1(sources) 38 | const s2 = Step2(sources) 39 | const ad = _AllDone(sources) 40 | 41 | const card = LargeCard({...sources, 42 | content$: $.combineLatest(t.DOM, s1.DOM, s2.DOM, ad.DOM), 43 | }) 44 | 45 | const queue$ = $.merge(s1.queue$, s2.queue$) 46 | queue$.subscribe(x => console.log('new queue task:', x)) 47 | 48 | return { 49 | DOM: combineDOMsToDiv('.cardcontainer', card), 50 | queue$, 51 | route$: ad.route$, 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/root/Engagement/Glance/Commitments.js: -------------------------------------------------------------------------------- 1 | import {Observable} from 'rx' 2 | const {just, combineLatest} = Observable 3 | 4 | // import isolate from '@cycle/isolate' 5 | 6 | import {div} from 'helpers' 7 | 8 | import { 9 | TitleListItem, 10 | QuotingListItem, 11 | } from 'components/ui' 12 | 13 | // import {log} from 'util' 14 | 15 | import { 16 | ListItemHeader, 17 | ListWithHeader, 18 | } from 'components/sdm' 19 | 20 | import {CommitmentItemPassive} from 'components/commitment' 21 | 22 | const CommitmentList = sources => ListWithHeader({...sources, 23 | headerDOM: ListItemHeader(sources).DOM, 24 | Control$: just(CommitmentItemPassive), 25 | }) 26 | 27 | export default sources => { 28 | const commitments$ = sources.commitments$ 29 | 30 | const title = TitleListItem({...sources, 31 | title$: just('This is your Energy Exchange.'), 32 | }) 33 | 34 | const info = QuotingListItem({...sources, 35 | title$: sources.opp$.pluck('description'), 36 | profileKey$: sources.project$.pluck('ownerProfileKey'), 37 | }) 38 | 39 | const gives = CommitmentList({...sources, 40 | title$: just('you GIVE'), 41 | rows$: commitments$.map(cs => cs.filter(({party}) => party === 'vol')), 42 | }) 43 | 44 | const gets = CommitmentList({...sources, 45 | title$: just('you GET'), 46 | rows$: commitments$.map(cs => cs.filter(({party}) => party === 'org')), 47 | }) 48 | 49 | const items = [title, info, gives, gets] 50 | 51 | const DOM = combineLatest( 52 | items.map(i => i.DOM), 53 | (...doms) => div({}, doms) 54 | ) 55 | 56 | return { 57 | DOM, 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/root/Engagement/Glance/index.js: -------------------------------------------------------------------------------- 1 | import {Observable} from 'rx' 2 | const {of} = Observable 3 | 4 | import ComingSoon from 'components/ComingSoon' 5 | import {TabbedPage} from 'components/ui' 6 | 7 | import Priority from './Priority' 8 | import Commitments from './Commitments' 9 | const More = ComingSoon('More Info') 10 | 11 | export default sources => ({ 12 | pageTitle: of('At a Glance'), 13 | 14 | ...TabbedPage({...sources, 15 | tabs$: of([ 16 | {path: '/', label: 'Priority'}, 17 | {path: '/commitments', label: 'Commitments'}, 18 | // {path: '/more', label: 'More'}, 19 | ]), 20 | routes$: of({ 21 | '/': Priority, 22 | '/commitments': Commitments, 23 | '/more': More, 24 | }), 25 | }), 26 | }) 27 | -------------------------------------------------------------------------------- /src/root/Engagement/OldApplication/index.js: -------------------------------------------------------------------------------- 1 | import {Observable} from 'rx' 2 | const {of} = Observable 3 | 4 | import {TabbedPage} from 'components/ui' 5 | 6 | import AnswerQuestion from './AnswerQuestion' 7 | import ChooseTeams from './ChooseTeams' 8 | import NextSteps from './NextSteps' 9 | 10 | export default sources => ({ 11 | pageTitle: of('Your Application'), 12 | 13 | ...TabbedPage({...sources, 14 | tabs$: of([ 15 | {path: '/', label: 'Next Steps'}, 16 | {path: '/question', label: 'Answer Question'}, 17 | {path: '/teams', label: 'Choose Teams'}, 18 | ]), 19 | routes$: of({ 20 | '/': NextSteps, 21 | '/question': AnswerQuestion, 22 | '/teams': ChooseTeams, 23 | }), 24 | }), 25 | }) 26 | -------------------------------------------------------------------------------- /src/root/Engagement/Priority/CardConfirmNow.js: -------------------------------------------------------------------------------- 1 | import {Observable as $} from 'rx' 2 | 3 | import {hideable} from 'util' 4 | 5 | import { 6 | TitledCard, 7 | } from 'components/sdm' 8 | 9 | import { 10 | ToDoListItem, 11 | } from 'components/ui' 12 | 13 | const ToDoShifts = sources => ToDoListItem({...sources, 14 | title$: $.of('Choose your preferred shifts.'), 15 | isDone$: sources.engagement$.map(m => !!m.isAssigned), 16 | path$: $.of(sources.router.createHref('/confirmation')), 17 | }) 18 | 19 | const ToDoPayment = sources => ToDoListItem({...sources, 20 | title$: $.of('Make your payments.'), 21 | isDone$: sources.engagement$.map(m => !!m.isPaid), 22 | path$: $.of(sources.router.createHref('/confirmation')), 23 | }) 24 | 25 | const CNCard = sources => { 26 | const sh = ToDoShifts(sources) 27 | const pmt = ToDoPayment(sources) 28 | 29 | const card = TitledCard({...sources, 30 | title$: $.just('Lock in Your Spot'), 31 | content$: $.combineLatest(sh.DOM, pmt.DOM), 32 | }) 33 | 34 | const route$ = $.merge( 35 | sh.route$, 36 | pmt.route$ 37 | .withLatestFrom(sources.engagements$ || $.just({isAccepted: false}), 38 | (route, eng) => eng.isAccepted ? route : false 39 | ).filter(Boolean) 40 | ) 41 | 42 | return { 43 | DOM: card.DOM, 44 | route$, 45 | } 46 | } 47 | 48 | export const CardConfirmNow = sources => hideable(CNCard)({...sources, 49 | elevation$: $.just(2), 50 | isVisible$: sources.engagement$ 51 | .map(e => e.isAccepted && !e.isConfirmed && !e.isPaid), 52 | title$: $.just('Confirm Now!'), 53 | }) 54 | -------------------------------------------------------------------------------- /src/root/Engagement/Priority/CardEnergyExchange.js: -------------------------------------------------------------------------------- 1 | import {Observable as $} from 'rx' 2 | 3 | import { 4 | Card, 5 | } from 'components/sdm' 6 | 7 | import EnergyExchange from '../Glance/Commitments' 8 | 9 | export const CardEnergyExchange = sources => { 10 | const ee = EnergyExchange(sources) 11 | return { 12 | ...Card({...sources, 13 | content$: $.just([ee.DOM]), 14 | }), 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/root/Engagement/Priority/CardPickMoreShifts.js: -------------------------------------------------------------------------------- 1 | import {Observable as $} from 'rx' 2 | import {hideable} from 'util' 3 | import {icon} from 'helpers' 4 | 5 | import { 6 | ListItemNavigating, 7 | TitledCard, 8 | } from 'components/sdm' 9 | 10 | const _Info = sources => ListItemNavigating({...sources, 11 | title$: $.combineLatest( 12 | sources.shifts$, 13 | sources.commitmentShifts$, 14 | sources.shiftsNeeded$, 15 | (shifts, shiftsReq, shiftsNeeded) => 16 | shiftsNeeded > 0 ? 17 | ` 18 | You need ${shiftsReq} shifts 19 | but you have only picked ${shifts.length}. 20 | Pick your shifts so you can fulfill your commitment! 21 | ` : 22 | `You've got enough shifts, just confirm your spot.` 23 | ), 24 | leftDOM$: $.just(icon('chevron-circle-right', 'accent')), 25 | path$: $.just('/confirmation'), 26 | }) 27 | 28 | export const CardPickMoreShifts = _sources => { 29 | const shiftsNeeded$ = $.combineLatest( 30 | _sources.shifts$, 31 | _sources.commitmentShifts$, 32 | (shifts, shiftsReq) => shiftsReq - shifts.length 33 | ) 34 | 35 | const sources = {..._sources, shiftsNeeded$} 36 | 37 | const info = _Info(sources) 38 | 39 | const isVisible$ = sources.engagement$ 40 | .map(({isAssigned, isPaid, isConfirmed}) => 41 | !isAssigned && isPaid && isConfirmed 42 | ) 43 | 44 | const content$ = $.of([ 45 | info.DOM, 46 | ]) 47 | 48 | const card = hideable(TitledCard)({...sources, 49 | title$: sources.shiftsNeeded$.map(needed => 50 | needed > 0 ? `Pick ${needed} more shifts!` : 51 | `Confirm your shift preferences and carry on` 52 | ), 53 | content$, 54 | isVisible$, 55 | }) 56 | 57 | return { 58 | DOM: card.DOM, 59 | route$: info.route$, 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/root/Engagement/Priority/CardUpcomingShifts.js: -------------------------------------------------------------------------------- 1 | import {Observable as $} from 'rx' 2 | import {hideable} from 'util' 3 | import {div, a} from 'cycle-snabbdom' 4 | 5 | import { 6 | List, 7 | ListItem, 8 | TitledCard, 9 | } from 'components/sdm' 10 | 11 | import { 12 | ShiftContentExtra, 13 | } from 'components/shift' 14 | 15 | import { 16 | DescriptionListItem, 17 | } from 'components/ui' 18 | 19 | const _Info = sources => ListItem({...sources, 20 | title$: sources.shifts$ 21 | .map(shifts => ` 22 | You've got ${shifts.length} shifts coming up. 23 | Are you ready to make a difference? 24 | `), 25 | }) 26 | 27 | const _Reschedule = sources => DescriptionListItem({...sources, 28 | title$: $.of( 29 | div('', [ 30 | 'Your schedule is locked. To request an unlock, email ', 31 | a({attrs: {href: 'mailto:help@sparks.network'}},'help@sparks.network'), 32 | '.', 33 | ]) 34 | ), 35 | }) 36 | 37 | const _Item = sources => ListItem({...sources, 38 | ...ShiftContentExtra(sources), 39 | }) 40 | 41 | const _List = sources => List({...sources, 42 | Control$: $.of(_Item), 43 | rows$: sources.shifts$, 44 | }) 45 | 46 | export const CardUpcomingShifts = sources => { 47 | const info = _Info(sources) 48 | const list = _List(sources) 49 | const rs = _Reschedule(sources) 50 | 51 | const isVisible$ = $.combineLatest( 52 | sources.engagement$, 53 | sources.commitmentShifts$, 54 | sources.shifts$, 55 | ({isAssigned, isPaid, isConfirmed}, shiftsReq, shifts) => 56 | isAssigned && isPaid && isConfirmed && shifts.length === shiftsReq 57 | ) 58 | 59 | const content$ = $.of([ 60 | info.DOM, 61 | list.DOM, 62 | rs.DOM, 63 | ]) 64 | 65 | return hideable(TitledCard)({...sources, 66 | title$: $.just('Ready to Work?'), 67 | content$, 68 | isVisible$, 69 | }) 70 | } 71 | 72 | -------------------------------------------------------------------------------- /src/root/Engagement/Priority/CardWhois.js: -------------------------------------------------------------------------------- 1 | import {Observable as $} from 'rx' 2 | 3 | import {hideable} from 'util' 4 | 5 | import { 6 | TitledCard, 7 | ListItem, 8 | } from 'components/sdm' 9 | 10 | import { 11 | Profiles, 12 | } from 'components/remote' 13 | 14 | /* 15 | const ToDoShifts = sources => ToDoListItem({...sources, 16 | title$: $.of('Choose when you\'d like to work.'), 17 | isDone$: sources.engagement$.map(m => !!m.isAssigned), 18 | path$: $.of(sources.router.createHref('/confirmation')), 19 | }) 20 | 21 | const ToDoPayment = sources => ToDoListItem({...sources, 22 | title$: $.of('Make your payments.'), 23 | isDone$: sources.engagement$.map(m => !!m.isPaid), 24 | path$: $.of(sources.router.createHref('/confirmation')), 25 | }) 26 | */ 27 | 28 | const BaseCard = sources => { 29 | const profile$ = sources.engagement$.pluck('profileKey') 30 | .flatMapLatest(Profiles.query.one(sources)) 31 | 32 | const li = ListItem({...sources, 33 | title$: profile$.pluck('fullName'), 34 | subtitle$: profile$.map(({email, $key}) => `${email} | ${$key}`), 35 | }) 36 | 37 | return TitledCard({...sources, 38 | title$: $.just('Applicant View'), 39 | content$: $.combineLatest(li.DOM), 40 | }) 41 | } 42 | 43 | export const CardWhois = sources => hideable(BaseCard)({...sources, 44 | elevation$: $.just(2), 45 | isVisible$: $.combineLatest( 46 | sources.engagement$.pluck('profileKey'), 47 | sources.userProfileKey$, 48 | (pk,upk) => pk !== upk 49 | ), 50 | }) 51 | -------------------------------------------------------------------------------- /src/root/Engagement/Priority/index.js: -------------------------------------------------------------------------------- 1 | import {Observable as $} from 'rx' 2 | import {combineDOMsToDiv} from 'util' 3 | 4 | import {CardUpcomingShifts} from './CardUpcomingShifts' 5 | import {CardApplicationNextSteps} from './CardApplicationNextSteps' 6 | import {CardEnergyExchange} from './CardEnergyExchange' 7 | import {CardConfirmNow} from './CardConfirmNow' 8 | import {CardPickMoreShifts} from './CardPickMoreShifts' 9 | import {CardWhois} from './CardWhois' 10 | 11 | export default sources => { 12 | const who = CardWhois(sources) 13 | const confirm = CardConfirmNow(sources) 14 | const app = CardApplicationNextSteps(sources) 15 | const r2w = CardUpcomingShifts(sources) 16 | const pms = CardPickMoreShifts(sources) 17 | const ee = CardEnergyExchange(sources) 18 | 19 | const DOM = combineDOMsToDiv('.cardcontainer',who,confirm,app,r2w,pms,ee) 20 | 21 | return { 22 | DOM, 23 | route$: $.merge(confirm.route$, app.route$), 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/root/Engagement/Schedule/index.js: -------------------------------------------------------------------------------- 1 | import {Observable} from 'rx' 2 | const {of} = Observable 3 | 4 | import ComingSoon from 'components/ComingSoon' 5 | import {TabbedPage} from 'components/ui' 6 | 7 | import Priority from './Priority' 8 | // const Priority = ComingSoon('Manage/Glance/Priority') 9 | const Find = ComingSoon('Manage/Glance/Find') 10 | const Recently = ComingSoon('Manage/Glance/Recently') 11 | 12 | export default sources => ({ 13 | pageTitle: of('Your Schedule'), 14 | 15 | ...TabbedPage({...sources, 16 | tabs$: of([ 17 | {path: '/', label: 'Priority'}, 18 | {path: '/find', label: 'Find'}, 19 | {path: '/recently', label: 'Recently'}, 20 | ]), 21 | routes$: of({ 22 | '/': Priority, 23 | '/find': Find, 24 | '/recently': Recently, 25 | }), 26 | }), 27 | }) 28 | -------------------------------------------------------------------------------- /src/root/Landing/index.js: -------------------------------------------------------------------------------- 1 | import {Observable} from 'rx' 2 | const {merge, combineLatest} = Observable 3 | 4 | import AppMenu from 'components/AppMenu' 5 | import {landing} from 'helpers' 6 | 7 | import {LoginButtons} from 'components/ui' 8 | 9 | // import {log} from 'util' 10 | 11 | import './styles.scss' 12 | 13 | export default (sources) => { 14 | const appMenu = AppMenu(sources) 15 | 16 | const logins = LoginButtons(sources) 17 | 18 | const DOM = combineLatest( 19 | appMenu.DOM, 20 | logins.DOM, 21 | landing 22 | ) 23 | 24 | return { 25 | DOM, 26 | auth$: merge(logins.auth$, appMenu.auth$), 27 | route$: sources.redirectLogin$, 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/root/Opp/Confirmed/AssignmentItem.js: -------------------------------------------------------------------------------- 1 | import {Observable as $} from 'rx' 2 | import isolate from '@cycle/isolate' 3 | 4 | import { 5 | ListItemWithMenu, 6 | MenuItem, 7 | } from 'components/sdm' 8 | 9 | import { 10 | ShiftContentExtra, 11 | } from 'components/shift' 12 | 13 | import { 14 | Assignments, 15 | } from 'components/remote' 16 | 17 | const _Remove = sources => MenuItem({...sources, 18 | iconName$: $.of('remove'), 19 | title$: $.of('Remove'), 20 | }) 21 | 22 | export const AssignmentItem = sources => { 23 | const rem = isolate(_Remove)(sources) 24 | 25 | const matchingAssignment$ = $.combineLatest( 26 | sources.assignments$, 27 | sources.item$, 28 | (assignments, shift) => assignments.find(i => i.shiftKey === shift.$key) 29 | ) 30 | 31 | const queue$ = rem.click$ 32 | .withLatestFrom(matchingAssignment$, (c, a) => a) 33 | .pluck('$key') 34 | .map(Assignments.action.remove) 35 | 36 | const li = ListItemWithMenu({...sources, 37 | ...ShiftContentExtra(sources), 38 | menuItems$: $.of([rem.DOM]), 39 | }) 40 | 41 | return { 42 | DOM: li.DOM, 43 | queue$, 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/root/Opp/Confirmed/index.js: -------------------------------------------------------------------------------- 1 | import {Observable as $} from 'rx' 2 | import {div} from 'helpers' 3 | import {combineDOMsToDiv} from 'util' 4 | 5 | import {FetchEngagements} from '../FetchEngagements' 6 | 7 | import { 8 | List, 9 | } from 'components/sdm' 10 | 11 | import { 12 | TitleListItem, 13 | } from 'components/ui' 14 | 15 | import {Item} from './Item' 16 | 17 | const _Header = sources => TitleListItem({...sources, 18 | title$: sources.confirmed$ 19 | .map(arr => `You have ${arr.length} Confirmed Volunteers.`), 20 | }) 21 | 22 | const AppList = sources => List({...sources, 23 | Control$: $.of(Item), 24 | rows$: sources.confirmed$, 25 | }) 26 | 27 | export default sources => { 28 | const _sources = {...sources, ...FetchEngagements(sources)} 29 | const hdr = _Header(_sources) 30 | const list = AppList(_sources) 31 | 32 | return { 33 | pageTitle: $.of('Confirmed Volunteers'), 34 | tabBarDOM: $.of(div('',[])), 35 | DOM: combineDOMsToDiv('',hdr,list), 36 | queue$: list.queue$, 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/root/Opp/Engaged/FilteredView.js: -------------------------------------------------------------------------------- 1 | import {Observable} from 'rx' 2 | const {of, merge} = Observable 3 | import {div} from 'helpers' 4 | 5 | import {combineLatestToDiv} from 'util' 6 | 7 | import { 8 | Profiles, 9 | } from 'components/remote' 10 | 11 | import { 12 | List, 13 | ListItemNavigating, 14 | } from 'components/sdm' 15 | 16 | import {Detail} from './Detail' 17 | 18 | const Item = sources => { 19 | const profile$ = sources.item$ 20 | .pluck('profileKey') 21 | .flatMapLatest(Profiles.query.one(sources)) 22 | // .flatMapLatest(({profileKey, $key}) => 23 | // profileKey ? 24 | // Profiles.query.one(sources)(profileKey) : 25 | // of({fullName: $key, $key}), 26 | // ) 27 | .shareReplay(1) 28 | 29 | return ListItemNavigating({...sources, 30 | title$: profile$.pluck('fullName'), 31 | iconSrc$: profile$.pluck('portraitUrl'), 32 | path$: sources.item$.map(({$key}) => 33 | sources.router.createHref(`/show/${$key}`) 34 | ), 35 | }) 36 | } 37 | 38 | const AppList = sources => List({...sources, 39 | Control$: of(Item), 40 | rows$: sources.engagements$, 41 | }) 42 | 43 | const EmptyNotice = sources => ({ 44 | DOM: sources.items$.map(i => 45 | i.length > 0 ? null : div({},['Empty notice']) 46 | ), 47 | }) 48 | 49 | const FilteredView = sources => { 50 | const detail = Detail(sources) 51 | const list = AppList(sources) 52 | const mt = EmptyNotice({...sources, items$: sources.engagements$}) 53 | 54 | return { 55 | DOM: combineLatestToDiv(mt.DOM, list.DOM, detail.DOM), 56 | route$: merge(list.route$, detail.route$.map(sources.router.createHref)), 57 | queue$: detail.queue$, 58 | } 59 | } 60 | 61 | export {FilteredView} 62 | -------------------------------------------------------------------------------- /src/root/Opp/Engaged/index.js: -------------------------------------------------------------------------------- 1 | import {Observable} from 'rx' 2 | const {of, combineLatest} = Observable 3 | 4 | import {TabbedPage} from 'components/ui' 5 | 6 | import {FilteredView} from './FilteredView' 7 | 8 | const _TabMaker = sources => ({ 9 | tabs$: combineLatest( 10 | sources.applied$, 11 | sources.priority$, 12 | sources.ok$, 13 | sources.never$, 14 | // sources.confirmed$, 15 | (ap,pr,ok,nv/*,cf*/) => [ 16 | {path: '/', label: `${ap.length} Applied`}, 17 | // cf.length > 0 && {path: '/confirmed', label: `${cf.length} Confirmed`}, 18 | pr.length > 0 && {path: '/priority', label: `${pr.length} Priority`}, 19 | ok.length > 0 && {path: '/ok', label: `${ok.length} OK`}, 20 | nv.length > 0 && {path: '/never', label: `${nv.length} Never`}, 21 | ].filter(x => !!x) 22 | ), 23 | }) 24 | 25 | const Applied = sources => FilteredView({...sources, 26 | engagements$: sources.applied$, 27 | }) 28 | const Priority = sources => FilteredView({...sources, 29 | engagements$: sources.priority$, 30 | }) 31 | const OK = sources => FilteredView({...sources, 32 | engagements$: sources.ok$, 33 | }) 34 | const Never = sources => FilteredView({...sources, 35 | engagements$: sources.never$, 36 | }) 37 | // const Confirmed = sources => FilteredView({...sources, 38 | // engagements$: sources.confirmed$, 39 | // }) 40 | 41 | export default sources => { 42 | // const _sources = {...sources, ...FetchEngagements(sources)} 43 | const _sources = sources 44 | 45 | return { 46 | pageTitle: of('Engaged Volunteers'), 47 | 48 | ...TabbedPage({..._sources, 49 | tabs$: _TabMaker(_sources).tabs$, 50 | routes$: of({ 51 | '/': Applied, 52 | // '/confirmed': Confirmed, 53 | '/priority': Priority, 54 | '/ok': OK, 55 | '/never': Never, 56 | }), 57 | }), 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/root/Opp/FetchEngagements.js: -------------------------------------------------------------------------------- 1 | import { 2 | Engagements, 3 | } from 'components/remote' 4 | 5 | const filterApplied = e => 6 | e.filter(x => !x.isAccepted && !x.declined && !x.isConfirmed) 7 | const filterPriority = e => 8 | e.filter(x => x.isAccepted && x.priority && !x.isConfirmed) 9 | const filterOK = e => 10 | e.filter(x => x.isAccepted && !x.priority && !x.isConfirmed) 11 | const filterNever = e => 12 | e.filter(x => !x.isAccepted && x.declined && !x.isConfirmed) 13 | const filterConfirmed = e => 14 | e.filter(x => x.isConfirmed) 15 | 16 | export const FetchEngagements = sources => { 17 | const all$ = sources.oppKey$ 18 | .flatMapLatest(Engagements.query.byOpp(sources)) 19 | .shareReplay(1) 20 | 21 | all$ // all errors logged here 22 | .map(engs => engs.filter(e => !e.profileKey)) // filter out naughty records 23 | .subscribe(engs => console.log('Applications with errors:', engs)) 24 | 25 | const e$ = all$ 26 | .map(engs => engs.filter(e => !!e.profileKey)) // filter out naughty records 27 | .shareReplay(1) 28 | 29 | return { 30 | engagements$: e$, 31 | applied$: e$.map(filterApplied).shareReplay(1), 32 | priority$: e$.map(filterPriority).shareReplay(1), 33 | ok$: e$.map(filterOK).shareReplay(1), 34 | never$: e$.map(filterNever).shareReplay(1), 35 | confirmed$: e$.map(filterConfirmed).shareReplay(1), 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/root/Opp/Glance/index.js: -------------------------------------------------------------------------------- 1 | import {Observable} from 'rx' 2 | const {of} = Observable 3 | 4 | import ComingSoon from 'components/ComingSoon' 5 | import {TabbedPage} from 'components/ui' 6 | 7 | import Priority from './Priority' 8 | const Find = ComingSoon('Opp | Glance | Find') 9 | const Recently = ComingSoon('Opp | Glance | Recently') 10 | 11 | export default sources => ({ 12 | pageTitle: of('At a Glance'), 13 | 14 | ...TabbedPage({...sources, 15 | tabs$: of([ 16 | {path: '/', label: 'Priority'}, 17 | // {path: '/find', label: 'Find'}, 18 | // {path: '/recently', label: 'Recently'}, 19 | ]), 20 | routes$: of({ 21 | '/': Priority, 22 | '/find': Find, 23 | '/recently': Recently, 24 | }), 25 | }), 26 | }) 27 | -------------------------------------------------------------------------------- /src/root/Opp/Manage/Describe.js: -------------------------------------------------------------------------------- 1 | import {Observable} from 'rx' 2 | const {just, combineLatest} = Observable 3 | 4 | import isolate from '@cycle/isolate' 5 | import {div, icon} from 'helpers' 6 | 7 | import { 8 | ListItemToggle, 9 | ListItemCollapsibleTextArea, 10 | 11 | } from 'components/sdm' 12 | 13 | import {Opps} from 'remote' 14 | 15 | import {RecruitmentLinkItem} from '../RecruitmentLinkItem' 16 | 17 | const TogglePublic = sources => ListItemToggle({...sources, 18 | titleTrue$: just('This is a Public Opportunity, and anyone can apply.'), 19 | titleFalse$: just('This is Private, and is only seen by people you invite.'), 20 | }) 21 | 22 | const TextareaDescription = sources => ListItemCollapsibleTextArea({ 23 | ...sources, 24 | title$: just('Describe this Opportunity to applicants.'), 25 | subtitle$: just(` 26 | Tell your prospective volunteers what they\'re going to acheive, 27 | and how rewarding it will be. 28 | `), 29 | leftDOM$: just(icon('playlist_add')), 30 | // iconName$: just('playlist_add'), 31 | okLabel$: just('this sounds great'), 32 | cancelLabel$: just('hang on ill do this later'), 33 | }) 34 | 35 | export default sources => { 36 | const preview = isolate(RecruitmentLinkItem)(sources) 37 | 38 | const togglePublic = isolate(TogglePublic)({...sources, 39 | value$: sources.opp$.pluck('isPublic'), 40 | }) 41 | 42 | const textareaDescription = isolate(TextareaDescription)({...sources, 43 | value$: sources.opp$.pluck('description'), 44 | }) 45 | 46 | const updateIsPublic$ = togglePublic.value$ 47 | .withLatestFrom(sources.oppKey$, (isPublic,key) => 48 | Opps.update(key,{isPublic}) 49 | ) 50 | 51 | const updateDescription$ = textareaDescription.value$ 52 | .withLatestFrom(sources.oppKey$, (description,key) => 53 | Opps.update(key,{description}) 54 | ) 55 | 56 | const queue$ = Observable.merge( 57 | updateIsPublic$, 58 | updateDescription$, 59 | ) 60 | 61 | const DOM = combineLatest( 62 | preview.DOM, 63 | togglePublic.DOM, 64 | textareaDescription.DOM, 65 | (...doms) => div({}, doms) 66 | ) 67 | 68 | return { 69 | DOM, 70 | queue$, 71 | route$: preview.route$, 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/root/Opp/Manage/index.js: -------------------------------------------------------------------------------- 1 | import {Observable} from 'rx' 2 | const {of} = Observable 3 | 4 | import {TabbedPage} from 'components/ui' 5 | 6 | import Describe from './Describe' 7 | import Exchange from './Exchange' 8 | import Applying from './Applying' 9 | 10 | export default sources => ({ 11 | pageTitle: of('Manage Opportunity'), 12 | 13 | ...TabbedPage({...sources, 14 | tabs$: of([ 15 | {path: '/', label: 'Describe'}, 16 | {path: '/exchange', label: 'Exchange'}, 17 | {path: '/applying', label: 'Applying'}, 18 | ]), 19 | routes$: of({ 20 | '/': Describe, 21 | '/exchange': Exchange, 22 | '/applying': Applying, 23 | }), 24 | }), 25 | }) 26 | -------------------------------------------------------------------------------- /src/root/Opp/OppNav.js: -------------------------------------------------------------------------------- 1 | import {Observable} from 'rx' 2 | const {just, merge, combineLatest} = Observable 3 | 4 | import isolate from '@cycle/isolate' 5 | 6 | import {div} from 'cycle-snabbdom' 7 | 8 | // import {log} from 'util' 9 | 10 | import {ListItemNavigating} from 'components/sdm' 11 | 12 | import {mergeSinks, combineLatestToDiv} from 'util' 13 | 14 | const _Glance = sources => ListItemNavigating({...sources, 15 | title$: just('At a Glance'), 16 | iconName$: just('home'), 17 | path$: just('/'), 18 | }) 19 | 20 | const _Manage = sources => ListItemNavigating({...sources, 21 | title$: just('Manage'), 22 | iconName$: just('settings'), 23 | path$: just('/manage'), 24 | }) 25 | 26 | const _Confirmed = sources => ListItemNavigating({...sources, 27 | title$: sources.confirmed$.map(c => `${c.length || 0} Confirmed`), 28 | iconName$: just('people'), 29 | path$: just('/confirmed'), 30 | }) 31 | 32 | const _Engaged = sources => ListItemNavigating({...sources, 33 | title$: sources.applied$.map(c => `${c.length || 0} Applied`), 34 | iconName$: just('event_available'), 35 | path$: just('/engaged'), 36 | }) 37 | 38 | const _List = sources => { 39 | const childs = [ 40 | isolate(_Glance,'glance')(sources), 41 | isolate(_Manage,'manage')(sources), 42 | isolate(_Confirmed,'confirmed')(sources), 43 | isolate(_Engaged,'enaged')(sources), 44 | ] 45 | 46 | return { 47 | DOM: combineLatestToDiv(...childs.map(c => c.DOM)), 48 | route$: merge(...childs.map(c => c.route$)) 49 | .map(sources.router.createHref), 50 | } 51 | } 52 | 53 | const OppNav = sources => { 54 | const l = _List(sources) 55 | 56 | const DOM = combineLatest( 57 | sources.isMobile$, sources.titleDOM, l.DOM, 58 | (isMobile, title, list) => 59 | div({}, [isMobile ? null : title, div('.rowwrap', [list])]) 60 | ) 61 | 62 | return { 63 | DOM, 64 | ...mergeSinks(l), 65 | } 66 | } 67 | 68 | export {OppNav} 69 | -------------------------------------------------------------------------------- /src/root/Opp/RecruitmentLinkItem.js: -------------------------------------------------------------------------------- 1 | import {Observable as $} from 'rx' 2 | 3 | import { 4 | ListItemNewTarget, 5 | } from 'components/sdm' 6 | 7 | export const RecruitmentLinkItem = sources => ListItemNewTarget({...sources, 8 | title$: $.just('Check out your Recruiting page in a new window.'), 9 | iconName$: $.just('link'), 10 | url$: $.combineLatest( 11 | sources.projectKey$, sources.oppKey$, 12 | (pk, ok) => '/apply/' + pk + '/opp/' + ok 13 | ), 14 | }) 15 | -------------------------------------------------------------------------------- /src/root/Organize/index.js: -------------------------------------------------------------------------------- 1 | import {Observable} from 'rx' 2 | import combineLatestObj from 'rx-combine-latest-obj' 3 | 4 | // import isolate from '@cycle/isolate' 5 | 6 | // import {log} from 'util' 7 | 8 | // import AppBar from 'components/AppBar' 9 | 10 | import {div} from 'cycle-snabbdom' 11 | 12 | import SoloFrame from 'components/SoloFrame' 13 | 14 | // import {log} from 'util' 15 | 16 | import {Organizers} from 'components/remote' 17 | 18 | export default sources => { 19 | const organizer$ = sources.organizerKey$ 20 | .flatMapLatest(Organizers.query.one(sources)) 21 | 22 | // const queue$ = profile$ 23 | // .sample(submit$) 24 | // .map(Profiles.create) 25 | 26 | const viewState = { 27 | organizer$, 28 | // auth$: sources.auth$, 29 | // userProfile$: sources.userProfile$, 30 | // profileFormDOM$: profileForm.DOM, 31 | } 32 | 33 | const pageDOM = combineLatestObj(viewState) 34 | .map(({organizer}) => 35 | div({},[organizer.inviteEmail]) 36 | ) 37 | 38 | const frame = SoloFrame({pageDOM, ...sources}) 39 | 40 | const route$ = Observable.merge( 41 | frame.route$, 42 | ) 43 | 44 | return { 45 | DOM: frame.DOM, 46 | route$, 47 | auth$: frame.auth$, 48 | // queue$, 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/root/Project/Glance/index.js: -------------------------------------------------------------------------------- 1 | import {Observable} from 'rx' 2 | const {of} = Observable 3 | 4 | import ComingSoon from 'components/ComingSoon' 5 | import {TabbedPage} from 'components/ui' 6 | 7 | import Priority from './Priority' 8 | import Checkin from './Checkin' 9 | 10 | // const Checkin = ComingSoon('Checkin') 11 | const Recently = ComingSoon('Manage/Glance/Recently') 12 | 13 | export default sources => ({ 14 | pageTitle: of('At a Glance'), 15 | 16 | ...TabbedPage({...sources, 17 | tabs$: of([ 18 | {path: '/', label: 'Priority'}, 19 | {path: '/checkin', label: 'Checkin'}, 20 | // {path: '/find', label: 'Find'}, 21 | // {path: '/recently', label: 'Recently'}, 22 | ]), 23 | routes$: of({ 24 | '/': Priority, 25 | '/checkin': Checkin, 26 | '/recently': Recently, 27 | }), 28 | }), 29 | }) 30 | -------------------------------------------------------------------------------- /src/root/Project/Manage/Describe.js: -------------------------------------------------------------------------------- 1 | import {Observable} from 'rx' 2 | const {just, merge, combineLatest} = Observable 3 | 4 | import isolate from '@cycle/isolate' 5 | 6 | import {div} from 'helpers' 7 | 8 | import SetImage from 'components/SetImage' 9 | 10 | // import {log} from 'util' 11 | 12 | import { 13 | Projects, 14 | ProjectImages, 15 | } from 'components/remote' 16 | 17 | import { 18 | ListItemCollapsibleTextArea, 19 | } from 'components/sdm' 20 | 21 | const DescriptionTextarea = sources => ListItemCollapsibleTextArea({ 22 | ...sources, 23 | title$: just('Describe this Project to the world.'), 24 | subtitle$: just(` 25 | This is shown to all the people involved in your project, 26 | so make it general. 27 | `), 28 | value$: sources.project$.pluck('description'), 29 | iconName$: just('playlist_add'), 30 | okLabel$: just('yes do it'), 31 | cancelLabel$: just('wait a sec'), 32 | }) 33 | 34 | export default sources => { 35 | const inputDataUrl$ = sources.projectImage$ 36 | .map(i => i && i.dataUrl) 37 | 38 | const setImage = SetImage({...sources, inputDataUrl$}) 39 | 40 | const descriptionTextarea = isolate(DescriptionTextarea)(sources) 41 | 42 | const updateDescription$ = descriptionTextarea.value$ 43 | .withLatestFrom(sources.projectKey$, (description,key) => ({ 44 | key, 45 | values: {description}, 46 | })) 47 | .map(Projects.action.update) 48 | 49 | const setImage$ = setImage.dataUrl$ 50 | .withLatestFrom( 51 | sources.projectKey$, 52 | (dataUrl,key) => ({key, values: {dataUrl}}) 53 | ) 54 | .map(ProjectImages.action.set) 55 | 56 | const queue$ = merge(updateDescription$, setImage$) 57 | 58 | const DOM = combineLatest( 59 | descriptionTextarea.DOM, 60 | setImage.DOM, 61 | (...doms) => div({},doms) 62 | ) 63 | return { 64 | DOM, 65 | queue$, 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/root/Project/Manage/Staff.js: -------------------------------------------------------------------------------- 1 | import {Observable} from 'rx' 2 | const {just, merge} = Observable 3 | 4 | import combineLatestObj from 'rx-combine-latest-obj' 5 | 6 | import isolate from '@cycle/isolate' 7 | 8 | import CreateOrganizerInvite from 'components/CreateOrganizerInvite' 9 | 10 | import listHeader from 'helpers/listHeader' 11 | 12 | import {col} from 'helpers' 13 | 14 | // import {log} from 'util' 15 | 16 | import {OrganizerInviteItem} from 'components/organizer' 17 | import {List} from 'components/sdm' 18 | 19 | const OrganizerInviteList = sources => List({...sources, 20 | Control$: just(OrganizerInviteItem), 21 | }) 22 | 23 | const _render = ({organizers, createOrganizerInviteDOM, listDOM}) => 24 | col( 25 | createOrganizerInviteDOM, 26 | organizers.length > 0 ? listHeader({title: 'Open Invites'}) : null, 27 | listDOM, 28 | ) 29 | 30 | import {rows} from 'util' 31 | 32 | export default sources => { 33 | const createOrganizerInvite = isolate(CreateOrganizerInvite)(sources) 34 | 35 | const list = OrganizerInviteList({...sources, 36 | rows$: sources.organizers$, 37 | }) 38 | 39 | const queue$ = merge( 40 | createOrganizerInvite.queue$, 41 | list.queue$, 42 | ) 43 | 44 | const viewState = { 45 | project$: sources.project$, 46 | createOrganizerInviteDOM$: createOrganizerInvite.DOM, 47 | organizers$: sources.organizers$.map(rows), 48 | listDOM$: list.DOM, 49 | } 50 | 51 | const DOM = combineLatestObj(viewState).map(_render) 52 | 53 | return { 54 | DOM, 55 | queue$, 56 | route$: list.route$, 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/root/Project/Manage/index.js: -------------------------------------------------------------------------------- 1 | import {Observable} from 'rx' 2 | const {of} = Observable 3 | 4 | import ComingSoon from 'components/ComingSoon' 5 | import {TabbedPage} from 'components/ui' 6 | 7 | import Describe from './Describe' 8 | import Staff from './Staff' 9 | const Connect = ComingSoon('Manage/Connect') 10 | 11 | export default sources => ({ 12 | pageTitle: of('Manage Project'), 13 | 14 | ...TabbedPage({...sources, 15 | tabs$: of([ 16 | {path: '/', label: 'Describe'}, 17 | {path: '/staff', label: 'Staff'}, 18 | // {path: '/connect', label: 'Connect'}, 19 | ]), 20 | routes$: of({ 21 | '/': Describe, 22 | '/staff': Staff, 23 | '/connect': Connect, 24 | }), 25 | }), 26 | }) 27 | -------------------------------------------------------------------------------- /src/root/SideNav.scss: -------------------------------------------------------------------------------- 1 | .mini-menu-item { 2 | height: 48px; 3 | min-height: 48px; 4 | } 5 | 6 | .menu-subheader { 7 | color: #999; 8 | border-top: 1px solid #CCC; 9 | height: 36px !important; 10 | margin-top: 12px; 11 | min-height: 36px !important; 12 | text-transform: uppercase; 13 | 14 | .title { 15 | font-size: 14px !important; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/root/Team/Glance/Priority.js: -------------------------------------------------------------------------------- 1 | import {Observable} from 'rx' 2 | const {just, merge, combineLatest} = Observable 3 | 4 | import isolate from '@cycle/isolate' 5 | 6 | import {div} from 'helpers' 7 | 8 | import { 9 | ListItemNavigating, 10 | } from 'components/sdm' 11 | 12 | const WhatItem = sources => ListItemNavigating({...sources, 13 | title$: just('What\'s this Team all about?'), 14 | iconName$: just('users'), 15 | path$: just('/manage'), 16 | }) 17 | 18 | const InviteItem = sources => ListItemNavigating({...sources, 19 | title$: just('Invite some people to Lead this Team'), 20 | iconName$: just('person_add'), 21 | path$: just('/manage/leads'), 22 | }) 23 | 24 | const HowItem = sources => ListItemNavigating({...sources, 25 | title$: just('How are volunteers joining this Team?'), 26 | iconName$: just('event_note'), 27 | path$: just('/manage/applying'), 28 | }) 29 | 30 | export default sources => { 31 | const what = isolate(WhatItem,'what')(sources) 32 | const invite = isolate(InviteItem,'invite')(sources) 33 | const how = isolate(HowItem,'how')(sources) 34 | 35 | const items = [what, invite, how] 36 | 37 | const route$ = merge(...items.map(i => i.route$)) 38 | .map(sources.router.createHref) 39 | 40 | const DOM = combineLatest( 41 | ...items.map(i => i.DOM), 42 | (...doms) => div({},doms) 43 | ) 44 | 45 | return { 46 | DOM, 47 | // queue$, 48 | route$, 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/root/Team/Glance/index.js: -------------------------------------------------------------------------------- 1 | import {Observable} from 'rx' 2 | const {of} = Observable 3 | 4 | import ComingSoon from 'components/ComingSoon' 5 | import {TabbedPage} from 'components/ui' 6 | 7 | import Priority from './Priority' 8 | const Find = ComingSoon('Manage/Glance/Find') 9 | const Recently = ComingSoon('Manage/Glance/Recently') 10 | 11 | export default sources => ({ 12 | pageTitle: of('At a Glance'), 13 | 14 | ...TabbedPage({...sources, 15 | tabs$: of([ 16 | {path: '/', label: 'Priority'}, 17 | // {path: '/find', label: 'Find'}, 18 | // {path: '/recently', label: 'Recently'}, 19 | ]), 20 | routes$: of({ 21 | '/': Priority, 22 | '/find': Find, 23 | '/recently': Recently, 24 | }), 25 | }), 26 | }) 27 | -------------------------------------------------------------------------------- /src/root/Team/Manage/Applying.js: -------------------------------------------------------------------------------- 1 | import {Observable} from 'rx' 2 | const {just, combineLatest} = Observable 3 | 4 | import isolate from '@cycle/isolate' 5 | 6 | import {div} from 'helpers' 7 | 8 | import { 9 | ListItemToggle, 10 | ListItemCollapsibleTextArea, 11 | } from 'components/sdm' 12 | 13 | import {Teams} from 'components/remote' 14 | 15 | const ToggleOpen = sources => ListItemToggle({...sources, 16 | titleTrue$: 17 | just('This is an Open Team, any volunteer who is accepted can join.'), 18 | titleFalse$: 19 | just('This is Restricted, volunteers have to apply to join.'), 20 | }) 21 | 22 | const TextAreaQuestion = sources => ListItemCollapsibleTextArea({ 23 | ...sources, 24 | title$: 25 | just('What question do you want to ask people who apply for this Team?'), 26 | iconName$: just('playlist_add'), 27 | okLabel$: just('this sounds great'), 28 | cancelLabel$: just('hang on ill do this later'), 29 | }) 30 | 31 | export default sources => { 32 | const toggle = isolate(ToggleOpen,'open')({...sources, 33 | value$: sources.team$.pluck('isPublic'), 34 | }) 35 | const ta = isolate(TextAreaQuestion)({...sources, 36 | value$: sources.team$.map(({question}) => question || ''), 37 | }) 38 | 39 | const updateQuestion$ = ta.value$ 40 | .withLatestFrom(sources.teamKey$, (question,key) => ({ 41 | key, 42 | values: {question}, 43 | })) 44 | .map(Teams.action.update) 45 | 46 | const updatePublic$ = toggle.value$ 47 | .withLatestFrom(sources.teamKey$, (isPublic,key) => ({ 48 | key, 49 | values: {isPublic}, 50 | })) 51 | .map(Teams.action.update) 52 | 53 | const queue$ = Observable.merge( 54 | updateQuestion$, 55 | updatePublic$, 56 | ) 57 | 58 | const DOM = combineLatest( 59 | toggle.DOM, 60 | ta.DOM, 61 | (...doms) => div({},doms) 62 | ) 63 | 64 | return { 65 | DOM, 66 | queue$, 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/root/Team/Manage/Describe.js: -------------------------------------------------------------------------------- 1 | import {Observable} from 'rx' 2 | const {just, combineLatest} = Observable 3 | 4 | import isolate from '@cycle/isolate' 5 | 6 | import {div} from 'helpers' 7 | 8 | import SetImage from 'components/SetImage' 9 | 10 | import { 11 | ListItemCollapsibleTextArea, 12 | } from 'components/sdm' 13 | 14 | import {Teams, TeamImages} from 'components/remote' 15 | 16 | const TextareaDescription = sources => ListItemCollapsibleTextArea({ 17 | ...sources, 18 | title$: just('Write a short tweet-length description'), 19 | iconName$: just('playlist_add'), 20 | okLabel$: just('this sounds great'), 21 | cancelLabel$: just('hang on ill do this later'), 22 | }) 23 | 24 | export default sources => { 25 | const inputDataUrl$ = sources.teamImage$ 26 | .map(i => i && i.dataUrl) 27 | 28 | const setImage = SetImage({...sources, 29 | inputDataUrl$, 30 | aspectRatio$: just(1), 31 | }) 32 | 33 | const setImage$ = setImage.dataUrl$ 34 | .withLatestFrom( 35 | sources.teamKey$, 36 | (dataUrl,key) => ({key, values: {dataUrl}}) 37 | ) 38 | .map(TeamImages.action.set) 39 | 40 | const textareaDescription = isolate(TextareaDescription)({...sources, 41 | value$: sources.team$.map(({description}) => description || ''), 42 | }) 43 | 44 | const updateDescription$ = textareaDescription.value$ 45 | .withLatestFrom(sources.teamKey$, (description,key) => ({ 46 | key, 47 | values: {description}, 48 | })) 49 | .map(Teams.action.update) 50 | 51 | const queue$ = Observable.merge( 52 | updateDescription$, 53 | setImage$, 54 | ) 55 | 56 | const DOM = combineLatest( 57 | textareaDescription.DOM, 58 | setImage.DOM, 59 | (...doms) => div({},doms) 60 | ) 61 | 62 | return { 63 | DOM, 64 | queue$, 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/root/Team/Manage/index.js: -------------------------------------------------------------------------------- 1 | import {Observable} from 'rx' 2 | const {of} = Observable 3 | 4 | import ComingSoon from 'components/ComingSoon' 5 | import {TabbedPage} from 'components/ui' 6 | 7 | import Describe from './Describe' 8 | const Leads = ComingSoon('Manage/Glance/Leads') 9 | import Applying from './Applying' 10 | 11 | export default sources => ({ 12 | pageTitle: of('Manage Team'), 13 | 14 | ...TabbedPage({...sources, 15 | tabs$: of([ 16 | {path: '/', label: 'Describe'}, 17 | // {path: '/leads', label: 'Leads'}, 18 | {path: '/applying', label: 'Applying'}, 19 | ]), 20 | routes$: of({ 21 | '/': Describe, 22 | '/leads': Leads, 23 | '/applying': Applying, 24 | }), 25 | }), 26 | }) 27 | -------------------------------------------------------------------------------- /src/root/Team/Members/Applied.js: -------------------------------------------------------------------------------- 1 | import {Observable} from 'rx' 2 | const {just} = Observable 3 | 4 | import { 5 | List, 6 | ListItem, 7 | } from 'components/sdm' 8 | 9 | import { 10 | Memberships, 11 | Profiles, 12 | Engagements, 13 | } from 'components/remote' 14 | 15 | const Item = sources => { 16 | const eKey$ = sources.item$ 17 | .pluck('engagementKey') 18 | 19 | const eng$ = eKey$ 20 | .flatMapLatest(Engagements.query.one(sources)) 21 | .combineLatest(sources.item$, (e, item) => ({...e, item})) 22 | .tap(e => e.profileKey || console.log('eng$',e)) 23 | .shareReplay(1) 24 | 25 | const profile$ = eng$ 26 | .pluck('profileKey') 27 | .flatMapLatest(Profiles.query.one(sources)) 28 | .shareReplay(1) 29 | 30 | return ListItem({...sources, 31 | title$: profile$.pluck('fullName'), 32 | }) 33 | } 34 | 35 | const AppList = sources => List({...sources, 36 | Control$: just(Item), 37 | rows$: sources.memberships$, 38 | }) 39 | 40 | const Fetch = sources => ({ 41 | memberships$: sources.teamKey$ 42 | .flatMapLatest(Memberships.query.byTeam(sources)), 43 | }) 44 | 45 | export default sources => { 46 | const _sources = {...sources, ...Fetch(sources)} 47 | 48 | // const inst = Instruct(_sources) 49 | const list = AppList(_sources) 50 | // const next = Next(_sources) 51 | 52 | // const items = [ 53 | // inst, 54 | // next, 55 | // list, 56 | // ] 57 | 58 | // const DOM = combineLatest( 59 | // _sources.memberships$.map(m => m.length > 0), 60 | // ...items.map(i => i.DOM), 61 | // (hasTeams, i, n, l) => div({},[ 62 | // hasTeams ? n : i, 63 | // l, 64 | // ]) 65 | // ) 66 | 67 | return { 68 | DOM: list.DOM, 69 | // queue$: list.queue$, 70 | // route$: next.route$, 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/root/Team/Members/FilteredView.js: -------------------------------------------------------------------------------- 1 | import {Observable} from 'rx' 2 | const {of, merge} = Observable 3 | import {div} from 'helpers' 4 | 5 | import {combineLatestToDiv} from 'util' 6 | 7 | import { 8 | Profiles, 9 | Engagements, 10 | } from 'components/remote' 11 | 12 | import { 13 | List, 14 | ListItemNavigating, 15 | } from 'components/sdm' 16 | 17 | import {Detail} from './Detail' 18 | 19 | const Item = sources => { 20 | const eng$ = sources.item$ 21 | .pluck('engagementKey') 22 | .flatMapLatest(Engagements.query.one(sources)) 23 | .shareReplay(1) 24 | 25 | // const profile$ = sources.item$.pluck('authorProfileKey') 26 | const profile$ = eng$ 27 | .map(x => x && x.profileKey || x.authorProfileKey) 28 | .filter(x => !!x) 29 | .flatMapLatest(Profiles.query.one(sources)) 30 | .shareReplay(1) 31 | 32 | const {DOM, route$} = ListItemNavigating({...sources, 33 | title$: profile$.pluck('fullName'), 34 | iconSrc$: profile$.pluck('portraitUrl'), 35 | path$: sources.item$.map(({$key}) => 36 | sources.router.createHref(`/show/${$key}`) 37 | ), 38 | }) 39 | 40 | return { 41 | DOM: DOM.startWith(div([null])), 42 | route$, 43 | } 44 | } 45 | 46 | const AppList = sources => List({...sources, 47 | Control$: of(Item), 48 | rows$: sources.memberships$, 49 | }) 50 | 51 | const EmptyNotice = sources => ({ 52 | DOM: sources.items$.map(i => 53 | i.length > 0 ? null : div({},['Empty notice']) 54 | ), 55 | }) 56 | 57 | const FilteredView = sources => { 58 | const detail = Detail(sources) 59 | const list = AppList(sources) 60 | const mt = EmptyNotice({...sources, items$: sources.memberships$}) 61 | 62 | return { 63 | DOM: combineLatestToDiv(mt.DOM, list.DOM, detail.DOM), 64 | route$: merge(list.route$, detail.route$.map(sources.router.createHref)), 65 | queue$: detail.queue$, 66 | } 67 | } 68 | 69 | export {FilteredView} 70 | -------------------------------------------------------------------------------- /src/root/Team/Schedule/AssignmentItem.js: -------------------------------------------------------------------------------- 1 | import {Observable as $} from 'rx' 2 | 3 | import { 4 | ProfileAvatar, 5 | ProfileFetcher, 6 | } from 'components/profile' 7 | 8 | import { 9 | ListItemWithMenu, 10 | MenuItem, 11 | } from 'components/sdm' 12 | 13 | import { 14 | Assignments, 15 | Engagements, 16 | } from 'components/remote' 17 | 18 | const _Remove = sources => MenuItem({...sources, 19 | iconName$: $.of('remove'), 20 | title$: $.of('Remove'), 21 | }) 22 | 23 | export const AssignmentItem = sources => { 24 | const engagement$ = sources.item$.pluck('engagementKey') 25 | .flatMapLatest(Engagements.query.one(sources)) 26 | 27 | const profileKey$ = engagement$.pluck('profileKey') 28 | 29 | const pf = ProfileFetcher({...sources, profileKey$}) 30 | const _sources = {...sources, profileKey$, profile$: pf.profile$} 31 | const title$ = $.combineLatest( 32 | _sources.profile$.map(p => p && p.fullName), 33 | _sources.item$.pluck('$key'), 34 | (fullName, $key) => fullName || `DELETE ME: ${$key}` 35 | ) 36 | 37 | const rem = _Remove(_sources) 38 | 39 | const li = ListItemWithMenu({..._sources, 40 | leftDOM$: ProfileAvatar(_sources).DOM, 41 | menuItems$: $.of([rem.DOM]), 42 | title$, 43 | }) 44 | 45 | const queue$ = rem.click$ 46 | .flatMapLatest(_sources.item$) 47 | .pluck('$key') 48 | .map(Assignments.action.remove) 49 | 50 | return { 51 | DOM: li.DOM, 52 | queue$, 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/root/Team/Schedule/Overview.js: -------------------------------------------------------------------------------- 1 | import {Observable} from 'rx' 2 | const {of} = Observable 3 | import {InputControl} from 'components/sdm' 4 | import {RaisedButton} from 'components/sdm' 5 | import {combineLatestToDiv} from 'util' 6 | 7 | export default (sources) => { 8 | const ic = InputControl({ 9 | label$: of('Choose a day to start adding shifts! (YYYY-MM-DD)'), 10 | ...sources, 11 | }) 12 | const rb = RaisedButton({label$: of('Add Date'), ...sources}) 13 | const route$ = ic.value$ 14 | .sample(rb.click$) 15 | .combineLatest( 16 | sources.teamKey$, 17 | (date, team) => `/team/${team}/schedule/shifts` + (date ? `/${date}` : '') 18 | ) 19 | return { 20 | DOM: combineLatestToDiv(ic.DOM, rb.DOM), 21 | route$, 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/root/Team/Schedule/ShiftForm.js: -------------------------------------------------------------------------------- 1 | import {Observable as $} from 'rx' 2 | 3 | import { 4 | //ListItemCheckbox, 5 | InputControl, 6 | } from 'components/sdm' 7 | 8 | import {Form} from 'components/ui/Form' 9 | 10 | const StartsInput = sources => InputControl({...sources, 11 | label$: $.of('Starts At Hour (24 hour)'), 12 | }) 13 | 14 | const HoursInput = sources => InputControl({...sources, 15 | label$: $.of('Hours'), 16 | }) 17 | 18 | const PeopleInput = sources => InputControl({...sources, 19 | label$: $.of('People (Number)'), 20 | }) 21 | 22 | // const ToggleBonus = sources => ListItemCheckbox({...sources, 23 | // titleTrue$: $.of('Bonus'), 24 | // titleFalse$: $.of('Normal'), 25 | // }) 26 | 27 | export const ShiftForm = sources => Form({...sources, 28 | Controls$: $.of([ 29 | {field: 'start', Control: StartsInput}, 30 | {field: 'hours', Control: HoursInput}, 31 | {field: 'people', Control: PeopleInput}, 32 | // {field: 'bonus', Control: ToggleBonus}, 33 | ]), 34 | }) 35 | -------------------------------------------------------------------------------- /src/root/styles.scss: -------------------------------------------------------------------------------- 1 | @import url("http://fonts.googleapis.com/css?family=Montserrat+Alternates"); 2 | 3 | @import url("http://fonts.googleapis.com/css?family=Roboto"); 4 | 5 | @import url("http://fonts.googleapis.com/css?family=Amatic+SC"); 6 | 7 | * { 8 | font-family: 'Roboto'; 9 | } 10 | 11 | a { 12 | color: #eb8c58; 13 | } 14 | 15 | html, body { 16 | height: 100%; 17 | } 18 | 19 | #root { 20 | display: flex; 21 | } 22 | 23 | #root, #root > div { 24 | xheight: 100%; 25 | min-height: 100%; 26 | } 27 | 28 | #root > div { 29 | display: flex; 30 | flex-flow: column; 31 | flex: 1 1 auto; 32 | } 33 | 34 | body { 35 | background-color: #fff; 36 | overflow-x: hidden; 37 | } 38 | 39 | .container { 40 | margin: 0 auto; 41 | padding: 0em 1em; 42 | max-width: 1024px; 43 | flex: 1 1 100%; 44 | } 45 | 46 | .secondary { 47 | color: #999; 48 | } 49 | 50 | .scrollable { 51 | overflow: auto; 52 | max-height: 400px; 53 | } 54 | 55 | .row { 56 | margin: 0; 57 | display: flex; 58 | } 59 | 60 | .nav-button .icon-menu { 61 | color: white !important; 62 | } 63 | 64 | .amount { 65 | font-size: 18px; 66 | font-weight: bold; 67 | color: #666; 68 | text-align: right; 69 | } 70 | 71 | .total { 72 | .right, .title { 73 | font-weight: bold; 74 | font-size: 18px; 75 | } 76 | } --------------------------------------------------------------------------------