├── .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 |
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 |
11 |
--------------------------------------------------------------------------------
/images/pitch/icon-direct.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
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 | }
--------------------------------------------------------------------------------