├── .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 | []
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 |
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
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
53 | }
54 |
55 | let Label = props.renderer
56 | return
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 |
--------------------------------------------------------------------------------