├── .editorconfig ├── .gitignore ├── README.md ├── assets ├── hacktime.gif ├── happyface.png ├── poppedoutwindow.png └── popup.html ├── index.js ├── lib └── styles.js ├── package.json └── pages ├── README.md └── home.js /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # generated gitignore file 2 | 3 | # Logs 4 | logs 5 | *.log 6 | npm-debug.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # node-waf configuration 27 | .lock-wscript 28 | 29 | # Compiled binary addons (http://nodejs.org/api/addons.html) 30 | build/Release 31 | dist 32 | 33 | # Dependency directories 34 | node_modules 35 | jspm_packages 36 | 37 | # Optional npm cache directory 38 | .npm 39 | 40 | # Optional REPL history 41 | .node_repl_history 42 | 43 | # Mac OSX 44 | *.DS_Store 45 | .AppleDouble 46 | .LSOverride 47 | 48 | # Icon must end with two \r 49 | Icon 50 | 51 | 52 | # Thumbnails 53 | ._* 54 | 55 | # Files that might appear in the root of a volume 56 | .DocumentRevisions-V100 57 | .fseventsd 58 | .Spotlight-V100 59 | .TemporaryItems 60 | .Trashes 61 | .VolumeIcon.icns 62 | .com.apple.timemachine.donotpresent 63 | 64 | # Directories potentially created on remote AFP share 65 | .AppleDB 66 | .AppleDesktop 67 | Network Trash Folder 68 | Temporary Items 69 | .apdisk 70 | 71 | 72 | # Linux 73 | 74 | # temporary files which can be created if a process still has a handle open of a deleted file 75 | .fuse_hidden* 76 | 77 | # KDE directory preferences 78 | .directory 79 | 80 | # Linux trash folder which might appear on any partition or disk 81 | .Trash-* 82 | 83 | # Windows 84 | 85 | # Windows image file caches 86 | Thumbs.db 87 | ehthumbs.db 88 | 89 | # Folder config file 90 | Desktop.ini 91 | 92 | # Recycle Bin used on file shares 93 | $RECYCLE.BIN/ 94 | 95 | # Windows Installer files 96 | *.cab 97 | *.msi 98 | *.msm 99 | *.msp 100 | 101 | # Windows shortcuts 102 | *.lnk 103 | 104 | # VS Code 105 | .vscode 106 | 107 | #IntelliJ 108 | 109 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm 110 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 111 | 112 | # User-specific stuff: 113 | .idea/workspace.xml 114 | .idea/tasks.xml 115 | .idea/dictionaries 116 | .idea/vcs.xml 117 | .idea/jsLibraryMappings.xml 118 | 119 | # Sensitive or high-churn files: 120 | .idea/dataSources.ids 121 | .idea/dataSources.xml 122 | .idea/dataSources.local.xml 123 | .idea/sqlDataSources.xml 124 | .idea/dynamic.xml 125 | .idea/uiDesigner.xml 126 | 127 | # Gradle: 128 | .idea/gradle.xml 129 | .idea/libraries 130 | 131 | # Mongo Explorer plugin: 132 | .idea/mongoSettings.xml 133 | 134 | ## File-based project format: 135 | *.iws 136 | 137 | ## Plugin-specific files: 138 | 139 | # IntelliJ 140 | /out/ 141 | 142 | # mpeltonen/sbt-idea plugin 143 | .idea_modules/ 144 | 145 | # JIRA plugin 146 | atlassian-ide-plugin.xml 147 | 148 | # Crashlytics plugin (for Android Studio and IntelliJ) 149 | com_crashlytics_export_strings.xml 150 | crashlytics.properties 151 | crashlytics-build.properties 152 | fabric.properties 153 | 154 | # misc 155 | 156 | node_modules 157 | temp 158 | npm-debug.log 159 | .vscode 160 | .idea 161 | .history 162 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # choo-time 2 | 3 | ![asdfasdf](./assets/hacktime.gif) 4 | 5 | ## Quickstart 6 | 7 | It is quick and easy to start hacking time. Add it to your project using npm, implement it as a choo middleware, and then add a line of code to your reducers hash. 8 | 9 | installation from npm: 10 | 11 | `npm install choo-time --save-dev` 12 | 13 | add as a middleware: 14 | 15 | ``` 16 | const chooTime = require('choo-time') 17 | 18 | app.use(chooTime()) 19 | ``` 20 | 21 | add the "refresh" reducer 22 | 23 | ``` 24 | var myModel = { 25 | reducers: { 26 | refresh: (data, state) => state, 27 | ... 28 | } 29 | } 30 | ``` 31 | 32 | start your app and then you should see a red smiley face button 33 | in the button right of your app. 34 | 35 | ![happy face](./assets/happyface.png) 36 | 37 | Start click through your app and watch the tool button enumerate. 38 | This is a count of your actions. 39 | 40 | Click on the button and check out the complete timeline of actions for your app 41 | 42 | ![popped out window](http://i.imgur.com/eWPtjio.png) 43 | -------------------------------------------------------------------------------- /assets/hacktime.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trainyard/choo-time/7490a8a20a273bbcf977f8163c910bd3b34c745c/assets/hacktime.gif -------------------------------------------------------------------------------- /assets/happyface.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trainyard/choo-time/7490a8a20a273bbcf977f8163c910bd3b34c745c/assets/happyface.png -------------------------------------------------------------------------------- /assets/poppedoutwindow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trainyard/choo-time/7490a8a20a273bbcf977f8163c910bd3b34c745c/assets/poppedoutwindow.png -------------------------------------------------------------------------------- /assets/popup.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const choo = require('choo') 2 | const html = require('choo/html') 3 | const fileSaver = require('filesaver.js') 4 | const { btnStyle, popupStyle } = require('./lib/styles') 5 | 6 | const view = choo() 7 | const completeHistory = [] 8 | const _isMinimized = true; 9 | const buttonContainer = document.createElement('div') 10 | let _focusPayload; 11 | const hostWindow = window 12 | let focusWasSet = false; 13 | let timelineWindow = null; 14 | buttonContainer.id = '__history' 15 | document.body.appendChild(buttonContainer) 16 | 17 | const initialState = { 18 | history: completeHistory, 19 | focusPayload: completeHistory.length && completeHistory.slice(-1)[0] || {}, 20 | } 21 | const leModel = { 22 | state: initialState, 23 | reducers: { 24 | export: (_, state) => { 25 | let exportData = JSON.stringify({ history: state.history }, null, 2) 26 | let blob = new Blob([exportData], { type: "text/plain;charset=utf-8" }) 27 | let x = new Date() 28 | let fileName = `${document.title}-${x.getFullYear()}-${x.getMonth()+1}-${x.getDate()}[${x.getHours()}.${x.getMinutes()}.${x.getSeconds()}]` 29 | fileSaver.saveAs(blob, `${fileName}.json`) 30 | return state 31 | }, 32 | update: (data, state) => { 33 | return { history: data } 34 | }, 35 | focus: (data, state) => { 36 | _focusPayload = state.history[data].resultingState 37 | let previousPayload = state.history[data -1] 38 | if (previousPayload) { 39 | 40 | } 41 | focusWasSet = false; 42 | hostWindow.dispatchEvent(new CustomEvent("UPDATE_HOST_STATE", { detail: true } )) 43 | return { focusPayload: state.history[data] } 44 | } 45 | }, 46 | effects: { 47 | import: (data, state, send, done) => { 48 | const history = JSON.parse(data).history 49 | send('update', history, () => send('focus', history.length - 1, done)) 50 | } 51 | }, 52 | subscriptions: [ 53 | (send, done) => { 54 | window.addEventListener('UPDATE_STATE', function (e) { 55 | if (e && e.detail && e.detail.completeHistory) { 56 | send('update', e.detail.completeHistory, done) 57 | } else { 58 | done() 59 | } 60 | }, false); 61 | } 62 | ] 63 | } 64 | 65 | view.model(leModel) 66 | view.router((route) => [ 67 | route('/', require('./pages/home')) 68 | ]) 69 | 70 | function tardis () { 71 | buttonContainer.appendChild(_isMinimized ? renderButton() : html`
`) 72 | 73 | return { 74 | wrapReducers: (reducer) => (data, state) => { 75 | if (_focusPayload && !focusWasSet) { 76 | focusWasSet = true; 77 | return reducer(null, _focusPayload) 78 | } 79 | return reducer(data, state) 80 | }, 81 | wrapSubscriptions: (subs) => (send, done) => { 82 | window.addEventListener('UPDATE_HOST_STATE', () => send('refresh', {}, done), false); 83 | }, 84 | onAction: (args, state, name, route) => { 85 | console.log(`OnAction (${name}):`, { args, state, name, route }) 86 | }, 87 | onError: (err, state, send) => { 88 | console.error({err, state}) 89 | }, 90 | onStateChange: (args, state, data, prev, send) => { 91 | if (prev !== 'refresh') { 92 | completeHistory.push({ actionName: prev, argsPassedToAction: args, resultingState: state }) 93 | window.dispatchEvent(new CustomEvent("UPDATE_STATE", { detail: { completeHistory } } )) 94 | console.log('onStateChange:', { args, state, data, prev, completeHistory } ) 95 | buttonContainer.innerHTML = '' 96 | buttonContainer.appendChild(_isMinimized ? renderButton() : html`
`) 97 | } 98 | } 99 | } 100 | } 101 | 102 | 103 | const renderButton = () => html` 104 | 105 | ` 106 | 107 | function popWindowOut() { 108 | if (timelineWindow === null || timelineWindow.closed) { 109 | timelineWindow = window.open(``, "MsgWindow", "width=750,height=500"); 110 | timelineWindow.document.write('-') 111 | timelineWindow.document.body.innerHTML = '' 112 | const styleElement = html`` 113 | timelineWindow.document.body.appendChild(styleElement) 114 | timelineWindow.document.body.appendChild(view.start()) 115 | timelineWindow.focus() 116 | } else { 117 | timelineWindow.focus() 118 | } 119 | } 120 | 121 | 122 | module.exports = tardis 123 | -------------------------------------------------------------------------------- /lib/styles.js: -------------------------------------------------------------------------------- 1 | const btnStyle = ` 2 | padding: inherit; 3 | margin: inherit; 4 | display: inherit; 5 | background-color: #38818F; 6 | border-radius: 999em; 7 | border: inherit; 8 | min-width: 56px; 9 | height: 56px; 10 | box-shadow: 0 2px 5px 0 rgba(0, 0, 0, 0.26); 11 | line-height: 1; 12 | color: white; 13 | bottom: 8px; 14 | right: 8px; 15 | position: fixed; 16 | z-index: 2; 17 | font-size: 18px; 18 | cursor: pointer; 19 | display: inline-block; 20 | ` 21 | 22 | 23 | const popupStyle = ` 24 | /* 25 | #1d1f20 26 | #343436 27 | #666 28 | #ccc 29 | */ 30 | 31 | body { 32 | margin: 0; 33 | font-size: 12px; 34 | font-family: 'Monaco', courier, monospace; 35 | } 36 | 37 | .list-header { 38 | font-weight: bold; 39 | border-bottom: thin solid #333; 40 | padding: 6px 12px; 41 | } 42 | .list { 43 | list-style-type: none; 44 | margin: 0; 45 | padding: 0; 46 | border-radius: 2px; 47 | overflow: hidden; 48 | position: relative; 49 | } 50 | .list li { 51 | border-bottom: thin solid #333; 52 | line-height: 1.2rem; 53 | margin: 0; 54 | } 55 | button { 56 | outline: none; 57 | background: inherit; 58 | font-size: inherit; 59 | line-height: inherit; 60 | text-align: inherit; 61 | color: inherit; 62 | border: none; 63 | display: inline-block; 64 | width: 100%; 65 | padding: 0; 66 | margin: 0; 67 | cursor: pointer; 68 | padding: 6px 12px; 69 | transition: 0.2s; 70 | } 71 | 72 | 73 | .list button:hover { 74 | background: #999; 75 | color: #333; 76 | } 77 | .list button:focus { 78 | font-style: oblique; 79 | text-decoration: underline; 80 | background: #444; 81 | } 82 | .list button:active { 83 | background: #eee; 84 | } 85 | 86 | .action-col { 87 | box-sizing: border-box; 88 | background: #1d1f20; 89 | color: #ccc; 90 | vertical-align: top; 91 | width: 33%; 92 | display: inline-block; 93 | overflow:hidden; 94 | padding: 5px; 95 | position:absolute; 96 | height: 100%; 97 | } 98 | 99 | .state-col { 100 | margin-left: 33%; 101 | box-sizing: border-box; 102 | vertical-align: top; 103 | width: 66%; 104 | display: inline-block; 105 | overflow:hidden; 106 | } 107 | 108 | .state-col textarea { 109 | padding: 0.5em; 110 | width: 100%; 111 | height: 100%; 112 | min-height: 420px; 113 | border: none; 114 | outline: none; 115 | background-color: #f6f6f6; 116 | font-size: 14px; 117 | font-family: 'Monaco', courier, monospace; 118 | overflow: hidden; 119 | } 120 | 121 | .btn-control { 122 | background: #8c8c8c; 123 | color: #ffffff; 124 | padding: 4px 8px; 125 | display: inline-block; 126 | width: auto; 127 | border-radius: 8px; 128 | margin-left: 4px; 129 | } 130 | .btn-control:hover { 131 | color: #777; 132 | background: #eee; 133 | } 134 | ` 135 | 136 | module.exports = { 137 | popupStyle, 138 | btnStyle 139 | } 140 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "choo-time", 3 | "version": "0.2.0", 4 | "main": "index", 5 | "description": "Created with choo-cli", 6 | "license": "MIT", 7 | "scripts": { 8 | "start": "budo ./index.js --live --pushstate --open -- -g es2040", 9 | "lint": "standard --verbose | snazzy", 10 | "test": "npm run lint" 11 | }, 12 | "dependencies": { 13 | "choo": "^3.2.0", 14 | "filesaver.js": "^0.2.0" 15 | }, 16 | "devDependencies": { 17 | "browserify": "^13.0.1", 18 | "budo": "8.3.0", 19 | "es2040": "1.2.2", 20 | "envify": "^3.4.1", 21 | "standard": "^7.1.2", 22 | "snazzy": "^4.0.0", 23 | "uglifyify": "^3.0.2", 24 | "unassertify": "^2.0.3", 25 | "yo-yoify": "^3.3.0" 26 | }, 27 | "standard": { 28 | "ignore": [ 29 | "scripts" 30 | ] 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /pages/README.md: -------------------------------------------------------------------------------- 1 | ## history-view pages. 2 | 3 | Views that are directly mounted on the router 4 | 5 | More information: https://github.com/yoshuawuyts/choo-handbook/blob/master/guides/designing-for-reusability.md 6 | 7 | ### Generate 8 | 9 | ```bash 10 | $ choo generate page my-page 11 | ``` 12 | -------------------------------------------------------------------------------- /pages/home.js: -------------------------------------------------------------------------------- 1 | const html = require('choo/html') 2 | 3 | const changeHistory = e => send('focus', idx) 4 | 5 | function readFile(file, done) { 6 | var reader = new FileReader(); 7 | reader.onload = e => done(e.target.result); 8 | reader.readAsText(file); 9 | } 10 | 11 | module.exports = (state, prev, send) => html` 12 |
13 |
14 |
15 | Actions 16 | 17 | 26 | 27 |
28 | 38 |
39 |
40 | 41 |
42 |
43 | ` 44 | --------------------------------------------------------------------------------