├── 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 |
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 |
10 |
11 |
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 |
25 | {makePrivateText}
26 |
27 |
28 |
33 | {shareText}
34 |
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 ;
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 ;
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 |
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 ;
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 ;
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 | Cancel
89 |
90 |
91 | Delete
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 | Delete
101 |
102 |
103 | Resume
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 |
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 |
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 |
Download now
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 |
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 | Export this data
207 |
208 |
209 |
210 |
Trails
211 |
212 | {(list.length > 0) ? (
213 |
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 |
--------------------------------------------------------------------------------