├── .gitignore ├── README.md ├── activities ├── assessment.jsx ├── localReviews.js └── reviewPage.jsx ├── cell.jsx ├── index.js ├── package.json ├── pickerModal.jsx ├── style.css ├── tests ├── package.json └── reviewer.js └── utils └── widgets.jsx /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Gitter chat](https://badges.gitter.im/livablemedia/reasons.png)] 2 | (https://gitter.im/livablemedia/reasons) 3 | # Reasons 4 | 5 | Tools to support the collection of and analysis of user reasons, as per http://nxhx.org/maximizing/ 6 | 7 | ## Usage 8 | 9 | There's a database API, and several React widgets that work with that database. 10 | 11 | The database is built on Firebase, and in order to use it you’ll need to have users authenticate into the db, using code like this: 12 | 13 | ```js 14 | let FIREBASE = new Firebase('https://lifestyles.firebaseio.com/') 15 | FIREBASE.authWithOAuthPopup('google').then(YOUR CODE HERE) 16 | ``` 17 | 18 | The database lets you add reasons, list them, and find completions for a partially typed reason string. 19 | 20 | ```js 21 | import Reasons from ‘reasons’ 22 | console.log(Reasons.types()) 23 | let newReasonId = Reasons.add(‘response’, ‘bored’) 24 | Reasons.commonForActivity(facebookUseActivity).then(console.log) 25 | ``` 26 | 27 | There are widgets for picking reasons, and collecting reasons and outcomes from users about a chain of activities. 28 | 29 | And activity looks like the following: 30 | 31 | ```js 32 | let exampleActivity = { 33 | blame: 'http://facebook.com', 34 | elapsed: 30*60*1000, 35 | over: [Date.now()-24*60*60*1000, Date.now()], 36 | recognizer: 'indirect', 37 | 38 | favIconUrl: 'https://static.xx.fbcdn.net/rsrc.php/yl/r/H3nktOa7ZMg.ico', 39 | 40 | verbPhrase: ‘browsing links’, 41 | examples: ‘A, B, and C’, 42 | } 43 | ``` 44 | 45 | An `assessment` consist of the reported reasons and outcomes for an activity: 46 | 47 | ```js 48 | let assessment = { 49 | author: userID, 50 | assessment: feeling, 51 | reasons: [reason.id], 52 | time: Date.now(), 53 | 54 | blame: activity.blame, 55 | elapsed: activity.elapsed, 56 | over: activity.over, 57 | recognizer: activity.recognizer, 58 | verbPhrase: activity.verbPhrase 59 | } 60 | ``` 61 | 62 | 63 | 64 | ## Getting started 65 | 66 | `npm install` 67 | 68 | 69 | 70 | ## Contributing 71 | 72 | * https://gitter.im/livablemedia/reasons 73 | * https://groups.google.com/forum/#!members/livable-media 74 | -------------------------------------------------------------------------------- /activities/assessment.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import moment from 'moment' 3 | import url from 'url' 4 | 5 | import { 6 | TableViewCell 7 | } from 'react-ratchet' 8 | 9 | import { 10 | LabelizingTextField, 11 | TableRenderer, 12 | ButtonBar, 13 | Pager 14 | } from '../utils/widgets.jsx' 15 | 16 | import {ReasonCell, ReasonLabel} from '../cell.jsx' 17 | import ReasonPickerModal from '../pickerModal.jsx' 18 | import Reasons from '..' 19 | 20 | let DAYS = 24*60*60*1000 21 | 22 | 23 | 24 | 25 | export default class ActivityAssessment extends React.Component { 26 | 27 | constructor(props){ 28 | super(props) 29 | this.state = { active: false, reason: null } 30 | } 31 | 32 | clicked(x){ 33 | if (!this.state.reason || !this.state.reason.id) return 34 | if (x == 'skipped') return this.props.onSkipped(this.props.activity) 35 | let {onWasAssessed, activity} = this.props 36 | Reasons.pushAssessment(activity, x, this.state.reason) 37 | onWasAssessed(activity, x, this.state.reason) 38 | } 39 | 40 | setState(x){ 41 | if (x.reason === undefined) return super.setState(x) 42 | if (x.reason && x.reason.id) x.matches = null 43 | else if (!x.reason){ 44 | Reasons.commonForActivity(this.props.activity, 3).then( 45 | m => this.setState({matches: m}) 46 | ) 47 | } else { 48 | Reasons.completions(null, this.state.reason, 3).then( 49 | m => this.setState({matches: m}) 50 | ) 51 | } 52 | super.setState(x) 53 | } 54 | 55 | render(){ 56 | let { active, reason, matches } = this.state 57 | let solidReason = reason && reason.id 58 | let { activity } = this.props 59 | let elapsed = moment.duration(activity.elapsed).humanize() 60 | let span = activity.over[1] - activity.over[0] 61 | var timeframe = moment(activity.over[0]).format('dddd'), connective = 'on' 62 | if (span > 2*DAYS){ 63 | connective = 'over' 64 | timeframe = moment.duration(span).humanize() 65 | } 66 | let hostname = url.parse(activity.blame).host 67 | 68 | return
69 | 70 | {elapsed} 71 | 72 |
73 | this.setState({reason:null, active:true})} 79 | onFocusChanged={x => this.setState({active:x})} 80 | onTyped={text => this.setState({reason:text})} 81 | accessory={ 82 | { 84 | this.context.pager.pushSubpage( 85 | this.setState({reason:obj})} 87 | /> 88 | ) 89 | }} 90 | > 91 | 92 | 93 | } 94 | /> 95 |
96 | 97 | { 105 | this.context.pager.pushSubpage( 106 | this.setState({reason:obj})} 109 | /> 110 | ) 111 | }} 112 | > 113 | Add a new reason 114 | 115 | } 116 | onClicked={obj => this.setState({reason: obj})} 117 | /> 118 | 119 | { 120 | solidReason && this.clicked(x)} 122 | disabled={!reason || !reason.id} 123 | buttons={[ 124 | ["Don't know yet", "refresh", "skipped"], 125 | ["Not good for this", "trash", "nogood"], 126 | ["Worked out", "star", "good"] 127 | ]}/> 128 | } 129 | 130 | 131 |
132 | You were {activity.verbPhrase} like {activity.examples} 133 |
134 | {connective} {timeframe} 135 |
136 |
137 |
138 | } 139 | 140 | } 141 | 142 | ActivityAssessment.contextTypes = { 143 | pager: React.PropTypes.instanceOf(Pager) 144 | } 145 | 146 | 147 | 148 | 149 | 150 | // import EntryField from '../entryfield.jsx' 151 | // export default class AssessmentCard extends Component { 152 | // 153 | // setReason(r){ 154 | // this.setState({reasons: r}) 155 | // console.log(r) 156 | // } 157 | // 158 | // done(how){ 159 | // let {onComplete, activity} = this.props 160 | // onComplete(activity, how, this.state.reason) 161 | // } 162 | // 163 | // render(){ 164 | // let {activity} = this.props 165 | // let elapsed = moment.duration(activity.elapsed).humanize() 166 | // let timeframe = moment.duration(activity.over[1] - activity.over[0]).humanize() 167 | // 168 | // return ( 169 | //
170 | // 171 | // 172 | // {elapsed} over {timeframe}, 173 | // {activity.desc.was}: {activity.blame} 174 | // 175 | // 176 | // {activity.desc.details} 177 | // 178 | // 179 | // 180 | // 181 | // 182 | // Bad choice 183 | // Skip 184 | // Good choice 185 | // 186 | // 187 | //
188 | // ) 189 | // } 190 | // 191 | // } 192 | -------------------------------------------------------------------------------- /activities/localReviews.js: -------------------------------------------------------------------------------- 1 | // TODO: dispositions should use dexie, not localStorage 2 | 3 | var activitiesSource, availableCb 4 | var dispositions = {} 5 | if (localStorage.dispositions) dispositions = JSON.parse(localStorage.dispositions) 6 | 7 | 8 | export default { 9 | 10 | setActivitiesSource(source){ 11 | activitiesSource = source 12 | if (availableCb) this.recalculate() 13 | }, 14 | 15 | onActivitiesForReviewAvailable(cb){ 16 | availableCb = cb 17 | if (activitiesSource) this.recalculate() 18 | }, 19 | 20 | forActivity(a){ 21 | let two_weeks_ago = Date.now() - 2*7*24*60*60*1000 22 | let id = `${a.blame}::${a.recognizer}` 23 | let d = dispositions[id] 24 | if (d && d[asOf] > two_weeks_ago) return d.feeling 25 | }, 26 | 27 | wasReviewed(activity, feeling){ 28 | let { blame, recognizer } = activity 29 | let id = `${blame}::${recognizer}` 30 | dispositions[id] = { 31 | feeling: feeling, 32 | asOf: Date.now() 33 | } 34 | this.recalculate() 35 | localStorage.dispositions = JSON.stringify(dispositions) 36 | // chrome.storage.local.set({dispositions:dispositions}) 37 | }, 38 | 39 | removeFeeling(feeling){ 40 | Object.keys(dispositions).forEach(k => { 41 | if (dispositions[k].feeling = feeling) delete dispositions[k] 42 | }) 43 | localStorage.dispositions = JSON.stringify(dispositions) 44 | this.recalculate() 45 | }, 46 | 47 | hasFeeling(feeling){ 48 | for (var k in dispositions){ 49 | if (dispositions[k].feeling == feeling) return true 50 | } 51 | }, 52 | 53 | recalculate(){ 54 | let two_weeks_ago = Date.now() - 2*7*24*60*60*1000 55 | activitiesSource().then(activities => { 56 | availableCb( 57 | activities.filter(a => { 58 | let id = `${a.blame}::${a.recognizer}` 59 | let d = dispositions[id] 60 | return !d || d.asOf < two_weeks_ago 61 | }) 62 | ) 63 | }) 64 | } 65 | 66 | } 67 | -------------------------------------------------------------------------------- /activities/reviewPage.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { NavBar, NavButton, Title } from 'react-ratchet' 3 | import { Pager } from '../utils/widgets.jsx' 4 | 5 | import Assessment from './assessment.jsx' 6 | import LocalReviews from './localReviews' 7 | 8 | 9 | export default class ActivitiesReviewPage extends React.Component { 10 | 11 | constructor(props){ 12 | super(props) 13 | LocalReviews.setActivitiesSource(props.activitySource) 14 | this.state = { activities: props.activities } 15 | } 16 | 17 | componentWillMount(){ 18 | LocalReviews.onActivitiesForReviewAvailable( 19 | (activities) => this.setState({activities: activities}) 20 | ) 21 | } 22 | 23 | onSkipped(activity){ 24 | LocalReviews.wasReviewed(activity, 'skipped') 25 | } 26 | 27 | onWasAssessed(activity, how, reason){ 28 | let { activitySource, wasReviewed } = this.props 29 | if (wasReviewed) wasReviewed(activity) 30 | LocalReviews.wasReviewed(activity, how) 31 | } 32 | 33 | unskip(){ 34 | LocalReviews.removeFeeling('skipped') 35 | } 36 | 37 | render(){ 38 | let { user } = this.props 39 | let { activities } = this.state 40 | if (!activities) return
Loading...
41 | let hasSkipped = LocalReviews.hasFeeling('skipped') 42 | if (!activities.length) return
43 | No activity to review! Just use Chrome for 20 minutes or so. 44 | { 45 | hasSkipped && this.unskip()}>Show skipped 46 | } 47 |
48 | return 49 | Assess! 50 |
51 | { 52 | activities.map(a => ( 53 | this.onSkipped(x)} 55 | onWasAssessed={(a,h,r) => this.onWasAssessed(a,h,r)} 56 | activity={a} 57 | key={`${a.blame} ${a.over[0]}`} 58 | /> 59 | )) 60 | } 61 |
62 |
63 | } 64 | 65 | } 66 | -------------------------------------------------------------------------------- /cell.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { TableViewCell, Button } from 'react-ratchet' 3 | import { TextField } from './utils/widgets.jsx' 4 | import Reasons from '.' 5 | 6 | 7 | export const ReasonLabel = ({x}) => { 8 | let sentence = Reasons.sentence(x.type) 9 | return
10 | {sentence[0]} 11 | {x.title} 12 | {sentence[1]} 13 |
14 | } 15 | 16 | export const ReasonCell = ({x, onClick}) => { 17 | let sentence = Reasons.sentence(x.type) 18 | let relationSentence = Reasons.relationSentence(x.rel) 19 | return 20 | {sentence[0]} 21 | {x.title} 22 | {sentence[1]} 23 | { 24 | x.rel &&
25 | ( 26 | {relationSentence[0]} 27 | {x.rel[1]} 28 | {relationSentence[1]} 29 | ) 30 |
31 | } 32 |
33 | } 34 | 35 | 36 | export class ReasonAdderCell extends React.Component { 37 | 38 | constructor(props){ 39 | super(props) 40 | this.state = { active: false, text: props.startingText || "" } 41 | } 42 | 43 | makeReason(){ 44 | let {text} = this.state 45 | let {type} = this.props 46 | if (!text) return 47 | let r = Reasons.addReason(type, text) 48 | this.props.onAdded(r) 49 | } 50 | 51 | render(){ 52 | let {text, active} = this.state 53 | let {type} = this.props 54 | let sentence = Reasons.sentence(type) 55 | return this.setState({active:true})} 57 | > 58 | {sentence[0]} 59 | this.setState({active:x})} 63 | onTyped={text => this.setState({text:text})} 64 | onSubmitted={() => this.makeReason()} 65 | /> 66 | {sentence[1]} 67 | { 68 | active && 69 | 75 | } 76 | 77 | } 78 | 79 | } 80 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | // DONE 2 | // Reasons.types 3 | // Reasons.type(x) => { prefix: ....} 4 | // Reasons.commonForActivity() => promise 5 | // Reasons.addReason() => promise 6 | // Reasons.completions => promise 7 | 8 | 9 | // TODO: auth rules, so reasons can be added but not edited 10 | 11 | // /reasons//: (prio is atime?) 12 | // /reasonsMeta/:{addedby, ctime} 13 | // /terms/:{syn, hypo/hyper, instr/yield} (prio is atime?) 14 | 15 | import Firebase from 'firebase' 16 | import 'core-js/fn/object/values' 17 | let FIREBASE = new Firebase('https://lifestyles.firebaseio.com') 18 | 19 | 20 | // MONITOR A COMPACT REPRESENTATION OF ALL REASONS 21 | var reasons 22 | FIREBASE.child('compactReasons').on('value', snap => { 23 | let newReasons = {} 24 | let val = snap.val() 25 | for (var type in val){ 26 | for (var id in val[type]){ 27 | newReasons[id] = { 28 | id: id, 29 | type: type, 30 | title: val[type][id] 31 | } 32 | } 33 | } 34 | reasons = newReasons 35 | }) 36 | 37 | 38 | 39 | var allTerms, termIndex, indexedReasons 40 | 41 | 42 | 43 | let sentences = { 44 | response: "I was feeling ___", 45 | tendency: "I often ___", 46 | become: "I want to be more ___", 47 | do: "I'd like to ___ more often", 48 | } 49 | let relationPhrases = { 50 | "syn": "___ means the same thing", 51 | "yield": "makes ___ possible", 52 | "hypo": "is a type of ___" 53 | } 54 | 55 | 56 | 57 | export default { 58 | 59 | addReason(type, title){ 60 | let ref = FIREBASE.child('compactReasons').child(type).push(title) 61 | let r = { 62 | id: ref.key(), 63 | type: type, 64 | title: title, 65 | author: FIREBASE.getAuth().uid, 66 | ctime: Date.now() 67 | } 68 | FIREBASE.child('metadata').child('reasons').child(ref.key()).update(r) 69 | return r 70 | }, 71 | 72 | pushAssessment(activity, feeling, reason){ 73 | let userID = FIREBASE.getAuth().uid 74 | let assessment = { 75 | author: userID, 76 | assessment: feeling, 77 | reasons: [reason.id], 78 | ctime: Date.now(), 79 | 80 | blame: activity.blame, 81 | elapsed: activity.elapsed, 82 | over: activity.over, 83 | recognizer: activity.recognizer, 84 | verbPhrase: activity.verbPhrase 85 | } 86 | 87 | console.log('pushAssessment', assessment) 88 | FIREBASE.child('assessments').child(userID).push(assessment) 89 | }, 90 | 91 | registerUser(data){ 92 | let authData = FIREBASE.getAuth() 93 | data.uid = authData.uid 94 | data.provider = authData.provider 95 | FIREBASE.child('metadata').child('users').child(authData.uid).update(data) 96 | }, 97 | 98 | 99 | ///// 100 | 101 | sentence(x){ 102 | if (!sentences[x]) return ['error', 'error'] 103 | else return sentences[x].split('___') 104 | }, 105 | 106 | relationSentence(rel){ 107 | if (!relationPhrases[rel]) return ['error', 'error'] 108 | return relationPhrases[rel].split('___') 109 | }, 110 | 111 | types(){ 112 | return Object.keys(sentences) 113 | }, 114 | 115 | 116 | ///// 117 | 118 | // TODO: Reasons#commonForActivity 119 | commonForActivity(a){ 120 | return new Promise((s,f) => s([])) 121 | }, 122 | 123 | 124 | // completions!! 125 | 126 | completions(type, str, count){ 127 | this.reindexReasons() 128 | let results = [] 129 | allTerms.filter( x => x.indexOf(str) != -1 ).slice(0,count).forEach( m => { 130 | let entries = termIndex[m] // [[reasonId, rel, term, reasonName]] 131 | entries.forEach(idx => { 132 | if (!reasons[idx[0]]) return console.log(idx[0], 'not found') 133 | if (!type || reasons[idx[0]].type == type) { 134 | if (idx[1] == 'is') results.push(reasons[idx[0]]) 135 | else results.push({ 136 | id: idx[0], 137 | type: reasons[idx[0]].type, 138 | title: idx[3], 139 | rel: [idx[1], idx[2]] 140 | }) 141 | } 142 | }) 143 | }) 144 | return new Promise((s,f) => s(results)) 145 | }, 146 | 147 | reindexReasons(){ 148 | if (indexedReasons == reasons) return 149 | indexedReasons = reasons 150 | var terms = termIndex = {} 151 | for (var reasonId in reasons){ 152 | var c = reasons[reasonId] 153 | if (!terms[c.title]) terms[c.title] = [] 154 | var x = terms[c.title] 155 | // console.log('terms[c.title]', x, c.title, x.push) 156 | x.push([c.id, 'is', c.title, c.title]); 157 | 158 | // add all aliases, hyper/hyponyms, and payoffs to the terms database 159 | (['syn', 'hypo', 'hyper', 'yield']).forEach( rel => { 160 | if (c[rel]) c[rel].forEach( x => { 161 | if (!terms[x]) terms[x] = [] 162 | terms[x].push([c.id, rel, x, c.title]) 163 | }) 164 | }) 165 | } 166 | allTerms = Object.keys(terms) 167 | } 168 | 169 | } 170 | 171 | 172 | // reasonsOfType(type){ 173 | // return Object.values(reasons).filter(r => r.type == type) 174 | // }, 175 | 176 | 177 | // commonReasons(resource, type){ 178 | // return [] 179 | // } 180 | // reasonData(id){ 181 | // return this.reasons[id] || { title: 'unknown' } 182 | // } 183 | // getReasons(resource){ 184 | // return Object.keys(this.reviews[resource] || {}) 185 | // } 186 | // addReasonWithId(u, resource, id){ 187 | // this.fbProfile(u,`reviews/${e(resource)}/${e(id)}/_`).set(true) 188 | // }, 189 | 190 | 191 | // fbreasons.on('value', snap => {reasons = decode(snap.val()||{})}) 192 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "reasons", 3 | "author": "Joe Edelman", 4 | "version": "0.1.3", 5 | "license": "ISC", 6 | "repository": "livable/reasons", 7 | "main": "index.js", 8 | "style": "style.css", 9 | "browserify": { 10 | "transform": [ 11 | [ 12 | "babelify", 13 | { 14 | "presets": [ 15 | "react", 16 | "es2015" 17 | ] 18 | } 19 | ] 20 | ] 21 | }, 22 | "peerDependencies": {}, 23 | "dependencies": { 24 | "core-js": "^2.2.1", 25 | "firebase": "latest", 26 | "moment": "^2.12.0", 27 | "react": "latest", 28 | "react-dom": "latest", 29 | "react-ratchet": "jxe/react-ratchet" 30 | }, 31 | "devDependencies": { 32 | "babel-preset-es2015": "", 33 | "babel-preset-react": "^6.5.0", 34 | "babelify": "^7.2.0", 35 | "browserify": "^13.0.0", 36 | "watchify": "^3.7.0", 37 | "css-img-datauri-stream": "^0.1.5" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /pickerModal.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { 3 | TextField, 4 | Modal, 5 | Pager, 6 | Tabs, 7 | TableRenderer 8 | } from './utils/widgets.jsx' 9 | 10 | import { ReasonCell, ReasonAdderCell } from './cell.jsx' 11 | import Reasons from '.' 12 | 13 | 14 | 15 | export default class ReasonPickerModal extends React.Component { 16 | 17 | constructor(props){ 18 | super(props) 19 | this.state = { text: "", type: Reasons.types()[0] } 20 | } 21 | 22 | componentDidMount(){ 23 | Reasons.completions(this.state.type, "", 200).then( 24 | m => this.setState({matches: m}) 25 | ) 26 | } 27 | 28 | setState(x){ 29 | if (x.text === undefined && !x.type) return super.setState(x) 30 | let newType = x.type || this.state.type 31 | let newText = x.text === undefined ? this.state.text : x.text 32 | Reasons.completions(newType, newText, 200).then( 33 | m => this.setState({matches: m}) 34 | ) 35 | super.setState(x) 36 | } 37 | 38 | render(){ 39 | let {text, type, matches, active} = this.state 40 | let onPicked = (obj) => { 41 | this.context.pager.popSubpage() 42 | this.props.onPicked(obj) 43 | } 44 | return 45 | 54 |
55 | this.setState({type:t})} 59 | /> 60 | 67 | } 68 | list={matches} 69 | cells={ReasonCell} 70 | onClicked={onPicked} 71 | /> 72 |
73 |
74 | } 75 | 76 | } 77 | 78 | ReasonPickerModal.contextTypes = { 79 | pager: React.PropTypes.instanceOf(Pager) 80 | } 81 | 82 | 83 | 84 | 85 | // export default class ReasonBrowser extends Component { 86 | // 87 | // render(){ 88 | // let {type} = this.state || {type: "furtherance"} 89 | // let pager = this.context.pager 90 | // return ( 91 | //
92 | // 93 | // Close 94 | // 95 | // Reasons 96 | // 97 | // 98 | // 112 | // 113 | // 114 | // { 115 | // Reasons.reasonsOfType(type).map(x => ( 116 | // {x.title} 117 | // )) 118 | // } 119 | // 120 | //
121 | // ) 122 | // } 123 | // 124 | // } 125 | -------------------------------------------------------------------------------- /style.css: -------------------------------------------------------------------------------- 1 | .Activity { 2 | position: relative; 3 | padding: 5px; 4 | padding-top: 30px; 5 | border-radius: 5px; 6 | background: #eee; 7 | margin-top:15px; 8 | } 9 | 10 | .Activity> b { 11 | font-size: 14px; 12 | text-transform: uppercase; 13 | position: absolute; 14 | right: 10px; 15 | top: 5px; 16 | color: #777; 17 | font-weight: normal; 18 | } 19 | 20 | .Activity > img { 21 | position: absolute; 22 | top: -14px; 23 | left: 5px; 24 | width: 32px; 25 | text-align: center; 26 | border-radius: 50%; 27 | } 28 | 29 | .Activity .Details { 30 | /*border-left: 4px gray solid;*/ 31 | /*padding-left: 10px;*/ 32 | font-size: 14px; 33 | color: #666; 34 | margin: 0px 20px; 35 | margin-top: 15px; 36 | } 37 | 38 | .Activity .tstamp { 39 | font-size: 10px; 40 | text-transform: uppercase; 41 | color: #777; 42 | font-weight: normal; 43 | } 44 | 45 | .Activity .footer { 46 | text-align: right; 47 | font-size: 14px; 48 | /*text-transform: uppercase;*/ 49 | } 50 | 51 | .Activity .footer .icon { 52 | font-size: 14px; 53 | } 54 | 55 | .LabelizingTextField { 56 | position: relative; 57 | } 58 | 59 | .LabelizingTextField .accessory { 60 | position: absolute; 61 | right: 10px; 62 | top: 5px; 63 | } 64 | 65 | .LabelizingTextField.LabelBox { 66 | border: solid thin #888; 67 | margin: 5px 5px; 68 | border-radius: 5px; 69 | position: relative; 70 | } 71 | 72 | .LabelizingTextField.LabelBox .tfLabel { 73 | margin: 3px 10px; 74 | padding: 2px 5px; 75 | background: #dedede; 76 | display: inline-block; 77 | border-radius: 5px; 78 | } 79 | 80 | .LabelizingTextField.LabelBox > span { 81 | position: absolute; 82 | right: 10px; 83 | top: 4px; 84 | } 85 | 86 | .ButtonBar { 87 | display: flex; 88 | } 89 | 90 | input[type="search"] { 91 | margin-bottom: 6px; 92 | } 93 | 94 | .bonusRow { 95 | color: #888; 96 | font-style: italic; 97 | } 98 | 99 | .ButtonBar .ButtonBarButton { 100 | flex: 1.0; 101 | padding: 5px; 102 | text-align: center; 103 | background: #e6e5e5; 104 | margin: 0px 4px; 105 | font-size: 15px; 106 | font-variant: small-caps; 107 | padding: 5px 10px; 108 | border-radius: 5px; 109 | } 110 | 111 | .ButtonBar .ButtonBarButton .icon { 112 | margin-right: 5px; 113 | font-size: 16px; 114 | } 115 | 116 | 117 | 118 | .AutocompleteField{ 119 | background-color: #eee; 120 | padding: 2px 0px; 121 | } 122 | 123 | .AutocompleteField input { 124 | text-align: center; 125 | display: block; 126 | width: 275px; 127 | padding: 4px 10px; 128 | margin: 5px 10px; 129 | box-sizing: content-box; 130 | font-size: 16px; 131 | } 132 | 133 | .AutocompleteField input:focus { 134 | text-align: left; 135 | } 136 | 137 | .suggestions { 138 | /*padding: 10px;*/ 139 | box-shadow: black 0px 2px 3px; 140 | background: white; 141 | } 142 | 143 | .completion > b { 144 | font-size: 14px; 145 | } 146 | 147 | .completion p { 148 | color: gray; 149 | font-size: 11px; 150 | } 151 | -------------------------------------------------------------------------------- /tests/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "reasons-tests", 3 | "dependencies": { 4 | "react": "^0.14.7", 5 | "react-dom": "^0.14.7", 6 | "reasons": ".." 7 | }, 8 | "devDependencies": { 9 | "babel-preset-es2015": "latest", 10 | "babel-preset-react": "^6.5.0", 11 | "babelify": "^7.2.0", 12 | "browserify": "^13.0.0", 13 | "watchify": "^3.7.0", 14 | "css-img-datauri-stream": "^0.1.5", 15 | "parcelify": "^2.1.0" 16 | }, 17 | "browserify": { 18 | "transform": [ 19 | ["babelify", { "presets": ["react", "es2015"] }] 20 | ] 21 | }, 22 | "scripts": { 23 | "budo": "budo reviewer.js --css _build.css -- --debug -p [ parcelify -t css-img-datauri-stream -d . -o _build.css ]", 24 | "build": "browserify --debug -p [ parcelify -t css-img-datauri-stream -d . -o _build.css ] reviewer.js -o _build.js", 25 | "watch": "watchify -d -p [ parcelify -t css-img-datauri-stream -wo _build.css ] reviewer.js -o _build.js" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /tests/reviewer.js: -------------------------------------------------------------------------------- 1 | import Firebase from 'firebase' 2 | import React from 'react' 3 | import ReactDOM from 'react-dom' 4 | import ActivitiesPage from 'reasons/activities/reviewPage.jsx' 5 | let FIREBASE = new Firebase('https://lifestyles.firebaseio.com/') 6 | 7 | // trailId: trail.ctime, 8 | // steps: subset, 9 | // examples: desc.examples 10 | 11 | // favIconUrl: trail.favIconUrl, 12 | 13 | // blame: trail.blameUrl, 14 | // elapsed: totalElapsed, 15 | // over: [subset[0][0], subset[subset.length-1][1]], 16 | // recognizer: k, 17 | // verbPhrase: desc.verbPhrase, 18 | 19 | 20 | let exampleActivity = { 21 | blame: 'http://facebook.com', 22 | elapsed: 30*60*1000, 23 | over: [Date.now()-24*60*60*1000, Date.now()], 24 | recognizer: 'indirect', 25 | 26 | favIconUrl: 'https://static.xx.fbcdn.net/rsrc.php/yl/r/H3nktOa7ZMg.ico', 27 | 28 | verbPhrase: 'browsing links', 29 | examples: 'A, B, and C', 30 | } 31 | 32 | let exampleActivitySource = { 33 | forReview(){ 34 | return new Promise((s,f)=>s([exampleActivity])) 35 | } 36 | } 37 | 38 | FIREBASE.authWithOAuthPopup('google').then(() => { 39 | ReactDOM.render( 40 | new Promise((s,f) => s([exampleActivity]))} 42 | />, 43 | document.body 44 | ) 45 | }) 46 | 47 | // activitySource={exampleActivitySource} 48 | -------------------------------------------------------------------------------- /utils/widgets.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { 3 | NavBar, 4 | NavButton, 5 | Title, 6 | TableView, 7 | TableViewCell, 8 | SegmentedControl, 9 | ControlItem 10 | } from 'react-ratchet' 11 | 12 | 13 | export const Tabs = ({states, state, onChanged}) => ( 14 | 15 | { 16 | states.map( s => 17 | onChanged(s)} 20 | active={s == state} 21 | > 22 | {s} 23 | 24 | ) 25 | } 26 | 27 | ) 28 | 29 | 30 | 31 | export const TableRenderer = ({list, cells, firstCell, lastCell, onClicked}) => { 32 | let CellRenderer = cells 33 | // if (!CellRenderer) throw "What the fuck!" 34 | // console.log('CellRenderer', CellRenderer) 35 | if (!firstCell && !lastCell && (!list || !list.length)) return
36 | if (!list) list = [] 37 | let cellList = list.map( 38 | i => onClicked(i)}/> 39 | ) 40 | if (firstCell) cellList.unshift(firstCell) 41 | if (lastCell) cellList.push(lastCell) 42 | return { cellList } 43 | } 44 | 45 | 46 | 47 | export const LabelizingTextField = (props) => { 48 | if (!props.value || !props.value.id){ 49 | return
50 | 51 | {props.accessory} 52 | 53 | } 54 | 55 | let Label = props.renderer 56 | return
57 |
62 | } 63 | 64 | 65 | /// BIGGER ONES: Pager and TextField 66 | 67 | export class Pager extends React.Component { 68 | constructor(props){ 69 | super(props) 70 | this.state = {} 71 | } 72 | pushSubpage(x){ 73 | // console.log('pushSubpage', x) 74 | this.setState({subpage: x}) 75 | } 76 | 77 | popSubpage(){ 78 | this.setState({subpage: null}) 79 | } 80 | 81 | render(){ 82 | // console.log(this.state.subpage) 83 | return
84 |
85 | {this.props.children} 86 |
87 | {this.state.subpage} 88 |
89 | } 90 | 91 | getChildContext() { 92 | return {pager: this} 93 | } 94 | } 95 | 96 | Pager.childContextTypes = { 97 | pager: React.PropTypes.instanceOf(Pager) 98 | } 99 | 100 | 101 | export class TextField extends React.Component { 102 | 103 | // onStatus, onTyped, onSubmitted 104 | 105 | render(){ 106 | return this.props.onFocusChanged(true)} 114 | onBlur={() => this.props.onFocusChanged(false)} 115 | /> 116 | } 117 | 118 | onChange(evt){ 119 | this.props.onTyped(evt.currentTarget.value) 120 | } 121 | 122 | onKeyDown(evt){ 123 | let {value, onSubmitted} = this.props 124 | if (evt.key == 'Enter'){ 125 | if (value && onSubmitted) onSubmitted(value) 126 | evt.preventDefault() 127 | } 128 | } 129 | 130 | componentDidMount(){ 131 | // if (this.props.focus) this.refs.input.focus() 132 | // if (this.props.focus === false) this.refs.input.blur() 133 | } 134 | 135 | } 136 | 137 | export const ButtonBar = ({buttons, disabled, onClicked}) => ( 138 | 152 | ) 153 | 154 | export const Modal = ({title, children}, {pager}) => ( 155 |
156 | 157 | pager.popSubpage()}>Close 158 | {title} 159 | 160 | {children} 161 |
162 | ) 163 | 164 | Modal.contextTypes = { 165 | pager: React.PropTypes.instanceOf(Pager) 166 | } 167 | --------------------------------------------------------------------------------