├── widget
├── .gitignore
├── GithubPulse
│ ├── Resources
│ │ ├── Images
│ │ │ ├── icon.png
│ │ │ ├── icon@2x.png
│ │ │ ├── icon_dark.png
│ │ │ ├── icon_dark@2x.png
│ │ │ ├── icon_notification.png
│ │ │ ├── icon_notification@2x.png
│ │ │ ├── icon_notification_dark.png
│ │ │ └── icon_notification_dark@2x.png
│ │ ├── Images.xcassets
│ │ │ └── AppIcon.appiconset
│ │ │ │ ├── icon16.png
│ │ │ │ ├── icon32.png
│ │ │ │ ├── icon64.png
│ │ │ │ ├── icon1024.png
│ │ │ │ ├── icon128.png
│ │ │ │ ├── icon256.png
│ │ │ │ ├── icon32_2.png
│ │ │ │ ├── icon512.png
│ │ │ │ ├── icon256_2.png
│ │ │ │ ├── icon512_2.png
│ │ │ │ └── Contents.json
│ │ └── Info.plist
│ ├── Other Sources
│ │ ├── BridgeHeader.h
│ │ ├── GithubUpdate.swift
│ │ └── AppDelegate.swift
│ ├── Components
│ │ └── CustomButton.swift
│ ├── Views
│ │ ├── ContentViewController.xib
│ │ └── MainMenu.xib
│ ├── Models
│ │ └── Contributions.swift
│ └── Controllers
│ │ └── ContentViewController.swift
├── GithubPulse.xcodeproj
│ ├── project.xcworkspace
│ │ └── contents.xcworkspacedata
│ └── project.pbxproj
├── Podfile
├── Makefile
└── Podfile.lock
├── chrome_extension
├── front
├── images
│ ├── blue
│ │ ├── icon19.png
│ │ └── icon38.png
│ ├── red
│ │ ├── icon19.png
│ │ └── icon38.png
│ └── icons
│ │ ├── icon128.png
│ │ ├── icon16.png
│ │ ├── icon32.png
│ │ └── icon48.png
├── manifest.json
└── js
│ ├── update.js
│ └── utils.js
├── .jshintrc
├── resources
├── .gitignore
├── icon.png
├── Tile440x280.png
├── github_pulse.ai
├── screenshot1.png
├── screenshot2.png
└── screenshot3.png
├── .gitignore
├── dist
├── GithubPulse.crx
└── GithubPulse.zip
├── front
├── images
│ ├── icon.png
│ ├── sync.svg
│ └── gear.svg
├── octicons
│ ├── octicons.woff
│ └── octicons.css
├── index.html
├── Makefile
└── webpack.config.js
├── javascript
├── styles
│ ├── GithubPulse.styl
│ ├── ActivityGraph.styl
│ ├── Stats.styl
│ ├── Login.styl
│ ├── Following.styl
│ ├── main.styl
│ ├── ProfileInfo.styl
│ ├── Config.styl
│ ├── Profile.styl
│ └── UserLine.styl
├── fonts
│ ├── open_sans
│ │ ├── OpenSans-Bold.ttf
│ │ └── OpenSans-Regular.ttf
│ └── montserrat
│ │ ├── Montserrat-Bold.ttf
│ │ ├── Montserrat-Regular.ttf
│ │ └── OFL.txt
├── main.jsx
├── components
│ ├── GithubPulse.react.jsx
│ ├── Routes.react.jsx
│ ├── Stats.react.jsx
│ ├── UserLine.react.jsx
│ ├── ProfileInfo.react.jsx
│ ├── ActivityGraph.react.jsx
│ ├── Login.react.jsx
│ ├── Config.react.jsx
│ ├── Following.react.jsx
│ └── Profile.react.jsx
├── github-api.js
└── utils.js
├── .yabsrc
├── Makefile
├── package.json
├── LICENSE
└── README.md
/widget/.gitignore:
--------------------------------------------------------------------------------
1 | Pods/
2 |
--------------------------------------------------------------------------------
/chrome_extension/front:
--------------------------------------------------------------------------------
1 | ../front
--------------------------------------------------------------------------------
/.jshintrc:
--------------------------------------------------------------------------------
1 | {
2 | "esnext": true
3 | }
4 |
--------------------------------------------------------------------------------
/resources/.gitignore:
--------------------------------------------------------------------------------
1 | GithubPulse.pem
2 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | npm-debug.log
3 | public
4 | xcuserdata
5 | *.xcworkspace
6 |
--------------------------------------------------------------------------------
/resources/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tadeuzagallo/GithubPulse/HEAD/resources/icon.png
--------------------------------------------------------------------------------
/dist/GithubPulse.crx:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tadeuzagallo/GithubPulse/HEAD/dist/GithubPulse.crx
--------------------------------------------------------------------------------
/dist/GithubPulse.zip:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tadeuzagallo/GithubPulse/HEAD/dist/GithubPulse.zip
--------------------------------------------------------------------------------
/front/images/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tadeuzagallo/GithubPulse/HEAD/front/images/icon.png
--------------------------------------------------------------------------------
/resources/Tile440x280.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tadeuzagallo/GithubPulse/HEAD/resources/Tile440x280.png
--------------------------------------------------------------------------------
/resources/github_pulse.ai:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tadeuzagallo/GithubPulse/HEAD/resources/github_pulse.ai
--------------------------------------------------------------------------------
/resources/screenshot1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tadeuzagallo/GithubPulse/HEAD/resources/screenshot1.png
--------------------------------------------------------------------------------
/resources/screenshot2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tadeuzagallo/GithubPulse/HEAD/resources/screenshot2.png
--------------------------------------------------------------------------------
/resources/screenshot3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tadeuzagallo/GithubPulse/HEAD/resources/screenshot3.png
--------------------------------------------------------------------------------
/front/octicons/octicons.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tadeuzagallo/GithubPulse/HEAD/front/octicons/octicons.woff
--------------------------------------------------------------------------------
/javascript/styles/GithubPulse.styl:
--------------------------------------------------------------------------------
1 | .github-pulse {
2 | position: relative;
3 |
4 | width: 400px;
5 | height: 384px;
6 | }
7 |
--------------------------------------------------------------------------------
/chrome_extension/images/blue/icon19.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tadeuzagallo/GithubPulse/HEAD/chrome_extension/images/blue/icon19.png
--------------------------------------------------------------------------------
/chrome_extension/images/blue/icon38.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tadeuzagallo/GithubPulse/HEAD/chrome_extension/images/blue/icon38.png
--------------------------------------------------------------------------------
/chrome_extension/images/red/icon19.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tadeuzagallo/GithubPulse/HEAD/chrome_extension/images/red/icon19.png
--------------------------------------------------------------------------------
/chrome_extension/images/red/icon38.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tadeuzagallo/GithubPulse/HEAD/chrome_extension/images/red/icon38.png
--------------------------------------------------------------------------------
/chrome_extension/images/icons/icon128.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tadeuzagallo/GithubPulse/HEAD/chrome_extension/images/icons/icon128.png
--------------------------------------------------------------------------------
/chrome_extension/images/icons/icon16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tadeuzagallo/GithubPulse/HEAD/chrome_extension/images/icons/icon16.png
--------------------------------------------------------------------------------
/chrome_extension/images/icons/icon32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tadeuzagallo/GithubPulse/HEAD/chrome_extension/images/icons/icon32.png
--------------------------------------------------------------------------------
/chrome_extension/images/icons/icon48.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tadeuzagallo/GithubPulse/HEAD/chrome_extension/images/icons/icon48.png
--------------------------------------------------------------------------------
/javascript/fonts/open_sans/OpenSans-Bold.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tadeuzagallo/GithubPulse/HEAD/javascript/fonts/open_sans/OpenSans-Bold.ttf
--------------------------------------------------------------------------------
/widget/GithubPulse/Resources/Images/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tadeuzagallo/GithubPulse/HEAD/widget/GithubPulse/Resources/Images/icon.png
--------------------------------------------------------------------------------
/javascript/fonts/montserrat/Montserrat-Bold.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tadeuzagallo/GithubPulse/HEAD/javascript/fonts/montserrat/Montserrat-Bold.ttf
--------------------------------------------------------------------------------
/javascript/fonts/open_sans/OpenSans-Regular.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tadeuzagallo/GithubPulse/HEAD/javascript/fonts/open_sans/OpenSans-Regular.ttf
--------------------------------------------------------------------------------
/widget/GithubPulse/Resources/Images/icon@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tadeuzagallo/GithubPulse/HEAD/widget/GithubPulse/Resources/Images/icon@2x.png
--------------------------------------------------------------------------------
/widget/GithubPulse/Resources/Images/icon_dark.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tadeuzagallo/GithubPulse/HEAD/widget/GithubPulse/Resources/Images/icon_dark.png
--------------------------------------------------------------------------------
/javascript/fonts/montserrat/Montserrat-Regular.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tadeuzagallo/GithubPulse/HEAD/javascript/fonts/montserrat/Montserrat-Regular.ttf
--------------------------------------------------------------------------------
/widget/GithubPulse/Resources/Images/icon_dark@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tadeuzagallo/GithubPulse/HEAD/widget/GithubPulse/Resources/Images/icon_dark@2x.png
--------------------------------------------------------------------------------
/widget/GithubPulse/Resources/Images/icon_notification.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tadeuzagallo/GithubPulse/HEAD/widget/GithubPulse/Resources/Images/icon_notification.png
--------------------------------------------------------------------------------
/javascript/main.jsx:
--------------------------------------------------------------------------------
1 | require('./styles/main');
2 | require('./components/Routes.react');
3 |
4 | document.body.addEventListener('contextmenu', e => e.preventDefault(), false);
5 |
--------------------------------------------------------------------------------
/widget/GithubPulse/Resources/Images/icon_notification@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tadeuzagallo/GithubPulse/HEAD/widget/GithubPulse/Resources/Images/icon_notification@2x.png
--------------------------------------------------------------------------------
/widget/GithubPulse/Resources/Images/icon_notification_dark.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tadeuzagallo/GithubPulse/HEAD/widget/GithubPulse/Resources/Images/icon_notification_dark.png
--------------------------------------------------------------------------------
/widget/GithubPulse/Resources/Images/icon_notification_dark@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tadeuzagallo/GithubPulse/HEAD/widget/GithubPulse/Resources/Images/icon_notification_dark@2x.png
--------------------------------------------------------------------------------
/widget/GithubPulse/Resources/Images.xcassets/AppIcon.appiconset/icon16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tadeuzagallo/GithubPulse/HEAD/widget/GithubPulse/Resources/Images.xcassets/AppIcon.appiconset/icon16.png
--------------------------------------------------------------------------------
/widget/GithubPulse/Resources/Images.xcassets/AppIcon.appiconset/icon32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tadeuzagallo/GithubPulse/HEAD/widget/GithubPulse/Resources/Images.xcassets/AppIcon.appiconset/icon32.png
--------------------------------------------------------------------------------
/widget/GithubPulse/Resources/Images.xcassets/AppIcon.appiconset/icon64.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tadeuzagallo/GithubPulse/HEAD/widget/GithubPulse/Resources/Images.xcassets/AppIcon.appiconset/icon64.png
--------------------------------------------------------------------------------
/widget/GithubPulse/Resources/Images.xcassets/AppIcon.appiconset/icon1024.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tadeuzagallo/GithubPulse/HEAD/widget/GithubPulse/Resources/Images.xcassets/AppIcon.appiconset/icon1024.png
--------------------------------------------------------------------------------
/widget/GithubPulse/Resources/Images.xcassets/AppIcon.appiconset/icon128.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tadeuzagallo/GithubPulse/HEAD/widget/GithubPulse/Resources/Images.xcassets/AppIcon.appiconset/icon128.png
--------------------------------------------------------------------------------
/widget/GithubPulse/Resources/Images.xcassets/AppIcon.appiconset/icon256.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tadeuzagallo/GithubPulse/HEAD/widget/GithubPulse/Resources/Images.xcassets/AppIcon.appiconset/icon256.png
--------------------------------------------------------------------------------
/widget/GithubPulse/Resources/Images.xcassets/AppIcon.appiconset/icon32_2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tadeuzagallo/GithubPulse/HEAD/widget/GithubPulse/Resources/Images.xcassets/AppIcon.appiconset/icon32_2.png
--------------------------------------------------------------------------------
/widget/GithubPulse/Resources/Images.xcassets/AppIcon.appiconset/icon512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tadeuzagallo/GithubPulse/HEAD/widget/GithubPulse/Resources/Images.xcassets/AppIcon.appiconset/icon512.png
--------------------------------------------------------------------------------
/widget/GithubPulse/Resources/Images.xcassets/AppIcon.appiconset/icon256_2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tadeuzagallo/GithubPulse/HEAD/widget/GithubPulse/Resources/Images.xcassets/AppIcon.appiconset/icon256_2.png
--------------------------------------------------------------------------------
/widget/GithubPulse/Resources/Images.xcassets/AppIcon.appiconset/icon512_2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tadeuzagallo/GithubPulse/HEAD/widget/GithubPulse/Resources/Images.xcassets/AppIcon.appiconset/icon512_2.png
--------------------------------------------------------------------------------
/.yabsrc:
--------------------------------------------------------------------------------
1 | {
2 | "package.json": "version",
3 | "chrome_extension/manifest.json": "version",
4 | "widget/GithubPulse/Resources/Info.plist": "CFBundleShortVersionString",
5 | "widget/GithubPulse/Resources/Info.plist": "CFBundleVersion"
6 | }
7 |
--------------------------------------------------------------------------------
/javascript/styles/ActivityGraph.styl:
--------------------------------------------------------------------------------
1 | .activity-graph {
2 | position: relative;
3 | }
4 |
5 | .activity-graph__canvas {
6 | display: block;
7 | position: absolute;
8 |
9 | top: 18px;
10 | width: 390px;
11 | height: 145px;
12 | }
13 |
--------------------------------------------------------------------------------
/widget/GithubPulse.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/front/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | all: osx chrome
2 |
3 | osx:
4 | $(MAKE) -C ./front -B osx
5 | $(MAKE) -C ./widget -B release
6 |
7 | chrome:
8 | $(MAKE) -C ./front -B chrome
9 | crx pack ./chrome_extension -o dist/GithubPulse.crx -p ./resources/GithubPulse.pem
10 |
11 | .PHONY: osx chrome all
12 |
--------------------------------------------------------------------------------
/widget/Podfile:
--------------------------------------------------------------------------------
1 | # Uncomment this line to define a global platform for your project
2 | # platform :ios, '6.0'
3 |
4 | target 'GithubPulse' do
5 |
6 | pod 'IYLoginItem'
7 | pod 'INPopoverController', git: 'https://github.com/indragiek/INPopoverController.git'
8 | pod 'SSZipArchive'
9 | pod 'EDSemver'
10 |
11 | end
12 |
13 |
--------------------------------------------------------------------------------
/javascript/components/GithubPulse.react.jsx:
--------------------------------------------------------------------------------
1 | var React = require('react');
2 |
3 | require('../styles/GithubPulse');
4 |
5 | var GithubPulse = React.createClass({
6 | render() {
7 | return (
8 |
9 | { this.props.children }
10 |
11 | );
12 | }
13 | });
14 |
15 | module.exports = GithubPulse;
16 |
--------------------------------------------------------------------------------
/widget/GithubPulse/Other Sources/BridgeHeader.h:
--------------------------------------------------------------------------------
1 | //
2 | // BridgeHeader.h
3 | // GithubPulse
4 | //
5 | // Created by Tadeu Zagallo on 12/31/14.
6 | // Copyright (c) 2014 Tadeu Zagallo. All rights reserved.
7 | //
8 |
9 | #import
10 | #import
11 | #import
12 | #import
--------------------------------------------------------------------------------
/front/Makefile:
--------------------------------------------------------------------------------
1 | WEBPACK := node ../node_modules/webpack/bin/webpack.js
2 | DEV_SERVER := node ../node_modules/webpack-dev-server/bin/webpack-dev-server.js
3 |
4 | chrome:
5 | TARGET=chrome $(WEBPACK)
6 |
7 | osx:
8 | TARGET=osx $(WEBPACK)
9 |
10 | chrome.watch:
11 | TARGET=chrome $(WEBPACK) --watch
12 |
13 | osx.watch:
14 | TARGET=osx $(DEV_SERVER)
15 |
16 | .PHONY: chrome osx chrome.watch osx.watch
17 |
--------------------------------------------------------------------------------
/widget/Makefile:
--------------------------------------------------------------------------------
1 | release: archive export zip clean
2 | mv GithubPulse.zip ../dist
3 |
4 | clean:
5 | rm -rf GithubPulse.app GithubPulse.xcarchive
6 |
7 | zip:
8 | zip -r GithubPulse.zip GithubPulse.app
9 |
10 | export:
11 | xcodebuild -exportArchive -archivePath GithubPulse.xcarchive -exportPath GithubPulse -exportFormat app
12 |
13 | archive:
14 | xcodebuild archive -workspace GithubPulse.xcworkspace -scheme GithubPulse -archivePath GithubPulse
15 |
--------------------------------------------------------------------------------
/widget/GithubPulse/Components/CustomButton.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CustomButton.swift
3 | // GithubPulse
4 | //
5 | // Created by Tadeu Zagallo on 12/30/14.
6 | // Copyright (c) 2014 Tadeu Zagallo. All rights reserved.
7 | //
8 |
9 | import Cocoa
10 |
11 | class CustomButton : NSButton {
12 | var rightAction:((NSEvent) -> Void)?
13 |
14 | override init(frame frameRect: NSRect) {
15 | super.init(frame: frameRect)
16 | }
17 |
18 | required init?(coder: NSCoder) {
19 | fatalError("init(coder:) has not been implemented")
20 | }
21 |
22 | override func rightMouseDown(theEvent: NSEvent) {
23 | if self.rightAction != nil {
24 | self.rightAction?(theEvent)
25 | }
26 | }
27 | }
--------------------------------------------------------------------------------
/front/webpack.config.js:
--------------------------------------------------------------------------------
1 | var utils = {
2 | osx: '../javascript/utils.js',
3 | chrome: '../chrome_extension/js/utils.js'
4 | };
5 |
6 | module.exports = {
7 | entry: [utils[process.env.TARGET || 'osx'], '../javascript/main.jsx'],
8 | devtool: 'source-map',
9 | output: {
10 | publicPath: 'public/',
11 | path: __dirname + '/public',
12 | filename: 'bundle.js'
13 | },
14 | module: {
15 | loaders: [
16 | {test: /\.jsx$/, loader: 'babel-loader'},
17 | {test: /\.styl$/, loader: 'style-loader!css-loader!stylus-loader'},
18 | {test: /\.json$/, loader: 'json-loader'},
19 | {test: /\.ttf$/, loader: 'file-loader' }
20 | ]
21 | },
22 | resolve: {
23 | extensions: ['', '.js', '.jsx', '.styl' ]
24 | }
25 | };
26 |
--------------------------------------------------------------------------------
/javascript/styles/Stats.styl:
--------------------------------------------------------------------------------
1 | .stats {
2 | position: absolute;
3 | bottom: 0;
4 | width: 100%;
5 | height: 70px;
6 |
7 | border-top: 1px solid #e9e9e9;
8 | font-family: montserrat;
9 | }
10 |
11 | .stat {
12 | float: left;
13 |
14 | width: 25%;
15 | padding-bottom: 10px;
16 |
17 | border-left: 1px solid #e9e9e9;
18 | text-align: center;
19 |
20 | &:first-child {
21 | border-left: none;
22 | }
23 | }
24 |
25 | .stat__count {
26 | margin: 15px 0 5px 0;
27 |
28 | color: #4183c4;
29 | font-size: 18pt;
30 | line-height: 18pt;
31 | }
32 |
33 | .stat__name {
34 | color: #a4a4a4;
35 | font-size: 8pt;
36 | }
37 |
38 | .notification {
39 | position: relative;
40 | top: -10px;
41 | color: #c44141;
42 | font-size: 11pt;
43 | }
44 |
--------------------------------------------------------------------------------
/javascript/styles/Login.styl:
--------------------------------------------------------------------------------
1 | .login {
2 | position: relative;
3 |
4 | top: 24px;
5 | width: 400px;
6 | height: 360px;
7 | padding-top: 20px;
8 |
9 | background: white;
10 | color: #808080;
11 | font-family: 'Open Sans';
12 | text-align: center;
13 |
14 | h1 {
15 | color: #000;
16 | }
17 | }
18 |
19 | .login__blue {
20 | color: #4183c4;
21 | }
22 |
23 | .login__input {
24 | width: 70%;
25 | padding: 10px;
26 | margin-top: 10px;
27 |
28 | border: 1px solid #c3c3c3;
29 | border-radius: 5px;
30 | color: #606060;
31 | font-size: 1.1em;
32 | text-align: center;
33 |
34 | &:focus {
35 | outline: none;
36 | }
37 | }
38 |
39 | .login__zen {
40 | position: absolute;
41 |
42 | bottom: 20px;
43 | width: 100%;
44 |
45 | font-size: .8em;
46 | }
47 |
--------------------------------------------------------------------------------
/javascript/components/Routes.react.jsx:
--------------------------------------------------------------------------------
1 | var React = require('react');
2 | var Router = require('react-router');
3 | var ReactDOM = require('react-dom');
4 |
5 | var GithubPulse = require('./GithubPulse.react');
6 | var Login = require('./Login.react');
7 | var Profile = require('./Profile.react');
8 | var Following = require('./Following.react');
9 |
10 | var Route = Router.Route;
11 | var IndexRoute = Router.IndexRoute;
12 |
13 | var routes = (
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 | );
22 |
23 | ReactDOM.render(routes, document.getElementById('github-pulse'));
24 |
--------------------------------------------------------------------------------
/javascript/github-api.js:
--------------------------------------------------------------------------------
1 | var GithubApi = {};
2 | GithubApi.host = 'https://api.github.com';
3 |
4 | var request = function (method, path, callback) {
5 | var request = new XMLHttpRequest();
6 | request.onload = function () {
7 | var result = request.responseText;
8 | if (~request.getResponseHeader('content-type').indexOf('json')) {
9 | result = JSON.parse(result);
10 | }
11 | callback(null, result);
12 | };
13 | request.onerror = function (err) {
14 | callback(err, null);
15 | };
16 | request.open(method, GithubApi.host + '/' + path, true);
17 | request.send();
18 | };
19 |
20 | GithubApi.get = function () {
21 | var args = [].slice.call(arguments);
22 | var callback = args.pop();
23 | var path = args.join('/');
24 | request('GET', path, callback);
25 | };
26 |
27 | module.exports = GithubApi;
28 |
--------------------------------------------------------------------------------
/chrome_extension/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "manifest_version": 2,
3 | "name": "GithubPulse",
4 | "description": "Chrome Extension to help you remember to contribute every day on Github",
5 | "version": "0.3.10",
6 | "background": {
7 | "scripts": [
8 | "js/update.js"
9 | ]
10 | },
11 | "permissions": [
12 | "management",
13 | "notifications",
14 | "storage",
15 | "alarms",
16 | "webRequest",
17 | "webRequestBlocking",
18 | "https://*/"
19 | ],
20 | "icons": {
21 | "16": "images/icons/icon16.png",
22 | "32": "images/icons/icon32.png",
23 | "48": "images/icons/icon48.png",
24 | "128": "images/icons/icon128.png"
25 | },
26 | "browser_action": {
27 | "default_icon": "images/blue/icon38.png",
28 | "default_popup": "front/index.html"
29 | },
30 | "content_security_policy": "script-src 'self' 'unsafe-eval'; object-src 'self'"
31 | }
--------------------------------------------------------------------------------
/widget/Podfile.lock:
--------------------------------------------------------------------------------
1 | PODS:
2 | - EDSemver (0.3.0)
3 | - INPopoverController (0.0.1)
4 | - IYLoginItem (0.1)
5 | - SSZipArchive (0.3.2)
6 |
7 | DEPENDENCIES:
8 | - EDSemver
9 | - INPopoverController (from `https://github.com/indragiek/INPopoverController.git`)
10 | - IYLoginItem
11 | - SSZipArchive
12 |
13 | EXTERNAL SOURCES:
14 | INPopoverController:
15 | :git: https://github.com/indragiek/INPopoverController.git
16 |
17 | CHECKOUT OPTIONS:
18 | INPopoverController:
19 | :commit: 4c7c674b25e3859bd3627df35538fbb470c938ab
20 | :git: https://github.com/indragiek/INPopoverController.git
21 |
22 | SPEC CHECKSUMS:
23 | EDSemver: 124a90685873628ec30f42504b461a1a61757568
24 | INPopoverController: e4c315804b6d227c84a6dd64adf6732600b659fc
25 | IYLoginItem: e57a86b39d3b9ab0e2fc033adedae01efafd9adf
26 | SSZipArchive: 11801759495fa4efb7512ef652eb0042d68305d7
27 |
28 | COCOAPODS: 0.39.0
29 |
--------------------------------------------------------------------------------
/javascript/styles/Following.styl:
--------------------------------------------------------------------------------
1 | $blue = #4183c4;
2 |
3 | .following {
4 | position: relative;
5 | width: 400px;
6 | height: 351px;
7 | background: white;
8 | overflow-y: scroll;
9 | }
10 |
11 | .following-title {
12 | height: 34px;
13 | padding-top: 4px;
14 | border-bottom: 1px solid #c3c3c3;
15 | background: linear-gradient(#fff, #e0e0e0);
16 | color: #696969;
17 | font-family: montserrat;
18 | font-size: 10.5pt;
19 | line-height: 26px;
20 | text-align: center;
21 | }
22 |
23 | .following-profile {
24 | position: absolute;
25 | z-index: 100;
26 | top: 4px;
27 | left: 8px;
28 | color: #4183c4;
29 | font-family: montserrat;
30 | font-size: 10.5pt;
31 | line-height: 26px;
32 | cursor: pointer;
33 | }
34 |
35 | .following-container__progress-bar {
36 | position: absolute;
37 | z-index: 101;
38 | top: 32px;
39 | left: 0;
40 | right: 0;
41 | height: 2px;
42 | }
43 |
44 | .following-container__progress-bar-complete {
45 | background-color: $blue;
46 | height: 100%;
47 | }
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "GithubPulse",
3 | "version": "0.3.10",
4 | "description": "Application to help you remember to contribute every day on Github",
5 | "main": "",
6 | "scripts": {
7 | "test": "echo \"Error: no test specified\" && exit 1"
8 | },
9 | "repository": {
10 | "type": "git",
11 | "url": "https://github.com/tadeuzagallo/GithubPulse"
12 | },
13 | "author": "Tadeu Zagallo ",
14 | "license": "MIT",
15 | "devDependencies": {
16 | "babel-core": "^5.8.29",
17 | "babel-loader": "^5.3.2",
18 | "chart.js": "^1.0.2",
19 | "classnames": "^2.2.0",
20 | "crx": "^3.0.2",
21 | "css-loader": "^0.21.0",
22 | "file-loader": "^0.8.1",
23 | "json-loader": "^0.5.3",
24 | "object-assign": "^4.0.1",
25 | "react": "^0.14.0",
26 | "react-addons-css-transition-group": "^0.14.0",
27 | "react-dom": "^0.14.0",
28 | "react-router": "^1.0.0-rc3",
29 | "style-loader": "^0.13.0",
30 | "stylus": "^0.52.4",
31 | "stylus-loader": "^1.4.0",
32 | "webpack": "^1.12.2",
33 | "webpack-dev-server": "^1.12.1",
34 | "yabs": "^0.0.3"
35 | }
36 | }
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2014-2015 Tadeu Zagallo
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
13 | all 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
21 | THE SOFTWARE.
22 |
--------------------------------------------------------------------------------
/javascript/styles/main.styl:
--------------------------------------------------------------------------------
1 | body {
2 | margin: 0;
3 | overflow: hidden;
4 | }
5 |
6 | @font-face {
7 | font-family: 'Montserrat';
8 | font-weight: normal;
9 | src: url('../fonts/montserrat/Montserrat-Regular.ttf') format('truetype');
10 | }
11 |
12 | @font-face {
13 | font-family: 'Montserrat';
14 | font-weight: bold;
15 | src: url('../fonts/montserrat/Montserrat-Bold.ttf') format('truetype');
16 | }
17 |
18 | @font-face {
19 | font-family: 'Open Sans';
20 | src: url('../fonts/open_sans/OpenSans-Regular.ttf') format('truetype');
21 | }
22 |
23 | @font-face {
24 | font-family: 'Open Sans';
25 | font-weight: bold;
26 | src: url('../fonts/open_sans/OpenSans-Bold.ttf') format('truetype');
27 | }
28 |
29 | .chrome {
30 | .profile-title {
31 | padding-top: 4px;
32 | }
33 |
34 | .config__gear {
35 | top: 8px;
36 | }
37 |
38 | .update {
39 | margin-top: 7px;
40 | }
41 |
42 | .config__panel {
43 | padding-top: 30px;
44 | }
45 |
46 | .config__item:nth-of-type(1)
47 | .config__separator:nth-of-type(2) {
48 | display: none;
49 | }
50 |
51 | .panel-enter,
52 | .panel-leave.panel-leave-active {
53 | -webkit-transform: scale(0.11, 0.25);
54 | }
55 | }
56 |
57 | * {
58 | box-sizing: border-box;
59 | }
60 |
--------------------------------------------------------------------------------
/javascript/styles/ProfileInfo.styl:
--------------------------------------------------------------------------------
1 | $width = 400px;
2 |
3 | .profile-info {
4 | position: relative;
5 |
6 | height: 90px;
7 |
8 | border-bottom: 1px solid #e9e9e9;
9 | }
10 |
11 | .profile-info__picture {
12 | position: absolute;
13 |
14 | left: 20px;
15 | top: 20px;
16 | width: 50px;
17 |
18 | border-radius: 50%;
19 | cursor: pointer;
20 | }
21 |
22 | .profile-info__data {
23 | position: absolute;
24 |
25 | left: 90px;
26 | top: 0px;
27 | width: 290px;
28 | height: 90px;
29 |
30 | line-height: 86px;
31 | }
32 |
33 | .profile-info__data__content {
34 | display: inline-block;
35 | vertical-align: middle;
36 | }
37 |
38 | .profile-info__name {
39 | margin-bottom: 4px;
40 |
41 | color: #4183c4;
42 | font-family: 'Open Sans';
43 | font-size: 16pt;
44 | font-weight: bold;
45 | line-height: 16pt;
46 | cursor: pointer;
47 | }
48 |
49 | .profile-info__username {
50 | margin: 0;
51 |
52 | color: #696969;
53 | font-family: 'Open Sans';
54 | font-weight: normal;
55 | font-size: 12pt;
56 | line-height: 12pt;
57 | cursor: pointer;
58 | }
59 |
60 | .profile-info__following {
61 | margin: 4px 0 -14px 0;
62 | color: #4183c4;
63 | font-family: 'Open Sans';
64 | font-weight: normal;
65 | font-size: 10pt;
66 | line-height: 10pt;
67 | cursor: pointer;
68 | }
69 |
70 | .profile-info__logout {
71 | margin-left: 6px;
72 |
73 | color: #444;
74 | cursor: pointer;
75 | }
76 |
--------------------------------------------------------------------------------
/javascript/styles/Config.styl:
--------------------------------------------------------------------------------
1 | .config__gear {
2 | position: absolute;
3 | z-index: 100;
4 |
5 | top: 9px;
6 | right: 8px;
7 |
8 | -webkit-transform: rotate(0deg);
9 | transition: -webkit-transform .5s;
10 | }
11 |
12 | .config__gear.true {
13 | -webkit-transform: rotate(-135deg);
14 | }
15 |
16 | .config__overlay {
17 | position: absolute;
18 | z-index: 98;
19 |
20 | top: 0;
21 | bottom: 0;
22 | left: 0;
23 | right: 0;
24 | }
25 |
26 | .config__panel {
27 | position: absolute;
28 | z-index: 99;
29 |
30 | top: 3px;
31 | right: 4px;
32 | padding: 20px 15px 15px 15px;
33 |
34 | background: white;
35 | border: 1px solid #c3c3c3;
36 | border-radius: 3px;
37 | color: #808080;
38 | font-family: 'Open Sans';
39 | font-size: 10pt;
40 | }
41 |
42 | .config__startup,
43 | .config__quit {
44 | margin-right: 8px;
45 | }
46 |
47 | .config__item * {
48 | cursor: pointer;
49 | }
50 |
51 | .config__separator {
52 | width: 100%;
53 | margin: 8px 0;
54 |
55 | border-bottom: 1px solid #aaa;
56 | }
57 |
58 |
59 | .panel-enter.panel-enter-active,
60 | .panel-leave {
61 | -webkit-transform: scale(1, 1);
62 | transition: -webkit-transform .2s;
63 |
64 | .content {
65 | opacity: 1;
66 | transition: opacity .15s;
67 | }
68 | }
69 |
70 | .panel-enter,
71 | .panel-leave {
72 | -webkit-transform-origin: top right;
73 | }
74 |
75 | .panel-enter,
76 | .panel-leave.panel-leave-active {
77 | -webkit-transform: scale(0.115, 0.21);
78 |
79 | .content {
80 | opacity: 0;
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/javascript/components/Stats.react.jsx:
--------------------------------------------------------------------------------
1 | var React = require('react');
2 |
3 | require('../styles/Stats');
4 |
5 | var p = (l, n) => n === 1 ? l : l + 's';
6 |
7 | var Stats = React.createClass({
8 | propTypes: {
9 | repos: React.PropTypes.number.isRequired,
10 | followers: React.PropTypes.number.isRequired,
11 | streak: React.PropTypes.number.isRequired,
12 | today: React.PropTypes.number.isRequired
13 | },
14 | render() {
15 | return (
16 |
17 |
18 |
{ this.props.repos }
19 |
{ p('repo', this.props.repos) }
20 |
21 |
22 |
23 |
{ this.props.followers }
24 |
{ p('follower', this.props.followers) }
25 |
26 |
27 |
28 |
{ this.props.streak }{ this.props.streak > 15 ? : '' }
29 |
{ p('day', this.props.streak) } streak
30 |
31 |
32 |
33 |
{ this.props.today }{ !this.props.today ? : '' }
34 |
{ p('commit', this.props.today) } today
35 |
36 |
37 | );
38 | }
39 | });
40 |
41 | module.exports = Stats;
42 |
--------------------------------------------------------------------------------
/widget/GithubPulse/Resources/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | en
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIconFile
10 |
11 | CFBundleIdentifier
12 | $(PRODUCT_BUNDLE_IDENTIFIER)
13 | CFBundleInfoDictionaryVersion
14 | 6.0
15 | CFBundleName
16 | $(PRODUCT_NAME)
17 | CFBundlePackageType
18 | APPL
19 | CFBundleShortVersionString
20 | 0.3.1
21 | CFBundleSignature
22 | ????
23 | CFBundleVersion
24 | 0.3.10
25 | GithubRepo
26 | tadeuzagallo/GithubPulse
27 | LSMinimumSystemVersion
28 | $(MACOSX_DEPLOYMENT_TARGET)
29 | LSUIElement
30 |
31 | NSAppTransportSecurity
32 |
33 | NSAllowsArbitraryLoads
34 |
35 |
36 | NSHumanReadableCopyright
37 | Copyright © 2014 Tadeu Zagallo. All rights reserved.
38 | NSMainNibFile
39 | MainMenu
40 | NSPrincipalClass
41 | NSApplication
42 |
43 |
--------------------------------------------------------------------------------
/widget/GithubPulse/Resources/Images.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "size" : "16x16",
5 | "idiom" : "mac",
6 | "filename" : "icon16.png",
7 | "scale" : "1x"
8 | },
9 | {
10 | "size" : "16x16",
11 | "idiom" : "mac",
12 | "filename" : "icon32.png",
13 | "scale" : "2x"
14 | },
15 | {
16 | "size" : "32x32",
17 | "idiom" : "mac",
18 | "filename" : "icon32_2.png",
19 | "scale" : "1x"
20 | },
21 | {
22 | "size" : "32x32",
23 | "idiom" : "mac",
24 | "filename" : "icon64.png",
25 | "scale" : "2x"
26 | },
27 | {
28 | "size" : "128x128",
29 | "idiom" : "mac",
30 | "filename" : "icon128.png",
31 | "scale" : "1x"
32 | },
33 | {
34 | "size" : "128x128",
35 | "idiom" : "mac",
36 | "filename" : "icon256.png",
37 | "scale" : "2x"
38 | },
39 | {
40 | "size" : "256x256",
41 | "idiom" : "mac",
42 | "filename" : "icon256_2.png",
43 | "scale" : "1x"
44 | },
45 | {
46 | "size" : "256x256",
47 | "idiom" : "mac",
48 | "filename" : "icon512.png",
49 | "scale" : "2x"
50 | },
51 | {
52 | "size" : "512x512",
53 | "idiom" : "mac",
54 | "filename" : "icon512_2.png",
55 | "scale" : "1x"
56 | },
57 | {
58 | "size" : "512x512",
59 | "idiom" : "mac",
60 | "filename" : "icon1024.png",
61 | "scale" : "2x"
62 | }
63 | ],
64 | "info" : {
65 | "version" : 1,
66 | "author" : "xcode"
67 | }
68 | }
--------------------------------------------------------------------------------
/front/images/sync.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/javascript/components/UserLine.react.jsx:
--------------------------------------------------------------------------------
1 | var React = require('react');
2 |
3 | var cx = require('classnames');
4 |
5 | require('../styles/UserLine');
6 |
7 | var UserLine = React.createClass({
8 | render() {
9 | var hasToday = this.props.user.today > 0;
10 | var hasStreak = this.props.user.streak > 0;
11 |
12 | var todayClass = cx({'user-line__today': true, 'user-line__today-zero': !hasToday});
13 | var streakClass = cx({'user-line__bar-streak': true, 'user-line__bar-streak-zero': !hasStreak});
14 | var usernameClass = cx({'user-line__username': true, 'user-line__username-zero': !hasStreak});
15 |
16 | var widthPercent = Math.round((this.props.user.streak / this.props.maxStreak) * 55) + 45;
17 | if (isNaN(widthPercent)) { widthPercent = 45; }
18 |
19 | return (
20 |
21 |
22 |
23 | { 'streak ' + (typeof this.props.user.streak !== 'undefined' ? this.props.user.streak : '-') }
24 |
25 |
26 |
@{ this.props.user.login }
27 |
28 | { typeof this.props.user.today !== 'undefined' ? this.props.user.today : '-' }
29 |
30 |
today
31 |

32 |
33 | );
34 | },
35 | _profile() {
36 | Utils.openURL('https://github.com/' + this.props.user.login);
37 | }
38 | });
39 |
40 | module.exports = UserLine;
41 |
--------------------------------------------------------------------------------
/javascript/styles/Profile.styl:
--------------------------------------------------------------------------------
1 | .profile {
2 | position: relative;
3 |
4 | width: 400px;
5 | height: 316px;
6 |
7 | background: white;
8 | }
9 |
10 | .update {
11 | height: 35px;
12 | padding: 7px 6px;
13 |
14 | background: linear-gradient(#fff, #e0e0e0);
15 | border-top: 1px solid #c3c3c3;
16 | color: #8d8d8d;
17 | font-family: 'Open Sans';
18 | font-size: 10pt;
19 | }
20 |
21 | .update__sync,
22 | .update__last {
23 | float: left;
24 | }
25 |
26 | .update__sync {
27 | width: 14px;
28 | height: 14px;
29 | margin: 3px;
30 | }
31 |
32 | .update__last {
33 | margin: 1px 0 0 3px;
34 | }
35 |
36 | @-webkit-keyframes rotate {
37 | from {
38 | -webkit-transform: rotate(0deg);
39 | }
40 |
41 | to {
42 | -webkit-transform: rotate(360deg);
43 | }
44 | }
45 |
46 | .rotate {
47 | -webkit-animation: rotate 2s infinite linear;
48 | }
49 |
50 | .profile-title {
51 | height: 34px;
52 | padding-top: 4px;
53 |
54 | border-bottom: 1px solid #c3c3c3;
55 | background: linear-gradient(#fff, #e0e0e0);
56 | color: #696969;
57 | font-family: montserrat;
58 | font-size: 10.5pt;
59 | line-height: 26px;
60 | text-align: center;
61 | }
62 |
63 | .version {
64 | float: right;
65 | margin: 1px 8px 0 0;
66 |
67 | color: #8d8d8d;
68 | font-weight: bold;
69 |
70 | .octicon-alert {
71 | cursor: pointer;
72 | margin-left: 3px;
73 | font-size: 9pt;
74 | }
75 |
76 | &.true:hover::before {
77 | content: 'Update available, click to restart and apply';
78 | position: absolute;
79 | display: block;
80 | width: 150px;
81 | height: 40px;
82 | bottom: 30px;
83 | padding: 10px;
84 | border-radius: 5px;
85 | background: black;
86 | font: open sans;
87 | color: white;
88 | right: 3px;
89 | }
90 | }
91 |
--------------------------------------------------------------------------------
/javascript/components/ProfileInfo.react.jsx:
--------------------------------------------------------------------------------
1 | var React = require('react');
2 |
3 | require('../styles/ProfileInfo');
4 |
5 | var ProfileInfo = React.createClass({
6 | propTypes: {
7 | picture: React.PropTypes.string.isRequired,
8 | name: React.PropTypes.string,
9 | username: React.PropTypes.string.isRequired
10 | },
11 | render() {
12 | return (
13 |
14 |
15 |

16 |
17 |
18 |
19 |
20 | {this.props.name}
21 |
22 |
25 |
26 |
27 | @{ this.props.username }
28 |
29 |
30 | Show Users I'm Following
31 |
32 |
33 |
34 |
35 |
36 | );
37 | },
38 | _logout() {
39 | console.log(this.props);
40 | Utils.clear('username');
41 | this.props.history.pushState(null, '/');
42 | },
43 | _gotoUsername() {
44 | Utils.openURL('https://github.com/' + this.props.username);
45 | },
46 | _showFollowing() {
47 | this.props.history.pushState(null, `/compare/following/${this.props.username}`);
48 | },
49 | });
50 |
51 | module.exports = ProfileInfo;
52 |
--------------------------------------------------------------------------------
/widget/GithubPulse/Views/ContentViewController.xib:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
--------------------------------------------------------------------------------
/javascript/components/ActivityGraph.react.jsx:
--------------------------------------------------------------------------------
1 | var React = require('react');
2 | var Chart = require('chart.js');
3 |
4 |
5 | require('../styles/ActivityGraph');
6 |
7 | var ActivityGraph = React.createClass({
8 | propTypes: {
9 | commits: React.PropTypes.arrayOf(React.PropTypes.number).isRequired
10 | },
11 | render() {
12 | return (
13 |
14 |
15 |
16 | );
17 | },
18 | shouldComponentUpdate(nextProps) {
19 | var pc = this.props.commits || [];
20 | var nc = nextProps.commits || [];
21 |
22 | if (pc.length != nc.length) {
23 | return true;
24 | }
25 |
26 | for (var i = 0, l = pc.length; i < l; i++) {
27 | if (pc[i] !== nc[i]) {
28 | return true;
29 | }
30 | }
31 |
32 | return false;
33 | },
34 | componentDidUpdate() {
35 | if (this.chart) {
36 | this.chart.destroy();
37 | }
38 |
39 | var canvas = this.refs.canvas;
40 | var ctx = canvas.getContext('2d');
41 | var commits = this.props.commits;
42 | var labels = [];
43 | var max = commits.reduce(function (a, b) {
44 | labels.push('');
45 | return Math.max(a, b);
46 | }, 0);
47 | var step = Math.ceil(max / 2);
48 |
49 | this.chart = new Chart(ctx).Line({
50 | labels: labels,
51 | datasets: [{
52 | label: 'Commits',
53 | fillColor: 'rgba(65,131,196,0.2)',
54 | strokeColor: 'rgba(65,131,196,1)',
55 | pointColor: 'rgba(65,131,196,1)',
56 | pointStrokeColor: '#fff',
57 | pointHighlightFill: '#fff',
58 | pointHighlightStroke: 'rgba(151,187,205,1)',
59 | data: commits
60 | }]
61 | }, {
62 | scaleShowVerticalLines: false,
63 | scaleOverride: true,
64 | scaleSteps: 2,
65 | scaleStepWidth: step,
66 | scaleStartValue: 0,
67 | scaleLabel: " <%=value%>",
68 | pointHitDetectionRadius: 3,
69 | tooltipXPadding: 10,
70 | });
71 | }
72 | });
73 |
74 | module.exports = ActivityGraph;
75 |
--------------------------------------------------------------------------------
/javascript/styles/UserLine.styl:
--------------------------------------------------------------------------------
1 | $blue = #4183c4;
2 | $red = #c44141;
3 | $zero = #EDEDED;
4 |
5 | .user-line {
6 | position: relative;
7 | margin: 2px 2px 2px 2px;
8 | height: 40px;
9 | border: 2px solid $zero;
10 | border-radius: 20px;
11 | cursor: pointer;
12 | }
13 |
14 | .user-line__picture {
15 | position: absolute;
16 | top: 0px;
17 | left: 36px;
18 | width: 36px;
19 | height: 36px;
20 | border-top-right-radius: 18px;
21 | border-bottom-right-radius: 18px;
22 | }
23 |
24 | .user-line__today {
25 | position: absolute;
26 | top: 0px;
27 | left: 0px;
28 | margin: 10px 0 0 2px;
29 | width: 36px;
30 | height: 36px;
31 | font-family: montserrat;
32 | text-align: center;
33 | color: $blue;
34 | font-size: 12pt;
35 | line-height: 12pt;
36 | border-top-left-radius: 18px;
37 | border-bottom-left-radius: 18px;
38 | }
39 |
40 | .user-line__today-label {
41 | position: absolute;
42 | top: 0px;
43 | left: 0px;
44 | margin: 24px 0 0 2px;
45 | width: 36px;
46 | height: 36px;
47 | font-family: montserrat;
48 | text-align: center;
49 | color: #a4a4a4;
50 | font-size: 7pt;
51 | line-height: 7pt;
52 | }
53 |
54 | .user-line__today-zero {
55 | color: $red;
56 | }
57 |
58 | .user-line__username {
59 | position: absolute;
60 | top: 0px;
61 | left: 72px;
62 | color: white;
63 | font-family: 'Open Sans';
64 | font-size: 10pt;
65 | font-weight: bold;
66 | line-height: 10pt;
67 | }
68 |
69 | .user-line__username-zero {
70 | color: $blue;
71 | }
72 |
73 | .user-line__bar {
74 | position: absolute;
75 | top: 0px;
76 | bottom: 0px;
77 | left: 36px;
78 | right: 0px;
79 | }
80 |
81 | .user-line__bar-streak {
82 | background-color: $blue;
83 | height: 100%;
84 | border-top-right-radius: 18px;
85 | border-bottom-right-radius: 18px;
86 |
87 | span {
88 | margin: 16px 16px 0 0;
89 | font-family: montserrat;
90 | color: white;
91 | font-size: 12pt;
92 | line-height: 12pt;
93 | float: right;
94 | }
95 | }
96 |
97 | .user-line__bar-streak-zero {
98 | background-color: $zero;
99 | }
100 |
--------------------------------------------------------------------------------
/javascript/components/Login.react.jsx:
--------------------------------------------------------------------------------
1 | var React = require('react');
2 | var assign = require('object-assign');
3 |
4 | var GithubApi = require('../github-api');
5 | var Config = require('./Config.react');
6 |
7 | require('../styles/Login');
8 |
9 | var Login = React.createClass({
10 | getInitialState() {
11 | return {
12 | username: '',
13 | zen: ''
14 | };
15 | },
16 | render() {
17 | return (
18 |
19 |
20 |
21 |
22 |

23 |
24 |
25 |
Github Pulse
26 |
27 |
28 |
29 |
30 |
31 |
{ this.state.zen }
32 |
api.github.com/zen
33 |
34 |
35 |
36 | );
37 | },
38 | componentDidMount() {
39 | this._mounted = true;
40 | this._fetchZen();
41 | this._fetchUserName();
42 | },
43 | componentWillUnmount() {
44 | this._mounted = false;
45 | },
46 | _fetchZen() {
47 | Utils.fetch('zen', 60 * 60 * 1000, (zen) => {
48 | if (zen) {
49 | this._mounted && this.setState({ zen: zen });
50 | } else {
51 | GithubApi.get('zen', (err, result) => {
52 | this._mounted && this.setState({ zen: result });
53 | Utils.save('zen', result);
54 | });
55 | }
56 | });
57 | },
58 | _fetchUserName() {
59 | Utils.fetch('username', (username) => {
60 | if (username) {
61 | this.props.history.pushState(null, `/${username}`);
62 | }
63 | });
64 | },
65 | _onChange(event) {
66 | this.setState({ username: event.target.value.trim() });
67 | },
68 | _onKeyDown(event) {
69 | if (event.keyCode === 13) {
70 | let username = this.state.username;
71 | Utils.save('username', username);
72 | this.props.history.pushState(null, `/${username}`);
73 | }
74 | }
75 | });
76 |
77 | module.exports = Login;
78 |
--------------------------------------------------------------------------------
/chrome_extension/js/update.js:
--------------------------------------------------------------------------------
1 | // jshint expr: true
2 |
3 | var notify = function () {
4 | chrome.notifications.create('gotta_commit', {
5 | type: 'basic',
6 | iconUrl: '../images/icons/icon128.png',
7 | title: 'You haven\'t committed yet today...',
8 | message: 'Rush to keep your streak going!'
9 | }, function (id) { });
10 | };
11 |
12 | var checkNotificationInterval = function () {
13 | var d = new Date();
14 |
15 | if (d.getHours() < 18) {
16 | return;
17 | }
18 |
19 | chrome.storage.sync.get('last_notification', function (r) {
20 | d.setHours(1);
21 | d.setMinutes(0);
22 |
23 | if (r.last_notification &&
24 | r.last_notification > d.getTime()) {
25 | return;
26 | }
27 |
28 | notify();
29 | chrome.storage.sync.set({
30 | last_notification: Date.now()
31 | });
32 | });
33 | };
34 |
35 | var checkShouldNotify = function () {
36 | chrome.storage.sync.get('dont_notify', function (r) {
37 | var dont_notify = r.dont_notify && JSON.parse(r.dont_notify).data;
38 |
39 | if (dont_notify) {
40 | return;
41 | }
42 |
43 | checkNotificationInterval();
44 | });
45 | };
46 |
47 | var update = function (username) {
48 | var request = new XMLHttpRequest();
49 | request.onload = function () {
50 | var parser = new DOMParser();
51 | var svg = parser.parseFromString(this.responseText, "image/svg+xml");
52 | var commits = svg.querySelectorAll('rect');
53 | var today = parseInt(commits[commits.length - 1].getAttribute('data-count'), 10);
54 | var color = today === 0 ? 'red' : 'blue';
55 | var imgs = {};
56 | [19, 38].forEach(function (size) {
57 | imgs[size] = '../images/' + color + '/icon' + size + '.png';
58 | });
59 |
60 | chrome.browserAction.setIcon({
61 | path: imgs
62 | });
63 |
64 | if (today === 0) {
65 | checkShouldNotify();
66 | }
67 | };
68 |
69 | request.open('GET', 'https://github.com/users/' + username + '/contributions', true);
70 | request.send(null);
71 | };
72 |
73 | function onAlarm(alarm) {
74 | chrome.storage.sync.get('username', function (r) {
75 | var item = r.username && JSON.parse(r.username);
76 | item && item.data && update(item.data);
77 | });
78 | }
79 | chrome.alarms.onAlarm.addListener(onAlarm);
80 |
81 |
82 | chrome.alarms.clearAll(function () {
83 | chrome.alarms.create('check_contributions', {
84 | delayInMinutes: 15,
85 | periodInMinutes: 15
86 | });
87 | });
88 |
89 | onAlarm();
90 |
--------------------------------------------------------------------------------
/front/images/gear.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/javascript/components/Config.react.jsx:
--------------------------------------------------------------------------------
1 | var React = require('react');
2 | var CSSTransitionGroup = require('react-addons-css-transition-group');
3 |
4 | require('../styles/Config');
5 |
6 | var Config = React.createClass({
7 | getInitialState() {
8 | return {
9 | open: false,
10 | active: false,
11 | notify: false
12 | };
13 | },
14 | render() {
15 | var panel = '';
16 | var overlay = '';
17 |
18 | if (this.state.open) {
19 | overlay = ;
20 |
21 | panel = (
22 |
23 |
24 |
25 |
31 |
32 |
33 |
34 |
35 |
41 |
42 |
43 |
44 |
45 |
46 | Quit Github Pulse
47 |
48 |
49 |
50 | );
51 | }
52 |
53 | return (
54 |
55 |

56 |
57 | { overlay }
58 |
59 | { panel }
60 |
61 |
62 | );
63 | },
64 | componentDidMount() {
65 | Utils.raw('check_login()', (active) => {
66 | this.setState({ active: active });
67 | });
68 |
69 | Utils.fetch('dont_notify', (dontNotify) => {
70 | this.setState({ notify: !dontNotify });
71 | });
72 | },
73 | _toggleActive() {
74 | this.setState({ active: !this.state.active });
75 | Utils.raw('toggle_login()');
76 | },
77 | _toggleNotifications() {
78 | var notify = !this.state.notify;
79 | this.setState({ notify: notify });
80 | Utils.save('dont_notify', !notify);
81 | },
82 | _togglePanel() {
83 | this.setState({ open: !this.state.open });
84 | },
85 | _quit() {
86 | Utils.quit();
87 | }
88 | });
89 |
90 | module.exports = Config;
91 |
--------------------------------------------------------------------------------
/javascript/utils.js:
--------------------------------------------------------------------------------
1 | window.Utils = (function () {
2 | var Utils = {};
3 |
4 | var queue = [];
5 | var running = false;
6 | var pop = function () {
7 | if (queue.length) {
8 | setTimeout(function() {
9 | window.location = encodeURI(queue.shift());
10 | pop();
11 | }, 0);
12 | } else {
13 | running = false;
14 | }
15 | };
16 |
17 | Utils.redirect = function (url) {
18 | queue.push(url);
19 | if (!running) {
20 | running = true;
21 | pop();
22 | }
23 | };
24 |
25 | Utils.log = function () {
26 | Utils.redirect('log:' + [].join.call(arguments, ' '));
27 | };
28 |
29 | Utils.save = function (key, value) {
30 | if (Array.isArray(key)) {
31 | key = key.join('/');
32 | }
33 |
34 | value = JSON.stringify({
35 | time: Date.now(),
36 | data: value
37 | });
38 |
39 | Utils.redirect('osx:set(' + key + '%%' + value + ')');
40 | };
41 |
42 | var callbacks = {};
43 | window.get = function (key, value, expiration) {
44 | key = decodeURI(key);
45 | value = decodeURI(value);
46 |
47 | var item = value && JSON.parse(value);
48 | var time = null;
49 |
50 | if (expiration !== -1 && item && Date.now() - item.time > expiration) {
51 | item = null;
52 | } else if (item) {
53 | time = item.time;
54 | item = item.data;
55 | }
56 |
57 | var callback = callbacks[key];
58 | callbacks[key] = null;
59 | callback(item, time);
60 | };
61 |
62 | Utils.fetch = function (key, expiration, callback) {
63 | if (Array.isArray(key)) {
64 | key = key.join('/');
65 | }
66 |
67 | if (typeof expiration === 'function') {
68 | callback = expiration;
69 | expiration = undefined;
70 | }
71 |
72 |
73 | callbacks[key] = callback;
74 | Utils.redirect('osx:get(' + key + '%%' + (expiration || -1) + ')');
75 | };
76 |
77 | var rawCallbacks = {};
78 | window.raw = function (fnName) {
79 | var args = [].slice.call(arguments, 1);
80 | var callback = rawCallbacks[fnName];
81 | rawCallbacks[fnName] = null;
82 | callback.apply(null, args);
83 | };
84 |
85 | Utils.raw = function (expression, callback) {
86 | if (callback) {
87 | fnName = expression.split('(').shift();
88 | rawCallbacks[fnName] = callback;
89 | }
90 |
91 | Utils.redirect('osx:' + expression);
92 | };
93 |
94 | Utils.clear = function (key) {
95 | if (Array.isArray(key)) {
96 | key = key.join('/');
97 | }
98 |
99 | Utils.redirect('osx:remove(' + key + ')');
100 | };
101 |
102 | Utils.openURL = function (url) {
103 | Utils.redirect('osx:open_url(' + url + ')');
104 | };
105 |
106 | var contributionsCallbacks = {};
107 | window.contributions = function (username) {
108 | var fn = contributionsCallbacks[username];
109 | if (fn) {
110 | contributionsCallbacks[username] = null;
111 | fn.apply(null, [].slice.call(arguments, 1));
112 | }
113 | };
114 |
115 | Utils.contributions = function (username, callback, skipUpdateIcon) {
116 | contributionsCallbacks[username] = callback;
117 | Utils.redirect('osx:contributions(' + username + (skipUpdateIcon?'%%false':'') + ')');
118 | };
119 |
120 | Utils.quit = function () {
121 | this.raw('quit()');
122 | };
123 |
124 | return Utils;
125 | })();
126 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | #  Github Pulse [](https://github.com/tadeuzagallo/GithubPulse/releases)
2 |
3 | Github Pulse is an app to help you keep your streaks, making a commit every day.
4 |
5 | It shows a graph of your last month commits and how long is your current streak. Its icon also turns red if you haven't committed today yet, and by the end of the afternoon it'll remind you once, in case you still haven't committed.
6 |
7 | 
8 | 
9 | 
10 |
11 | ## Installation
12 |
13 | Right now it is available for OSX and Google Chrome
14 |
15 | For OSX you can just download it [here](https://github.com/tadeuzagallo/GithubPulse/raw/master/dist/GithubPulse.zip), unzip and copy to your applications folder.
16 |
17 | The Google Chrome Extension is available on the [Chrome Web Store](https://chrome.google.com/webstore/detail/githubpulse/ppkickbgijieebbgfipephpafiiebapg) and if, for any reason, you want to download directly, it's also available [here](https://github.com/tadeuzagallo/GithubPulse/raw/master/dist/GithubPulse.crx)
18 |
19 | ## What's being used
20 |
21 | The OSX application has a small shell of native code, written in Swift and renders a React app (using JSX) on a WebView and uses `window.location` redirects to communicate with the native app.
22 |
23 | The same react code is used for the Chrome Extension, just replacing the storage helpers and background workers.
24 |
25 | ### 3rd party libraries (Front)
26 |
27 | * React
28 | * Webpack
29 | * Stylus
30 | * Octicons
31 | * react-router
32 | * Chart.js
33 |
34 | ### Pods
35 |
36 | * IYLoginItem
37 | * INPopoverController
38 |
39 | ## Building it locally
40 |
41 | As mentioned above I'm using Swift and React, so you'll need to have XCode 6+, Node and CocoaPods already installed in order to build.
42 |
43 | ### To get started:
44 |
45 | * Clone the repo: `$ git clone https://github.com/tadeuzagallo/GithubPulse.git`
46 | * Install the npm dependencies: `$ npm install`
47 | * Install the pods: `$ cd widget && pod install`. If you haven't used pod before, you will need to run `pod setup` first.
48 |
49 | ### Debug building
50 |
51 | The debug build points the WebView to `webpack-dev-server`'s default address: `localhost:8080`, so in order to get it running
52 |
53 | * Start the webpack dev server: `$ cd front && make osx.watch`
54 | * Just build the app through XCode interface
55 |
56 | ### Release building
57 |
58 | __NOTICE__: If you just cloned the repo and wants `make` it, you'll have to open the file `widget/GithubPulse.xcworkspace` on XCode at least once for the build to succeed.
59 |
60 | Just run `$ make osx` and the file `GithubPulse.zip` will be placed inside the `dist` directory.
61 |
62 | ### Chrome Extension
63 |
64 | There actually is a target on the root `Makefile` called `chrome` but it won't work, because the private key is, well... private.
65 |
66 | But you can still build the front end and load the unpacked extensions. Here is how:
67 |
68 | * Run `$ cd front && make chrome` ( or `make chrome.watch` if you want to watch for changes)
69 | * Then go to `chrome:extension`
70 | * click on `Load unpacked extension...` (make sure `Developer mode` is checked on the top right of the page)
71 | * Select `GithubPulse/chrome_extension`
72 | * Done!
73 |
74 | ## Credits and Motivation
75 |
76 | I really believe committing every day on an open source project is the best practice a developer can keep, so I made this project to show my love to Github and make I sure I never miss a commit! :D
77 |
--------------------------------------------------------------------------------
/widget/GithubPulse/Models/Contributions.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Contributions.swift
3 | // GithubPulse
4 | //
5 | // Created by Tadeu Zagallo on 12/30/14.
6 | // Copyright (c) 2014 Tadeu Zagallo. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | typealias FetchCallback = ((Bool, [Int], Int, Int) -> Void)!
12 |
13 | class Contributions {
14 | var username = ""
15 | var year = [Int]()
16 | var commits = [Int]()
17 | var today = 0
18 | var streak = 0
19 | var succeeded = false
20 | var state = 0
21 | var callback: FetchCallback = nil
22 | let streakRegex = try? NSRegularExpression(pattern: "Current streak\\s*]*?>(\\d+)\\s*days", options: NSRegularExpressionOptions.CaseInsensitive)
23 | let dayRegex = try? NSRegularExpression(pattern: " Void) {
30 | let url = NSURL(string: URLString)
31 | let request = NSMutableURLRequest(URL: url!, cachePolicy: NSURLRequestCachePolicy.ReloadIgnoringLocalAndRemoteCacheData, timeoutInterval: 30)
32 | request.HTTPShouldHandleCookies = false
33 |
34 | NSURLConnection.sendAsynchronousRequest(request, queue: NSOperationQueue.mainQueue()) { (response, data, error) in
35 | if error != nil || data == nil {
36 | self.invokeCallback(false)
37 | return
38 | }
39 |
40 | completionBlock(String(data: data!, encoding: NSUTF8StringEncoding)!)
41 | }
42 | }
43 |
44 | func fetch(username: String, completionBlock: FetchCallback) {
45 | self.username = username
46 | self.year = []
47 | self.callback = completionBlock
48 |
49 | baseFetch("https://github.com/\(username)") { (body) in
50 | self.parse(body)
51 | }
52 | }
53 |
54 | func fetchContributions() {
55 | baseFetch("https://github.com/users/\(username)/contributions") { (body) in
56 | let range = self.getRange(body)
57 |
58 | if range.location == NSNotFound {
59 | self.invokeCallback(false)
60 | return
61 | }
62 |
63 | self.parseStreak(body, range: range)
64 | self.invokeCallback(true)
65 | }
66 | }
67 |
68 | private func invokeCallback(success: Bool) {
69 | if callback != nil {
70 | callback(success, commits, streak, today)
71 | }
72 |
73 | self.callback = nil
74 | }
75 |
76 | private func getRange(input: String) -> NSRange {
77 | let start = (input as NSString).rangeOfString("