├── release └── .keep ├── src ├── style │ ├── tour.manifest.scss │ ├── popup.manifest.scss │ ├── main-ui.manifest.scss │ ├── tour │ │ ├── _action.scss │ │ ├── components │ │ │ ├── _task.scss │ │ │ ├── _folder-icon.scss │ │ │ ├── _favourite-button.scss │ │ │ ├── _trailblazer-button.scss │ │ │ ├── _stop-button.scss │ │ │ └── _record-button.scss │ │ └── main.scss │ ├── main-ui │ │ ├── _link.scss │ │ ├── _legend.scss │ │ ├── _public-map.scss │ │ ├── trails.scss │ │ ├── _node.scss │ │ ├── _node-popover.scss │ │ └── map.scss │ ├── _color.scss │ ├── _reset.scss │ ├── objects │ │ └── _button.scss │ └── popup │ │ └── popup.scss ├── content-scripts │ └── page-title.js ├── components │ ├── views │ │ ├── assignments.js │ │ ├── offline-data.js │ │ ├── popup │ │ │ ├── loading.jsx │ │ │ ├── recording.jsx │ │ │ ├── sign-in.jsx │ │ │ └── idle.jsx │ │ ├── popup.js │ │ ├── tour.js │ │ ├── public-map.js │ │ ├── tour │ │ │ ├── step-4.jsx │ │ │ ├── step-2.jsx │ │ │ ├── step-6.jsx │ │ │ ├── step-5.jsx │ │ │ ├── step-3.jsx │ │ │ ├── step-1.jsx │ │ │ ├── sign-in.jsx │ │ │ └── step-7.jsx │ │ ├── offline-data │ │ │ ├── show.jsx │ │ │ └── index.jsx │ │ └── assignments │ │ │ ├── show.jsx │ │ │ └── index.jsx │ ├── layouts │ │ ├── main-ui.jsx │ │ ├── tour.jsx │ │ └── popup.jsx │ ├── image-button.js │ ├── public-map-title.js │ ├── popover.js │ ├── link.js │ ├── assignment-item.js │ ├── share-map.js │ ├── star.js │ ├── assignment-title.js │ ├── map-view.js │ ├── legend.js │ ├── node.js │ ├── node-popover.js │ └── trail.js ├── util │ ├── message-channel.js │ ├── send-page-title.js │ ├── logger.js │ └── random-name.js ├── markup │ ├── popup.html │ ├── main-ui.html │ ├── offline-data.html │ └── tour.html ├── helpers │ ├── assignment-helper.js │ └── node-helper.js ├── lib │ └── store.js ├── config.js ├── core │ ├── install-hooks.js │ ├── extension-states.js │ └── extension-ui-state.js ├── services.js ├── scripts │ ├── public-map.jsx │ ├── popup.jsx │ ├── offline-data.jsx │ ├── main-ui.jsx │ ├── tour.jsx │ └── background.js ├── services │ ├── remote │ │ ├── assignment-service.js │ │ └── node-service.js │ └── local │ │ ├── node-service.js │ │ └── assignment-service.js ├── stores.js ├── background │ ├── content-scripts.js │ ├── proxy-change.js │ └── chrome-events.js ├── queries.js ├── db.js ├── stores │ ├── error-store.js │ ├── map-store.js │ ├── authentication-store.js │ └── assignment-store.js ├── decorators.js ├── constants.js └── adapter │ ├── trailblazer_http_storage_adapter.js │ └── chrome_identity_adapter.js ├── assets ├── icons │ ├── 1.jpg │ ├── 16.png │ ├── 19.png │ ├── 2.jpg │ ├── 3.jpg │ ├── 38.png │ ├── 48.png │ ├── 128.png │ ├── 19-unknown.png │ ├── 38-unknown.png │ ├── 19-recording.png │ ├── 38-recording.png │ ├── 36-onboarding-1.png │ ├── 36-onboarding-2.png │ ├── 36-onboarding-3.png │ ├── 36-onboarding-4.png │ ├── 36-onboarding-5.png │ ├── 36-onboarding-6.png │ ├── 36-onboarding-7.png │ ├── favourite.svg │ ├── dropdown-icon.svg │ ├── stop-icon.svg │ ├── new-icon.svg │ ├── delete-icon.svg │ ├── sign-out-icon.svg │ ├── tutorial-icon.svg │ ├── editable-icon.svg │ ├── tutorial-icon-light.svg │ ├── folder-icon.svg │ ├── trail-icon.svg │ ├── logo-watermark.svg │ └── logo-trailblazer.svg └── images │ ├── flag.png │ └── idle-background.png ├── .babelrc ├── NOTES ├── .gitignore ├── .editorconfig ├── CONTRIBUTING ├── script └── build.js ├── .env-example ├── manifest.json ├── LICENSE ├── webpack.config.js ├── package.json └── README.md /release/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/style/tour.manifest.scss: -------------------------------------------------------------------------------- 1 | @import 'tour/main'; 2 | -------------------------------------------------------------------------------- /src/style/popup.manifest.scss: -------------------------------------------------------------------------------- 1 | @import 'popup/popup'; 2 | -------------------------------------------------------------------------------- /src/style/main-ui.manifest.scss: -------------------------------------------------------------------------------- 1 | @import 'main-ui/map'; 2 | @import 'main-ui/trails'; 3 | -------------------------------------------------------------------------------- /assets/icons/1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twingl/trailblazer-extension/HEAD/assets/icons/1.jpg -------------------------------------------------------------------------------- /assets/icons/16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twingl/trailblazer-extension/HEAD/assets/icons/16.png -------------------------------------------------------------------------------- /assets/icons/19.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twingl/trailblazer-extension/HEAD/assets/icons/19.png -------------------------------------------------------------------------------- /assets/icons/2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twingl/trailblazer-extension/HEAD/assets/icons/2.jpg -------------------------------------------------------------------------------- /assets/icons/3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twingl/trailblazer-extension/HEAD/assets/icons/3.jpg -------------------------------------------------------------------------------- /assets/icons/38.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twingl/trailblazer-extension/HEAD/assets/icons/38.png -------------------------------------------------------------------------------- /assets/icons/48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twingl/trailblazer-extension/HEAD/assets/icons/48.png -------------------------------------------------------------------------------- /assets/icons/128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twingl/trailblazer-extension/HEAD/assets/icons/128.png -------------------------------------------------------------------------------- /assets/images/flag.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twingl/trailblazer-extension/HEAD/assets/images/flag.png -------------------------------------------------------------------------------- /assets/icons/19-unknown.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twingl/trailblazer-extension/HEAD/assets/icons/19-unknown.png -------------------------------------------------------------------------------- /assets/icons/38-unknown.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twingl/trailblazer-extension/HEAD/assets/icons/38-unknown.png -------------------------------------------------------------------------------- /assets/icons/19-recording.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twingl/trailblazer-extension/HEAD/assets/icons/19-recording.png -------------------------------------------------------------------------------- /assets/icons/38-recording.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twingl/trailblazer-extension/HEAD/assets/icons/38-recording.png -------------------------------------------------------------------------------- /assets/icons/36-onboarding-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twingl/trailblazer-extension/HEAD/assets/icons/36-onboarding-1.png -------------------------------------------------------------------------------- /assets/icons/36-onboarding-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twingl/trailblazer-extension/HEAD/assets/icons/36-onboarding-2.png -------------------------------------------------------------------------------- /assets/icons/36-onboarding-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twingl/trailblazer-extension/HEAD/assets/icons/36-onboarding-3.png -------------------------------------------------------------------------------- /assets/icons/36-onboarding-4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twingl/trailblazer-extension/HEAD/assets/icons/36-onboarding-4.png -------------------------------------------------------------------------------- /assets/icons/36-onboarding-5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twingl/trailblazer-extension/HEAD/assets/icons/36-onboarding-5.png -------------------------------------------------------------------------------- /assets/icons/36-onboarding-6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twingl/trailblazer-extension/HEAD/assets/icons/36-onboarding-6.png -------------------------------------------------------------------------------- /assets/icons/36-onboarding-7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twingl/trailblazer-extension/HEAD/assets/icons/36-onboarding-7.png -------------------------------------------------------------------------------- /assets/images/idle-background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twingl/trailblazer-extension/HEAD/assets/images/idle-background.png -------------------------------------------------------------------------------- /src/content-scripts/page-title.js: -------------------------------------------------------------------------------- 1 | import domready from 'domready'; 2 | import { sendPageTitle } from '../util/send-page-title'; 3 | 4 | domready(sendPageTitle); 5 | -------------------------------------------------------------------------------- /src/style/tour/_action.scss: -------------------------------------------------------------------------------- 1 | .action-group { 2 | .fulfilled { display: none; } 3 | 4 | &.fulfilled { 5 | .request { display: none; } 6 | .fulfilled { display: block; } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015", "stage-3", "react"], 3 | "plugins": [ 4 | "transform-decorators-legacy", 5 | "transform-runtime", 6 | "transform-class-properties" 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /src/components/views/assignments.js: -------------------------------------------------------------------------------- 1 | import Index from './assignments/index.jsx'; 2 | import Show from './assignments/show.jsx'; 3 | 4 | export { Index, Show }; 5 | export default { Index, Show }; 6 | -------------------------------------------------------------------------------- /src/components/views/offline-data.js: -------------------------------------------------------------------------------- 1 | import Index from './offline-data/index.jsx'; 2 | import Show from './offline-data/show.jsx'; 3 | 4 | export { Index, Show }; 5 | export default { Index, Show }; 6 | -------------------------------------------------------------------------------- /NOTES: -------------------------------------------------------------------------------- 1 | Building to public map: 2 | 3 | gulp build && cp build/public-map.js ~git/twingl/edu/trailblazer-web/app/assets/javascripts/map.js && cp build/content.css ~/git/twingl/edu/trailblazer-web/public/content.css 4 | -------------------------------------------------------------------------------- /src/components/layouts/main-ui.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default class Layout extends React.Component { 4 | 5 | render() { 6 | return this.props.children; 7 | } 8 | 9 | }; 10 | -------------------------------------------------------------------------------- /src/components/views/popup/loading.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default class Loading extends React.Component { 4 | render() { 5 | return
; 6 | } 7 | }; 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/* 2 | ui/pages/js/build/* 3 | ui/popup/js/build/* 4 | build/* 5 | .env* 6 | !.env-example 7 | doc/ 8 | .DS_Store 9 | .module-cache 10 | .env 11 | npm-debug.log 12 | release/ 13 | .nvmrc 14 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | [*] 3 | end_of_line = lf 4 | insert_final_newline = true 5 | 6 | [*.{js,json,css,scss,html}] 7 | charset = utf-8 8 | indent_style = space 9 | indent_size = 2 10 | trim_trailing_whitespace = true 11 | -------------------------------------------------------------------------------- /src/style/main-ui/_link.scss: -------------------------------------------------------------------------------- 1 | .link { 2 | stroke: #eee; 3 | stroke-width: 2px; 4 | stroke-linecap: round; 5 | opacity: 1; 6 | vector-effect: non-scaling-stroke; 7 | 8 | &.delete-pending { 9 | stroke: $red-500; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/components/layouts/tour.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default class Layout extends React.Component { 4 | 5 | render() { 6 | return
7 | {this.props.children} 8 |
; 9 | } 10 | 11 | }; 12 | -------------------------------------------------------------------------------- /src/util/message-channel.js: -------------------------------------------------------------------------------- 1 | export function send(message) { 2 | chrome.runtime.sendMessage(message); 3 | }; 4 | 5 | export function listen(listener) { 6 | chrome.runtime.onMessage.addListener(listener); 7 | }; 8 | 9 | export default { send, listen }; 10 | -------------------------------------------------------------------------------- /src/style/_color.scss: -------------------------------------------------------------------------------- 1 | $teal-300: #4DB6AC; 2 | $teal-500: #009688; 3 | 4 | $red-100: #FFEBEE; 5 | $red-500: #F44336; 6 | $red-700: #B71C1C; 7 | 8 | $blue-grey-200: #B0BEC5; 9 | $blue-grey-300: #90A4AE; 10 | $blue-grey-400: #78909C; 11 | $blue-grey-500: #607D8B; 12 | $blue-grey-600: #546E7A; 13 | -------------------------------------------------------------------------------- /src/components/views/popup.js: -------------------------------------------------------------------------------- 1 | import Idle from './popup/idle.jsx'; 2 | import Loading from './popup/loading.jsx'; 3 | import Recording from './popup/recording.jsx'; 4 | import SignIn from './popup/sign-in.jsx'; 5 | 6 | export { Idle, Loading, Recording, SignIn }; 7 | export default { Idle, Loading, Recording, SignIn }; 8 | -------------------------------------------------------------------------------- /src/style/tour/components/_task.scss: -------------------------------------------------------------------------------- 1 | .task { 2 | font-size: 14px; 3 | padding: 24px 36px; 4 | border: 3px solid rgba(123, 122, 120, 0.3); 5 | border-radius: 8px; 6 | display: block; 7 | max-width: 640px; 8 | margin: 12px auto; 9 | background: rgba(255, 255, 255, 0.7); 10 | 11 | p { 12 | font-size: 14px; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/style/tour/components/_folder-icon.scss: -------------------------------------------------------------------------------- 1 | .folder-icon { 2 | width: 28px; 3 | height: 24px; 4 | display: inline-block; 5 | background-image: url(/assets/icons/folder-icon.svg); 6 | background-size: 28px; 7 | background-position: 50%; 8 | background-repeat: no-repeat; 9 | vertical-align: middle; 10 | margin: auto 6px; 11 | } 12 | -------------------------------------------------------------------------------- /src/style/tour/components/_favourite-button.scss: -------------------------------------------------------------------------------- 1 | .favourite-button { 2 | width: 24px; 3 | height: 24px; 4 | display: inline-block; 5 | background-image: url(/assets/icons/favourite.svg); 6 | background-size: 22px; 7 | background-position: 50%; 8 | background-repeat: no-repeat; 9 | vertical-align: middle; 10 | margin: auto 6px; 11 | } 12 | -------------------------------------------------------------------------------- /src/util/send-page-title.js: -------------------------------------------------------------------------------- 1 | export function sendPageTitle() { 2 | var title = document.title 3 | , url = window.location.href; 4 | 5 | var payload = { 6 | title: title, 7 | url: url 8 | }; 9 | 10 | chrome.runtime.sendMessage({ 11 | type: "content_script", 12 | role: "title", 13 | payload: payload 14 | }); 15 | } 16 | -------------------------------------------------------------------------------- /src/components/image-button.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default class ImageButton extends React.Component { 4 | render() { 5 | return 10 | 11 | ; 12 | } 13 | }; 14 | -------------------------------------------------------------------------------- /src/style/tour/components/_trailblazer-button.scss: -------------------------------------------------------------------------------- 1 | .trailblazer-button { 2 | width: 24px; 3 | height: 24px; 4 | display: inline-block; 5 | background-size: 22px; 6 | background-position: 50%; 7 | background-repeat: no-repeat; 8 | vertical-align: middle; 9 | margin: auto 6px; 10 | 11 | &, &.idle { background-image: url(/assets/icons/38.png); } 12 | &.recording { background-image: url(/assets/icons/38-recording.png); } 13 | } 14 | -------------------------------------------------------------------------------- /src/markup/popup.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/helpers/assignment-helper.js: -------------------------------------------------------------------------------- 1 | export default { 2 | getAPIData: function (assignment) { 3 | var data = { 4 | assignment: { 5 | title: assignment.title 6 | } 7 | }; 8 | 9 | if (assignment.description) data.assignment.description = assignment.description; 10 | if (assignment.visible === true || assignment.visible === false) data.assignment.visible = assignment.visible; 11 | 12 | return data; 13 | } 14 | }; 15 | -------------------------------------------------------------------------------- /src/lib/store.js: -------------------------------------------------------------------------------- 1 | import FluxxorStore from 'fluxxor/lib/store'; 2 | 3 | class Store extends FluxxorStore { 4 | 5 | constructor(options) { 6 | super(options); 7 | 8 | // Bind the handlers for flux actions 9 | if (this.fluxActions) { 10 | for (var [action, handler] of this.fluxActions) { 11 | this.bindActions(action, this[handler]); 12 | } 13 | } 14 | } 15 | 16 | onBoot() { } 17 | 18 | } 19 | 20 | export default Store; 21 | -------------------------------------------------------------------------------- /assets/icons/favourite.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/config.js: -------------------------------------------------------------------------------- 1 | var keen = { 2 | enabled: process.env.KEEN_ENABLED, 3 | projectId: process.env.KEEN_PROJECT_ID, 4 | writeKey: process.env.KEEN_WRITE_KEY 5 | }; 6 | 7 | var api = { 8 | clientId: process.env.CLIENT_ID, 9 | host: process.env.API_HOST, 10 | nameSpace: "api", 11 | version: "v1" 12 | }; 13 | 14 | var raven = { 15 | url: process.env.SENTRY_URL 16 | }; 17 | 18 | export { keen }; 19 | export { api }; 20 | export { raven }; 21 | 22 | export default { 23 | keen, api, raven 24 | }; 25 | -------------------------------------------------------------------------------- /src/style/main-ui/_legend.scss: -------------------------------------------------------------------------------- 1 | .legend { 2 | position: absolute; 3 | bottom: 20px; 4 | left: 20px; 5 | color: rgba(77, 77, 77, 1); 6 | 7 | ul { 8 | list-style-type: none; 9 | padding: 0; 10 | margin: 0; 11 | 12 | svg { 13 | vertical-align: middle; 14 | position: relative; 15 | top: -2px; 16 | margin-right: 0.5em; 17 | } 18 | } 19 | 20 | .node { 21 | cursor: default; 22 | } 23 | 24 | .root .node-halo { 25 | stroke-width: 2px; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/style/tour/components/_stop-button.scss: -------------------------------------------------------------------------------- 1 | .stop-button { 2 | width: 70px; 3 | padding: 15px 0; 4 | border-radius: 3px; 5 | display: inline-block; 6 | vertical-align: middle; 7 | text-align: center; 8 | text-decoration: none; 9 | background-color: rgb(249, 126, 118); 10 | color: rgb(255, 255, 255); 11 | 12 | margin: auto 6px; 13 | 14 | background-image: url(/assets/icons/stop-icon.svg); 15 | background-repeat: no-repeat; 16 | background-position: 50% 50%; 17 | background-size: 9px auto; 18 | } 19 | -------------------------------------------------------------------------------- /src/components/views/tour.js: -------------------------------------------------------------------------------- 1 | import SignIn from './tour/sign-in.jsx'; 2 | import Step1 from './tour/step-1.jsx'; 3 | import Step2 from './tour/step-2.jsx'; 4 | import Step3 from './tour/step-3.jsx'; 5 | import Step4 from './tour/step-4.jsx'; 6 | import Step5 from './tour/step-5.jsx'; 7 | import Step6 from './tour/step-6.jsx'; 8 | import Step7 from './tour/step-7.jsx'; 9 | 10 | export { SignIn, Step1, Step2, Step3, Step4, Step5, Step6, Step7 }; 11 | export default { SignIn, Step1, Step2, Step3, Step4, Step5, Step6, Step7 }; 12 | -------------------------------------------------------------------------------- /src/markup/main-ui.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 | 14 | 15 | -------------------------------------------------------------------------------- /src/components/public-map-title.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default class PublicMapTitle extends React.Component { 4 | render() { 5 | return
6 |
9 |

{this.props.title}

10 |
11 |
13 | Get Trailblazer 14 |
15 |
; 16 | } 17 | }; 18 | -------------------------------------------------------------------------------- /src/markup/offline-data.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 | 14 | 15 | -------------------------------------------------------------------------------- /src/style/tour/components/_record-button.scss: -------------------------------------------------------------------------------- 1 | .record-button { 2 | width: 70px; 3 | padding: 17px 0; 4 | border-radius: 3px; 5 | display: inline-block; 6 | vertical-align: middle; 7 | text-align: center; 8 | text-decoration: none; 9 | background-color: rgb(97, 97, 97); 10 | color: rgb(255, 255, 255); 11 | 12 | margin: auto 6px; 13 | 14 | background-image: url(/assets/icons/new-icon.svg), url(/assets/icons/trail-icon.svg); 15 | background-repeat: no-repeat, no-repeat; 16 | background-position: 52px 6px, 50% 50%; 17 | background-size: 7px auto, 35px auto; 18 | } 19 | -------------------------------------------------------------------------------- /src/util/logger.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | 3 | var levels = { 4 | INFO: '#78909C', 5 | DEBUG: '#00BFA5', 6 | WARN: '#FF8F00', 7 | ERROR: '#D50000' 8 | }; 9 | 10 | export default function Logger(filename) { 11 | 12 | // Prefix console.log with a colour tag for each log level 13 | return _.transform(levels, function(result, color, level) { 14 | if (process.env.LOGGING_ENABLED === 'true') { 15 | result[level.toLowerCase()] = 16 | console.log.bind(console, `%c ${level} `, `color: white; background: ${color}`); 17 | } else { 18 | result[level.toLowerCase()] = () => {}; 19 | } 20 | }); 21 | } 22 | -------------------------------------------------------------------------------- /src/components/popover.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default class Popover extends React.Component { 4 | 5 | render() { 6 | var arrowStyle = this.props.arrowStyle; 7 | var display = this.props.display ? 'block' : 'none'; 8 | var inlineStyle = { 9 | display: display 10 | } 11 | 12 | return
16 |
17 |
18 | {this.props.children} 19 |
20 |
; 21 | } 22 | }; 23 | -------------------------------------------------------------------------------- /assets/icons/dropdown-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Triangle 2 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/markup/tour.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | -------------------------------------------------------------------------------- /assets/icons/stop-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Rectangle 122 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/core/install-hooks.js: -------------------------------------------------------------------------------- 1 | import ChromeIdentityAdapter from '../adapter/chrome_identity_adapter'; 2 | import actions from '../actions'; 3 | 4 | function onInstall(details) { 5 | switch(details.reason) { 6 | case "update": 7 | var identity = new ChromeIdentityAdapter(); 8 | identity.getToken().then(token => identity.storeToken(token)); 9 | actions.extensionUpdated(details.previousVersion); 10 | break; 11 | 12 | case "install": 13 | // Show onboarding 14 | chrome.tabs.create({ active: true, url: chrome.runtime.getURL("/build/tour.html") }); 15 | actions.extensionInstalled(); 16 | break; 17 | 18 | case "chrome_update": 19 | actions.chromeUpdated(); 20 | break; 21 | } 22 | }; 23 | 24 | export { onInstall } 25 | export default onInstall; 26 | -------------------------------------------------------------------------------- /CONTRIBUTING: -------------------------------------------------------------------------------- 1 | This repo contains the Chrome extension for Trailblazer. 2 | 3 | Please check out our wiki (https://github.com/twingl/trailblazer-wash/wiki), 4 | look through the code, open up discussions in our forum 5 | (https://trailblazing.community/), fork the repo and make changes. 6 | 7 | If you open a pull request (PR) with a feature, fix, or tweak; make sure you 8 | include a description of what you've changed and why. For visual changes, a 9 | screenshot of them would be appreciated too. 10 | 11 | Once you've opened a PR, a handy little bot should automatically ask you to 12 | e-sign our CLA (https://gist.github.com/4f7d2735ced6d47533bac601a785becc). 13 | With that confirmed (and a healthy measure of peer-review) your contributions 14 | will be ready to be integrated into the project. 15 | 16 | Happy hacking! 17 | -------------------------------------------------------------------------------- /src/style/_reset.scss: -------------------------------------------------------------------------------- 1 | /* Eric Meyer's Reset CSS v2.0 - http://cssreset.com */ 2 | html,body,div,span,applet,object,iframe,h1,h2,h3,h4,h5,h6,p,blockquote,pre,a,abbr,acronym,address,big,cite,code,del,dfn,em,img,ins,kbd,q,s,samp,small,strike,strong,sub,sup,tt,var,b,u,i,center,dl,dt,dd,ol,ul,li,fieldset,form,label,legend,table,caption,tbody,tfoot,thead,tr,th,td,article,aside,canvas,details,embed,figure,figcaption,footer,header,hgroup,menu,nav,output,ruby,section,summary,time,mark,audio,video{border:0;font-size:100%;font:inherit;vertical-align:baseline;margin:0;padding:0}article,aside,details,figcaption,figure,footer,header,hgroup,menu,nav,section{display:block}body{line-height:1}ol,ul{list-style:none}blockquote,q{quotes:none}blockquote:before,blockquote:after,q:before,q:after{content:none}table{border-collapse:collapse;border-spacing:0} 3 | -------------------------------------------------------------------------------- /src/core/extension-states.js: -------------------------------------------------------------------------------- 1 | export default { 2 | recording: { 3 | popup: "/build/popup.html", 4 | browserAction: { 5 | 19: "/assets/icons/19-recording.png", 6 | 38: "/assets/icons/38-recording.png" 7 | } 8 | }, 9 | idle: { 10 | popup: "/build/popup.html", 11 | browserAction: { 12 | 19: "/assets/icons/19.png", 13 | 38: "/assets/icons/38.png" 14 | } 15 | }, 16 | notAuthenticated: { 17 | popup: "/build/popup.html", 18 | browserAction: { 19 | 19: "/assets/icons/19.png", 20 | 38: "/assets/icons/38.png" 21 | } 22 | }, 23 | default: { 24 | popup: "/build/popup.html", 25 | browserAction: { 26 | 19: "/assets/icons/19-unknown.png", 27 | 38: "/assets/icons/38-unknown.png" 28 | } 29 | }, 30 | currentTabId: undefined 31 | }; 32 | -------------------------------------------------------------------------------- /src/services.js: -------------------------------------------------------------------------------- 1 | import { AssignmentService as LocalAssignmentService } from './services/local/assignment-service'; 2 | import { NodeService as LocalNodeService } from './services/local/node-service'; 3 | 4 | import { AssignmentService as RemoteAssignmentService } from './services/remote/assignment-service'; 5 | import { NodeService as RemoteNodeService } from './services/remote/node-service'; 6 | 7 | 8 | export { 9 | LocalAssignmentService, 10 | LocalNodeService, 11 | RemoteAssignmentService, 12 | RemoteNodeService 13 | }; 14 | 15 | export var Local = { 16 | AssignmentService: LocalAssignmentService, 17 | NodeService: LocalNodeService, 18 | }; 19 | 20 | export var Remote = { 21 | AssignmentService: RemoteAssignmentService, 22 | NodeService: RemoteNodeService, 23 | }; 24 | 25 | export default { Local, Remote }; 26 | -------------------------------------------------------------------------------- /src/helpers/node-helper.js: -------------------------------------------------------------------------------- 1 | export default { 2 | isChild: function(node, candidateChild) { 3 | return (candidateChild.localParentId && candidateChild.localParentId === node.localId); 4 | }, 5 | 6 | isParent: function(node, canidateParent) { 7 | return (canidateParent.localId && canidateParent.localId === node.localParentId); 8 | }, 9 | 10 | isOpenTab: function(node) { 11 | return !!node.tabId; 12 | }, 13 | 14 | getAPIData: function (node) { 15 | var data = { 16 | url: node.url, 17 | title: node.title 18 | }; 19 | 20 | data.rank = (node.rank) ? node.rank : 0; 21 | if (node.parentId) data.parent_id = node.parentId; 22 | if (node.redirect) data.redirect = node.redirect; 23 | if (node.redirectedFrom) data.redirected_from = node.redirectedFrom; 24 | 25 | return data; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /assets/icons/new-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | New 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/style/main-ui/_public-map.scss: -------------------------------------------------------------------------------- 1 | .public { 2 | #map-container { 3 | background-image: none; 4 | } 5 | 6 | #title { 7 | position: absolute; 8 | top: 32px; 9 | left: 32px; 10 | background: none; 11 | 12 | h1 { 13 | color: rgb(255, 255, 255); 14 | font-size: 36px; 15 | font-weight: 400; 16 | margin: 0; 17 | } 18 | } 19 | 20 | #get-trailblazer { 21 | position: absolute; 22 | top: 56px; 23 | right: 84px; 24 | 25 | a { 26 | color: rgb(246, 100, 63); 27 | font-weight: 600; 28 | text-decoration: underline; 29 | } 30 | 31 | &::after { 32 | content: " "; 33 | background-image: url(/assets/icons/38.png); 34 | position: absolute; 35 | display: block; 36 | top: -8px; 37 | right: -48px; 38 | width: 38px; 39 | height: 38px; 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /script/build.js: -------------------------------------------------------------------------------- 1 | var argv = require('yargs').argv 2 | , dotenv = require('dotenv') 3 | , rimraf = require('rimraf') 4 | , webpack = require('webpack') 5 | , config = require('../webpack.config') 6 | 7 | // Load our env configuration before everything 8 | if (argv.production) { 9 | console.log('Loading production environment'); 10 | dotenv.load({ path: '.env-production' }); 11 | } else if (argv.staging) { 12 | console.log('Loading staging environment'); 13 | dotenv.load({ path: '.env-staging' }); 14 | } else { 15 | console.log('Loading development environment'); 16 | dotenv.load(); 17 | } 18 | 19 | // Remove the old build output 20 | rimraf('build/**/*', function(err) { 21 | if (err) console.log('rimraf: Err: ', err); 22 | }); 23 | 24 | webpack(config, function(err, stats) { 25 | console.log(stats.toString({ 26 | colors: true, 27 | chunks: false 28 | })); 29 | }); 30 | -------------------------------------------------------------------------------- /src/scripts/public-map.jsx: -------------------------------------------------------------------------------- 1 | //helpers 2 | var React = require('react'); 3 | var ReactDOM = require('react-dom'); 4 | 5 | //components 6 | var PublicMapView = require('../components/views/public-map'); 7 | 8 | window.renderMap = function(assignment, nodes) { 9 | var data = { 10 | nodes: {}, 11 | assignment: undefined 12 | }; 13 | 14 | data.assignment = assignment; 15 | 16 | nodes.map((node) => { 17 | node.localId = node.id; 18 | 19 | node.parentId = node.parent_id; 20 | node.localParentId = node.parent_id; 21 | 22 | node.assignmentId = node.assignment_id; 23 | node.localAssignmentId = node.assignment_id; 24 | 25 | data.nodes[node.id] = node; 26 | }); 27 | 28 | console.log(data); 29 | 30 | ReactDOM.render( 31 | , 34 | document.getElementById('wrap') 35 | ); 36 | }; 37 | -------------------------------------------------------------------------------- /src/scripts/popup.jsx: -------------------------------------------------------------------------------- 1 | import styles from '../style/popup.manifest.scss'; 2 | import markup from '../markup/popup.html'; 3 | 4 | import React from 'react'; 5 | import ReactDOM from 'react-dom'; 6 | import domready from 'domready'; 7 | 8 | import Actions from '../actions'; 9 | 10 | import Layout from '../components/layouts/popup.jsx'; 11 | 12 | // Start tracking errors 13 | import Raven from 'raven-js'; 14 | import { raven as config } from '../config'; 15 | 16 | if (config.url) Raven.config(config.url).install(); 17 | 18 | domready(() => { 19 | let container = document.getElementById('container'); 20 | 21 | if (!container) { 22 | container = document.createElement('div'); 23 | container.id = 'container'; 24 | document.body.appendChild(container); 25 | } 26 | 27 | chrome.tabs.query({ currentWindow: true, active: true }, (tabs) => { 28 | ReactDOM.render(, container); 29 | }); 30 | }); 31 | -------------------------------------------------------------------------------- /src/services/remote/assignment-service.js: -------------------------------------------------------------------------------- 1 | import HTTPAdapter from '../../adapter/trailblazer_http_storage_adapter'; 2 | 3 | class AssignmentService { 4 | constructor(flux) { 5 | this.flux = flux; 6 | } 7 | 8 | list(params) { 9 | return new HTTPAdapter().list('assignments', params); 10 | } 11 | 12 | create(attributes, options) { 13 | return new HTTPAdapter().create('assignments', attributes, options); 14 | } 15 | 16 | get(id, params) { 17 | return new HTTPAdapter().read('assignments', id, params); 18 | } 19 | 20 | update(id, attributes, options) { 21 | return new HTTPAdapter().update('assignments', id, attributes, options); 22 | } 23 | 24 | destroy(id) { 25 | return new HTTPAdapter().destroy('assignments', id); 26 | } 27 | 28 | destroyMany(ids) { 29 | return new HTTPAdapter().bulkDestroy('assignments', ids); 30 | } 31 | }; 32 | 33 | export { AssignmentService }; 34 | export default AssignmentService; 35 | -------------------------------------------------------------------------------- /.env-example: -------------------------------------------------------------------------------- 1 | # Trailblazer OAuth2 Client ID 2 | # 3 | # You can create a new one at https://app.trailblazer.io/oauth/applications/new 4 | CLIENT_ID=trailblazer_api_client_id 5 | 6 | # Trailblazer API host. 7 | # 8 | # We have two hosted instances. 9 | # 10 | # Staging: https://staging.trailblazer.io 11 | # Production: https://app.trailblazer.io 12 | API_HOST=https://app.trailblazer.io 13 | 14 | # Turn logging on or off. 15 | # When on, all actions and their dispatches will be logged to the console, 16 | # along with some other output - anything that uses the bundled logger wrapper 17 | # will be controlled by this flag. 18 | # 19 | # It's useful to keep this enabled in development, production builds have 20 | # logging disabled. 21 | LOGGING_ENABLED=true 22 | 23 | # Keen Analytics configuration 24 | # 25 | # KEEN_PROJECT_ID=keen project id 26 | # KEEN_WRITE_KEY=keen write key 27 | # 28 | # KEEN_ENABLED=false 29 | 30 | # Error reporting with Sentry 31 | # SENTRY_URL=https://something/like/a/sentry/url 32 | -------------------------------------------------------------------------------- /assets/icons/delete-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Path 41 + Path 42 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /assets/icons/sign-out-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Logout Icon 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 2, 3 | "name": "Trailblazer", 4 | "version": "0.2.14", 5 | "minimum_chrome_version": "29", 6 | "permissions": [ 7 | "*://api.keen.io/", 8 | "https://app.trailblazer.io/", 9 | "http://staging.trailblazer.io/", 10 | "http://localhost:3000/", 11 | "chrome://favicon/", 12 | "*://*/", 13 | "identity", 14 | "storage", 15 | "unlimitedStorage", 16 | "tabs", 17 | "webNavigation" 18 | ], 19 | "icons": { 20 | "16": "assets/icons/16.png", 21 | "48": "assets/icons/48.png", 22 | "128": "assets/icons/128.png" 23 | }, 24 | "background": { 25 | "scripts": [ 26 | "build/background.js" 27 | ] 28 | }, 29 | "browser_action": { 30 | "default_icon": { 31 | "19": "/assets/icons/19-unknown.png", 32 | "38": "/assets/icons/38-unknown.png" 33 | }, 34 | "default_popup": "/build/popup.html" 35 | }, 36 | "content_security_policy": "script-src 'self' https://platform.twitter.com; object-src 'self'" 37 | } 38 | -------------------------------------------------------------------------------- /src/components/views/public-map.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | //components 4 | import PublicMapTitle from '../public-map-title'; 5 | import ShareMap from '../share-map'; 6 | import Legend from '../legend'; 7 | 8 | import Trail from '../trail'; 9 | 10 | export default class PublicMap extends React.Component { 11 | 12 | render() { 13 | var visible, shareText, title, url; 14 | 15 | visible = this.props.assignment.visible; //state 16 | shareText = (visible) ? "Shared" : "Share"; //state 17 | title = this.props.assignment.title; 18 | url = this.props.assignment.url; 19 | 20 | return
21 | 22 |
23 | 24 |
25 | 26 |
; 27 | } 28 | 29 | }; 30 | -------------------------------------------------------------------------------- /src/services/remote/node-service.js: -------------------------------------------------------------------------------- 1 | import HTTPAdapter from '../../adapter/trailblazer_http_storage_adapter'; 2 | 3 | class NodeService { 4 | constructor(flux) { 5 | this.flux = flux; 6 | } 7 | 8 | list(assignmentId, params) { 9 | const resource = `assignments/${assignmentId}/nodes` 10 | return new HTTPAdapter().list(resource, params); 11 | } 12 | 13 | create(assignmentId, attributes, options) { 14 | const resource = `assignments/${assignmentId}/nodes` 15 | return new HTTPAdapter().create(resource, attributes, options); 16 | } 17 | 18 | get(id, params) { 19 | return new HTTPAdapter().read('nodes', id, params); 20 | } 21 | 22 | update(id, attributes, options) { 23 | return new HTTPAdapter().update('nodes', id, attributes, options); 24 | } 25 | 26 | destroy(id) { 27 | return new HTTPAdapter().destroy('nodes', id); 28 | } 29 | 30 | destroyMany(ids) { 31 | return new HTTPAdapter().bulkDestroy('nodes', ids); 32 | } 33 | }; 34 | 35 | export { NodeService }; 36 | export default NodeService; 37 | -------------------------------------------------------------------------------- /src/style/objects/_button.scss: -------------------------------------------------------------------------------- 1 | @import '../color'; 2 | 3 | .button { 4 | font-family: 'Open Sans', sans-serif; 5 | font-weight: 400; 6 | font-size: 14px; 7 | 8 | border: none; 9 | border-radius: 2px; 10 | 11 | padding: 6px 20px; 12 | 13 | cursor: pointer; 14 | 15 | color: rgba(255, 255, 255, 0.9); 16 | & { background: $teal-300; } 17 | &:hover { background: $teal-500; } 18 | } 19 | 20 | .button-secondary { 21 | color: rgba(255, 255, 255, 0.9); 22 | & { background: $blue-grey-400; } 23 | &:hover { background: $blue-grey-600; } 24 | } 25 | 26 | .button-secondary-inverted { 27 | color: $blue-grey-600; 28 | & { background: rgba(255, 255, 255, 0.9); } 29 | &:hover { background: rgba(255, 255, 255, 0.7); } 30 | } 31 | 32 | .button-danger { 33 | color: rgba(255, 255, 255, 0.9); 34 | & { background: $red-500; } 35 | &:hover { background: $red-700; } 36 | } 37 | 38 | .button-danger-inverted { 39 | color: $red-700; 40 | & { background: rgba(255, 255, 255, 0.9); } 41 | &:hover { background: rgba(255, 255, 255, 0.7); } 42 | } 43 | -------------------------------------------------------------------------------- /assets/icons/tutorial-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /assets/icons/editable-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Pencil 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /assets/icons/tutorial-icon-light.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014–2016 Twingl Ltd. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/components/link.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import classnames from 'classnames'; 4 | 5 | export default class Link extends React.Component { 6 | 7 | constructor(props) { 8 | super(props); 9 | 10 | this.position = props.position; 11 | } 12 | 13 | // We want to set the dom attributes ourselves to avoid triggering React's 14 | // diffing algorithm every draw 15 | updatePosition(position) { 16 | this.position = position; 17 | let domNode = React.findDOMNode(this); 18 | 19 | domNode.setAttribute('x1', this.position.from.x); 20 | domNode.setAttribute('y1', this.position.from.y); 21 | domNode.setAttribute('x2', this.position.to.x); 22 | domNode.setAttribute('y2', this.position.to.y); 23 | } 24 | 25 | componentWillReceiveProps(newProps) { 26 | this.position = newProps.position; 27 | } 28 | 29 | render() { 30 | let classes = classnames('link', { 31 | 'delete-pending': !!this.props.link.data.deletePending 32 | }); 33 | 34 | return ; 37 | } 38 | }; 39 | -------------------------------------------------------------------------------- /src/stores.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Initializes the Flux stores, and exports them in an object ready 3 | * to be passed to flux. 4 | */ 5 | 6 | import { objectStores } from './db'; 7 | 8 | import TabStore from './stores/tab-store'; 9 | import NodeStore from './stores/node-store'; 10 | import AssignmentStore from './stores/assignment-store'; 11 | import AuthenticationStore from './stores/authentication-store'; 12 | import SyncStore from './stores/sync-store'; 13 | import MetricsStore from './stores/metrics-store'; 14 | import MapStore from './stores/map-store'; 15 | import ErrorStore from './stores/error-store'; 16 | 17 | /** 18 | * Initialize the Flux stores 19 | */ 20 | export default { 21 | TabStore: new TabStore({ db: objectStores }), 22 | NodeStore: new NodeStore({ db: objectStores }), 23 | AssignmentStore: new AssignmentStore({ db: objectStores }), 24 | AuthenticationStore: new AuthenticationStore({ db: objectStores }), 25 | SyncStore: new SyncStore({ db: objectStores }), 26 | MetricsStore: new MetricsStore({ db: objectStores }), 27 | MapStore: new MapStore({ db: objectStores }), 28 | ErrorStore: new ErrorStore() 29 | }; 30 | -------------------------------------------------------------------------------- /src/components/views/tour/step-4.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Helmet from 'react-helmet'; 3 | 4 | import actions from '../../../actions'; 5 | import constants from '../../../constants'; 6 | 7 | import { sendPageTitle } from '../../../util/send-page-title'; 8 | 9 | export default class Step4 extends React.Component { 10 | 11 | onCloseClicked(evt) { 12 | evt.stopPropagation(); 13 | 14 | actions.completedOnboardingStep(constants.onboarding.STEP_4) 15 | chrome.tabs.getCurrent(tab => chrome.tabs.remove(tab.id)); 16 | } 17 | 18 | componentDidMount() { 19 | sendPageTitle(); 20 | } 21 | 22 | render() { 23 | return
24 | 25 | 26 | 27 |

Safe and sound

28 | 29 |

Diversions and multitasking are taken care of.

30 | 31 |
32 |

33 | Tabs you open when Trailblazer is active will also be added to your 34 | trail.
In those tabs, the icon will be orange too. 35 |

36 | Safely close this tab 37 |
38 |
; 39 | } 40 | 41 | }; 42 | -------------------------------------------------------------------------------- /assets/icons/folder-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | folder 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /assets/icons/trail-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Trail 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/style/main-ui/trails.scss: -------------------------------------------------------------------------------- 1 | .wrap.assignment-index { 2 | max-width: 640px; 3 | margin: 0 auto; 4 | text-align: center; 5 | 6 | h1 { font-size: 28px; } 7 | p { font-size: 16px; } 8 | 9 | a { 10 | color: #666; 11 | 12 | &.primary { 13 | color: rgb(246, 100, 63); 14 | } 15 | } 16 | } 17 | 18 | .assignment-menu { 19 | color: #555; 20 | font-family: 'Open Sans', sans-serif; 21 | margin: 0; 22 | padding: 1em 0 0 0; /*1em to obliterate the annoying scroll bar from the li margin*/ 23 | } 24 | 25 | .assignment-menu li { 26 | text-align: left; 27 | list-style-type: none; 28 | cursor: pointer; 29 | background-color: #eee; 30 | width: 320px; 31 | margin: 1em; 32 | padding: 1em; 33 | margin-right: auto; 34 | margin-left: auto; 35 | } 36 | 37 | .assignment-menu li:after { 38 | content:""; 39 | display:table; 40 | clear:both; 41 | } 42 | 43 | .assignment-menu li a{ 44 | float: right; 45 | } 46 | 47 | .assignment-menu li a:hover{ 48 | fill: red; 49 | } 50 | 51 | .assignment-menu li:hover { 52 | background-color: #ddd; 53 | 54 | } 55 | 56 | .show { 57 | opacity: 1; 58 | -webkit-transition: opacity 0.4s ease-in; 59 | } 60 | 61 | .destroy { 62 | opacity: 0; 63 | -webkit-transition: opacity 0.4s ease-in; 64 | } 65 | -------------------------------------------------------------------------------- /src/scripts/offline-data.jsx: -------------------------------------------------------------------------------- 1 | import styles from '../style/main-ui.manifest.scss'; 2 | import markup from '../markup/offline-data.html'; 3 | 4 | import React from 'react'; 5 | import ReactDOM from 'react-dom'; 6 | import domready from 'domready'; 7 | import { Router, Route, Redirect, useRouterHistory } from 'react-router'; 8 | import { createHashHistory } from 'history'; 9 | import Actions from '../actions'; 10 | 11 | import Layout from '../components/layouts/main-ui.jsx'; 12 | 13 | import * as OfflineData from '../components/views/offline-data'; 14 | 15 | var routes = 16 | 17 | 18 | 19 | 20 | ; 21 | 22 | domready(() => { 23 | let container = document.getElementById('container'); 24 | 25 | if (!container) { 26 | container = document.createElement('div'); 27 | container.id = 'container'; 28 | document.body.appendChild(container); 29 | } 30 | 31 | let appHistory = useRouterHistory(createHashHistory)({ queryKey: false }); 32 | 33 | ReactDOM.render(, container); 34 | }); 35 | -------------------------------------------------------------------------------- /src/style/main-ui/_node.scss: -------------------------------------------------------------------------------- 1 | @import '../color'; 2 | 3 | /* nodes */ 4 | .node { 5 | fill: #eee; 6 | fill-rule: evenodd; 7 | } 8 | 9 | .node-halo { 10 | fill-opacity: 0; 11 | } 12 | 13 | .node-core { 14 | opacity: 1; 15 | } 16 | 17 | /* 18 | .open .node-halo{ 19 | stroke: #44C0FF; 20 | stroke-width: 1px; 21 | stroke-opacity: .9; 22 | } 23 | */ 24 | 25 | @-webkit-keyframes pulse { 26 | 0% { 27 | fill-opacity: .7; 28 | } 29 | 50% { 30 | fill-opacity: .5; 31 | } 32 | 100% { 33 | fill-opacity: .7; 34 | } 35 | } 36 | 37 | .open .node-halo { 38 | fill: #FB4949; 39 | -webkit-animation-name: pulse; 40 | -webkit-animation-duration: 2s; 41 | -webkit-animation-iteration-count: infinite; 42 | -webkit-animation-direction: alternate; 43 | -webkit-animation-timing-function: ease-in-out; 44 | animation-name: pulse; 45 | animation-duration: 2s; 46 | animation-iteration-count: infinite; 47 | animation-direction: alternate; 48 | animation-timing-function: ease-in-out; 49 | } 50 | 51 | .root .node-halo { 52 | fill-opacity: 0.6; 53 | fill: #4B4B4B; 54 | stroke: rgba(255, 255, 255, 0.7); 55 | } 56 | 57 | .hub .node-halo { 58 | fill-opacity: 1; 59 | } 60 | 61 | .favourite .node-halo { 62 | fill: gold; 63 | fill-opacity: 1; 64 | } 65 | 66 | .delete-pending .node-halo { 67 | fill: $red-500; 68 | fill-opacity: 0.8; 69 | } 70 | -------------------------------------------------------------------------------- /src/components/views/tour/step-2.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Link } from 'react-router'; 3 | import Helmet from 'react-helmet'; 4 | 5 | import actions from '../../../actions'; 6 | import constants from '../../../constants'; 7 | 8 | import { sendPageTitle } from '../../../util/send-page-title'; 9 | 10 | class Step2 extends React.Component { 11 | 12 | onContinueClicked(evt) { 13 | actions.completedOnboardingStep(constants.onboarding.STEP_2); 14 | } 15 | 16 | componentDidMount() { 17 | sendPageTitle(); 18 | } 19 | 20 | render() { 21 | return
22 | 23 | 24 | 25 |

Liftoff

26 | 27 |

Great job! See how the icon turned orange? That means you're good to go.

28 | 29 |

Cool huh?

30 | 31 |
32 |

33 | We've also given your trail a temporary name, so you don't have to 34 | worry about naming it up front. But if you want to, just tap the 35 | title and start typing. 36 |

37 | Continue 38 |
39 |
40 | } 41 | 42 | }; 43 | 44 | Step2.contextTypes = { 45 | router: React.PropTypes.object.isRequired 46 | }; 47 | 48 | export default Step2; 49 | -------------------------------------------------------------------------------- /src/background/content-scripts.js: -------------------------------------------------------------------------------- 1 | import actions from '../actions';; 2 | 3 | chrome.runtime.onMessage.addListener(function(message, senderInfo, respond) { 4 | if (message.type === "content_script" && message.role === "title") { 5 | var tabId = senderInfo.tab.id; 6 | actions.tabTitleUpdated(tabId, message.payload.url, message.payload.title); 7 | } 8 | }); 9 | 10 | chrome.webNavigation.onHistoryStateUpdated.addListener(function(details) { 11 | if (details.frameId === 0) { 12 | window.setTimeout( function() { 13 | chrome.tabs.executeScript(details.tabId, { file: "/build/page-title.js" }); 14 | }, 1000); 15 | // FIXME detecting page title changes for SPAs 16 | // SPA sites use this hook as they don't fire the DOM content loaded event 17 | // on page navigation. They do, however, change the history state so we can 18 | // listen for that. Unfortunately the title often changes after that event, 19 | // so we back off for a second before running the content script. If after 20 | // that duration it isn't updated, then it's incorrect and we're going to 21 | // have a bad time. Need to find a better way of addressing this one. 22 | } 23 | }); 24 | 25 | chrome.webNavigation.onDOMContentLoaded.addListener(function(details) { 26 | if (details.frameId === 0) { 27 | chrome.tabs.executeScript(details.tabId, { file: "/build/page-title.js" }); 28 | } 29 | }); 30 | 31 | -------------------------------------------------------------------------------- /src/scripts/main-ui.jsx: -------------------------------------------------------------------------------- 1 | import styles from '../style/main-ui.manifest.scss'; 2 | import markup from '../markup/main-ui.html'; 3 | 4 | import React from 'react'; 5 | import ReactDOM from 'react-dom'; 6 | import domready from 'domready'; 7 | import { Router, Route, Redirect, useRouterHistory } from 'react-router'; 8 | import { createHashHistory } from 'history'; 9 | import Actions from '../actions'; 10 | 11 | import Layout from '../components/layouts/main-ui.jsx'; 12 | 13 | import * as Assignments from '../components/views/assignments'; 14 | 15 | // Start tracking errors 16 | import Raven from 'raven-js'; 17 | import { raven as config } from '../config'; 18 | 19 | if (config.url) Raven.config(config.url).install(); 20 | 21 | var routes = 22 | 23 | 24 | 25 | 26 | ; 27 | 28 | domready(() => { 29 | let container = document.getElementById('container'); 30 | 31 | if (!container) { 32 | container = document.createElement('div'); 33 | container.id = 'container'; 34 | document.body.appendChild(container); 35 | } 36 | 37 | let appHistory = useRouterHistory(createHashHistory)({ queryKey: false }); 38 | 39 | ReactDOM.render(, container); 40 | }); 41 | -------------------------------------------------------------------------------- /src/components/views/tour/step-6.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Helmet from 'react-helmet'; 3 | 4 | import actions from '../../../actions'; 5 | import constants from '../../../constants'; 6 | 7 | import { sendPageTitle } from '../../../util/send-page-title'; 8 | 9 | export default class Step6 extends React.Component { 10 | 11 | revealNextStep() { 12 | actions.completedOnboardingStep(constants.onboarding.STEP_6); 13 | this.context.router.push('/step-7'); 14 | } 15 | 16 | // When we receive a notification of the map being viewed, advance 17 | onMessage(msg) { 18 | if (msg.action === constants.VIEWED_MAP) this.revealNextStep(); 19 | } 20 | 21 | componentDidMount() { 22 | this.__messageHandler = this.onMessage.bind(this); 23 | chrome.runtime.onMessage.addListener(this.__messageHandler); 24 | 25 | sendPageTitle(); 26 | } 27 | 28 | componentWillUnmount() { 29 | chrome.runtime.onMessage.removeListener(this.__messageHandler); 30 | } 31 | 32 | render() { 33 | return
34 | 35 | 36 | 37 |

Map time

38 | 39 |

Now let's take a look at the trail you've started, and see how it's growing

40 | 41 |

Just open the menu again and tap "View Trail"

42 |
; 43 | } 44 | 45 | }; 46 | 47 | Step6.contextTypes = { 48 | router: React.PropTypes.object.isRequired 49 | }; 50 | 51 | export default Step6; 52 | -------------------------------------------------------------------------------- /src/queries.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Makes a set of query functions available over Chrome's runtime messaging to 3 | * the UI. 4 | * 5 | * These are processed separately from the flux dispatcher, and behave like the 6 | * query functions that would be available on a store instance if the UI shared 7 | * the same memory as the background script. 8 | * 9 | * Queries are denoted by a list of function names on the class, set by the 10 | * @query decorator 11 | */ 12 | import Promise from 'promise'; 13 | import stores from './stores'; 14 | import _ from 'lodash'; 15 | 16 | var logger = Logger('queries.js'); 17 | import Logger from './util/logger'; 18 | 19 | var sendMessage = function(message) { 20 | return new Promise(function(resolve, reject) { 21 | 22 | var responder = function(...args) { 23 | if (!chrome.runtime.lastError) { 24 | resolve(...args); 25 | } else { 26 | reject(...args, chrome.runtime.lastError); 27 | } 28 | }; 29 | 30 | chrome.runtime.sendMessage(message, responder); 31 | }); 32 | }; 33 | 34 | var exports = {}; 35 | 36 | _.each(stores, (store, name) => { 37 | if (store.queryFunctions) store.queryFunctions.map( (fn) => { 38 | exports[name] = exports[name] || {}; 39 | 40 | exports[name][fn] = function(...args) { 41 | return sendMessage({ 42 | query: fn, 43 | store: name, 44 | args: args 45 | }); 46 | }; 47 | }) 48 | }); 49 | 50 | export default exports; 51 | -------------------------------------------------------------------------------- /src/db.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Initialize IndexedDB and its object stores, ready for use elsewhere 3 | * (predominantly by services and flux stores) 4 | */ 5 | import treo from 'treo'; 6 | import treoPromise from 'treo/plugins/treo-promise'; 7 | 8 | import Logger from './util/logger'; 9 | var logger = Logger('db.js'); 10 | 11 | logger.info("Initializing Indexdb"); 12 | var schema = treo.schema() 13 | .version(1) 14 | // Node storage 15 | .addStore('nodes', { keyPath: 'localId', increment: true }) 16 | .addIndex('id', 'id', { unique: true }) 17 | .addIndex('tabId', 'tabId', { unique: false }) 18 | .addIndex('assignmentId', 'assignmentId', { unique: false }) 19 | .addIndex('localAssignmentId', 'localAssignmentId', { unique: false }) 20 | .addIndex('url', 'url', { unique: false }) 21 | 22 | // Assignment storage 23 | .addStore('assignments', { keyPath: 'localId', increment: true }) 24 | .addIndex('id', 'id', { unique: true }) 25 | .version(2) 26 | // Node parent ID indices 27 | .getStore('nodes') 28 | .addIndex('parentId', 'parentId', { unique: false }) 29 | .addIndex('localParentId', 'localParentId', { unique: false }) 30 | 31 | var db = treo('trailblazer-wash', schema) 32 | .use(treoPromise()); 33 | 34 | var objectStores = { 35 | assignments: db.store('assignments'), 36 | nodes: db.store('nodes'), 37 | }; 38 | 39 | export { db, objectStores }; 40 | export default db; 41 | -------------------------------------------------------------------------------- /src/stores/error-store.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | import constants from '../constants'; 3 | import Raven from 'raven-js'; 4 | 5 | import Store from '../lib/store'; 6 | 7 | import globalConfig from '../config'; 8 | var config = globalConfig.raven; 9 | 10 | import Logger from '../util/logger'; 11 | var logger = Logger('stores/error-store.js'); 12 | 13 | class ErrorStore extends Store { 14 | 15 | constructor (options = {}) { 16 | super(options); 17 | 18 | var actionHandlers = _.map([ 19 | constants.FETCH_ASSIGNMENTS_FAIL, 20 | constants.UPDATE_ASSIGNMENT_CACHE_FAIL, 21 | constants.FETCH_NODES_FAIL, 22 | constants.UPDATE_NODE_CACHE_FAIL, 23 | constants.PERSIST_ASSIGNMENT_FAIL, 24 | constants.START_RECORDING_FAIL, 25 | constants.RESUME_RECORDING_FAIL 26 | ], (action) => { 27 | var reporter = (payload) => { 28 | this.report(action, payload); 29 | }; 30 | 31 | return [action, reporter]; 32 | }); 33 | 34 | this.bindActions.apply(this, _.flatten(actionHandlers)); 35 | } 36 | 37 | report (type, data) { 38 | data = data || {}; 39 | 40 | data.manifest = chrome.runtime.getManifest(); 41 | 42 | chrome.runtime.getPlatformInfo((platformInfo) => { 43 | data.platformInfo = platformInfo; 44 | 45 | Raven.captureMessage("Action Failed: " + type, { 46 | tags: { 47 | type: type, 48 | extensionVersion: data.manifest.version 49 | }, 50 | extra: data 51 | }); 52 | }); 53 | } 54 | }; 55 | 56 | export default ErrorStore; 57 | -------------------------------------------------------------------------------- /assets/icons/logo-watermark.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Trailblazer Logo 2 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/components/assignment-item.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | 4 | class AssignmentItem extends React.Component { 5 | 6 | constructor(props) { 7 | super(props); 8 | 9 | this.state = { 10 | show: true 11 | }; 12 | 13 | this.onClick = this.onClick.bind(this); 14 | } 15 | 16 | render() { 17 | var klass = this.state.show ? 'show' : 'destroy'; 18 | 19 | //TODO change path 20 | return
21 |
  • 22 | 23 | {this.props.item.title} 24 | 25 | 26 | 27 | 28 |
  • 29 |
    ; 30 | } 31 | 32 | componentDidMount() { 33 | ReactDOM.getDOMNode(this).addEventListener('webkitTransitionEnd', this.destroy) 34 | } 35 | 36 | onClick() { 37 | console.log(this.props.item.localId); 38 | this.context.router.push(`/assignments/${this.props.item.localId}`); 39 | } 40 | 41 | onClickDestroy(evt) { 42 | evt.stopPropagation(); 43 | 44 | var confirmation = window.confirm("Are you sure you want to delete " + this.props.item.title + "?"); 45 | 46 | if (confirmation) { 47 | this.setState({show: false}); 48 | this.props.actions.destroyAssignment(this.props.item.localId); 49 | } 50 | } 51 | 52 | }; 53 | 54 | AssignmentItem.contextTypes = { 55 | router: React.PropTypes.object.isRequired 56 | }; 57 | 58 | export default AssignmentItem; 59 | -------------------------------------------------------------------------------- /src/stores/map-store.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | import constants from '../constants'; 3 | 4 | import Logger from '../util/logger'; 5 | var logger = Logger('stores/map-store.js'); 6 | 7 | import Store from '../lib/store'; 8 | import { action } from '../decorators'; 9 | 10 | class MapStore extends Store { 11 | 12 | constructor (options = {}) { 13 | super(options); 14 | 15 | this.db = options.db; 16 | } 17 | 18 | //NOTES 19 | //When an XHR goes out, an entry is added to 20 | //SyncStore.pending.{assignment,node} specifying the localId. When the 21 | //response comes in, the entry is removed. This is a very basic semaphore to 22 | //ensure that multiple requests don't go out for the same resource resulting 23 | //in duplicates. 24 | 25 | /** 26 | * Invokes the persistence event chain for a newly created Assignment. 27 | */ 28 | @action(constants.SAVE_MAP_LAYOUT) 29 | handleSaveMapLayout(payload) { 30 | logger.info('handleSaveMapLayout', { payload: payload }); 31 | this.db.nodes.db.transaction("readwrite", ["nodes"], (err, tx) => { 32 | var store = tx.objectStore("nodes"); 33 | 34 | _.each(payload.coordinates, (coord, key) => { 35 | store.get(parseInt(key)).onsuccess = (evt) => { 36 | var node = evt.target.result; 37 | node.x = coord.x; 38 | node.y = coord.y; 39 | store.put(node); 40 | }; 41 | }); 42 | 43 | tx.oncomplete = () => { 44 | this.flux.actions.persistMapLayout(payload.localId, payload.coordinates); 45 | }; 46 | }); 47 | } 48 | 49 | }; 50 | 51 | export default MapStore; 52 | -------------------------------------------------------------------------------- /src/decorators.js: -------------------------------------------------------------------------------- 1 | import Logger from './util/logger'; 2 | 3 | /** 4 | * Sets a list on the target's class (if not already present) and appends the 5 | * method's name to that list, indicating that it is one of a store's query 6 | * method 7 | */ 8 | export function query(target, name, descriptor) { 9 | target.queryFunctions = target.queryFunctions || []; 10 | target.queryFunctions.push(name); 11 | }; 12 | 13 | /** 14 | * Populates a map of flux action constants on the target's class in order for 15 | * them to be bound on instantiation of said class. 16 | * 17 | * Motivation here is that we don't want to have to manually write out the bind 18 | * call in the constructor every time we add a new action handler; it's much 19 | * more fitting as a decorator to the handler function. 20 | */ 21 | export function action(actionName) { 22 | return function(target, name, descriptor) { 23 | if (!actionName) throw new Error(`Invalid action name for ${name}`); 24 | target.fluxActions = target.fluxActions || new Map(); 25 | target.fluxActions.set(actionName, name); 26 | } 27 | }; 28 | 29 | /** 30 | * Marks a function as deprecated, logging a warning each time it is called. 31 | */ 32 | export function deprecated(target, name, descriptor) { 33 | let description = (target.constructor) ? `${target.constructor.name}::${name}` : name; 34 | let logger = Logger(description); 35 | let fn = descriptor.value; 36 | 37 | descriptor.value = function(...args) { 38 | logger.warn("DEPRECATED"); 39 | fn.apply(this, args); 40 | }; 41 | }; 42 | 43 | export default { 44 | deprecated, 45 | action, 46 | query 47 | }; 48 | -------------------------------------------------------------------------------- /src/scripts/tour.jsx: -------------------------------------------------------------------------------- 1 | import styles from '../style/tour.manifest.scss'; 2 | import markup from '../markup/tour.html'; 3 | 4 | import React from 'react'; 5 | import ReactDOM from 'react-dom'; 6 | import domready from 'domready'; 7 | import { Router, Route, Redirect, useRouterHistory } from 'react-router'; 8 | import { createHashHistory } from 'history'; 9 | 10 | import Layout from '../components/layouts/tour.jsx'; 11 | 12 | import * as Tour from '../components/views/tour'; 13 | 14 | // Start tracking errors 15 | import Raven from 'raven-js'; 16 | import { raven as config } from '../config'; 17 | 18 | if (config.url) Raven.config(config.url).install(); 19 | 20 | var routes = 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | ; 32 | 33 | domready(() => { 34 | let container = document.getElementById('container'); 35 | 36 | if (!container) { 37 | container = document.createElement('div'); 38 | container.id = 'container'; 39 | document.body.appendChild(container); 40 | } 41 | 42 | let appHistory = useRouterHistory(createHashHistory)({ queryKey: false }); 43 | 44 | ReactDOM.render(, container); 45 | }); 46 | -------------------------------------------------------------------------------- /src/components/views/tour/step-5.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Helmet from 'react-helmet'; 3 | 4 | import actions from '../../../actions'; 5 | import constants from '../../../constants'; 6 | 7 | import { sendPageTitle } from '../../../util/send-page-title'; 8 | 9 | class Step5 extends React.Component { 10 | 11 | revealNextStep() { 12 | actions.completedOnboardingStep(constants.onboarding.STEP_5); 13 | this.context.router.push('/step-6'); 14 | } 15 | 16 | onMessage(msg) { 17 | if (msg.action === constants.RANK_NODE_FAVOURITE) this.revealNextStep(); 18 | } 19 | 20 | componentDidMount() { 21 | this.__messageHandler = this.onMessage.bind(this); 22 | chrome.runtime.onMessage.addListener(this.__messageHandler); 23 | 24 | sendPageTitle(); 25 | } 26 | 27 | componentWillUnmount() { 28 | chrome.runtime.onMessage.removeListener(this.__messageHandler); 29 | } 30 | 31 | render() { 32 | return
    33 | 34 | 35 | 36 |

    Welcome back

    37 | 38 |

    Now that we've got that extra tab under control, let's talk about something called Favouriting.

    39 | 40 |
    41 |

    When you come across something you find important, you can make it stand out.

    42 | 43 |

    Give it a try now: Tap the Trailblazer icon and Favourite this page

    44 |
    45 |
    ; 46 | } 47 | 48 | }; 49 | 50 | Step5.contextTypes = { 51 | router: React.PropTypes.object.isRequired 52 | } 53 | 54 | export default Step5; 55 | -------------------------------------------------------------------------------- /src/components/views/tour/step-3.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Link } from 'react-router'; 3 | import Helmet from 'react-helmet'; 4 | 5 | import actions from '../../../actions'; 6 | import constants from '../../../constants'; 7 | 8 | import { sendPageTitle } from '../../../util/send-page-title'; 9 | 10 | class Step3 extends React.Component { 11 | 12 | onContinueClicked() { 13 | actions.completedOnboardingStep(constants.onboarding.STEP_3) 14 | 15 | // Sneakily navigate to the next step in the background 16 | window.setTimeout(() => { 17 | this.context.router.push('/step-5'); 18 | }, 200); 19 | 20 | // The click event will propagate out to the browser's default action 21 | // with _blank links, so hopefully popups aren't blocked and then we'll 22 | // get a new tab. 23 | } 24 | 25 | componentDidMount() { 26 | sendPageTitle(); 27 | } 28 | 29 | render() { 30 | return
    31 | 32 | 33 | 34 |

    Set and forget

    35 | 36 |

    See how the icon is still orange? Trailblazer is still active.

    37 | 38 |
    39 |

    40 | When you turn it on, Trailblazer will keep track of what you do in 41 | this tab and automagically add it to your trail. 42 |

    43 | Continue 44 |
    45 |
    46 | } 47 | 48 | }; 49 | 50 | Step3.contextTypes = { 51 | router: React.PropTypes.object.isRequired 52 | }; 53 | 54 | export default Step3; 55 | -------------------------------------------------------------------------------- /src/core/extension-ui-state.js: -------------------------------------------------------------------------------- 1 | import ChromeIdentityAdapter from '../adapter/chrome_identity_adapter'; 2 | import extensionStates from './extension-states'; 3 | 4 | // Initialize our logger 5 | import Logger from '../util/logger'; 6 | var logger = Logger('core/extension-ui-sctate.js'); 7 | 8 | /** 9 | * Sets the initial extension UI state based on authentication state 10 | */ 11 | export function init() { 12 | new ChromeIdentityAdapter().isSignedIn().then((signedIn) => { 13 | if (signedIn) { 14 | // Set the extension to Idle 15 | chrome.browserAction.setIcon({ 16 | path: extensionStates.idle.browserAction 17 | }); 18 | 19 | //TODO fetch existing assignments and query which tabs are currently 20 | //recording, restoring their recording state where needed 21 | } else { 22 | // Set the extension to Idle 23 | chrome.browserAction.setIcon({ 24 | path: extensionStates.notAuthenticated.browserAction 25 | }); 26 | } 27 | }); 28 | }; 29 | 30 | /** 31 | * Updates the extension state on the supplied tab ID 32 | */ 33 | export function update(tabId, state) { 34 | logger.info('Updating extension UI state', tabId, state); 35 | switch (state) { 36 | case "recording": 37 | case "notAuthenticated": 38 | case "idle": 39 | chrome.browserAction.setIcon({ 40 | tabId: tabId, 41 | path: extensionStates[state].browserAction 42 | }); 43 | break; 44 | case "unknown": 45 | default: 46 | chrome.browserAction.setIcon({ 47 | tabId: tabId, 48 | path: extensionStates.default.browserAction 49 | }); 50 | } 51 | }; 52 | 53 | export default { init, update }; 54 | -------------------------------------------------------------------------------- /src/background/proxy-change.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | import constants from '../constants'; 3 | import messageChannel from '../util/message-channel'; 4 | 5 | import Logger from '../util/logger'; 6 | var logger = Logger('background/proxy-change.js'); 7 | 8 | /** 9 | * This listens for 'change' events in the background, and sends them over 10 | * chrome.runtime to a listener in the UI 11 | */ 12 | export default function ProxyChange(flux, stores) { 13 | var dispatcher = { 14 | 15 | /** 16 | * Bind each store to a proxying function so that 'change' events can be 17 | * sent to the UI 18 | */ 19 | initialize: function() { 20 | logger.info('initialize proxy-change dispatcher'); 21 | _.each(stores, function (storeName) { 22 | flux.store(storeName).on('change', function (payload = {}) { 23 | payload.store = storeName; 24 | logger.info('Proxying change event from ' + storeName, { payload: payload }); 25 | this.proxy(storeName, payload); 26 | }.bind(this)); 27 | 28 | logger.info('Bound proxy for ' + storeName); 29 | }.bind(this)); 30 | logger.info("Initialized ProxyChange: " + stores.join(", ")); 31 | }, 32 | 33 | /** 34 | * Dispatches a 'change' event over chrome.runtime messaging 35 | */ 36 | proxy: function(storeName, payload) { 37 | var message = { 38 | action: constants.__change__, 39 | storeName: storeName, 40 | payload: payload 41 | }; 42 | messageChannel.send(message); 43 | logger.info("Dispatched to UI", { message: message }); 44 | } 45 | 46 | } 47 | 48 | return dispatcher.initialize(); 49 | }; 50 | -------------------------------------------------------------------------------- /assets/icons/logo-trailblazer.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Trailblazer Logo 5 | Created with Sketch. 6 | 7 | 8 | 9 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/util/random-name.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Special thanks to Adrien from a SO post. 3 | * http://stackoverflow.com/questions/7666516/fancy-name-generator-in-node-js 4 | */ 5 | module.exports = { 6 | get: function () { 7 | var adjs = ["autumn", "hidden", "bitter", "misty", "silent", "empty", "dry", 8 | "dark", "summer", "icy", "delicate", "quiet", "white", "cool", "spring", 9 | "winter", "patient", "twilight", "dawn", "crimson", "wispy", "weathered", 10 | "blue", "billowing", "broken", "cold", "damp", "falling", "frosty", "green", 11 | "long", "late", "lingering", "bold", "little", "morning", "muddy", "old", 12 | "red", "rough", "still", "small", "sparkling", "throbbing", "shy", 13 | "wandering", "withered", "wild", "black", "young", "holy", "solitary", 14 | "fragrant", "aged", "snowy", "proud", "floral", "restless", "divine", 15 | "polished", "ancient", "purple", "lively", "nameless"] 16 | 17 | , nouns = ["waterfall", "river", "breeze", "moon", "rain", "wind", "sea", 18 | "morning", "snow", "lake", "sunset", "pine", "shadow", "leaf", "dawn", 19 | "glitter", "forest", "hill", "cloud", "meadow", "sun", "glade", "bird", 20 | "brook", "butterfly", "bush", "dew", "dust", "field", "fire", "flower", 21 | "firefly", "feather", "grass", "haze", "mountain", "night", "pond", 22 | "darkness", "snowflake", "silence", "sound", "sky", "shape", "surf", 23 | "thunder", "violet", "water", "wildflower", "wave", "water", "resonance", 24 | "sun", "wood", "dream", "cherry", "tree", "fog", "frost", "voice", "paper", 25 | "frog", "smoke", "star"]; 26 | 27 | return adjs[Math.floor(Math.random()*(adjs.length-1))]+" "+nouns[Math.floor(Math.random()*(nouns.length-1))]; 28 | } 29 | }; 30 | -------------------------------------------------------------------------------- /src/style/tour/main.scss: -------------------------------------------------------------------------------- 1 | @import '../reset'; 2 | @import 'components/task'; 3 | @import 'components/record-button'; 4 | @import 'components/stop-button'; 5 | @import 'components/trailblazer-button'; 6 | @import 'components/favourite-button'; 7 | @import 'components/folder-icon'; 8 | 9 | @import 'action'; 10 | 11 | body { 12 | font-family: 'Open Sans', sans-serif; 13 | background-color: rgb(255, 250, 243); 14 | color: darken(rgb(161, 160, 159), 15%); 15 | } 16 | 17 | a { 18 | cursor: pointer; 19 | } 20 | 21 | #twitter-share-button { 22 | position: relative; 23 | top: 3px; 24 | } 25 | 26 | .wrap { 27 | margin: 0 auto; 28 | text-align: center; 29 | } 30 | 31 | .btn-group { 32 | margin-top: 24px; 33 | } 34 | 35 | .btn { 36 | display: block; 37 | margin: 12px auto; 38 | width: 180px; 39 | padding: 12px; 40 | border-radius: 4px; 41 | background: rgb(29, 175, 173); 42 | color: white; 43 | text-decoration: none; 44 | 45 | font-variant: small-caps; 46 | text-transform: uppercase; 47 | font-weight: 600; 48 | font-size: 14px; 49 | 50 | &:hover { 51 | background: lighten(rgb(29, 175, 173), 5%); 52 | } 53 | } 54 | 55 | .secondary { 56 | font-size: 13px; 57 | font-weight: 400; 58 | color: rgb(120, 120, 120); 59 | } 60 | 61 | h1 { 62 | font-size: 36px; 63 | line-height: 48px; 64 | font-weight: 400; 65 | 66 | margin: 24px auto; 67 | margin-top: 10%; 68 | } 69 | 70 | h2 { 71 | font-size: 28px; 72 | line-height: 48px; 73 | font-weight: 400; 74 | 75 | margin: 24px auto; 76 | margin-top: 5%; 77 | } 78 | 79 | p { 80 | line-height: 24px; 81 | font-size: 18px; 82 | 83 | &.aside { 84 | font-size: 14px; 85 | } 86 | 87 | max-width: 640px; 88 | 89 | margin: 0 auto; 90 | margin-bottom: 12px; 91 | } 92 | -------------------------------------------------------------------------------- /src/stores/authentication-store.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | import constants from '../constants'; 3 | 4 | import Store from '../lib/store'; 5 | 6 | import { query, action } from '../decorators'; 7 | 8 | import Logger from '../util/logger'; 9 | var logger = Logger('stores/authentication-store.js'); 10 | 11 | import ChromeIdentityAdapter from '../adapter/chrome_identity_adapter'; 12 | 13 | class AuthenticationStore extends Store { 14 | 15 | constructor(options = {}) { 16 | super(options); 17 | 18 | this.authenticated = false; 19 | 20 | this.db = options.db; 21 | 22 | new ChromeIdentityAdapter().isSignedIn().then((signedIn) => { 23 | this.authenticated = signedIn; 24 | }); 25 | 26 | } 27 | 28 | getState() { 29 | return { 30 | authenticated: this.authenticated 31 | }; 32 | } 33 | 34 | /** 35 | * Call on the ChromeIdentityAdapter to initiate the sign in process. 36 | */ 37 | @action(constants.SIGN_IN) 38 | handleSignIn() { 39 | new ChromeIdentityAdapter().signIn().done( 40 | () => { 41 | this.authenticated = true; 42 | this.emit('change', this.getState()); 43 | this.flux.actions.requestAssignments(); 44 | this.flux.actions.signInSuccess(); 45 | }, 46 | () => { 47 | this.authenticated = false; 48 | this.emit('change', this.getState()); 49 | }); 50 | } 51 | 52 | /** 53 | * Call on the ChromeIdentityAdapter to invalidate the current session. 54 | */ 55 | @action(constants.SIGN_OUT) 56 | handleSignOut() { 57 | new ChromeIdentityAdapter().signOut().done(() => { 58 | this.authenticated = false; 59 | this.emit('change', this.getState()); 60 | }); 61 | } 62 | 63 | }; 64 | 65 | export default AuthenticationStore; 66 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | var argv = require('yargs').argv 2 | , webpack = require('webpack') 3 | , ExtractTextPlugin = require('extract-text-webpack-plugin'); 4 | 5 | var cssExtractor = new ExtractTextPlugin('css', '[name].css'); 6 | var htmlExtractor = new ExtractTextPlugin('html', '[name].html'); 7 | 8 | module.exports = { 9 | context: __dirname + '/src', 10 | entry: { 11 | 'background': './scripts/background.js', 12 | 'main-ui': './scripts/main-ui.jsx', 13 | 'offline-data': './scripts/offline-data.jsx', 14 | 'popup': './scripts/popup.jsx', 15 | 'public-map': './scripts/public-map.jsx', 16 | 'tour': './scripts/tour.jsx', 17 | 'page-title': './content-scripts/page-title.js' 18 | }, 19 | 20 | output: { 21 | path: __dirname + '/build', 22 | filename: '[name].js' 23 | }, 24 | 25 | module: { 26 | loaders: [ 27 | { 28 | test: /\.html$/, 29 | exclude: /(node_modules)/, 30 | loader: htmlExtractor.extract('html-loader') 31 | }, 32 | { 33 | test: /\.scss$/, 34 | exclude: /(node_modules)/, 35 | loader: cssExtractor.extract('style-loader', 'css-loader!sass-loader') 36 | }, 37 | { 38 | test: /\.jsx?$/, 39 | exclude: /(node_modules)/, 40 | loader: 'babel', 41 | query: { 42 | cacheDirectory: true 43 | } 44 | }, 45 | { 46 | test: /\.jsx?$/, 47 | exclude: /(node_modules)/, 48 | loader: 'transform/cacheable?envify', 49 | cacheable: true 50 | } 51 | ] 52 | }, 53 | 54 | plugins: (argv.production || argv.staging) 55 | // Staging/Production plugins 56 | ? [ 57 | // new webpack.optimize.UglifyJsPlugin({ minimize: true }), 58 | cssExtractor, 59 | htmlExtractor 60 | ] 61 | 62 | // Development plugins 63 | : [ 64 | cssExtractor, 65 | htmlExtractor 66 | ], 67 | 68 | devtool: 'source-map' 69 | } 70 | -------------------------------------------------------------------------------- /src/components/share-map.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ImageButton from './image-button'; 3 | import Popover from './popover'; 4 | 5 | export default class ShareMap extends React.Component { 6 | 7 | render() { 8 | var content = "Your map is now public and viewable at:"; 9 | var makePrivateText = "Make Private"; 10 | var shareText = this.props.visible ? 'Shared' : 'Share'; 11 | var shareTitle = this.props.visible ? "Change map privacy" : "Allow others to view"; 12 | 13 | return
    14 | 15 |
    16 | {content} 17 |
    18 | 21 | 27 |
    28 | 35 |
    ; 36 | } 37 | 38 | makePrivate() { 39 | if (this.props.visible) { 40 | this.props.actions.makeAssignmentHidden(this.props.localAssignmentId); 41 | }; 42 | this.props.togglePopover(); 43 | } 44 | 45 | share() { 46 | if (!this.props.visible) { 47 | this.props.actions.makeAssignmentVisible(this.props.localAssignmentId); 48 | }; 49 | this.props.togglePopover(); 50 | } 51 | 52 | togglePopover() { 53 | var bool = !this.state.popoverDisplay; 54 | this.setState({popoverDisplay: bool}); 55 | return false; 56 | } 57 | }; 58 | -------------------------------------------------------------------------------- /src/background/chrome-events.js: -------------------------------------------------------------------------------- 1 | import actions from '../actions'; 2 | 3 | // Initialize our logger 4 | import Logger from '../util/logger'; 5 | var logger = Logger('background/chrome-events.js'); 6 | 7 | export default function bind(flux) { 8 | chrome.tabs.onCreated.addListener(function(tab) { 9 | actions.tabCreated(tab.id, tab.url, tab.title, tab.openerTabId, tab); 10 | }); 11 | 12 | chrome.tabs.onActivated.addListener(function(focusInfo) { 13 | actions.tabFocused(focusInfo.tabId); 14 | }); 15 | 16 | chrome.tabs.onUpdated.addListener(function(tabId, changeInfo, tab) { 17 | // filter the update events, so only url/title etc. changes are fired 18 | if (changeInfo.url) { 19 | actions.tabUpdated(tabId, changeInfo.url, tab.title, tab); 20 | } 21 | }); 22 | 23 | chrome.tabs.onRemoved.addListener(function(tabId, removeInfo) { 24 | actions.tabClosed(tabId); 25 | }); 26 | 27 | chrome.webNavigation.onHistoryStateUpdated.addListener(function(details) { 28 | if (details.frameId === 0) { 29 | actions.historyStateUpdated( 30 | details.tabId, 31 | details.url, 32 | details.transitionType, 33 | details.transitionQualifiers, 34 | details.timestamp); 35 | } 36 | }); 37 | 38 | chrome.tabs.onReplaced.addListener(function(addedTabId, removedTabId) { 39 | actions.tabReplaced(addedTabId, removedTabId); 40 | }); 41 | 42 | chrome.webNavigation.onCreatedNavigationTarget.addListener(function(details) { 43 | actions.createdNavigationTarget( 44 | details.sourceTabId, 45 | details.tabId, 46 | details.url, 47 | details.timestamp); 48 | }); 49 | 50 | chrome.webNavigation.onCommitted.addListener(function(details) { 51 | if (details.frameId === 0) { 52 | actions.webNavCommitted(details.tabId, 53 | details.url, 54 | details.transitionType, 55 | details.transitionQualifiers, 56 | details.timestamp); 57 | } 58 | }); 59 | } 60 | -------------------------------------------------------------------------------- /src/components/views/tour/step-1.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Helmet from 'react-helmet'; 3 | 4 | import actions from '../../../actions'; 5 | import constants from '../../../constants'; 6 | import queries from '../../../queries'; 7 | 8 | import { sendPageTitle } from '../../../util/send-page-title'; 9 | 10 | class Step1 extends React.Component { 11 | 12 | revealNextStep() { 13 | actions.completedOnboardingStep(constants.onboarding.STEP_1); 14 | this.context.router.push('/step-2'); 15 | } 16 | 17 | onMessage(msg) { 18 | if (msg.action === constants.START_RECORDING_SUCCESS) { 19 | this.revealNextStep(); 20 | } 21 | } 22 | 23 | componentDidMount() { 24 | this.__messageHandler = this.onMessage.bind(this); 25 | chrome.runtime.onMessage.addListener(this.__messageHandler); 26 | 27 | // Check if we're already recording 28 | chrome.tabs.getCurrent((tab) => { 29 | queries.TabStore.getTabState(this.props.tabId).then(({ recording }) => { 30 | if (recording) this.revealNextStep(); 31 | }); 32 | }); 33 | 34 | sendPageTitle(); 35 | } 36 | 37 | componentWillUnmount() { 38 | chrome.runtime.onMessage.removeListener(this.__messageHandler); 39 | } 40 | 41 | render() { 42 | return
    43 | 44 | 45 | 46 |

    Let's start making a trail

    47 | 48 |

    To get the most out of Trailblazer, it's good to start the trail before you start browsing.

    49 | 50 |

    Tap on the Trailblazer Icon in your browser toolbar.
    (it's up in the top right corner)

    51 |

    52 | Activate Trailblazer by tapping the activate button 53 | 54 |

    55 |
    56 | } 57 | 58 | }; 59 | 60 | Step1.contextTypes = { 61 | router: React.PropTypes.object.isRequired 62 | }; 63 | 64 | export default Step1; 65 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "trailblazer-wash", 3 | "version": "0.0.0", 4 | "description": "Trailblazer \"Wash\" Chrome extension", 5 | "scripts": { 6 | "build": "node script/build" 7 | }, 8 | "repository": { 9 | "type": "git", 10 | "url": "git://github.com/twingl/trailblazer-wash.git" 11 | }, 12 | "bugs": { 13 | "url": "https://github.com/twingl/trailblazer-wash/issues" 14 | }, 15 | "homepage": "http://trailblazer.io", 16 | "devDependencies": { 17 | "babel": "^6.3.26", 18 | "babel-core": "^6.4.5", 19 | "babel-loader": "^6.2.1", 20 | "babel-plugin-transform-class-properties": "^6.4.0", 21 | "babel-plugin-transform-decorators-legacy": "^1.3.4", 22 | "babel-plugin-transform-runtime": "^6.4.3", 23 | "babel-preset-es2015": "^6.3.13", 24 | "babel-preset-react": "^6.3.13", 25 | "babel-preset-stage-3": "^6.3.13", 26 | "babel-runtime": "^6.3.19", 27 | "camelize": "~1.0.0", 28 | "classnames": "https://registry.npmjs.org/classnames/-/classnames-2.1.3.tgz", 29 | "css-loader": "^0.23.1", 30 | "domready": "^1.0.7", 31 | "dotenv": "^1.2.0", 32 | "envify": "^1.2.1", 33 | "envify-loader": "^0.1.0", 34 | "extract-text-webpack-plugin": "^1.0.1", 35 | "fluxxor": "^1.5.0", 36 | "history": "^2.0.0-rc2", 37 | "html-loader": "^0.4.0", 38 | "jsdoc": "~3.3.0-alpha9", 39 | "keen.io": "^0.1.3", 40 | "lodash": "^4.2.0", 41 | "merge-stream": "^0.1.7", 42 | "ngraph.forcelayout": "0.0.21", 43 | "ngraph.graph": "0.0.11", 44 | "node-sass": "^3.8.0", 45 | "node-uuid": "~1.4.1", 46 | "pretty-hrtime": "^0.2.2", 47 | "promise": "^6.0.1", 48 | "raven-js": "^1.1.18", 49 | "react": "^0.14.7", 50 | "react-dom": "^0.14.7", 51 | "react-helmet": "^2.3.1", 52 | "react-router": "^2.0.0-rc5", 53 | "require-dir": "~0.1.0", 54 | "rimraf": "^2.5.1", 55 | "sass-flex-mixin": "^1.0.0", 56 | "sass-loader": "^3.1.2", 57 | "style-loader": "^0.13.0", 58 | "superagent": "^0.20.0", 59 | "svg-pan-zoom": "^3.2.2", 60 | "transform-loader": "^0.2.3", 61 | "treo": "0.5.1", 62 | "webpack": "^1.12.12", 63 | "yargs": "^3.6.0" 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/components/views/popup/recording.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import Constants from '../../../constants'; 4 | import Star from '../../star'; 5 | import AssignmentTitle from '../../assignment-title'; 6 | 7 | class Recording extends React.Component { 8 | 9 | onStopRecordingClicked(evt) { 10 | chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => { 11 | var tab = tabs[0]; 12 | this.props.actions.stopRecording(tab.id); 13 | }); 14 | } 15 | 16 | onSignOutClicked(evt) { 17 | this.props.actions.signOut(); 18 | } 19 | 20 | onTutorialClicked(evt) { 21 | chrome.tabs.create({ active: true, url: chrome.runtime.getURL("/build/tour.html") }); 22 | } 23 | 24 | onViewTrailClicked(evt) { 25 | var url = chrome.runtime.getURL(`/build/main-ui.html#/assignments/${this.props.assignment.localId}`); 26 | chrome.tabs.create({ url }); 27 | } 28 | 29 | render() { 30 | return
    31 |
    32 | 33 |
    34 | 35 |

    Recording

    36 | 37 | 39 | View Trail 40 | 41 | 42 | 44 | 45 | 46 | 47 |
    48 | 49 |
    50 | 51 | 54 | 55 | 56 | 57 | 58 | 61 | 62 | 63 | 64 |
    ; 65 | } 66 | 67 | }; 68 | 69 | export default Recording; 70 | -------------------------------------------------------------------------------- /src/components/star.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import messageChannel from '../util/message-channel'; 4 | import Actions from '../actions'; 5 | import Constants from '../constants'; 6 | 7 | export default class Star extends React.Component { 8 | 9 | constructor(props) { 10 | super(props); 11 | 12 | this.state = { 13 | rank: props.node.rank 14 | }; 15 | } 16 | 17 | componentDidMount() { 18 | // listen for `change` evts for the current node 19 | messageChannel.listen( function (message) { 20 | switch (message.action) { 21 | case Constants.__change__: 22 | if (message.payload.store === "NodeStore" && 23 | message.payload.node.localId === this.props.node.localId) { 24 | this.props.node.rank = message.payload.node.rank; 25 | this.setState({ rank: message.payload.node.rank }); 26 | } 27 | break; 28 | } 29 | }.bind(this)); 30 | } 31 | 32 | render() { 33 | var width = this.props.width + "px"; 34 | var height= this.props.height + "px"; 35 | var viewBox = "0 0 " + this.props.width + " " + this.props.height; 36 | var favouriteClass = this.state.rank === 1 ? "favourite-active" : "favourite"; 37 | 38 | return 41 | 46 | 48 | 49 | 50 | ; 51 | } 52 | 53 | onClick() { 54 | messageChannel.send({ 55 | action: "trackUIEvent", 56 | eventName: "ui.popup.favourite.toggle", 57 | eventData: { } 58 | }); 59 | 60 | if (this.props.node.rank === 1) { 61 | Actions.rankNodeNeutral(this.props.node.localId); 62 | } else { 63 | Actions.rankNodeFavourite(this.props.node.localId); 64 | } 65 | } 66 | 67 | }; 68 | -------------------------------------------------------------------------------- /src/components/views/popup/sign-in.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Classnames from 'classnames'; 3 | 4 | import Constants from '../../../constants'; 5 | 6 | class SignIn extends React.Component { 7 | 8 | constructor(props) { 9 | super(props); 10 | 11 | this.state = { 12 | working: false, 13 | buttonText: 'Sign In' 14 | }; 15 | } 16 | 17 | onSignInClicked(evt) { 18 | if (this.state.working) return; 19 | 20 | // Set the working flag so the spinner shows on update 21 | var buttonText = this.state.buttonText; 22 | this.setState({ working: true, buttonText: '' }); 23 | 24 | // Set a timeout so if the call fails, the user can retry 25 | window.setTimeout(() => { 26 | this.setState({ buttonText, working: false }); 27 | }, 10000); 28 | 29 | // Initiate the sign in process 30 | this.props.actions.signIn(); 31 | } 32 | 33 | onTutorialClicked(evt) { 34 | chrome.tabs.create({ active: true, url: chrome.runtime.getURL("/build/tour.html") }); 35 | } 36 | 37 | onManageDataClicked(evt) { 38 | evt.preventDefault(); 39 | chrome.tabs.create({ active: true, url: chrome.runtime.getURL("/build/offline-data.html") }); 40 | } 41 | 42 | render() { 43 | let classes = Classnames('login', 'button', 'sign-in', { 44 | 'throbber': this.state.working 45 | }); 46 | 47 | return
    48 |
    49 |

    Trailblazer is shutting down

    50 |

    51 | Manage your data 54 |

    55 |
    56 | 57 | 70 |
    ; 71 | } 72 | }; 73 | 74 | export default SignIn; 75 | -------------------------------------------------------------------------------- /src/components/views/tour/sign-in.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Helmet from 'react-helmet'; 3 | 4 | import actions from '../../../actions'; 5 | import constants from '../../../constants'; 6 | import Identity from '../../../adapter/chrome_identity_adapter'; 7 | 8 | import { sendPageTitle } from '../../../util/send-page-title'; 9 | 10 | class SignIn extends React.Component { 11 | 12 | constructor(props) { 13 | super(props); 14 | 15 | this.state = { 16 | signedIn: undefined 17 | } 18 | } 19 | 20 | onMessage(msg) { 21 | if (msg.action === constants.SIGN_IN_SUCCESS) this.revealNextStep(); 22 | } 23 | 24 | componentDidMount() { 25 | //Page title 26 | this.__messageHandler = this.onMessage.bind(this); 27 | chrome.runtime.onMessage.addListener(this.__messageHandler); 28 | 29 | new Identity().isSignedIn().then((signedIn) => { 30 | if (signedIn) { 31 | this.revealNextStep(); 32 | } else { 33 | this.setState({ signedIn: false }); 34 | } 35 | }); 36 | 37 | sendPageTitle(); 38 | } 39 | 40 | componentWillUnmount() { 41 | chrome.runtime.onMessage.removeListener(this.__messageHandler); 42 | } 43 | 44 | revealNextStep() { 45 | // Navigate 46 | this.context.router.push('/step-1'); 47 | } 48 | 49 | onSignInClicked(evt) { 50 | evt.preventDefault(); 51 | 52 | // Launch the combined sign in/up flow through flux 53 | actions.signIn(); 54 | } 55 | 56 | render() { 57 | if (typeof this.state.signedIn === "undefined") { 58 | return
    ; 59 | } else { 60 | return
    61 | 62 | 63 | 64 |

    Installation complete!

    65 | 66 |

    That's the first step out of the way.

    67 |

    To keep your data safe and sound, you'll need to sign in below.

    68 | 69 |
    70 |

    If you're new to Trailblazer, we'll just need your email address and a new password to get you signed up.

    71 | Sign Up 72 | Already have an account? Sign In 73 |
    74 |
    ; 75 | } 76 | } 77 | 78 | }; 79 | 80 | SignIn.contextTypes = { 81 | router: React.PropTypes.object.isRequired 82 | }; 83 | 84 | export default SignIn; 85 | -------------------------------------------------------------------------------- /src/components/layouts/popup.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import ChromeIdentityAdapter from '../../adapter/chrome_identity_adapter'; 4 | import Constants from '../../constants'; 5 | import queries from '../../queries'; 6 | 7 | import * as Popup from '../views/popup'; 8 | 9 | class Layout extends React.Component { 10 | 11 | constructor(props) { 12 | super(props); 13 | 14 | this.state = { 15 | signedIn: null, 16 | assignment: null, 17 | node: null, 18 | recording: null 19 | } 20 | } 21 | 22 | componentWillMount() { 23 | // Set up auth information 24 | new ChromeIdentityAdapter().isSignedIn().then((signedIn) => { 25 | this.setState({ signedIn }); 26 | }); 27 | 28 | chrome.runtime.onMessage.addListener((message) => { 29 | switch (message.action) { 30 | case Constants.__change__: 31 | // If we hear about a change in the tab store, update the tab state 32 | if (message.payload.store === "TabStore") { 33 | queries.TabStore.getTabState(this.props.tabId).then(({ recording, assignment, node }) => { 34 | this.setState({ recording, assignment, node }); 35 | }); 36 | } 37 | 38 | // If we hear about an authentication change, check auth state 39 | if (message.payload.store === "AuthenticationStore") { 40 | new ChromeIdentityAdapter().isSignedIn().then((signedIn) => { 41 | this.setState({ signedIn }); 42 | }); 43 | } 44 | } 45 | }); 46 | 47 | queries.TabStore.getTabState(this.props.tabId).then(({ recording, assignment, node }) => { 48 | this.setState({ recording, assignment, node }); 49 | }); 50 | } 51 | 52 | render() { 53 | if (this.state.assignment && this.state.assignment && this.state.recording === true) { 54 | return 59 | 60 | } else if (this.state.signedIn === true) { 61 | return 62 | 63 | } else if (this.state.signedIn === false) { 64 | return 65 | 66 | } else { 67 | return 68 | } 69 | } 70 | 71 | }; 72 | 73 | export default Layout; 74 | -------------------------------------------------------------------------------- /src/components/assignment-title.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import Actions from '../actions'; 4 | import Constants from '../constants'; 5 | 6 | export default class AssignmentTitle extends React.Component { 7 | 8 | constructor(props) { 9 | super(props); 10 | 11 | this.state = { 12 | editable: false, 13 | title: props.assignment.title 14 | }; 15 | } 16 | 17 | componentDidMount() { 18 | chrome.runtime.onMessage.addListener((message) => { 19 | switch (message.action) { 20 | case Constants.__change__: 21 | if (message.storeName === "AssignmentStore" && 22 | message.payload.assignment && 23 | message.payload.assignment.localId === this.props.assignment.localId) { 24 | this.setState({ title: message.payload.assignment.title }); 25 | this.forceUpdate(); 26 | } 27 | } 28 | }); 29 | } 30 | 31 | render() { 32 | var editable = this.state.editable; 33 | 34 | 35 | if (editable) { 36 | return ; 45 | } else { 46 | return 47 | {this.state.title} 48 | 51 | ; 52 | }; 53 | } 54 | 55 | onFocus(evt) { 56 | evt.target.select(); 57 | } 58 | 59 | onKeyPress(evt) { 60 | if (evt.key === 'Enter') this.onBlur(); 61 | if (evt.key === 'Escape') { 62 | this.setState({title: this.props.assignment.title}); 63 | this.setState({editable: false}); 64 | this.onBlur(); 65 | } 66 | } 67 | 68 | onIconClick(evt) { 69 | evt.preventDefault(); 70 | this.setState({editable: true}); 71 | } 72 | 73 | onChange(evt) { 74 | this.setState({title: evt.target.value}); 75 | } 76 | 77 | onBlur(evt) { 78 | this.setState({editable: false}); 79 | if (this.state.title !== this.props.assignment.title) { 80 | Actions.updateAssignmentTitle(this.props.assignment.localId, this.state.title); 81 | }; 82 | } 83 | 84 | }; 85 | -------------------------------------------------------------------------------- /src/services/local/node-service.js: -------------------------------------------------------------------------------- 1 | import { objectStores } from '../../db'; 2 | 3 | /** 4 | * Wrapper around the indexeddb operations commonly performed on Nodes. 5 | * 6 | * This will emit flux actions to notify stores and other listeners of updated 7 | * data. 8 | */ 9 | class NodeService { 10 | constructor(flux) { 11 | this.flux = flux; 12 | this.objectStore = objectStores.nodes; 13 | } 14 | 15 | /** 16 | * Resolves with all nodes in storage 17 | */ 18 | list() { 19 | return this.objectStore.all(); 20 | } 21 | 22 | /** 23 | * Creates a new Node with `attributes`, resolving with the new record 24 | */ 25 | create(attributes) { 26 | return new Promise((resolve, reject) => { 27 | // Add the new record to the IDB store, then resolve with the newly 28 | // allocated localId on the record 29 | this.objectStore.put(attributes) 30 | .then(localId => { 31 | attributes.localId = localId; 32 | resolve(attributes); 33 | }) 34 | .catch(reject); 35 | }); 36 | } 37 | 38 | /** 39 | * Attempts to read a specific record from the store 40 | */ 41 | read(localId) { 42 | return this.objectStore.get(localId); 43 | } 44 | 45 | /** 46 | * Updates the local copy of an node with `updatedAttributes` and saves 47 | * it, resolving with the updated record 48 | */ 49 | update(localId, updatedAttributes) { 50 | return new Promise((resolve, reject) => { 51 | // Lock the DB before performing the update so we don't have problems 52 | // with race conditions and lose data 53 | this.objectStore.db.transaction('readwrite', ['nodes'], (err, tx) => { 54 | const store = tx.objectStore('nodes'); 55 | 56 | // Fetch the existing record 57 | store.get(localId).onsuccess = (evt) => { 58 | let node = evt.target.result; 59 | 60 | // Apply the new attributes to the node 61 | Object.assign(node, updatedAttributes); 62 | 63 | // Save it 64 | store.put(node); 65 | 66 | // When we're done saving, resolve the promise with the new record 67 | tx.oncomplete = evt => resolve(evt.target.result); 68 | 69 | // Let the caller be aware of any errors during the transaction 70 | tx.onerror = reject; 71 | }; 72 | }); 73 | }); 74 | } 75 | 76 | /** 77 | * Destroy a record from the store 78 | */ 79 | destroy(localId) { 80 | return this.objectStore.del(localId); 81 | } 82 | }; 83 | 84 | export { NodeService }; 85 | export default NodeService; 86 | -------------------------------------------------------------------------------- /src/components/views/popup/idle.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import Constants from '../../../constants'; 4 | 5 | /** 6 | * The Idle popup view. 7 | * Displayed when the user is signed in, but the current tab is not recording. 8 | */ 9 | class Idle extends React.Component { 10 | 11 | onRecordClicked(evt) { 12 | chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => { 13 | var tab = tabs[0]; 14 | this.props.actions.startRecording(tab.id, tab); 15 | }); 16 | } 17 | 18 | onViewTrailsClicked(evt) { 19 | chrome.tabs.create({ url: "/build/main-ui.html" }); 20 | } 21 | 22 | onSignOutClicked(evt) { 23 | this.props.actions.signOut(); 24 | } 25 | 26 | onTutorialClicked(evt) { 27 | chrome.tabs.create({ active: true, url: chrome.runtime.getURL("/build/tour.html") }); 28 | } 29 | 30 | onManageDataClicked(evt) { 31 | evt.preventDefault(); 32 | chrome.tabs.create({ active: true, url: chrome.runtime.getURL("/build/offline-data.html") }); 33 | } 34 | 35 | render() { 36 | return
    37 |
    38 |

    Trailblazer is shutting down

    39 |

    40 | Manage your data 43 |

    44 |
    45 | 74 |
    ; 75 | } 76 | 77 | }; 78 | 79 | export default Idle; 80 | -------------------------------------------------------------------------------- /src/services/local/assignment-service.js: -------------------------------------------------------------------------------- 1 | import { objectStores } from '../../db'; 2 | 3 | /** 4 | * Wrapper around the indexeddb operations commonly performed on Assignments. 5 | * 6 | * This will emit flux actions to notify stores and other listeners of updated 7 | * data. 8 | */ 9 | class AssignmentService { 10 | constructor(flux) { 11 | this.flux = flux; 12 | this.objectStore = objectStores.assignments; 13 | } 14 | 15 | /** 16 | * Resolves with all assignments in storage 17 | */ 18 | list() { 19 | return this.objectStore.all(); 20 | } 21 | 22 | /** 23 | * Creates a new Assignment with `attributes`, resolving with the new record 24 | */ 25 | create(attributes) { 26 | return new Promise((resolve, reject) => { 27 | // Add the new record to the IDB store, then resolve with the newly 28 | // allocated localId on the record 29 | this.objectStore.put(attributes) 30 | .then(localId => { 31 | attributes.localId = localId; 32 | resolve(attributes); 33 | }) 34 | .catch(reject); 35 | }); 36 | } 37 | 38 | /** 39 | * Attempts to read a specific record from the store 40 | */ 41 | read(localId) { 42 | return this.objectStore.get(localId); 43 | } 44 | 45 | /** 46 | * Updates the local copy of an assignment with `updatedAttributes` and saves 47 | * it, resolving with the updated record 48 | */ 49 | update(localId, updatedAttributes) { 50 | return new Promise((resolve, reject) => { 51 | // Lock the DB before performing the update so we don't have problems 52 | // with race conditions and lose data 53 | this.objectStore.db.transaction('readwrite', ['assignments'], (err, tx) => { 54 | const store = tx.objectStore('assignments'); 55 | 56 | // Fetch the existing record 57 | store.get(localId).onsuccess = (evt) => { 58 | let assignment = evt.target.result; 59 | 60 | // Apply the new attributes to the assignment 61 | Object.assign(assignment, updatedAttributes); 62 | 63 | // Save it 64 | store.put(assignment); 65 | 66 | // When we're done saving, resolve the promise with the new record 67 | tx.oncomplete = evt => resolve(evt.target.result); 68 | 69 | // Let the caller be aware of any errors during the transaction 70 | tx.onerror = reject; 71 | }; 72 | }); 73 | }); 74 | } 75 | 76 | /** 77 | * Destroy a record from the store 78 | */ 79 | destroy(localId) { 80 | return this.objectStore.del(localId); 81 | } 82 | }; 83 | 84 | export { AssignmentService }; 85 | export default AssignmentService; 86 | -------------------------------------------------------------------------------- /src/style/main-ui/_node-popover.scss: -------------------------------------------------------------------------------- 1 | @import '../color'; 2 | @import '../objects/button'; 3 | 4 | $width: 320px; 5 | $min-height: 80px; 6 | 7 | $arrow-size: 8px; 8 | 9 | $background: #FFF; 10 | 11 | $offset: 10px; 12 | 13 | .node-popover { 14 | width: $width; 15 | min-height: $min-height; 16 | 17 | position: absolute; 18 | bottom: $arrow-size + $offset; 19 | left: $width / -2; 20 | 21 | background: $background; 22 | border-radius: 2px; 23 | box-shadow: 0px 3px 10px rgba(0, 0, 0, 0.3); 24 | 25 | &:after { 26 | content: ''; 27 | position: absolute; 28 | border-style: solid; 29 | bottom: 1px - $arrow-size; // 1px adjustment 30 | left: ($width / 2) - $arrow-size; 31 | border-width: $arrow-size $arrow-size 0px $arrow-size; 32 | border-color: $background transparent; 33 | } 34 | 35 | &.delete-pending { 36 | background: $red-500; 37 | color: white; 38 | 39 | .url { 40 | &, &:visited { color: rgba(255, 255, 255, 0.7) !important; } 41 | &:hover { color: rgba(255, 255, 255, 0.9) !important; } 42 | } 43 | 44 | &:after { border-color: $red-500 transparent; } 45 | } 46 | 47 | .content { 48 | padding: 16px; 49 | box-sizing: border-box; 50 | 51 | h1 { 52 | font-size: 16px; 53 | font-weight: normal; 54 | word-break: break-word; 55 | margin: 0; 56 | margin-bottom: 0.5rem; 57 | } 58 | 59 | .detail { 60 | margin-bottom: 0.7rem; 61 | 62 | .url { 63 | overflow-x: hidden; 64 | white-space: nowrap; 65 | display: block; 66 | text-overflow: ellipsis; 67 | 68 | &, &:visited { color: #757575; } 69 | &:hover { color: #1E88E5; } 70 | } 71 | } 72 | 73 | .warning { 74 | font-size: 11px; 75 | color: $red-100; 76 | text-align: center; 77 | } 78 | 79 | .actions { 80 | @include flexbox; 81 | @include flex-direction($value: row); 82 | 83 | .primary { 84 | display: inline-block; 85 | 86 | .resume { 87 | @extend .button; 88 | } 89 | 90 | .confirm-delete { 91 | @extend .button; 92 | @extend .button-danger-inverted; 93 | } 94 | } 95 | 96 | .secondary { 97 | @include flex($fg: 1); 98 | 99 | .delete { 100 | @extend .button; 101 | @extend .button-secondary; 102 | } 103 | 104 | .cancel-delete { 105 | @extend .button; 106 | @extend .button-secondary-inverted; 107 | } 108 | } 109 | } 110 | } 111 | 112 | } 113 | 114 | -------------------------------------------------------------------------------- /src/components/map-view.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Link } from 'react-router'; 3 | import _ from 'lodash'; 4 | 5 | import Constants from '../constants'; 6 | 7 | //components 8 | import AssignmentTitle from './assignment-title'; 9 | import ShareMap from './share-map'; 10 | import Legend from './legend'; 11 | import Trail from './trail'; 12 | 13 | import Logger from '../util/logger'; 14 | var logger = Logger('map-view'); 15 | 16 | export default class MapView extends React.Component { 17 | 18 | constructor(props) { 19 | super(props); 20 | 21 | this.state = { 22 | sharePopoverState: false 23 | }; 24 | } 25 | 26 | render() { 27 | var nodeObj = {}; 28 | this.props.nodes.map(node => nodeObj[node.localId] = node); 29 | 30 | var visible, shareText, title, url; 31 | 32 | visible = this.props.assignment.visible; //state 33 | shareText = (visible) ? "Shared" : "Share"; //state 34 | title = this.props.assignment.title; 35 | url = this.props.assignment.url; 36 | 37 | //nodes are immutable 38 | var data = { 39 | nodeObj: nodeObj, 40 | assignment: this.props.assignment 41 | }; 42 | 43 | return
    44 | 45 | 46 | 47 | 48 | 49 | 54 | Feedback | 55 | 56 | 61 | 62 | 63 | 70 | 71 |
    ; 72 | } 73 | 74 | handleClick(evt) { 75 | //remove popover when clicking anywhere else 76 | if (this.state.sharePopoverState && 77 | !document.getElementById('share-popover').contains(evt.target)) { 78 | this.setState({sharePopoverState: false}); 79 | }; 80 | } 81 | 82 | togglePopover() { 83 | var bool = !this.state.sharePopoverState; 84 | this.setState({sharePopoverState: bool}); 85 | } 86 | }; 87 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | **Repository name change! If you have a copy of the extension that points to `github.com/twingl/trailblazer-wash` you will want to update that to the new one** 2 | 3 | # Trailblazer Extension 4 | 5 | Trailblazer is a Chrome extension built to track a user's browsing activity and 6 | build up a map, helping them make sense of the places they visit. 7 | 8 | ## Prerequisites 9 | 10 | ### Node 11 | 12 | If you have a modern version of node, excellent - you're good to go. This has 13 | been tested on 5.x so far so if you run into problems create an issue and we 14 | can address it. 15 | 16 | ### EditorConfig 17 | 18 | You also should have the [EditorConfig plugin](http://editorconfig.org/) 19 | installed for your editor before editing any of the source. 20 | 21 | ## Setup 22 | 23 | See the [Getting Started](https://github.com/twingl/trailblazer-wash/wiki/Getting-Started) guide 24 | 25 | ## Domain Concepts 26 | 27 | ### Map / Trail 28 | 29 | This is what the user sees when they view an assignment using Trailblazer's UI, 30 | i.e. it's the sum of an Assignment and all of its Nodes rendered in the graph 31 | layout. 32 | 33 | ### Assignment 34 | 35 | This is the container to which browsing activity (Nodes) is attached. 36 | 37 | As far as vernacular is concerned, it should be noted here that Assignment and 38 | Map essentially refer to the same construct, but from different contexts (and 39 | subsequently different boundaries regarding what they encompass). 40 | 41 | When referring to it as an Assignment, this is usually from a context where the 42 | data model is being considered in some detail (e.g. interacting with the API). 43 | In these kinds of contexts it is often important to make the distinction 44 | between the Map as a whole (which encompasses the Assignment, its Nodes and 45 | often the User), and the Assignment component which acts as a container object 46 | for Nodes and a join model between a Project and a User. In the case where it 47 | is being referred to as a Map, it's less important to consider the intricacies 48 | of the data model, considering it as a 'sum of its parts' - often in the 49 | context of design and UI/UX. 50 | 51 | ### Node 52 | 53 | This is the 'smallest' item in the data model, encompassing a visit to some web 54 | address in the context of an Assignment. It houses information about the site, 55 | as well as meta-data such as when the address was first visited. In future it 56 | may also house information such as return visits and time spent viewing/idle. 57 | 58 | ## Workflow 59 | 60 | Git, GitHub workflow for this Chrome Extension 61 | 62 | 1. Pull from GitHub so the local copy of `master` is up to date. 63 | 2. Start a feature branch, named appropriate to the story (e.g. the story may 64 | be called "Assignment list", so the branch is named `assignment-list`). 65 | 3. \[Do work things\]. 66 | 4. When the feature is finished and is ready to go for code review, **prefix the 67 | branch with "[needs review]", open a new Pull Request**. 68 | 5. Someone else reviews the changes and either **closes the PR, merges the 69 | branch**, or gives **feedback to action before it can be 70 | closed/merged/delivered**. 71 | -------------------------------------------------------------------------------- /src/components/legend.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import _ from 'lodash'; 3 | 4 | var nodeHaloD = "M18.9063355 0.1 L33.942812 9 C34.6940305 9.5 35.3 10.5 35.3 11.4 L35.3030134 29.2 C35.3030134 30.1 34.7 31.2 33.9 31.6 L18.9063355 40.5 C18.1551171 40.9 16.9 40.9 16.2 40.5 L1.14945629 31.6 C0.39823781 31.2 -0.2 30.1 -0.2 29.2 L-0.21074509 11.4 C-0.21074509 10.5 0.4 9.5 1.1 9 L16.1859328 0.1 C16.9371513 -0.3 18.2 -0.3 18.9 0.1 Z" 5 | , nodeCoreD = "M18.2007195 11.2 L24.9141649 15.1 C25.2495669 15.3 25.5 15.8 25.5 16.2 L25.5214639 24.2 C25.5214639 24.5 25.2 25 24.9 25.2 L18.2007195 29.2 C17.8653175 29.4 17.3 29.4 17 29.2 L10.272676 25.2 C9.93727401 25 9.7 24.5 9.7 24.2 L9.66537697 16.2 C9.66537697 15.8 9.9 15.3 10.3 15.1 L16.9861214 11.2 C17.3215234 11 17.9 11 18.2 11.2 Z"; 6 | 7 | var iconWidth = "29" 8 | , iconHeight = "32" 9 | , iconTransform = "translate(2,2)scale(0.7)" 10 | 11 | export default class Legend extends React.Component { 12 | 13 | render() { 14 | var active; 15 | if (!_.includes(this.props.hide, "active")) { 16 | active =
  • 17 | 18 | 19 | 20 | 21 | 22 | 23 | Trailblazer is active on this page 24 |
  • ; 25 | } 26 | 27 | return
    28 |
      29 |
    • 30 | 31 | 32 | 33 | 34 | 35 | 36 | A regular page 37 |
    • 38 | 39 |
    • 40 | 41 | 42 | 43 | 44 | 45 | 46 | The first page in the trail 47 |
    • 48 | 49 | { active } 50 | 51 |
    • 52 | 53 | 54 | 55 | 56 | 57 | 58 | This page split off into many directions 59 |
    • 60 | 61 |
    • 62 | 63 | 64 | 65 | 66 | 67 | 68 | This page is important (a favourite) 69 |
    • 70 |
    71 |
    ; 72 | } 73 | }; 74 | -------------------------------------------------------------------------------- /src/components/views/tour/step-7.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Helmet from 'react-helmet'; 3 | 4 | import actions from '../../../actions'; 5 | import constants from '../../../constants'; 6 | 7 | import { sendPageTitle } from '../../../util/send-page-title'; 8 | 9 | export default class Step7 extends React.Component { 10 | 11 | constructor(props) { 12 | super(props); 13 | 14 | this.state = { 15 | fulfilled: false 16 | }; 17 | } 18 | 19 | revealNextStep() { 20 | actions.completedOnboardingStep(constants.onboarding.STEP_7); 21 | this.setState({ fulfilled: true }); 22 | } 23 | 24 | onMessage(msg) { 25 | if (msg.action === constants.STOP_RECORDING_SUCCESS) this.revealNextStep(); 26 | } 27 | 28 | componentDidMount() { 29 | this.__messageHandler = this.onMessage.bind(this); 30 | chrome.runtime.onMessage.addListener(this.__messageHandler); 31 | 32 | window.twttr.widgets.createShareButton( 33 | "http:\/\/www.trailblazer.io\/", 34 | document.getElementById('twitter-share-button'), 35 | { 36 | related: "TrailblazerApp,LetsTwingl", 37 | text: "I just made my first trail using @TrailblazerApp", 38 | dnt: true 39 | } 40 | ); 41 | 42 | window.twttr.events.bind('click', () => { 43 | actions.completedOnboardingStep(constants.onboarding.STEP_7_TWEET); 44 | }); 45 | 46 | sendPageTitle(); 47 | } 48 | 49 | componentWillUnmount() { 50 | chrome.runtime.onMessage.removeListener(this.__messageHandler); 51 | } 52 | 53 | render() { 54 | let className = ''; 55 | 56 | if (this.state.fulfilled) { 57 | className = 'fulfilled' 58 | } 59 | 60 | return
    61 | 62 | 63 | 64 |
    65 |

    Congratulations!

    66 | 67 |

    68 | You've just made your first trail! 69 |

    70 | 71 |

    There's just one small thing left.

    72 | 73 |
    74 |

    When you're done exploring, it's a good idea to stop Trailblazer.

    75 | 76 |

    Try it now: Tap the Trailblazer icon and press stop

    77 |
    78 |
    79 | 80 |
    81 |

    Well done!

    82 | 83 |

    Now go out into the world wide web and indulge your curiosity and share your journey with others as you go.

    84 | 85 |

    Good things to know

    86 | 87 |

    88 | See your previous trails by tapping the Trailblazer icon and tapping the folder . 89 |

    90 | 91 |

    92 | When viewing a previous trail, just tap any node to resume from that point.
    93 | Trailblazer will open a new tab and automatically activate. 94 |

    95 |
    96 |
    ; 97 | } 98 | 99 | }; 100 | -------------------------------------------------------------------------------- /src/components/views/offline-data/show.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import _ from 'lodash'; 3 | 4 | import queries from '../../../queries'; 5 | import Constants from '../../../constants'; 6 | 7 | import MapView from '../../map-view'; 8 | 9 | import Logger from '../../../util/logger'; 10 | var logger = Logger('assignments-show'); 11 | 12 | 13 | /** 14 | * The Assignments Show view. 15 | * 16 | * Shows a single Assignment, rendering it as a MapView 17 | */ 18 | export default class Show extends React.Component { 19 | 20 | constructor(props) { 21 | super(props); 22 | 23 | this.state = { 24 | localAssignmentId: parseInt(props.params.id), 25 | assignment: undefined, 26 | nodes: [] 27 | }; 28 | } 29 | 30 | componentWillReceiveProps(newProps) { 31 | if (this.state.localAssignmentId !== newProps.params.id) { 32 | this.setState({ localAssignmentId: parseInt(newProps.params.id) }); 33 | 34 | queries.AssignmentStore.getAssignmentByLocalId(this.state.localAssignmentId) 35 | .then(assignment => this.setState({ assignment })); 36 | 37 | queries.NodeStore.getNodesByLocalAssignmentId(this.state.localAssignmentId) 38 | .then(nodes => this.setState({ nodes })); 39 | } 40 | } 41 | 42 | getStateFromFlux(message) { 43 | if (message.action === Constants.__change__ && message.storeName === "AssignmentStore") { 44 | console.log("setting state: assignment", this.state.localAssignmentId); 45 | queries.AssignmentStore.getAssignmentByLocalId(this.state.localAssignmentId) 46 | .then(assignment => this.setState({ assignment })); 47 | } 48 | 49 | if (message.action === Constants.__change__ && message.storeName === "NodeStore") { 50 | console.log("setting state: nodes", this.state.localAssignmentId); 51 | queries.NodeStore.getNodesByLocalAssignmentId(this.state.localAssignmentId) 52 | .then(nodes => this.setState({ nodes })); 53 | } 54 | } 55 | 56 | componentDidMount() { 57 | console.log(this); 58 | queries.AssignmentStore.getAssignmentByLocalId(this.state.localAssignmentId) 59 | .then(assignment => this.setState({ assignment })); 60 | 61 | queries.NodeStore.getNodesByLocalAssignmentId(this.state.localAssignmentId) 62 | .then(nodes => this.setState({ nodes })); 63 | 64 | var __fluxHandler = this.getStateFromFlux.bind(this); 65 | 66 | chrome.runtime.onMessage.addListener(__fluxHandler); 67 | 68 | this.setState({ __fluxHandler }); 69 | } 70 | 71 | componentWillUnmount() { 72 | chrome.runtime.onMessage.removeListener(this.state.__fluxHandler); 73 | } 74 | 75 | render() { 76 | var el; 77 | if (this.state.assignment) { 78 | document.title = this.state.assignment.title; 79 | } 80 | 81 | if (this.state.assignment && this.state.nodes) { 82 | el = ; 86 | } else { 87 | el = Loading; 88 | } 89 | 90 | return
    {el}
    ; 91 | } 92 | 93 | }; 94 | 95 | Show.contextTypes = { 96 | router: React.PropTypes.object.isRequired 97 | }; 98 | 99 | export default Show; 100 | -------------------------------------------------------------------------------- /src/components/views/assignments/show.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import _ from 'lodash'; 3 | 4 | import queries from '../../../queries'; 5 | import Constants from '../../../constants'; 6 | 7 | import MapView from '../../map-view'; 8 | 9 | import Logger from '../../../util/logger'; 10 | var logger = Logger('assignments-show'); 11 | 12 | 13 | /** 14 | * The Assignments Show view. 15 | * 16 | * Shows a single Assignment, rendering it as a MapView 17 | */ 18 | export default class Show extends React.Component { 19 | 20 | constructor(props) { 21 | super(props); 22 | 23 | this.state = { 24 | localAssignmentId: parseInt(props.params.id), 25 | assignment: undefined, 26 | nodes: [] 27 | }; 28 | } 29 | 30 | componentWillReceiveProps(newProps) { 31 | if (this.state.localAssignmentId !== newProps.params.id) { 32 | this.setState({ localAssignmentId: parseInt(newProps.params.id) }); 33 | 34 | queries.AssignmentStore.getAssignmentByLocalId(this.state.localAssignmentId) 35 | .then(assignment => this.setState({ assignment })); 36 | 37 | queries.NodeStore.getNodesByLocalAssignmentId(this.state.localAssignmentId) 38 | .then(nodes => this.setState({ nodes })); 39 | } 40 | } 41 | 42 | getStateFromFlux(message) { 43 | if (message.action === Constants.__change__ && message.storeName === "AssignmentStore") { 44 | console.log("setting state: assignment", this.state.localAssignmentId); 45 | queries.AssignmentStore.getAssignmentByLocalId(this.state.localAssignmentId) 46 | .then(assignment => this.setState({ assignment })); 47 | } 48 | 49 | if (message.action === Constants.__change__ && message.storeName === "NodeStore") { 50 | console.log("setting state: nodes", this.state.localAssignmentId); 51 | queries.NodeStore.getNodesByLocalAssignmentId(this.state.localAssignmentId) 52 | .then(nodes => this.setState({ nodes })); 53 | } 54 | } 55 | 56 | componentDidMount() { 57 | console.log(this); 58 | queries.AssignmentStore.getAssignmentByLocalId(this.state.localAssignmentId) 59 | .then(assignment => this.setState({ assignment })); 60 | 61 | queries.NodeStore.getNodesByLocalAssignmentId(this.state.localAssignmentId) 62 | .then(nodes => this.setState({ nodes })); 63 | 64 | var __fluxHandler = this.getStateFromFlux.bind(this); 65 | 66 | chrome.runtime.onMessage.addListener(__fluxHandler); 67 | 68 | this.setState({ __fluxHandler }); 69 | 70 | this.props.route.actions.viewedMap(this.state.localAssignmentId); 71 | this.props.route.actions.requestNodes(this.state.localAssignmentId); 72 | } 73 | 74 | componentWillUnmount() { 75 | chrome.runtime.onMessage.removeListener(this.state.__fluxHandler); 76 | } 77 | 78 | render() { 79 | var el; 80 | if (this.state.assignment) { 81 | document.title = this.state.assignment.title; 82 | } 83 | 84 | if (this.state.assignment && this.state.nodes) { 85 | el = ; 89 | } else { 90 | el = Loading; 91 | } 92 | 93 | return
    {el}
    ; 94 | } 95 | 96 | }; 97 | 98 | Show.contextTypes = { 99 | router: React.PropTypes.object.isRequired 100 | }; 101 | 102 | export default Show; 103 | -------------------------------------------------------------------------------- /src/components/node.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import classnames from 'classnames'; 4 | 5 | const HALO = "M18.9063355 0.1 L33.942812 9 C34.6940305 9.5 35.3 10.5 35.3 11.4 L35.3030134 29.2 C35.3030134 30.1 34.7 31.2 33.9 31.6 L18.9063355 40.5 C18.1551171 40.9 16.9 40.9 16.2 40.5 L1.14945629 31.6 C0.39823781 31.2 -0.2 30.1 -0.2 29.2 L-0.21074509 11.4 C-0.21074509 10.5 0.4 9.5 1.1 9 L16.1859328 0.1 C16.9371513 -0.3 18.2 -0.3 18.9 0.1 Z"; 6 | const CORE = "M18.2007195 11.2 L24.9141649 15.1 C25.2495669 15.3 25.5 15.8 25.5 16.2 L25.5214639 24.2 C25.5214639 24.5 25.2 25 24.9 25.2 L18.2007195 29.2 C17.8653175 29.4 17.3 29.4 17 29.2 L10.272676 25.2 C9.93727401 25 9.7 24.5 9.7 24.2 L9.66537697 16.2 C9.66537697 15.8 9.9 15.3 10.3 15.1 L16.9861214 11.2 C17.3215234 11 17.9 11 18.2 11.2 Z"; 7 | 8 | const OFFSET_X = 17.5; 9 | const OFFSET_Y = 21; 10 | 11 | const FAVOURITE_THRESHOLD = 4; // # links until considered favourite (includes link to parent) 12 | 13 | const VIEWER_ENVIRONMENT = chrome.windows ? "extension" : "public"; 14 | 15 | export default class Node extends React.Component { 16 | 17 | constructor(props) { 18 | super(props); 19 | 20 | this.position = props.position; 21 | } 22 | 23 | // We want to set the dom attributes ourselves to avoid triggering React's 24 | // diffing algorithm every draw 25 | updatePosition(position) { 26 | this.position = position; 27 | let domNode = React.findDOMNode(this); 28 | domNode.setAttribute('transform', `translate(${this.position.x - OFFSET_X},${this.position.y - OFFSET_Y})`); 29 | } 30 | 31 | componentWillReceiveProps(newProps) { 32 | this.position = newProps.position; 33 | } 34 | 35 | getScreenPosition() { 36 | let domNode = React.findDOMNode(this.refs.center); 37 | let ctm = domNode.getScreenCTM(); 38 | return { 39 | x: ctm.e, 40 | y: ctm.f 41 | } 42 | } 43 | 44 | // trigger resume?-- trigger event callback on parent. 45 | onClick(evt) { 46 | (this.props.onClick || (() => {}))(evt); 47 | } 48 | 49 | // trigger a popup-- trigger event callback on parent. 50 | onMouseEnter(evt) { 51 | (this.props.onMouseEnter || (() => {}))(evt); 52 | } 53 | 54 | onMouseLeave(evt) { 55 | (this.props.onMouseLeave || (() => {}))(evt); 56 | } 57 | 58 | render() { 59 | var classes = classnames('node', { 60 | 'delete-pending': !!this.props.node.data.deletePending, 61 | hub: (this.props.node.links.length >= FAVOURITE_THRESHOLD), 62 | favourite: (this.props.node.data.rank === 1), 63 | root: (!this.props.node.data.parentId && !this.props.node.data.localParentId), 64 | open: (!!this.props.node.data.tabId) 65 | }); 66 | 67 | let href = (VIEWER_ENVIRONMENT === 'public') ? d.url : false; 68 | let target = (VIEWER_ENVIRONMENT === 'public') ? '_blank' : false; 69 | 70 | // We can't set xlink:href through React, so we need to do it manually. 71 | let imageContainer = `}}>; 72 | 73 | return 81 | 82 | 83 | 84 | {imageContainer} 85 | 86 | } 87 | }; 88 | -------------------------------------------------------------------------------- /src/scripts/background.js: -------------------------------------------------------------------------------- 1 | // config 2 | import config from '../config'; 3 | import constants from '../constants'; 4 | 5 | // helpers 6 | import _ from 'lodash'; 7 | 8 | // Initialize our logger 9 | import Logger from '../util/logger'; 10 | var logger = Logger('background.js'); 11 | 12 | // Start tracking errors 13 | import Raven from 'raven-js'; 14 | if (config.raven.url) Raven.config(config.raven.url).install(); 15 | 16 | /** 17 | * Main dependencies. 18 | */ 19 | import actions from '../actions' 20 | import Fluxxor from 'fluxxor'; 21 | 22 | logger.info("Initializing Trailblazer!"); 23 | 24 | /** 25 | * Initialize the extension. 26 | * 27 | * Set the initial UI state and run the install hooks 28 | */ 29 | logger.info("Initializing extension UI state"); 30 | import extensionUIState from '../core/extension-ui-state'; 31 | extensionUIState.init(); 32 | 33 | logger.info("Running install hooks"); 34 | import onInstall from '../core/install-hooks'; 35 | chrome.runtime.onInstalled.addListener(onInstall); 36 | 37 | /** 38 | * Set up Flux. 39 | * 40 | * Initialize with the stores and actions, and set up a listener to log all 41 | * dispatches to the console. 42 | */ 43 | 44 | import stores from '../stores'; 45 | logger.info("Initializing Flux", { stores: stores, actions: actions }); 46 | var flux = new Fluxxor.Flux(stores, actions); 47 | 48 | var send = actions.getMessageSender(); 49 | actions.setMessageSender(function(message) { 50 | if (message.action) { 51 | var e = { 52 | type: message.action, 53 | payload: message.payload || {} 54 | } 55 | flux.dispatcher.dispatch(e); 56 | logger.info("Dispatched Background Event", e); 57 | } 58 | 59 | // Send through old channel as well 60 | send(message); 61 | }); 62 | 63 | _.each(stores, (s) => { 64 | console.log(s.onBoot, s); 65 | s.onBoot(); 66 | }); 67 | 68 | flux.store('TabStore').on('change', () => { 69 | const state = flux.store('TabStore').getState(); 70 | _.each(state.tabs, (val, key) => { 71 | extensionUIState.update(parseInt(key), (val) ? "recording" : "idle"); 72 | }); 73 | }); 74 | 75 | // Wire up Flux's dispatcher to listen for chrome.runtime messages 76 | // FIXME Candidate for refactor/extraction into a better location 77 | chrome.runtime.onMessage.addListener(function(message, sender, responder) { 78 | logger.info('message listener', { message }); 79 | // if (message.action === "change") return; 80 | if (message.action) { 81 | var o = { type: message.action }; 82 | 83 | o.payload = message.payload || {}; 84 | o.payload.responder = responder; 85 | 86 | flux.dispatcher.dispatch(o); 87 | logger.info("Dispatched", o); 88 | }; 89 | 90 | if (message.query) { 91 | // TODO: query method currently needs to respond with a promise. This 92 | // shouldn't be a requirement, or at least it shouldn't present itself in 93 | // the source: synchronous methods should work seamlessly. 94 | flux.store(message.store)[message.query](...message.args).then(res => { 95 | logger.info(`Query response: ${message.store}.${message.query}`, res); 96 | responder(res); 97 | }); 98 | logger.info(`Query: ${message.store}.${message.query}`, message); 99 | return true; 100 | }; 101 | }); 102 | 103 | // Allow 'change' events to proxy through chrome.runtime messaging to the UI 104 | import proxyChange from '../background/proxy-change'; 105 | proxyChange(flux, [ 106 | 'AssignmentStore', 107 | 'TabStore', 108 | 'NodeStore', 109 | 'AuthenticationStore', 110 | 'SyncStore', 111 | 'MapStore' 112 | ]); 113 | 114 | // Wire up Chrome events to fire the appropriate actions 115 | import bindChromeEvents from '../background/chrome-events'; 116 | bindChromeEvents(flux); 117 | 118 | // Inject content-scripts into pages 119 | import contentScripts from '../background/content-scripts'; 120 | -------------------------------------------------------------------------------- /src/constants.js: -------------------------------------------------------------------------------- 1 | export default { 2 | __change__: 'change', // NOTE: reserved for emit('change') 3 | 4 | COMPLETED_ONBOARDING_STEP: "COMPLETED_ONBOARDING_STEP", 5 | 6 | onboarding: { 7 | STEP_1: "STEP_1", 8 | STEP_2: "STEP_2", 9 | STEP_3: "STEP_3", 10 | STEP_4: "STEP_4", 11 | STEP_5: "STEP_5", 12 | STEP_6: "STEP_6", 13 | STEP_7: "STEP_7", 14 | STEP_7_TWEET: "STEP_7_TWEET" 15 | }, 16 | 17 | /** 18 | * Importing data 19 | */ 20 | IMPORT_DATA: 'IMPORT_DATA', 21 | 22 | /** 23 | * Actions to request 'change' be emitted with current data 24 | */ 25 | REQUEST_ASSIGNMENTS: 'REQUEST_ASSIGNMENTS', 26 | REQUEST_NODES: 'REQUEST_NODES', 27 | 28 | /** 29 | * Synchronisation actions 30 | */ 31 | FETCH_ASSIGNMENTS: 'FETCH_ASSIGNMENTS', 32 | FETCH_ASSIGNMENTS_SUCCESS: 'FETCH_ASSIGNMENTS_SUCCESS', 33 | FETCH_ASSIGNMENTS_FAIL: 'FETCH_ASSIGNMENTS_FAIL', 34 | UPDATE_ASSIGNMENT_CACHE: 'UPDATE_ASSIGNMENT_CACHE', 35 | UPDATE_ASSIGNMENT_CACHE_SUCCESS: 'UPDATE_ASSIGNMENT_CACHE_SUCCESS', 36 | UPDATE_ASSIGNMENT_CACHE_FAIL: 'UPDATE_ASSIGNMENT_CACHE_FAIL', 37 | ASSIGNMENTS_SYNCHRONIZED: 'ASSIGNMENTS_SYNCHRONIZED', 38 | 39 | FETCH_NODES: 'FETCH_NODES', 40 | FETCH_NODES_SUCCESS: 'FETCH_NODES_SUCCESS', 41 | FETCH_NODES_FAIL: 'FETCH_NODES_FAIL', 42 | UPDATE_NODE_CACHE: 'UPDATE_NODE_CACHE', 43 | UPDATE_NODE_CACHE_SUCCESS: 'UPDATE_NODE_CACHE_SUCCESS', 44 | UPDATE_NODE_CACHE_FAIL: 'UPDATE_NODE_CACHE_FAIL', 45 | NODES_SYNCHRONIZED: 'NODES_SYNCHRONIZED', 46 | 47 | PERSIST_ASSIGNMENT: 'PERSIST_ASSIGNMENT', 48 | PERSIST_ASSIGNMENT_SUCCESS: 'PERSIST_ASSIGNMENT_SUCCESS', 49 | PERSIST_ASSIGNMENT_FAIL: 'PERSIST_ASSIGNMENT_FAIL', 50 | PERSIST_NODE: 'PERSIST_NODE', 51 | PERSIST_NODE_SUCCESS: 'PERSIST_NODE_SUCCESS', 52 | 53 | SAVE_MAP_LAYOUT: 'SAVE_MAP_LAYOUT', 54 | PERSIST_MAP_LAYOUT: 'PERSIST_MAP_LAYOUT', 55 | PERSIST_MAP_LAYOUT_FAIL: 'PERSIST_MAP_LAYOUT_FAIL', 56 | 57 | /** 58 | * Actions invoked by Chrome 59 | */ 60 | SET_NODE_TITLE: 'SET_NODE_TITLE', 61 | TAB_TITLE_UPDATED: 'TAB_TITLE_UPDATED', 62 | TAB_CREATED: 'TAB_CREATED', 63 | TAB_FOCUSED: 'TAB_FOCUSED', 64 | CREATED_NAVIGATION_TARGET: 'CREATED_NAVIGATION_TARGET', 65 | TAB_UPDATED: 'TAB_UPDATED', 66 | HISTORY_STATE_UPDATED: 'HISTORY_STATE_UPDATED', 67 | WEB_NAV_COMMITTED: 'WEB_NAV_COMMITTED', 68 | TAB_CLOSED: 'TAB_CLOSED', 69 | TAB_REPLACED: 'TAB_REPLACED', 70 | EXTENSION_INSTALLED: 'EXTENSION_INSTALLED', 71 | EXTENSION_UPDATED: 'EXTENSION_UPDATED', 72 | CHROME_UPDATED: 'CHROME_UPDATED', 73 | 74 | /** 75 | * User actions 76 | */ 77 | CREATE_NODE_SUCCESS: 'CREATE_NODE_SUCCESS', 78 | UPDATE_NODE_SUCCESS: 'UPDATE_NODE_SUCCESS', 79 | DESTROY_NODE: 'DESTROY_NODE', 80 | BULK_DESTROY_NODES: 'BULK_DESTROY_NODES', 81 | CREATE_ASSIGNMENT_SUCCESS: 'CREATE_ASSIGNMENT_SUCCESS', 82 | DESTROY_ASSIGNMENT: 'DESTROY_ASSIGNMENT', 83 | DESTROY_ASSIGNMENT_SUCCESS: 'DESTROY_ASSIGNMENT_SUCCESS', 84 | UPDATE_ASSIGNMENT_TITLE: 'UPDATE_ASSIGNMENT_TITLE', 85 | 86 | START_RECORDING: 'START_RECORDING', 87 | START_RECORDING_SUCCESS: 'START_RECORDING_SUCCESS', 88 | START_RECORDING_FAIL: 'START_RECORDING_FAIL', 89 | RESUME_RECORDING: 'RESUME_RECORDING', 90 | RESUME_RECORDING_FAIL: 'RESUME_RECORDING_FAIL', 91 | STOP_RECORDING: 'STOP_RECORDING', 92 | STOP_RECORDING_SUCCESS: 'STOP_RECORDING_SUCCESS', 93 | 94 | VIEWED_ASSIGNMENT_LIST: 'VIEWED_ASSIGNMENT_LIST', 95 | VIEWED_MAP: 'VIEWED_MAP', 96 | 97 | RANK_NODE_FAVOURITE: 'RANK_NODE_FAVOURITE', 98 | RANK_NODE_NEUTRAL: 'RANK_NODE_NEUTRAL', 99 | 100 | MAKE_ASSIGNMENT_VISIBLE: 'MAKE_ASSIGNMENT_VISIBLE', 101 | MAKE_ASSIGNMENT_HIDDEN: 'MAKE_ASSIGNMENT_HIDDEN', 102 | 103 | SIGN_IN: 'SIGN_IN', 104 | SIGN_IN_SUCCESS: 'SIGN_IN_SUCCESS', 105 | SIGN_OUT: 'SIGN_OUT' 106 | } 107 | -------------------------------------------------------------------------------- /src/components/node-popover.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import classnames from 'classnames'; 4 | 5 | export default class NodePopover extends React.Component { 6 | 7 | constructor(props) { 8 | super(props); 9 | 10 | this.state = { 11 | visible: false 12 | }; 13 | 14 | this.position = props.position; 15 | this.mouseInBounds; 16 | this.mouseInParentBounds; 17 | } 18 | 19 | // We want to set the dom attributes ourselves to avoid triggering React's 20 | // diffing algorithm every draw 21 | updatePosition(position) { 22 | this.position = position; 23 | let domNode = React.findDOMNode(this); 24 | domNode.setAttribute('style', `transform: translate(${this.position.x}px, ${this.position.y}px)`); 25 | } 26 | 27 | componentWillReceiveProps(newProps) { 28 | this.position = newProps.position; 29 | } 30 | 31 | activate() { 32 | clearTimeout(this.dismissTimeout); 33 | this.setState({ visible: true }); 34 | } 35 | 36 | 37 | // Dismisses the popup if the mouse is not in its bounds or its parent's 38 | // bounds. 39 | softDismiss() { 40 | clearTimeout(this.dismissTimeout); 41 | this.dismissTimeout = setTimeout( () => { 42 | if (!this.mouseInBounds && !this.mouseInParentBounts && !this.state.deletePending) { 43 | this.setState({ visible: false }); 44 | } 45 | }, 100); 46 | } 47 | 48 | onMouseEnter(evt) { 49 | this.mouseInBounds = true; 50 | } 51 | 52 | onMouseLeave(evt) { 53 | this.mouseInBounds = false; 54 | this.softDismiss(); 55 | } 56 | 57 | onResumeClicked(evt) { 58 | this.props.actions.resumeRecording(this.props.node.localId); 59 | } 60 | 61 | onDeleteClicked(evt) { 62 | this.setState({ deletePending: true }); 63 | (this.props.onDeletePending || (() => {}))(evt); 64 | } 65 | 66 | onConfirmDeleteClicked(evt) { 67 | this.setState({ deletePending: false }); 68 | (this.props.onDeleteConfirmed || (() => {}))(evt); 69 | } 70 | 71 | onCancelDeleteClicked(evt) { 72 | this.setState({ deletePending: false }); 73 | (this.props.onDeleteCancelled || (() => {}))(evt); 74 | } 75 | 76 | render() { 77 | let content; 78 | let actions; 79 | let title = this.props.node.title || No title; 80 | 81 | if (this.state.deletePending) { 82 | actions = [ 83 |

    84 | This will delete all pages coloured red. Are you sure? 85 |

    , 86 |
    87 |
    88 | 89 |
    90 |
    91 | 92 |
    93 |
    94 | ]; 95 | } else if (this.props.node.deletePending) { 96 | // No actions while another node is being considered for deletion 97 | } else { 98 | actions =
    99 |
    100 | 101 |
    102 |
    103 | 104 |
    105 |
    106 | } 107 | 108 | if (this.state.visible) { 109 | let classNames = classnames('node-popover', { 110 | 'delete-pending': this.state.deletePending 111 | }); 112 | 113 | content =
    114 |
    115 | 116 |

    {this.isGoogleUrl(title, this.props.node.url)}

    117 | 118 |
    119 | {this.props.node.url} 120 |
    121 | 122 | {actions} 123 | 124 |
    125 |
    ; 126 | } 127 | 128 | return
    131 | {content} 132 |
    133 | } 134 | 135 | isGoogleUrl(title, url){ 136 | var res = url; 137 | if( url.indexOf("https://www.google") == 0 ){ 138 | var qpos = url.indexOf("#q") 139 | if( qpos > -1 ) { 140 | res = url.slice(qpos, url.length); 141 | res = decodeURI(res) 142 | res = res.replace('#q=', '') 143 | res = res.replace(/\+/g, ' ') 144 | return res; 145 | }else{ 146 | return title; 147 | } 148 | }else{ 149 | return title; 150 | } 151 | } 152 | 153 | }; 154 | -------------------------------------------------------------------------------- /src/style/popup/popup.scss: -------------------------------------------------------------------------------- 1 | 2 | *:focus {outline: none;} 3 | 4 | body { 5 | margin: 0; 6 | padding: 0; 7 | border: 0; 8 | font-family: 'Open Sans', sans-serif; 9 | font-weight: 400; 10 | font-size: 14px; 11 | } 12 | 13 | body .wrap { 14 | width: 160px; 15 | padding: 34px 18px 10px 18px; 16 | } 17 | 18 | body .wrap.loading { 19 | height: 390px; 20 | } 21 | 22 | h1 { 23 | font-size: inherit; 24 | font-weight: inherit; 25 | margin: 0; 26 | } 27 | 28 | 29 | .idle { 30 | background-image: url(/assets/images/idle-background.png); 31 | background-repeat: none; 32 | background-size: 100%; 33 | background-position: top center; 34 | } 35 | 36 | .cf:before, 37 | .cf:after { 38 | content: " "; /* 1 */ 39 | display: table; /* 2 */ 40 | } 41 | 42 | .cf:after { 43 | clear: both; 44 | } 45 | 46 | a { 47 | cursor: pointer; 48 | } 49 | 50 | /* Recording Title */ 51 | 52 | .recording-title { 53 | position: relative; 54 | font-size: 20px; 55 | font-weight: 400; 56 | text-align: center; 57 | margin: 0; 58 | } 59 | 60 | .recording-title a { 61 | position: relative; 62 | text-decoration: none; 63 | color: #222; 64 | } 65 | 66 | .recording-title a img { 67 | position: absolute; 68 | right: -12px; 69 | } 70 | 71 | .recording-title input { 72 | text-align: center; 73 | font-size: 12px; 74 | } 75 | 76 | .recording-title.state { 77 | color: #aaa; 78 | font-size: 10px; 79 | font-weight: 400; 80 | margin-bottom: 30px; 81 | } 82 | 83 | /* Buttons */ 84 | 85 | .button { 86 | width: 130px; 87 | padding: 20px 0; 88 | margin: 0 auto 20px auto; 89 | border-radius: 3px; 90 | display: block; 91 | text-align: center; 92 | text-decoration: none; 93 | background-color: #616161; 94 | color: #fff; 95 | } 96 | 97 | .button:hover { 98 | background-color: #5B5B5B; 99 | } 100 | 101 | .button-stop { 102 | background-color: #F97E76; 103 | padding: 10px 0; 104 | margin-bottom: 40px; 105 | width: 100px; 106 | } 107 | 108 | .button-stop:hover { 109 | background-color: #F7766C; 110 | } 111 | 112 | .folder { 113 | display: block; 114 | width: 78px; 115 | margin: 80px auto 60px auto; 116 | } 117 | 118 | .start-recording{ 119 | background-image:url(/assets/icons/new-icon.svg); 120 | background-repeat: no-repeat; 121 | background-position:92px 16px; 122 | } 123 | 124 | .folder img {margin-bottom: 8px} 125 | 126 | .favourite { 127 | fill: #C5C5C5; 128 | text-decoration: none; 129 | float: left; 130 | margin-right: 1em; 131 | } 132 | 133 | .favourite:hover { 134 | fill: #989898; 135 | } 136 | 137 | /* Retain? - Consider after removing tutorial element 138 | 139 | .favourite:hover:after { 140 | content: "Favourite"; 141 | font-size: 10px; 142 | color: #989898; 143 | position: relative; 144 | top: -3px; 145 | } 146 | 147 | /Retain? */ 148 | 149 | .favourite-active { 150 | fill: #FFD463; 151 | float: left; 152 | margin-right: 1em; 153 | } 154 | 155 | .sign-out { 156 | float: right; 157 | fill: #C5C5C5; 158 | } 159 | 160 | .sign-out:hover { 161 | fill: #989898; 162 | } 163 | 164 | .sign-in { 165 | width: 150px; 166 | padding: 12px 0; 167 | background-color: #76A9F9; 168 | } 169 | 170 | .tutorial path{ 171 | fill: #444; 172 | } 173 | 174 | .tutorial:hover path{ 175 | fill: #777; 176 | } 177 | 178 | .sign-in { 179 | width: 150px; 180 | padding: 12px 0; 181 | background-color: #76A9F9; 182 | position: relative; 183 | } 184 | 185 | .throbber:hover { background-color: #76A9F9; } 186 | 187 | .throbber { 188 | width: 100%; 189 | height: 100%; 190 | display: -webkit-box; 191 | display: -webkit-flex; 192 | display: flex; 193 | -webkit-box-align: center; 194 | -webkit-align-items: center; 195 | align-items: center; 196 | -webkit-box-pack: center; 197 | -webkit-justify-content: center; 198 | justify-content: center; 199 | } 200 | 201 | .throbber:after { 202 | display: block; 203 | position: relative; 204 | width: 20px; 205 | height: 20px; 206 | -webkit-animation: rotate .6s linear infinite; 207 | animation: rotate .6s linear infinite; 208 | -webkit-border-radius: 100%; 209 | border-radius: 100%; 210 | border-top: 1px solid #76A9F9; 211 | border-bottom: 1px solid #fff; 212 | border-left: 1px solid #76A9F9; 213 | border-right: 1px solid #fff; 214 | content: ''; 215 | opacity: 1; 216 | } 217 | 218 | @keyframes rotate { 219 | 0% { 220 | transform: rotateZ(-360deg); 221 | -webkit-transform: rotateZ(-360deg); 222 | } 223 | 100% { 224 | transform: rotateZ(0deg); 225 | -webkit-transform: rotateZ(0deg); 226 | } 227 | } 228 | 229 | @-webkit-keyframes rotate { 230 | 0% { 231 | transform: rotateZ(-360deg); 232 | -webkit-transform: rotateZ(-360deg); 233 | } 234 | 100% { 235 | transform: rotateZ(0deg); 236 | -webkit-transform: rotateZ(0deg); 237 | } 238 | } 239 | 240 | /* Stop Gap - First to go when we remove tutorial element from dropdown! */ 241 | 242 | .tutorial-recording { 243 | padding-left: 8px; 244 | } 245 | 246 | .tutorial-recording path { 247 | fill: #C5C5C5; 248 | } 249 | 250 | /* /Stop Gap - First to go! */ 251 | -------------------------------------------------------------------------------- /src/style/main-ui/map.scss: -------------------------------------------------------------------------------- 1 | @import '../../../node_modules/sass-flex-mixin/flex.scss'; 2 | @import 'legend'; 3 | @import 'public-map'; 4 | 5 | @import 'node'; 6 | @import 'node-popover'; 7 | @import 'link'; 8 | 9 | .wrap.assignment-show *, 10 | .wrap.assignment-show *:after, 11 | .wrap.assignment-show *:before { 12 | box-sizing:border-box; 13 | -webkit-box-sizing:border-box; 14 | -moz-box-sizing:border-box; 15 | -webkit-font-smoothing:antialiased; 16 | font-smoothing:antialiased; 17 | text-rendering:optimizeLegibility; 18 | } 19 | 20 | body { 21 | margin: 0; 22 | padding: 0; 23 | border: 0; 24 | } 25 | 26 | html, body, .wrap.assignment-show, .map-view-wrapper { 27 | width: 100vw; 28 | height: 100vh; 29 | } 30 | 31 | body .wrap.assignment-show { 32 | background-color: #FFFAF3; 33 | font-family: 'Open Sans', sans-serif; 34 | font-weight: 400; 35 | overflow: hidden; 36 | } 37 | 38 | /* 39 | Gradient Combos 40 | 41 | #757F9A, #D7DDE8 42 | #DE6262, #FFB88C 43 | #5CC8CB, #80E0D5 44 | #9bc5c3, #616161 45 | */ 46 | 47 | /* Temp !!! */ 48 | .temp-help { 49 | padding-top: 8px; 50 | color: #F57584; 51 | } 52 | 53 | #map-container { 54 | width: 100%; 55 | height: 100%; 56 | } 57 | 58 | .nav-assignment-list { 59 | position: absolute; 60 | top: 34px; 61 | left: 34px; 62 | 63 | display: block; 64 | width: 30px; 65 | height: 30px; 66 | 67 | background-color: #616161; 68 | -webkit-mask: url(/assets/icons/logo-watermark.svg) no-repeat 50% 50%; 69 | -webkit-mask-size: 30px 30px; 70 | mask: url(/assets/icons/logo-watermark.svg) no-repeat 50% 50%; 71 | mask-size: 30px 30px; 72 | } 73 | 74 | .map-title { 75 | text-decoration: none; 76 | color: #4D4D4D; 77 | background-color: #FFFAF3; 78 | border-radius: 2px; 79 | position: absolute; 80 | font-size: 16px; 81 | font-weight: 400; 82 | padding: 8px 12px; 83 | top: 31px; 84 | left: 70px; 85 | } 86 | 87 | .map-title img { 88 | position: relative; 89 | top: -8px; 90 | right: -5px; 91 | padding-right: 1px; 92 | } 93 | 94 | .map-title:focus { 95 | border-style: none; 96 | font-family: 'Open Sans', sans-serif; 97 | font-weight: 400; 98 | } 99 | 100 | /* dropdown */ 101 | /* temp made invisable untill in working order */ 102 | 103 | .click-nav { 104 | margin:30px auto; 105 | width:200px; 106 | position: fixed; 107 | left: 0; 108 | right: 0; 109 | display: none; 110 | } 111 | .click-nav ul { 112 | font-weight:900; 113 | } 114 | .click-nav ul li { 115 | position:relative; 116 | list-style:none; 117 | cursor:pointer; 118 | } 119 | .click-nav ul li ul { 120 | position:absolute; 121 | left:0; 122 | right:0; 123 | } 124 | .click-nav ul .clicker { 125 | background: #444; 126 | color:#FFF; 127 | } 128 | 129 | .click-nav ul .clicker img { 130 | float: right; 131 | padding: 6px 6px 6px 6px; 132 | } 133 | 134 | .click-nav ul .clicker:hover, 135 | .click-nav ul .active { 136 | background:#333; 137 | } 138 | 139 | .click-nav ul li a { 140 | transition:background-color 0.2s ease-in-out; 141 | -webkit-transition:background-color 0.2s ease-in-out; 142 | -moz-transition:background-color 0.2s ease-in-out; 143 | display:block; 144 | padding:12px 12px 12px 16px; 145 | background:#FFF; 146 | color:#333; 147 | text-decoration:none; 148 | } 149 | .click-nav ul li a:hover { 150 | background:#ddd; 151 | } 152 | /* Fallbacks */ 153 | .click-nav .no-js ul { 154 | display:none; 155 | } 156 | .click-nav .no-js:hover ul { 157 | display:block; 158 | } 159 | 160 | .help { 161 | position: fixed; 162 | right: 30px; 163 | bottom: 24px; 164 | font-size: 14px; 165 | color: rgb(77, 77, 77); 166 | } 167 | 168 | .help a { 169 | opacity: 0.7; 170 | } 171 | 172 | .help a:hover { 173 | opacity: 0.8; 174 | } 175 | 176 | .tutorial { 177 | padding-left: 1px; 178 | float: right; 179 | position: fixed; 180 | bottom: 30px; 181 | right: 126px; 182 | } 183 | 184 | .tutorial path { 185 | fill-rule: evenodd; 186 | fill: #000; 187 | } 188 | 189 | .feedback { 190 | position: relative; 191 | bottom: 3px; 192 | text-decoration: none; 193 | color: inherit; 194 | float: right; 195 | position: fixed; 196 | bottom: 33px; 197 | right: 146px; 198 | } 199 | 200 | .popover { 201 | padding: 18px; 202 | border-radius: 2px; 203 | background-color: #fff; 204 | position: fixed; 205 | bottom: 70px; 206 | right: 30px; 207 | box-shadow: 0px 3px 10px rgba(0, 0, 0, 0.3); 208 | } 209 | 210 | .popover:after { 211 | content:""; 212 | position:absolute; 213 | border-style:solid; 214 | bottom:-8px; /* controls vertical position */ 215 | right:30px; /* value = - border-left-width - border-right-width */ 216 | border-width:8px 8px 0px 8px; 217 | border-color:#fff transparent; 218 | } 219 | 220 | 221 | .btn { 222 | border: none; 223 | border-radius: 2px; 224 | font-size: 14px; 225 | cursor: pointer; 226 | font-family: 'Open Sans', sans-serif; 227 | font-weight: 400; 228 | padding: 6px; 229 | } 230 | 231 | .btn-share { 232 | color: #4D4D4D; 233 | background-color: rgba(0, 0, 0, 0.1); 234 | width: 76px; 235 | position: fixed; 236 | bottom: 27px; 237 | right: 30px; 238 | } 239 | 240 | .btn-share:hover{ 241 | background-color: rgba(0, 0, 0, 0.2); 242 | 243 | } 244 | 245 | .btn-share:focus { 246 | outline: none; 247 | 248 | } 249 | 250 | .share-message { 251 | font-size: 12px; 252 | color: #777; 253 | padding-bottom: 6px; 254 | 255 | } 256 | 257 | .share-url { 258 | text-align: center; 259 | color: #555; 260 | padding-bottom: 12px; 261 | } 262 | 263 | .share-url a { 264 | color: inherit; 265 | text-decoration: inherit; 266 | opacity: 1; 267 | } 268 | 269 | .btn-make-private { 270 | float: right; 271 | color: #fff; 272 | background-color: #FFAAAA; 273 | padding: 6px 8px; 274 | font-size: 12px; 275 | 276 | } 277 | -------------------------------------------------------------------------------- /src/adapter/trailblazer_http_storage_adapter.js: -------------------------------------------------------------------------------- 1 | // config 2 | import config from '../config'; 3 | 4 | // adapters 5 | import ChromeIdentityAdapter from './chrome_identity_adapter'; 6 | 7 | // helpers 8 | import Promise from 'promise'; 9 | import superagent from 'superagent'; 10 | 11 | /** 12 | * Creates a new TrailblazerHTTPStorageAdapter 13 | * 14 | * @class TrailblazerHTTPStorageAdapter 15 | * @classdesc 16 | * Provides an abstracted interface to the REST-like API Trailblazer 17 | * implements. Access is, by default, through CRUD methods (create, read, 18 | * update, destroy, and list). 19 | */ 20 | 21 | export default class TrailblazerHTTPStorageAdapter { 22 | 23 | /** 24 | * Make an HTTP request. Used to implement the CRUD methods. 25 | * @function TrailblazerHTTPStorageAdapter#_request 26 | * @param {string} url 27 | * @param {string} httpMethod 28 | * @param {Object} opts 29 | * @param {Object} opts.params - Parameters to append to the request URL 30 | * @param {Object} opts.data - Data to send with the request (i.e. request 31 | * body) 32 | * @private 33 | */ 34 | _request(url, httpMethod, opts) { 35 | var httpMethod = httpMethod || "GET" 36 | , opts = opts || {}; 37 | 38 | // To make an authenticated request we must ensure that we are signed in 39 | var promise = new Promise((resolve, reject) => { 40 | new ChromeIdentityAdapter().getToken().then((auth) => { 41 | superagent(httpMethod, url) 42 | .set("Authorization", "Bearer " + auth.access_token) 43 | .set("Content-Type", "application/json") 44 | .set("Accept", "application/json") 45 | .send(opts.data || {}) 46 | .query(opts.params || {}) 47 | .end((error, response) => { 48 | if (error) { 49 | reject(error, response); 50 | } else { 51 | if (response.ok) { 52 | resolve(response.body); 53 | } else { 54 | reject(error, response); 55 | } 56 | } 57 | }); //superagent 58 | }, () => { 59 | throw "Tried to make authenticated request without token!"; 60 | }); //_stateManager.signIn() 61 | }); //promise 62 | 63 | return promise; 64 | } 65 | 66 | /** 67 | * Read a resource from the server. Makes a request `GET 68 | * http(s)://server.com/resource/id` with optional URL params 69 | * @function TrailblazerHTTPStorageAdapter#read 70 | * @param {string} resourceName - Name of the resource to fetch 71 | * @param {string} id - (Optional) id of the resource 72 | * @param {Object} params - URL params to append to the request 73 | * @returns {Promise} 74 | */ 75 | read(resourceName, id, params) { 76 | if (!resourceName) throw "You need to specify a resource"; 77 | if (!id) throw "You need to specify an ID - maybe you want list instead"; 78 | 79 | var url = [ 80 | config.api.host, 81 | config.api.nameSpace, 82 | config.api.version, 83 | resourceName, 84 | id 85 | ].join("/"); 86 | 87 | return this._request(url, "GET", { params: params }); 88 | } 89 | 90 | /** 91 | * Retrieve a list of resources from the server. Makes a request `GET 92 | * http(s)://server.com/resource` with optional URL params 93 | * @function TrailblazerHTTPStorageAdapter#list 94 | * @param {string} resourceName - Name of the resource to fetch 95 | * @param {Object} params - URL params to append to the request 96 | */ 97 | list(resourceName, params) { 98 | if (!resourceName) throw "You need to specify a resource"; 99 | 100 | var url = [ 101 | config.api.host, 102 | config.api.nameSpace, 103 | config.api.version, 104 | resourceName 105 | ].join("/"); 106 | 107 | return this._request(url, "GET", { params: params }); 108 | } 109 | 110 | /** 111 | * @todo Create a new resource 112 | * @function TrailblazerHTTPStorageAdapter#create 113 | */ 114 | create(resourceName, props, options) { 115 | if (!resourceName) throw "You need to specify a resource"; 116 | 117 | var url = [ 118 | config.api.host, 119 | config.api.nameSpace, 120 | config.api.version 121 | ]; 122 | 123 | if (options.parentResource) { 124 | url.push(options.parentResource.name, options.parentResource.id); 125 | }; 126 | 127 | url.push(resourceName); 128 | 129 | url = url.join("/"); 130 | 131 | return this._request(url, "POST", { data: props }); 132 | } 133 | 134 | /** 135 | * @todo Update a resource 136 | * @function TrailblazerHTTPStorageAdapter#update 137 | */ 138 | update(resourceName, id, props, options) { 139 | if (!resourceName) throw "You need to specify a resource"; 140 | if (!id) throw "You need to specify an ID"; 141 | 142 | var url = [ 143 | config.api.host, 144 | config.api.nameSpace, 145 | config.api.version 146 | ]; 147 | 148 | if (options.parentResource) { 149 | url.push(options.parentResource.name, options.parentResource.id); 150 | }; 151 | 152 | url.push(resourceName, id); 153 | url = url.join("/"); 154 | 155 | return this._request(url, "PUT", { data: props }); 156 | } 157 | 158 | /** 159 | * @todo Destroy a resource 160 | * @function TrailblazerHTTPStorageAdapter#destroy 161 | */ 162 | destroy(resourceName, id) { 163 | if (!resourceName) throw "You need to specify a resource"; 164 | if (!id) throw "You need to specify an ID"; 165 | 166 | var url = [ 167 | config.api.host, 168 | config.api.nameSpace, 169 | config.api.version, 170 | resourceName, 171 | id 172 | ].join("/"); 173 | 174 | return this._request(url, "DELETE"); 175 | } 176 | 177 | /** 178 | * @todo Bulk destroy a resource 179 | * @function TrailblazerHTTPStorageAdapter#bulkDestroy 180 | */ 181 | bulkDestroy(resourceName, ids) { 182 | if (!resourceName) throw "You need to specify a resource"; 183 | if (!ids) throw "You need to specify some IDs"; 184 | 185 | var url = [ 186 | config.api.host, 187 | config.api.nameSpace, 188 | config.api.version, 189 | resourceName, 190 | 'bulk_delete' 191 | ].join("/"); 192 | 193 | return this._request(url, "DELETE", { params: `ids=${ids.join(',')}` }); 194 | } 195 | 196 | }; 197 | 198 | module.exports = TrailblazerHTTPStorageAdapter; 199 | -------------------------------------------------------------------------------- /src/stores/assignment-store.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | import constants from '../constants'; 3 | import TrailblazerHTTPStorageAdapter from '../adapter/trailblazer_http_storage_adapter'; 4 | 5 | import Store from '../lib/store'; 6 | import { query, action } from '../decorators'; 7 | 8 | import Logger from '../util/logger'; 9 | var logger = Logger('stores/assignment-store.js'); 10 | 11 | class AssignmentStore extends Store { 12 | 13 | constructor (options = {}) { 14 | super(options); 15 | 16 | this.db = options.db; 17 | } 18 | 19 | @query 20 | async getAssignments() { 21 | var assignments = await this.db.assignments.all(); 22 | return assignments; 23 | } 24 | 25 | @query 26 | async getAssignmentByLocalId(localId) { 27 | var assignment = await this.db.assignments.get(localId); 28 | return assignment; 29 | } 30 | 31 | @query 32 | async getAssignmentByRemoteId(remoteId) { 33 | var assignment = await this.db.assignments.index('id').get(remoteId); 34 | return assignment; 35 | } 36 | 37 | @action(constants.SIGN_OUT) 38 | handleSignOut() { 39 | this.db.assignments.clear(); 40 | } 41 | 42 | /** 43 | * Emit all assignment data 44 | */ 45 | @action(constants.REQUEST_ASSIGNMENTS) 46 | handleRequestAssignments() { 47 | logger.warn("DEPRECATED"); 48 | // Get the assignments from the DB, fire a change, and fire a fetch assignments 49 | this.db.assignments.all().then((assignments) => { 50 | this.emit('change', { assignments: assignments }); 51 | this.flux.actions.fetchAssignments(); 52 | }); 53 | } 54 | 55 | /** 56 | * Emit all assignment data 57 | */ 58 | @action(constants.CREATE_ASSIGNMENT_SUCCESS) 59 | handleCreateAssignmentSuccess() { 60 | this.db.assignments.all().then((assignments) => { 61 | this.emit('change', { assignments: assignments }); 62 | }); 63 | } 64 | 65 | /** 66 | * Updates an assignment record with a new title 67 | */ 68 | @action(constants.UPDATE_ASSIGNMENT_TITLE) 69 | handleUpdateAssignmentTitle(payload) { 70 | logger.info('handleUpdateAssignmentTitle', { payload: payload }); 71 | this.db.assignments.db.transaction("readwrite", ["assignments"], (err, tx) => { 72 | var store = tx.objectStore("assignments") 73 | , oncomplete = []; 74 | 75 | store.get(payload.localId).onsuccess = (evt) => { 76 | var assignment = evt.target.result; 77 | 78 | assignment.title = payload.title; 79 | store.put(assignment).onsuccess = (evt) => { 80 | this.emit('change', { assignment: assignment }); 81 | 82 | oncomplete.push(() => { 83 | setTimeout(() => this.flux.actions.persistAssignment(assignment.localId)); 84 | }); 85 | }; 86 | }; 87 | 88 | tx.oncomplete = () => { 89 | _.each(oncomplete, cb => cb()); 90 | }; 91 | 92 | }); 93 | } 94 | 95 | /** 96 | * Emits a change with the assignment list 97 | */ 98 | @action(constants.DESTROY_ASSIGNMENT_SUCCESS) 99 | handleDestroyAssignmentSuccess(payload) { 100 | this.db.assignments.all().then((assignments) => { 101 | this.emit('change', { assignments: assignments }); 102 | }); 103 | } 104 | 105 | /** 106 | * Emits a change event from this store with the complete list of assignments 107 | */ 108 | @action(constants.ASSIGNMENTS_SYNCHRONIZED) 109 | handleAssignmentsSynchronized() { 110 | this.db.assignments.all().then((assignments) => { 111 | this.emit('change', { assignments: assignments }); 112 | }); 113 | } 114 | 115 | @action(constants.MAKE_ASSIGNMENT_VISIBLE) 116 | handleMakeAssignmentVisible(payload) { 117 | logger.info('handleMakeAssignmentVisible'); 118 | 119 | this.db.assignments.get(payload.localId).then((assignment) => { 120 | if (assignment && assignment.id) { 121 | var data = { 122 | assignment: { 123 | visible: true 124 | } 125 | }; 126 | 127 | new TrailblazerHTTPStorageAdapter().update("assignments", assignment.id, data, {}) 128 | .then((response) => { 129 | //success 130 | this.db.assignments.db.transaction("readwrite", ["assignments"], (err, tx) => { 131 | var successCallbacks = []; 132 | 133 | var store = tx.objectStore("assignments"); 134 | 135 | store.get(assignment.localId).onsuccess = (evt) => { 136 | var assignment = evt.target.result; 137 | 138 | assignment.visible = response.visible; 139 | assignment.url = response.url; 140 | 141 | store.put(assignment).onsuccess = (evt) => { 142 | successCallbacks.push(() => { 143 | this.emit('change', { assignment: assignment }); 144 | }); 145 | }; 146 | 147 | }; 148 | 149 | tx.oncomplete = () => { 150 | _.each(successCallbacks, (cb) => { cb(); }); 151 | }; 152 | 153 | }); 154 | }, 155 | (response) => { 156 | //error 157 | } 158 | ); 159 | } 160 | }); 161 | } 162 | 163 | @action(constants.MAKE_ASSIGNMENT_HIDDEN) 164 | handleMakeAssignmentHidden(payload) { 165 | logger.info('handleMakeAssignmentHidden'); 166 | 167 | this.db.assignments.get(payload.localId).then((assignment) => { 168 | if (assignment && assignment.id) { 169 | var data = { 170 | assignment: { 171 | visible: false 172 | } 173 | }; 174 | 175 | new TrailblazerHTTPStorageAdapter().update("assignments", assignment.id, data, {}) 176 | .then((response) => { 177 | //success 178 | this.db.assignments.db.transaction("readwrite", ["assignments"], (err, tx) => { 179 | var successCallbacks = []; 180 | 181 | var store = tx.objectStore("assignments"); 182 | 183 | store.get(assignment.localId).onsuccess = (evt) => { 184 | var assignment = evt.target.result; 185 | 186 | assignment.visible = response.visible; 187 | assignment.url = response.url; 188 | 189 | store.put(assignment).onsuccess = (evt) => { 190 | successCallbacks.push(() => { 191 | this.emit('change', { assignment: assignment }); 192 | }); 193 | }; 194 | 195 | }; 196 | 197 | tx.oncomplete = () => { 198 | _.each(successCallbacks, (cb) => { cb(); }); 199 | }; 200 | 201 | }); 202 | }, 203 | function (response) { 204 | //error 205 | }); 206 | } 207 | }); 208 | } 209 | 210 | }; 211 | 212 | export default AssignmentStore; 213 | -------------------------------------------------------------------------------- /src/components/views/assignments/index.jsx: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | import React from 'react'; 3 | 4 | import Constants from '../../../constants'; 5 | import queries from '../../../queries'; 6 | 7 | var articles = { 8 | "http://en.wikipedia.org/wiki/Anthropodermic_bibliopegy": "Anthropodermic bibliopegy", 9 | "http://en.wikipedia.org/wiki/Elm_Farm_Ollie": "Elm Farm Ollie", 10 | "http://en.wikipedia.org/wiki/EURion_constellation": "EURion constellation", 11 | "http://en.wikipedia.org/wiki/Demon_core": "(the) Demon core", 12 | "http://en.wikipedia.org/wiki/Pole_of_inaccessibility": "Pole of inaccessibility", 13 | "http://en.wikipedia.org/wiki/Globster": "Globster", 14 | "http://en.wikipedia.org/wiki/Hoba_meteorite": "Hoba meteorite", 15 | "http://en.wikipedia.org/wiki/Seattle_Windshield_Pitting_Epidemic": "Seattle Windshield Pitting Epidemic", 16 | "http://en.wikipedia.org/wiki/GRB_971214": "GRB 971214", 17 | "http://en.wikipedia.org/wiki/Resolute_desk": "\"Resolute\" desk", 18 | "http://en.wikipedia.org/wiki/Candace_Newmaker": "Candace Newmaker", 19 | "http://en.wikipedia.org/wiki/Cryptomnesia": "Cryptomnesia", 20 | "http://en.wikipedia.org/wiki/Hans_Island": "Hans Island", 21 | "http://en.wikipedia.org/wiki/Harrowing_of_Hell": "Harrowing of Hell", 22 | "http://en.wikipedia.org/wiki/Semantic_satiation": "Semantic satiation", 23 | "http://en.wikipedia.org/wiki/Dempster_Highway": "Dempster Highway", 24 | "http://en.wikipedia.org/wiki/Dalton_Highway": "Dalton Highway", 25 | "http://en.wikipedia.org/wiki/Paul_Armand_Delille": "Paul Felix Armand-Delille", 26 | "http://en.wikipedia.org/wiki/Herschel_Island": "Herschel Island", 27 | "http://en.wikipedia.org/wiki/Stone_spheres_of_Costa_Rica": "Stone spheres of Costa Rica", 28 | "http://en.wikipedia.org/wiki/Paternoster": "Paternoster", 29 | "http://en.wikipedia.org/wiki/Self-immolation": "Self-immolation", 30 | "http://en.wikipedia.org/wiki/Narco_submarine": "Narco submarine", 31 | "http://en.wikipedia.org/wiki/Louis_Slotin": "Louis Slotin", 32 | "http://en.wikipedia.org/wiki/Language_deprivation_experiments": "Language deprivation experiments", 33 | "http://en.wikipedia.org/wiki/London_Stone": "London Stone", 34 | "http://en.wikipedia.org/wiki/Cit%C3%A9_Soleil": "Cité Soleil", 35 | "http://en.wikipedia.org/wiki/Blood_chit": "Blood chit", 36 | "http://en.wikipedia.org/wiki/Parsley_Massacre": "Parsley Massacre", 37 | "http://en.wikipedia.org/wiki/Ribbon_Creek_Incident": "Ribbon Creek Incident", 38 | "http://en.wikipedia.org/wiki/Art_intervention": "Art intervention", 39 | "http://en.wikipedia.org/wiki/Impostor": "Impostor", 40 | "http://en.wikipedia.org/wiki/Bata_LoBagola": "Bata LoBagola", 41 | "http://en.wikipedia.org/wiki/Cheating_at_the_Paralympic_Games": "Cheating at the Paralympic Games", 42 | "http://en.wikipedia.org/wiki/David_Hempleman-Adams": "David Hempleman-Adams", 43 | "http://en.wikipedia.org/wiki/The_Kafka_Machine": "The Kafka Machine", 44 | "http://en.wikipedia.org/wiki/Park_Young_Seok": "Park Young Seok", 45 | "http://en.wikipedia.org/wiki/Houston_Riot_(1917)": "Houston Riot (1917)", 46 | "http://en.wikipedia.org/wiki/Henry_Pierrepoint": "Albert Pierrepoint", 47 | "http://en.wikipedia.org/wiki/Discoveries_of_human_feet_on_British_Columbia_beaches,_2007%E2%80%932008": "Discoveries of human feet on British Columbia beaches, 2007–2008", 48 | "http://en.wikipedia.org/wiki/Taman_Shud_Case": "Taman Shud Case", 49 | "http://en.wikipedia.org/wiki/Who_put_Bella_in_the_Wych_Elm%3F": "Who put Bella in the Wych Elm?", 50 | "http://en.wikipedia.org/wiki/First_flying_machine": "First flying machine", 51 | "http://en.wikipedia.org/wiki/Defeat_in_detail": "Defeat in Detail", 52 | "http://en.wikipedia.org/wiki/Peppered_moth_evolution": "Peppered moth evolution", 53 | "http://en.wikipedia.org/wiki/Resource_holding_potential": "Resource holding potential", 54 | "http://en.wikipedia.org/wiki/Dismas": "Saint Dismas", 55 | "http://en.wikipedia.org/wiki/Target_girl": "Target girl", 56 | "http://en.wikipedia.org/wiki/Longevity_myths": "Longevity myths", 57 | "http://en.wikipedia.org/wiki/SL-1": "SL-1" 58 | } 59 | 60 | import AssignmentItem from '../../assignment-item'; 61 | 62 | class Index extends React.Component { 63 | 64 | constructor(props) { 65 | super(props); 66 | 67 | this.state = { 68 | assignments: [] 69 | }; 70 | } 71 | 72 | componentDidMount() { 73 | console.log(this); 74 | queries.AssignmentStore.getAssignments().then( (assignments) => { 75 | this.setState({ assignments }); 76 | }); 77 | 78 | let _assignmentListener = (message) => { 79 | if (message.action === Constants.__change__ && message.storeName === "AssignmentStore") { 80 | queries.AssignmentStore.getAssignments().then( (assignments) => { 81 | this.setState({ assignments }); 82 | }); 83 | } 84 | }; 85 | 86 | this.setState({ _assignmentListener }); 87 | 88 | chrome.runtime.onMessage.addListener(_assignmentListener); 89 | 90 | this.props.route.actions.requestAssignments(); 91 | this.props.route.actions.viewedAssignmentList(); 92 | } 93 | 94 | componentWillUnmount() { 95 | chrome.runtime.onMessage.removeListener(this.state._assignmentListener); 96 | } 97 | 98 | onAssignmentClicked(assignment, evt) { 99 | console.log(assignment, evt); 100 | } 101 | 102 | startMeandering(evt) { 103 | evt.preventDefault(); 104 | 105 | var url = evt.currentTarget.href; 106 | 107 | chrome.tabs.create({ url: url, active: true }, (tab) => { 108 | this.props.route.actions.startRecording(tab.id, tab); 109 | }); 110 | } 111 | 112 | render() { 113 | document.title = "Resume a Trail"; 114 | 115 | var list = this.state.assignments.map((item) => { 116 | return 121 | }); 122 | 123 | if (list.length > 0) { 124 | return
    125 |

    Your Trails

    126 |
      {list}
    127 |
    ; 128 | } else { 129 | var link, title; 130 | 131 | link = _.sample(Object.keys(articles)); 132 | title = articles[link]; 133 | 134 | return
    135 |

    Your Trails

    136 |

    137 | Oops, there's nothing here yet. 138 |

    139 |

    140 | Perhaps you might like to start by checking out {title} on Wikipedia?
    141 | We found it on a list of 50 interesting Wiki articles here (thanks Holly and Ray) 142 |

    143 |

    144 | The links above will start you on a new trail, so you can get straight into exploring! 145 |

    146 |
    ; 147 | } 148 | } 149 | 150 | }; 151 | 152 | Index.contextTypes = { 153 | router: React.PropTypes.object.isRequired 154 | }; 155 | 156 | export default Index; 157 | -------------------------------------------------------------------------------- /src/components/views/offline-data/index.jsx: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | import React from 'react'; 3 | 4 | import Constants from '../../../constants'; 5 | import queries from '../../../queries'; 6 | 7 | import AssignmentItem from '../../assignment-item'; 8 | 9 | const OFFLINE_ASSIGNMENT_STYLE = { 10 | background: '#EEE', 11 | boxShadow: '0px 3px 3px rgba(32, 32, 32, 0.3)', 12 | borderRadius: '3px', 13 | padding: '12px', 14 | margin: '12px', 15 | }; 16 | 17 | class Index extends React.Component { 18 | 19 | constructor(props) { 20 | super(props); 21 | 22 | this.state = { 23 | assignments: [] 24 | }; 25 | } 26 | 27 | getStateFromFlux(message) { 28 | if (message.action === Constants.__change__ && message.storeName === "AssignmentStore" && this.tempAssignmentHandler) { 29 | queries.AssignmentStore.getAssignments().then((assignments) => { 30 | this.setState({ assignments }); 31 | this.tempAssignmentHandler(assignments); 32 | }); 33 | } 34 | } 35 | 36 | componentDidMount() { 37 | console.log(this); 38 | queries.AssignmentStore.getAssignments().then( (assignments) => { 39 | this.setState({ assignments }); 40 | }); 41 | 42 | let _assignmentListener = (message) => { 43 | if (message.action === Constants.__change__ && message.storeName === "AssignmentStore") { 44 | queries.AssignmentStore.getAssignments().then( (assignments) => { 45 | this.setState({ assignments }); 46 | }); 47 | } 48 | }; 49 | 50 | var __fluxHandler = this.getStateFromFlux.bind(this); 51 | 52 | this.setState({ __fluxHandler, _assignmentListener }); 53 | 54 | chrome.runtime.onMessage.addListener(__fluxHandler); 55 | chrome.runtime.onMessage.addListener(_assignmentListener); 56 | } 57 | 58 | componentWillUnmount() { 59 | chrome.runtime.onMessage.removeListener(this.state.__fluxHandler); 60 | chrome.runtime.onMessage.removeListener(this.state._assignmentListener); 61 | } 62 | 63 | onFetchDataClicked(evt) { 64 | this.tempAssignmentHandler = (assignments) => { 65 | assignments.map(assignment => this.props.route.actions.fetchNodes(assignment.id)); 66 | } 67 | 68 | this.props.route.actions.fetchAssignments(); 69 | } 70 | 71 | onAssignmentClicked(assignment, evt) { 72 | console.log(assignment, evt); 73 | } 74 | 75 | importData(evt) { 76 | evt.preventDefault(); 77 | 78 | let file = this.refs.file.files[0]; 79 | 80 | let reader = new FileReader(); 81 | 82 | reader.onload = (evt) => { 83 | if (reader.readyState === FileReader.DONE) { 84 | const contents = reader.result; 85 | 86 | const { assignments, nodes } = JSON.parse(contents); 87 | 88 | this.props.route.actions.importData({ assignments, nodes }); 89 | } 90 | } 91 | 92 | reader.readAsText(file); 93 | } 94 | 95 | exportData(evt) { 96 | evt.preventDefault(); 97 | 98 | Promise.all([ 99 | queries.AssignmentStore.getAssignments(), 100 | queries.NodeStore.getNodes() 101 | ]) 102 | .then(([assignments, nodes]) => { 103 | console.log({ assignments, nodes }); 104 | 105 | const blob = new Blob([JSON.stringify({ assignments, nodes })]); 106 | 107 | const link = window.document.createElement("a"); 108 | link.href = window.URL.createObjectURL(blob, { type: "text/plain" }); 109 | link.download = "trails.trailblazerbackup"; 110 | document.body.appendChild(link); 111 | link.click(); 112 | URL.revokeObjectURL(link.href); 113 | document.body.removeChild(link); 114 | }) 115 | .catch(console.error.bind(console)); 116 | 117 | } 118 | 119 | 120 | render() { 121 | document.title = "Resume a Trail"; 122 | 123 | var list = this.state.assignments.map((item) => { 124 | //return ; 125 | return ( 126 |
    127 |
  • this.context.router.push(`/assignments/${item.localId}`)}> 128 | {item.title} 129 |
  • 130 |
    131 | ); 132 | }); 133 | 134 | console.log(list); 135 | 136 | return ( 137 |
    138 |

    Manage your data

    139 |
    147 |

    Download data from remote servers

    148 |

    149 | This process will download fresh data from Trailblazer's servers, 150 | and may overwrite any conflicting data that exists locally 151 |

    152 |

    153 | 154 | Warning: this will fail after the shutdown date and may destroy 155 | data permanently. We recommend exporting existing data before 156 | performing this action. 157 | 158 |

    159 |

    160 |
    161 | 162 |
    170 |

    Import a trailblazer backup

    171 |

    Load a copy of the data you've previously exported from Trailblazer

    172 |

    173 | 174 | Warning: this will delete any existing data in your extension 175 | before loading the backup. 176 | 177 |

    178 |

    179 |

    180 | 181 | 182 |
    183 |

    184 |
    185 | 186 |
    194 |

    Your restored trails

    195 |

    196 | These are the trails that are currently loaded in Trailblazer. You 197 | can continue to view them, however resuming and recording may not 198 | work as expected. 199 |

    200 |

    201 | You may export these to make a backup of this data using the button 202 | below. 203 |

    204 | 205 |

    206 | 207 |

    208 |
    209 | 210 |

    Trails

    211 | 212 | {(list.length > 0) ? ( 213 |
      {list}
    214 | ) : ( 215 |

    216 | Oops, there's nothing here yet. 217 |

    218 | )} 219 |
    220 | ); 221 | } 222 | 223 | }; 224 | 225 | Index.contextTypes = { 226 | router: React.PropTypes.object.isRequired 227 | }; 228 | 229 | export default Index; 230 | -------------------------------------------------------------------------------- /src/adapter/chrome_identity_adapter.js: -------------------------------------------------------------------------------- 1 | // config 2 | import config from '../config'; 3 | 4 | // helpers 5 | import Promise from 'promise'; 6 | import superagent from 'superagent'; 7 | import _ from 'lodash'; 8 | 9 | /** 10 | * Creates a new ChromeIdentityAdapter 11 | * 12 | * @class ChromeIdentityAdapter 13 | * @classdesc 14 | * Maintains the authenticated state of the extension and provides a common 15 | * interface to authenticate with the chrome.identity.* APIs 16 | */ 17 | 18 | export default class ChromeIdentityAdapter { 19 | 20 | /** 21 | * Consults chrome.storage.sync to check if a token already exists, resolving 22 | * the returned promise if it does. If a token is not already stored, the 23 | * sign in flow will be launched. 24 | * 25 | * Then, if resolved (i.e. on a successful sign in), the callback will be 26 | * passed an object containing details about the access token; 27 | * ```javascript 28 | * { 29 | * access_token: "string", 30 | * token_type: "bearer", 31 | * expires_in: number(seconds) 32 | * } 33 | * ``` 34 | * 35 | * If rejected (user denies access or the prompt is closed) the callback will 36 | * either be passed an object detailing the rejection; 37 | * ```javascript 38 | * { 39 | * error: "error code", 40 | * error_description: "description" 41 | * } 42 | * ``` 43 | * in the case of explicit denial, or the exception in the event of some 44 | * other failure. 45 | * 46 | * @function ChromeIdentityAdapter#signIn 47 | * @returns {Promise} 48 | */ 49 | signIn() { 50 | var redirect = encodeURIComponent("https://" + chrome.runtime.id + ".chromiumapp.org/") 51 | , host = config.api.host 52 | , version = config.api.version 53 | , clientId = config.api.clientId; 54 | 55 | var params = "?" + [ 56 | "client_id="+clientId, 57 | "response_type=token", 58 | "redirect_uri="+redirect 59 | ].join("&"); 60 | 61 | var authUrl = [host, "oauth/authorize"].join("/") + params; 62 | 63 | var promise = new Promise((resolve, reject) => { 64 | this.getToken().then((token) => { 65 | resolve(token); 66 | }, () => { 67 | chrome.identity.launchWebAuthFlow({ url: authUrl, interactive: true }, (redirectUrl) => { 68 | if (redirectUrl) { 69 | // Slice up the redirect and parse the response object from url hash 70 | var response = redirectUrl.substring(redirectUrl.indexOf("#") + 1) 71 | , responseObject = {}; 72 | 73 | _.each(response.split("&"), (item) => { 74 | var i = item.split("="); 75 | responseObject[i[0]] = i[1]; 76 | }); 77 | 78 | if (responseObject.access_token) { 79 | // If we have an access token then add some useful properties, 80 | // store it, and resolve the promise with it 81 | responseObject.expires_in = parseInt(responseObject.expires_in); 82 | responseObject.expires_at = Date.now() + responseObject.expires_in; 83 | 84 | this.storeToken(responseObject); 85 | resolve(responseObject); 86 | } else { 87 | // Otherwise, reject it with the details 88 | reject(responseObject); 89 | } 90 | } else { 91 | reject(chrome.runtime.lastError); 92 | } 93 | }); //chrome.identity.launchWebAuthFlow 94 | }); //getToken(); 95 | }); //promise 96 | 97 | return promise; 98 | } 99 | 100 | /** 101 | * Terminate the currently authenticated session. Returns a promise which 102 | * resolves if the token was successfully revoked (passing no parameters). 103 | * 104 | * @function ChromeIdentityAdapter#signOut 105 | * @returns {Promise} 106 | */ 107 | signOut() { 108 | var promise = new Promise((resolve, reject) => { 109 | chrome.storage.sync.get("token", (token) => { 110 | if (token.token) { 111 | var token = JSON.parse(token.token); 112 | 113 | this._clearToken().then(resolve); 114 | 115 | // Best effort 116 | superagent.post(config.api.host + "/oauth/revoke") 117 | .send({ token: token.access_token }) 118 | .set("Content-Type", "application/x-www-form-urlencoded") 119 | .end(); 120 | } else { 121 | resolve(); 122 | } 123 | 124 | var signOutUrl = [config.api.host, "sign_out"].join("/"); 125 | chrome.identity.launchWebAuthFlow({ url: signOutUrl, interactive: false }, (url) => { 126 | // Ignore everything 127 | console.log("The warning from chrome is fine - we don't want user intervention here as we're terminating a session inside the auth frame", 128 | chrome.runtime.lastError); 129 | }); 130 | 131 | }); //chrome.storage.sync.get 132 | }); //promise 133 | 134 | return promise; 135 | } 136 | 137 | /** 138 | * Check whether there is a valid token available. Promise will always 139 | * resolve, passing a single boolean parameter indicating whether there is a 140 | * token available or not. 141 | * @function ChromeIdentityAdapter#isSignedIn 142 | * @returns {Promise} 143 | */ 144 | isSignedIn() { 145 | return new Promise((resolve, reject) => { 146 | this.getToken().then( 147 | () => { resolve(true); }, 148 | () => { resolve(false); }) 149 | }); 150 | } 151 | 152 | /** 153 | * @function ChromeIdentityAdapter#profile 154 | * @TODO Get the currently authenticated person's profile information 155 | */ 156 | profile() {}; 157 | 158 | /** 159 | * Retrieves the token stored in chrome.storage.sync 160 | * @function ChromeIdentityAdapter#getToken 161 | * @returns {Promise} A promise which resolves with the token object if it 162 | * was found 163 | */ 164 | getToken() { 165 | return new Promise((resolve, reject) => { 166 | chrome.storage.sync.get("token", (token) => { 167 | if (token.token) { 168 | resolve(JSON.parse(token.token)); 169 | } else if (chrome.runtime.lastError) { 170 | reject(chrome.runtime.lastError); 171 | } else { 172 | reject(); 173 | } 174 | }); //chrome.storage.sync.get 175 | }); //promise 176 | } 177 | 178 | /** 179 | * Stores the provided token in chrome.storage.sync 180 | * @function ChromeIdentityAdapter#storeToken 181 | * @param {Object} tokenObject - the token passed to {@link 182 | * ChromeIdentityAdapter#signIn}'s `resolve` 183 | * @returns {Promise} A promise which resolves if chrome.runtime.lastError is 184 | * not set 185 | */ 186 | storeToken(tokenObject) { 187 | return new Promise((resolve, reject) => { 188 | var userUrl = [ 189 | config.api.host, 190 | config.api.nameSpace, 191 | config.api.version, 192 | "me" 193 | ].join("/"); 194 | 195 | superagent("GET", userUrl) 196 | .set("Authorization", "Bearer " + tokenObject.access_token) 197 | .set("Accept", "application/json") 198 | .end((response) => { 199 | tokenObject.user_id = response.body.id; 200 | chrome.storage.sync.set({ token: JSON.stringify(tokenObject) }, () => { 201 | if (chrome.runtime.lastError) { 202 | reject(chrome.runtime.lastError); 203 | } else { 204 | resolve(); 205 | } 206 | }); //chrome.storage.sync.set 207 | }); 208 | }); //promise 209 | } 210 | 211 | /** 212 | * Clears the stored token from chrome.storage.sync 213 | * @function ChromeIdentityAdapter#_clearToken 214 | * @returns {Promise} A promise which resolves if chrome.runtime.lastError is 215 | * not set 216 | * @private 217 | */ 218 | _clearToken() { 219 | return new Promise((resolve, reject) => { 220 | chrome.storage.sync.remove("token", () => { 221 | if (chrome.runtime.lastError) { 222 | reject(chrome.runtime.lastError); 223 | } else { 224 | resolve(); 225 | } 226 | }); //chrome.storage.sync.remove 227 | }); //promise 228 | } 229 | 230 | }; 231 | -------------------------------------------------------------------------------- /src/components/trail.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import _ from 'lodash'; 3 | 4 | import createGraph from 'ngraph.graph'; 5 | import createLayout from 'ngraph.forcelayout'; 6 | import svgPanZoom from 'svg-pan-zoom'; 7 | 8 | import Node from "./node"; 9 | import NodePopover from "./node-popover"; 10 | import Link from "./link"; 11 | 12 | import Logger from '../util/logger'; 13 | var logger = Logger('trail.js'); 14 | 15 | export default class Trail extends React.Component { 16 | 17 | constructor(props) { 18 | super(props); 19 | 20 | this.state = { 21 | graph: createGraph(), 22 | nodesPendingDeletion: [] 23 | }; 24 | } 25 | 26 | componentWillMount() { 27 | // Prepare the initial graph data 28 | this.props.nodes.map(n => this.state.graph.addNode(n.localId, n)); 29 | this.props.nodes.map((n) => { 30 | if (n.localParentId) { 31 | this.state.graph.addLink(n.localParentId, n.localId, { deletePending: false }); 32 | } 33 | }); 34 | 35 | let layout = createLayout(this.state.graph, { 36 | springLength: 60 37 | }); 38 | 39 | this.props.nodes.map(n => layout.setNodePosition(n.localId, n.x, n.y)); 40 | 41 | this.setState({ layout }); 42 | } 43 | 44 | componentDidMount() { 45 | let updatePopovers = () => { 46 | let fn = () => { 47 | this.updateNodePositions(); 48 | }; 49 | 50 | setTimeout(fn, 30); 51 | }; 52 | 53 | this.svgPanZoom = svgPanZoom( React.findDOMNode(this.refs.svg), { 54 | viewportSelector: '.viewport', 55 | fit: true, 56 | center: true, 57 | zoomEnabled: true, 58 | maxZoom: 2.5, 59 | zoomScaleSensitivity: 0.3, 60 | onPan: updatePopovers, 61 | onZoom: updatePopovers 62 | }); 63 | } 64 | 65 | componentWillUnmount() { 66 | this.disposed = true; 67 | this.svgPanZoom.destroy(); 68 | this.state.layout.dispose(); 69 | } 70 | 71 | componentDidUpdate() { 72 | let changed = false; 73 | 74 | this.state.graph.forEachNode((node) => { 75 | if (!_.find(this.props.nodes, { localId: node.id})) { 76 | this.state.graph.removeNode(node.id); 77 | changed = true; 78 | } 79 | }); 80 | 81 | this.props.nodes.map((n) => { 82 | // Trigger an update if a new node is added 83 | if (!this.state.graph.getNode(n.localId)) changed = true; 84 | 85 | // Update any existing nodes with data changes if needed 86 | this.state.graph.addNode(n.localId, n); 87 | if (n.x && n.y) this.state.layout.setNodePosition(n.localId, n.x, n.y); 88 | }); 89 | 90 | this.props.nodes.map((n) => { 91 | // Add in any links that are missing after the node updates, triggering a 92 | // change if needed 93 | if (n.localParentId && !this.state.graph.hasLink(n.localParentId, n.localId)) { 94 | this.state.graph.addLink(n.localParentId, n.localId, { deletePending: false }); 95 | changed = true; 96 | } 97 | }); 98 | 99 | if (changed) this.setState({ stable: false }); 100 | this.animationLoop(); 101 | this.updateNodePositions(); 102 | } 103 | 104 | updateNodePositions() { 105 | this.state.graph.forEachNode((n) => { 106 | let reactNode = this.refs[`node-${n.id}`]; 107 | 108 | if (reactNode) { 109 | let position = this.state.layout.getNodePosition(n.id); 110 | if (position) reactNode.updatePosition(position); 111 | 112 | let popover = this.refs[`node-popover-${n.id}`]; 113 | let screenPosition = reactNode.getScreenPosition(); 114 | if (popover && screenPosition) popover.updatePosition(screenPosition); 115 | } 116 | }); 117 | } 118 | 119 | updateLinkPositions() { 120 | this.state.graph.forEachLink((l) => { 121 | let reactNode = this.refs[`link-${l.id}`]; 122 | 123 | if (reactNode) { 124 | let position = this.state.layout.getLinkPosition(l.id); 125 | if (position) reactNode.updatePosition(position); 126 | } 127 | }); 128 | } 129 | 130 | onGraphStabilityReached() { 131 | let coords = {}; 132 | this.state.graph.forEachNode((node) => { 133 | let position = this.state.layout.getNodePosition(node.id); 134 | coords[node.data.localId] = { x: position.x, y: position.y }; 135 | }); 136 | this.props.actions.saveMapLayout(this.props.assignment.localId, coords); 137 | } 138 | 139 | animationLoop() { 140 | if (this.disposed || this.state.stable) return; 141 | requestAnimationFrame(this.animationLoop.bind(this)); 142 | 143 | if (this.state.layout && this.props.nodes.length > 0 && !this.state.stable) { 144 | let stable = this.state.layout.step(); 145 | 146 | if (stable !== this.state.stable) { 147 | if (stable) this.onGraphStabilityReached(); 148 | this.setState({ stable }); 149 | } 150 | } 151 | 152 | this.updateNodePositions(); 153 | this.updateLinkPositions(); 154 | } 155 | 156 | // This is sharing state between react components which is dicey at 157 | // best, but until we can get a better interaction model between SVG and DOM 158 | // hover events (i.e. not using this div anchor business) this is about as 159 | // best it can be. 160 | // Moving the trail rendering implementation and this popover to canvas might 161 | // solve a lot of these problems if we can handle everything in one 162 | // environment. 163 | activatePopover(node, evt = null) { 164 | let popover = this.refs[`node-popover-${node.id}`]; 165 | popover.mouseInParentBounds = true; 166 | popover.activate(); 167 | } 168 | 169 | softDismissPopover(node, evt = null) { 170 | let popover = this.refs[`node-popover-${node.id}`]; 171 | popover.mouseInParentBounds = false; 172 | popover.softDismiss(); 173 | } 174 | 175 | onDeletePending(node, evt = null) { 176 | let fn = (nodeIds, processedIds) => { 177 | 178 | let nextIds = _.flatten( _.without(nodeIds, ...processedIds).map((id) => { 179 | let node = this.state.graph.getNode(id); 180 | node.data.deletePending = true; 181 | this.state.graph.addNode(node.id, node.data); 182 | 183 | processedIds.push(node.id); 184 | 185 | // Next set of IDs to process 186 | return node.links.map((l) => { 187 | if (l.toId !== node.id) { 188 | l.data.deletePending = true; 189 | } 190 | return l.toId; 191 | }); 192 | })); 193 | 194 | if (nextIds.length > 0) { 195 | fn(nextIds, processedIds); 196 | } else { 197 | this.setState({ nodesPendingDeletion: processedIds }) 198 | } 199 | }; 200 | 201 | fn([node.id], []); 202 | 203 | this.setState({ changed: true }); 204 | } 205 | 206 | onDeleteConfirmed(node, evt = null) { 207 | this.props.actions.bulkDestroyNodes(this.state.nodesPendingDeletion); 208 | this.setState({ nodesPendingDeletion: [] }); 209 | } 210 | 211 | onDeleteCancelled(node, evt = null) { 212 | this.state.graph.forEachNode((node) => { 213 | delete node.data.deletePending; 214 | node.links.map(l => l.data.deletePending = false); 215 | this.state.graph.addNode(node.id, node.data); 216 | }); 217 | this.setState({ changed: true, nodesPendingDeletion: [] }); 218 | } 219 | 220 | render() { 221 | let nodes = []; 222 | let popovers = []; 223 | let links = []; 224 | 225 | this.state.graph.forEachNode((n) => { 226 | var position = this.state.layout.getNodePosition(n.id); 227 | let key = `node-${n.id}`; 228 | let popoverKey = `node-popover-${n.id}`; 229 | 230 | popovers.push( 231 | ); 241 | 242 | nodes.push( 243 | ); 251 | }); 252 | 253 | this.state.graph.forEachLink((l) => { 254 | let position = this.state.layout.getLinkPosition(l.id); 255 | let key = `link-${l.id}`; 256 | 257 | links.push(); 258 | }); 259 | 260 | return
    261 |
    {popovers}
    262 | 269 | 270 | {links} 271 | {nodes} 272 | 273 | 274 |
    ; 275 | } 276 | }; 277 | --------------------------------------------------------------------------------