├── public ├── dist │ └── empty.txt ├── favicon.ico ├── less │ ├── _mini-list.less │ ├── _footer.less │ ├── components.less │ ├── _readme.less │ ├── _base.less │ ├── _search-input.less │ ├── _front.less │ ├── _variables.less │ ├── _content.less │ ├── _component-info.less │ ├── _search.less │ ├── _header.less │ ├── _loader.less │ └── _codemirror.less ├── js │ ├── analytics.js │ └── lodash.min.js ├── img │ └── react.svg ├── favicon.svg └── css │ ├── codemirror.css │ ├── pure-min.css │ └── components.css ├── data └── purpose.md ├── app ├── actions │ ├── search.js │ ├── routing.js │ └── api.js ├── util │ ├── escape-regex.js │ ├── deep.js │ ├── number-formatter.js │ └── github-account.js ├── routes.js ├── components │ ├── container.jsx │ ├── footer.jsx │ ├── no-result.jsx │ ├── react-logo.jsx │ ├── loader.jsx │ ├── component-item.jsx │ ├── component-list.jsx │ ├── home-link.jsx │ ├── layout.jsx │ ├── results-item.jsx │ ├── component-link.jsx │ ├── results-table.jsx │ ├── markdown-readme.jsx │ └── search-input.jsx ├── stores │ ├── component-store.shared.js │ ├── components-store.browser.js │ └── components-store.js ├── config.js ├── api │ ├── github-api.js │ ├── components-api.js │ └── npm-api.js ├── controllers │ └── components.js ├── search │ └── filter.js ├── react │ └── renderer.js ├── router.js ├── pages │ ├── search.jsx │ ├── front.jsx │ └── component-info.jsx ├── root.js ├── database │ └── index.js └── server │ └── routes.js ├── .jshintrc ├── CONTRIBUTING.md ├── cron ├── fetch-components.js └── fetch-stars.js ├── .gitignore ├── templates └── default.html ├── LICENSE ├── webpack.config.js ├── package.json ├── README.md ├── index.js └── gulpfile.js /public/dist/empty.txt: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vaffel/react-components/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /public/less/_mini-list.less: -------------------------------------------------------------------------------- 1 | .component-lists { 2 | ul { 3 | padding: 0 0 0 10px; 4 | } 5 | } -------------------------------------------------------------------------------- /data/purpose.md: -------------------------------------------------------------------------------- 1 | # What's this folder for? 2 | 3 | Basically, it caches data. 4 | Github stars, NPM download counts, things like this. 5 | -------------------------------------------------------------------------------- /app/actions/search.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var Reflux = require('reflux'); 4 | 5 | module.exports = Reflux.createActions([ 6 | 'search' 7 | ]); -------------------------------------------------------------------------------- /app/actions/routing.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var Reflux = require('reflux'); 4 | 5 | module.exports = Reflux.createActions([ 6 | 'locationChange' 7 | ]); -------------------------------------------------------------------------------- /app/util/escape-regex.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = function escapeRegExp(str) { 4 | return ('' + str).replace(/([-.*+?^${}()|[\]\/\\])/g, '\\$1'); 5 | }; -------------------------------------------------------------------------------- /app/actions/api.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var Reflux = require('reflux'); 4 | 5 | module.exports = Reflux.createActions([ 6 | 'fetchComponents', 7 | 'fetchFailed', 8 | 'componentsFetched', 9 | 10 | 'fetchComponentInfo', 11 | 'fetchComponentFailed', 12 | 'componentFetched' 13 | ]); -------------------------------------------------------------------------------- /public/less/_footer.less: -------------------------------------------------------------------------------- 1 | @import '_variables'; 2 | 3 | footer { 4 | max-width: @page-max-width; 5 | margin: 0 auto; 6 | padding: 10px; 7 | border-top: 1px solid @react-light-text; 8 | 9 | opacity: 0.5; 10 | text-align: center; 11 | 12 | &:hover { 13 | opacity: 1; 14 | } 15 | } -------------------------------------------------------------------------------- /public/less/components.less: -------------------------------------------------------------------------------- 1 | @import '_base'; 2 | @import '_header'; 3 | @import '_content'; 4 | @import '_search-input'; 5 | @import '_loader'; 6 | @import '_front'; 7 | @import '_mini-list'; 8 | @import '_search'; 9 | @import '_codemirror'; 10 | @import '_component-info'; 11 | @import '_readme'; 12 | @import '_footer'; 13 | -------------------------------------------------------------------------------- /app/routes.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var Front = require('app/pages/front.jsx'); 4 | var Search = require('app/pages/search.jsx'); 5 | var ComponentInfo = require('app/pages/component-info.jsx'); 6 | 7 | module.exports = { 8 | '/': Front, 9 | '/search/:query': Search, 10 | '/component/:componentName': ComponentInfo 11 | }; -------------------------------------------------------------------------------- /app/util/deep.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | function deep(obj, prop) { 4 | var segs = prop.split('.'); 5 | while (segs.length) { 6 | obj = obj[segs.shift()]; 7 | } 8 | return obj; 9 | } 10 | 11 | module.exports = function(prop) { 12 | return function(item) { 13 | return deep(item, prop); 14 | }; 15 | }; -------------------------------------------------------------------------------- /public/less/_readme.less: -------------------------------------------------------------------------------- 1 | .readme { 2 | 3 | line-height: 1.4em; 4 | 5 | h1:first-child { 6 | margin-top: 5px; 7 | font-size: 2.5em; 8 | } 9 | 10 | h1, h2, h3, h4, h5 { 11 | text-align: left; 12 | font-family: @page-sans-serif-fonts; 13 | line-height: 1em; 14 | } 15 | 16 | img { 17 | max-width: 100%; 18 | } 19 | 20 | } -------------------------------------------------------------------------------- /public/js/analytics.js: -------------------------------------------------------------------------------- 1 | (function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){ 2 | (i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o), 3 | m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m) 4 | })(window,document,'script','//www.google-analytics.com/analytics.js','ga'); 5 | ga('create', 'UA-54404455-1', 'auto'); 6 | ga('send', 'pageview'); -------------------------------------------------------------------------------- /app/components/container.jsx: -------------------------------------------------------------------------------- 1 | /** @jsx React.DOM */ 2 | 'use strict'; 3 | 4 | var React = require('react'); 5 | 6 | module.exports = React.createClass({ 7 | displayName: 'Container', 8 | 9 | /* jshint trailing:false, quotmark:false, newcap:false */ 10 | render: function() { 11 | return this.transferPropsTo( 12 |
{this.props.component.description}
17 | 18 || Name | 50 |Author | 51 |Stars | 52 |Updated | 53 |
|---|
npm install you run into the following error:
47 | ```
48 | ld: library not found for -lgcc_s.10.5
49 | make: *** [Release/hiredis.node] Error 1
50 | ```
51 | You probably have a problem with your version of Xcode. The easiest way to solve it, is to update your Xcode and open it.
52 |
53 | #### Using Macports
54 |
55 | If you use macports to install your dependencies, then the issue might be that you are not using the [default gcc compiler](http://apple.stackexchange.com/questions/129608/switch-back-to-clang-after-installing-gcc-through-macports-on-mavericks). To verify that, do:
56 | ```
57 | gcc --version
58 | ```
59 | If what you get is not the default clang compiler from Xcode, edit your ~/.bash_profile as follows:
60 | ```
61 | vim ~/.bash_profile
62 | ```
63 | And comment out the line:
64 | ```
65 | export PATH=/opt/local/bin:/opt/local/sbin:$PATH
66 | ```
67 |
--------------------------------------------------------------------------------
/app/stores/components-store.browser.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | var _ = require('lodash');
4 | var moment = require('moment');
5 | var Reflux = require('reflux');
6 | var sharedMethods = require('app/stores/component-store.shared');
7 | var ApiActions = require('app/actions/api');
8 |
9 | var ComponentStore = Reflux.createStore(_.merge({}, sharedMethods, {
10 | init: function() {
11 | this.components = {};
12 | this.componentSummaries = [];
13 |
14 | this.listenTo(ApiActions.componentsFetched, this.populate);
15 | this.listenTo(ApiActions.componentFetched, this.addComponentInfo);
16 | },
17 |
18 | get: function(name) {
19 | if (this.components[name]) {
20 | return this.components[name];
21 | }
22 |
23 | ApiActions.fetchComponentInfo(name);
24 | },
25 |
26 | getMostRecentlyCreated: function(limit) {
27 | return (
28 | _.sortBy(this.componentSummaries, 'created')
29 | .reverse()
30 | .slice(0, limit || 10)
31 | );
32 | },
33 |
34 | getMostRecentlyUpdated: function(limit) {
35 | var mostRecent = this.getMostRecentlyCreated();
36 | var lastUpdated = _.sortBy(this.componentSummaries, 'modified').reverse();
37 |
38 | return _.without.apply(null,
39 | [lastUpdated].concat(mostRecent)
40 | ).slice(0, limit || 10);
41 | },
42 |
43 | getMostStarred: function(limit) {
44 | return (
45 | _.sortBy(this.componentSummaries, 'stars')
46 | .reverse()
47 | .slice(0, limit || 10)
48 | );
49 | },
50 |
51 | populateFromDatabase: function() {
52 | ApiActions.fetchComponents();
53 | },
54 |
55 | populate: function(components) {
56 | components.map(this.addComponent);
57 | this.trigger('change');
58 | },
59 |
60 | parseComponent: function(component) {
61 | component.modified = moment.utc(component.modified);
62 | component.created = moment.utc(component.created);
63 |
64 | return component;
65 | },
66 |
67 | addComponent: function(component) {
68 | component = this.parseComponent(component);
69 |
70 | this.componentSummaries.push(component);
71 | },
72 |
73 | addComponentInfo: function(component) {
74 | this.components[component.name] = component;
75 | this.trigger('change');
76 | }
77 | }));
78 |
79 | module.exports = _.bindAll(ComponentStore);
--------------------------------------------------------------------------------
/app/components/markdown-readme.jsx:
--------------------------------------------------------------------------------
1 | /** @jsx React.DOM */
2 | 'use strict';
3 | var _ = require('lodash');
4 | var React = require('react');
5 | var marked = require('marked');
6 | var config = require('app/config');
7 | var escapeRegex = require('app/util/escape-regex');
8 | var getGithubAccount = require('app/util/github-account');
9 | var codeMirror = typeof window === 'undefined' ? function() {} : window.CodeMirror;
10 |
11 | var mirrorOptions = {
12 | lineNumbers: false,
13 | lineWrapping: true,
14 | smartIndent: false,
15 | matchBrackets: true,
16 | theme: 'solarized-light',
17 | readOnly: true
18 | };
19 |
20 | module.exports = React.createClass({
21 | displayName: 'MarkdownReadme',
22 |
23 | componentDidMount: function() {
24 | // Apply CodeMirror to multi-line code elements
25 | var codeEls = this.getDOMNode().querySelectorAll('pre > code'), preEl, lang;
26 | for (var i = 0; i < codeEls.length; i++) {
27 | preEl = codeEls[i].parentNode;
28 | lang = (codeEls[i].getAttribute('class') || '');
29 | lang = lang.replace(/.*?lang\-(.*)/, '$1').split(/\s+/)[0];
30 |
31 | codeMirror(function(elt) {
32 | preEl.parentNode.replaceChild(elt, preEl);
33 | }, _.merge({
34 | value: codeEls[i].innerText.trim(),
35 | mode: config['codemirror-modes'][lang] || 'javascript'
36 | }, mirrorOptions));
37 | }
38 | },
39 |
40 | fixRelativeUrls: function(html) {
41 | var branch = '/blob/' + this.props.component.branch;
42 | var githubUrl = this.getGithubUrl() + branch, matches;
43 | var matcher = /', 'g'),
54 | ''
55 | );
56 | }
57 |
58 | return html;
59 | },
60 |
61 | getGithubUrl: function() {
62 | var account = getGithubAccount(this.props.component);
63 | if (!account) {
64 | return false;
65 | }
66 |
67 | return 'https://github.com/' + account;
68 | },
69 |
70 | /* jshint quotmark:false, newcap:false */
71 | render: function() {
72 | var html = this.fixRelativeUrls(marked(this.props.component.readme));
73 | return (
74 |
78 | );
79 | }
80 | });
--------------------------------------------------------------------------------
/app/api/npm-api.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | var config = require('app/config'),
4 | request = require('request'),
5 | JSONStream = require('JSONStream'),
6 | eventStream = require('event-stream'),
7 | qs = require('querystring');
8 |
9 | function getError(err, res) {
10 | return err || (res.statusCode !== 200 ? 'HTTP ' + res.statusCode : null);
11 | }
12 |
13 | var registryUrl = 'https://registry.npmjs.org',
14 | viewsPath = '-/_view',
15 | keywordView = 'byKeyword',
16 | keyword = config['npm-keyword'],
17 | dlCountUrl = 'https://api.npmjs.org/downloads/point/last-week',
18 | isFetching = false;
19 |
20 | function parseModule(mod) {
21 | return {
22 | name: mod[1],
23 | description: mod[2]
24 | };
25 | }
26 |
27 | var NpmApi = {
28 |
29 | getModules: function(callback) {
30 | var url = [registryUrl, viewsPath, keywordView].join('/') + '?' + qs.stringify({
31 | startkey: '["' + keyword + '"]',
32 | endkey: '["' + keyword + '",{}]',
33 | group_level: 3
34 | });
35 |
36 | var onComplete = function(err, modules) {
37 | isFetching = false;
38 | callback(err, modules);
39 | };
40 |
41 | isFetching = true;
42 | request({ url: url })
43 | .on('error', function(err) { console.error('Error fetching modules: ', err); })
44 | .pipe(JSONStream.parse('rows.*.key'))
45 | .pipe(eventStream.mapSync(parseModule))
46 | .pipe(eventStream.map(NpmApi.getModuleInfo))
47 | .on('error', function(err) { console.error('Error fetching module info: ', err); })
48 | .pipe(eventStream.map(NpmApi.getModuleDownloadCount))
49 | .on('error', function(err) { console.error('Error fetching module download count: ', err); })
50 | .pipe(eventStream.writeArray(onComplete));
51 | },
52 |
53 | getModuleDownloadCount: function(module, callback) {
54 | var moduleName = module.name ? module.name : module,
55 | url = [dlCountUrl, encodeURIComponent(moduleName)].join('/');
56 |
57 | request({ url: url, json: true }, function(err, res, response) {
58 | module.downloads = (response || {}).downloads;
59 | callback(getError(err, res), module);
60 | });
61 | },
62 |
63 | getModuleInfo: function(module, callback) {
64 | var moduleName = module.name ? module.name : module,
65 | url = [registryUrl, moduleName].join('/');
66 |
67 | request({ url: url, json: true }, function(err, res, body) {
68 | err = getError(err, res);
69 | if (typeof body !== 'object') {
70 | err = 'Body was not an object';
71 | }
72 |
73 | callback(err, body);
74 | });
75 | }
76 | };
77 |
78 | module.exports = NpmApi;
79 |
--------------------------------------------------------------------------------
/app/root.js:
--------------------------------------------------------------------------------
1 | /** @jsx React.DOM */
2 | 'use strict';
3 |
4 | var _ = require('lodash');
5 | var React = require('react/addons');
6 | var Reflux = require('reflux');
7 | var router = require('app/router');
8 | var isBrowser = typeof window !== 'undefined';
9 |
10 | var RoutingActions = require('app/actions/routing');
11 | var ComponentStore = require('app/stores/components-store');
12 | var ComponentApi = require('app/api/components-api');
13 | var SearchIndex = require('app/search/filter');
14 |
15 | // We'll want to use react-router when server-side rendering is ready
16 | router.setRoutes(require('app/routes'));
17 |
18 | var App = React.createClass({
19 | displayName: 'App',
20 |
21 | mixins: [Reflux.ListenerMixin],
22 |
23 | propTypes: {
24 | path: React.PropTypes.string.isRequired
25 | },
26 |
27 | getInitialState: function() {
28 | return {
29 | path: false
30 | };
31 | },
32 |
33 | componentDidMount: function() {
34 | window.addEventListener('popstate', this.onLocationChanged, false);
35 |
36 | this.listenTo(RoutingActions.locationChange, this.onLocationChanged);
37 | },
38 |
39 | componentWillUnmount: function() {
40 | window.removeEventListener('popstate', this.onLocationChanged, false);
41 | },
42 |
43 | onLocationChanged: function() {
44 | this.setState({ path: window.location.pathname + window.location.search });
45 | },
46 |
47 | render: function() {
48 | var match = router.match(this.state.path || this.props.path);
49 |
50 | if (!match) {
51 | console.error('Could not match URL (' + this.props.path + ')');
52 | return null; // @todo render error-view?
53 | }
54 |
55 | return new match.page(_.merge({}, this.state, {
56 | query: match.query || {},
57 | route: match.route || {}
58 | }));
59 | }
60 | });
61 |
62 | // Have the API and search index listen for dispatcher events
63 | ComponentApi.listen();
64 | SearchIndex.listen();
65 |
66 | if (isBrowser) {
67 | // Allow React to leak into global namespace - enables devtools etc
68 | window.React = React;
69 |
70 | // Wait for components list to be ready
71 | ComponentStore.listen(function() {
72 | // Render the app once the components list is ready
73 | // (Normally, we'd just show a "loading"-state, but since
74 | // we're rendering on server side...)
75 | React.renderComponent(new App({
76 | path: window.location.pathname + window.location.search
77 | }), document.getElementById('root'));
78 | });
79 | }
80 |
81 | // Prime component store
82 | ComponentStore.populateFromDatabase();
83 |
84 | // Have component store fetch new components every once in a while
85 | setInterval(
86 | ComponentStore.populateFromDatabase.bind(ComponentStore),
87 | isBrowser ? 1000 * 60 * 15 : 1000 * 60 * 5 // 15 minutes in browser, 5 on server
88 | );
89 |
90 | module.exports = App;
--------------------------------------------------------------------------------
/app/database/index.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | var _ = require('lodash');
4 | var config = require('app/config');
5 | var redis = require('redis');
6 | var async = require('async');
7 | var client;
8 |
9 | function getDb() {
10 | if (client) {
11 | return client;
12 | }
13 |
14 | client = redis.createClient();
15 | client.select(config.redis.databaseNumber);
16 |
17 | client.on('error', function(err) {
18 | console.error('Redis error: ', err);
19 | });
20 |
21 | return client;
22 | }
23 |
24 | function getModuleNames(callback) {
25 | getDb().smembers('module:list', callback);
26 | }
27 |
28 | function getModule(name, callback) {
29 | getDb().get('module:info:' + name, function(err, data) {
30 | callback(err, err ? null : jsonParse(data));
31 | });
32 | }
33 |
34 | function getModuleWithStars(name, callback) {
35 | var stars = 0, module, error;
36 | var done = _.after(2, function() {
37 | module.starCount = stars;
38 | callback(error, module);
39 | });
40 |
41 | getModule(name, function(err, data) {
42 | module = data;
43 | error = err;
44 | done();
45 | });
46 |
47 | getModuleStars(name, function(err, starCount) {
48 | stars = starCount | 0;
49 | done();
50 | });
51 | }
52 |
53 | function getModules(callback) {
54 | getModuleNames(function(err, names) {
55 | if (err) {
56 | return callback(err);
57 | }
58 |
59 | async.map(names, getModuleWithStars, callback);
60 | });
61 | }
62 |
63 | function setModule(module, callback) {
64 | var client = getDb();
65 |
66 | var error, done = _.after(2, callback);
67 | var setComplete = function(err) {
68 | error = error || err;
69 | done(error);
70 | };
71 |
72 | client.set('module:info:' + module._id, JSON.stringify(module), setComplete);
73 | client.sadd('module:list', module._id, setComplete);
74 | }
75 |
76 | function getModuleStars(name, callback) {
77 | getDb().get('module:stars:' + name, callback);
78 | }
79 |
80 | function setModuleStars(name, stars, callback) {
81 | getDb().set('module:stars:' + name, stars, callback);
82 | }
83 |
84 | function unref() {
85 | if (client) {
86 | client.unref();
87 | }
88 | }
89 |
90 | function quit() {
91 | if (client) {
92 | client.quit();
93 | }
94 | }
95 |
96 | function jsonParse(json) {
97 | try {
98 | return JSON.parse(json);
99 | } catch (e) {
100 | return null;
101 | }
102 | }
103 |
104 | module.exports = {
105 | getModuleNames: getModuleNames,
106 | getModules: getModules,
107 | getModule: getModule,
108 | setModule: setModule,
109 |
110 | getModuleStars: getModuleStars,
111 | setModuleStars: setModuleStars,
112 |
113 | getModuleWithStars: getModuleWithStars,
114 |
115 | unref: unref,
116 | quit: quit
117 | };
--------------------------------------------------------------------------------
/app/components/search-input.jsx:
--------------------------------------------------------------------------------
1 | /** @jsx React.DOM */
2 | 'use strict';
3 |
4 | var _ = require('lodash');
5 | var React = require('react');
6 | var config = require('app/config');
7 | var router = require('app/router');
8 |
9 | module.exports = React.createClass({
10 | displayName: 'SearchInput',
11 |
12 | propTypes: {
13 | autoFocus: React.PropTypes.bool,
14 | placeholder: React.PropTypes.string,
15 | query: React.PropTypes.string
16 | },
17 |
18 | getDefaultProps: function() {
19 | return {
20 | autoFocus: true,
21 | placeholder: 'Component name, keyword or similar',
22 | query: ''
23 | };
24 | },
25 |
26 | getInitialState: function() {
27 | return {
28 | query: this.props.query
29 | };
30 | },
31 |
32 | componentDidMount: function() {
33 | // Use to bring up the "looking glass"-icon
34 | this.getDOMNode().setAttribute('results', 5);
35 |
36 | this.changeQueryDebounced = _.debounce(this.changeQuery, 250);
37 |
38 | // Focus the END of the input (if it has a value and autofocus is set to true)
39 | if (this.props.query && this.props.autoFocus) {
40 | this.moveCaretToEnd();
41 | }
42 | },
43 |
44 | getPageTitle: function(query) {
45 | return (query ? (query + ' - ') : '') + config['page-title'];
46 | },
47 |
48 | moveCaretToEnd: function() {
49 | var el = this.getDOMNode();
50 | if (typeof el.selectionStart === 'number') {
51 | el.selectionStart = el.selectionEnd = el.value.length;
52 | } else if (typeof el.createTextRange !== 'undefined') {
53 | el.focus();
54 | var range = el.createTextRange();
55 | range.collapse(false);
56 | range.select();
57 | }
58 | },
59 |
60 | onQueryChange: function(e) {
61 | this.changeQueryDebounced(e.target.value.replace(/^\s+$/g, ''));
62 | },
63 |
64 | changeQuery: function(query) {
65 | var state = { query: query },
66 | url = state.query ? '/search/' + encodeURIComponent(state.query) : '/',
67 | title = this.getPageTitle(state.query);
68 |
69 | if (this.state.query) {
70 | history.replaceState(state, title, url);
71 | } else {
72 | history.pushState(state, title, url);
73 | }
74 |
75 | router.locationChanged();
76 |
77 | window.document.title = title;
78 | this.setState(state);
79 | },
80 |
81 | /* jshint trailing:false, quotmark:false, newcap:false */
82 | render: function() {
83 | return (
84 |
92 | );
93 | }
94 | });
95 |
--------------------------------------------------------------------------------
/app/server/routes.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | var basePath = require('path').resolve(__dirname + '/../../');
4 | var _ = require('lodash');
5 | var config = require('app/config');
6 | var isDev = process.env.NODE_ENV === 'development';
7 | var render = require('app/react/renderer');
8 | var template = basePath + '/templates/default.html';
9 | var pkgInfo = require(basePath + '/package.json');
10 | var params = {
11 | 'package': pkgInfo,
12 | 'page': {
13 | description: pkgInfo.description,
14 | title: 'React Components'
15 | }
16 | };
17 |
18 | var controllers = {
19 | components: require('app/controllers/components')
20 | };
21 |
22 | var stores = {
23 | components: require('app/stores/components-store')
24 | };
25 |
26 | function getPageTitle(query) {
27 | if (!query) {
28 | return params.page.title;
29 | }
30 |
31 | return query + ' - ' + params.page.title;
32 | }
33 |
34 | function handleRequest(request, reply) {
35 | var reqParams = _.merge({}, params, {
36 | page: {
37 | title: getPageTitle(request.params.query || request.params.component)
38 | }
39 | });
40 |
41 | var liveReloadSrc = isDev ? ' localhost:35729' : '';
42 | var analyticsSrc = ' http://www.google-analytics.com https://www.google-analytics.com';
43 |
44 | reply(render(request, reqParams, template))
45 | .header('X-UA-Compatible', 'IE=edge,chrome=1')
46 | .header('Cache-Control', 'public, must-revalidate, max-age=150')
47 | .header('Content-Security-Policy', [
48 | 'script-src \'self\'' + liveReloadSrc + analyticsSrc,
49 | 'frame-src \'none\'',
50 | 'object-src \'none\''
51 | ].join(';'));
52 | }
53 |
54 | module.exports = function(server) {
55 | server.route({
56 | method: 'GET',
57 | path: '/',
58 | handler: handleRequest
59 | });
60 |
61 | server.route({
62 | method: 'GET',
63 | path: '/search/{query}',
64 | handler: handleRequest
65 | });
66 |
67 | server.route({
68 | method: 'GET',
69 | path: '/component/{component}',
70 | handler: handleRequest
71 | });
72 |
73 | server.route({
74 | method: 'GET',
75 | path: '/api/components',
76 | config: {
77 | handler: controllers.components.componentsList,
78 | cache: { expiresIn: config['poll-interval'], privacy: 'public' }
79 | }
80 | });
81 |
82 | server.route({
83 | method: 'GET',
84 | path: '/api/components/{component}',
85 | config: {
86 | handler: controllers.components.componentInfo
87 | }
88 | });
89 |
90 | server.route({
91 | method: 'GET',
92 | path: '/{param*}',
93 | config: {
94 | handler: {
95 | directory: {
96 | path: 'public',
97 | lookupCompressed: true
98 | }
99 | },
100 | cache: { expiresIn: 3600 * 1000, privacy: 'public' }
101 | }
102 | });
103 | };
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | require('node-jsx').install({ extension: '.jsx' });
4 |
5 | var isDev = process.env.NODE_ENV === 'development';
6 | var winston = require('winston');
7 | var Pushover = require('winston-pushover').Pushover;
8 |
9 | if (!isDev) {
10 | var pushOpts = {
11 | userKey: process.env.PUSHOVER_USER_KEY,
12 | token: process.env.PUSHOVER_TOKEN,
13 | handleExceptions: true
14 | };
15 |
16 | winston.add(Pushover, pushOpts);
17 | winston.handleExceptions(new Pushover(pushOpts));
18 | }
19 |
20 | var _ = require('lodash');
21 | var Hapi = require('hapi');
22 | var server = new Hapi.Server(process.env.REACT_COMPONENTS_PORT || 3000);
23 | var render = require('app/react/renderer');
24 | var pkgInfo = require('./package.json');
25 | var tpl = function(file) { return __dirname + '/templates/' + file + '.html'; };
26 | var params = {
27 | 'package': pkgInfo,
28 | 'page': {
29 | description: pkgInfo.description,
30 | title: 'React Components'
31 | },
32 | 'resources': {
33 | css: [
34 | '/css/pure-min.css',
35 | '/css/codemirror.css',
36 | '/css/components.css'
37 | ],
38 | js: [
39 | '/js/codemirror-compressed.js',
40 | '/js/analytics.js',
41 | '/dist/vendor.bundle.js',
42 | '/dist/bundle.js',
43 | ]
44 | }
45 | };
46 |
47 | var controllers = {
48 | components: require('app/controllers/components')
49 | };
50 |
51 | function getPageTitle(query) {
52 | if (!query) {
53 | return params.page.title;
54 | }
55 |
56 | return query + ' - ' + params.page.title;
57 | }
58 |
59 | function handleRequest(request, reply) {
60 | var reqParams = _.merge({}, params, {
61 | page: {
62 | title: getPageTitle(request.params.query || request.params.component)
63 | }
64 | });
65 |
66 | var liveReloadSrc = isDev ? ' localhost:35729' : '';
67 | var analyticsSrc = ' http://www.google-analytics.com https://www.google-analytics.com';
68 |
69 | reply(render(request, reqParams, tpl('default')))
70 | .header('X-UA-Compatible', 'IE=edge,chrome=1')
71 | .header('Cache-Control', 'public, must-revalidate, max-age=150')
72 | .header('Content-Security-Policy', [
73 | 'script-src \'self\'' + liveReloadSrc + analyticsSrc,
74 | 'frame-src \'none\'',
75 | 'object-src \'none\''
76 | ].join(';'));
77 | }
78 |
79 | server.route({
80 | method: 'GET',
81 | path: '/',
82 | handler: handleRequest
83 | });
84 |
85 | server.route({
86 | method: 'GET',
87 | path: '/search/{query}',
88 | handler: handleRequest
89 | });
90 |
91 | server.route({
92 | method: 'GET',
93 | path: '/component/{component}',
94 | handler: handleRequest
95 | });
96 |
97 | server.route({
98 | method: 'GET',
99 | path: '/api/components',
100 | config: {
101 | handler: controllers.components.componentsList,
102 | cache: { expiresIn: 1000 * 60 * 5, privacy: 'public' }
103 | }
104 | });
105 |
106 | server.route({
107 | method: 'GET',
108 | path: '/api/components/{component}',
109 | config: {
110 | handler: controllers.components.componentInfo
111 | }
112 | });
113 |
114 | server.route({
115 | method: 'GET',
116 | path: '/{param*}',
117 | config: {
118 | handler: {
119 | directory: {
120 | path: 'public',
121 | lookupCompressed: true
122 | }
123 | },
124 | cache: { expiresIn: 3600 * 1000, privacy: 'public' }
125 | }
126 | });
127 |
128 | server.start(function() {
129 | console.log('Server running at:', server.info.uri);
130 | });
131 |
--------------------------------------------------------------------------------
/app/stores/components-store.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | var _ = require('lodash');
4 | var moment = require('moment');
5 | var Reflux = require('reflux');
6 | var db = require('app/database');
7 | var winston = require('winston');
8 |
9 | var sharedMethods = require('app/stores/component-store.shared');
10 |
11 | var ComponentStore = Reflux.createStore(_.merge({}, sharedMethods, {
12 | components: {},
13 | componentSummaries: [],
14 | lastUpdated: Date.now(),
15 |
16 | get: function(name) {
17 | return this.components[name];
18 | },
19 |
20 | getLastUpdated: function() {
21 | return this.lastUpdated;
22 | },
23 |
24 | getMostRecentlyCreated: function(limit) {
25 | return (
26 | _.sortBy(this.components, 'created')
27 | .reverse()
28 | .slice(0, limit || 10)
29 | );
30 | },
31 |
32 | getMostRecentlyUpdated: function(limit) {
33 | var mostRecent = this.getMostRecentlyCreated();
34 | var lastUpdated = _.sortBy(this.components, 'modified').reverse();
35 |
36 | return _.without.apply(null,
37 | [lastUpdated].concat(mostRecent)
38 | ).slice(0, limit || 10);
39 | },
40 |
41 | getMostStarred: function(limit) {
42 | return (
43 | _.sortBy(this.componentSummaries, 'stars')
44 | .reverse()
45 | .slice(0, limit || 10)
46 | );
47 | },
48 |
49 | populateFromDatabase: function() {
50 | var addComponent = this.addComponent.bind(this);
51 |
52 | db.getModules(function(err, modules) {
53 | if (err || !modules || !modules.length) {
54 | return winston.error(
55 | 'Failed to fetch modules from DB: ' +
56 | (err || 'No modules returned')
57 | );
58 | }
59 |
60 | modules.forEach(addComponent);
61 |
62 | this.trigger('change');
63 | this.lastUpdated = Date.now();
64 | }.bind(this));
65 | },
66 |
67 | parseAuthor: function(component) {
68 | var distTags = component['dist-tags'] || {},
69 | latest = component.versions[distTags.latest] || {};
70 |
71 | return (latest._npmUser || component.author).name;
72 | },
73 |
74 | parseComponentSummary: function(component) {
75 | return {
76 | name: component.name,
77 | description: component.description,
78 | author: this.parseAuthor(component),
79 | modified: moment.utc(component.time.modified),
80 | created: moment.utc(component.time.created),
81 | keywords: (component.keywords || []).filter(this.isUncommonKeyword),
82 | downloads: component.downloads || 0,
83 | stars: component.starCount
84 | };
85 | },
86 |
87 | parseComponent: function(component) {
88 | var distTags = component['dist-tags'] || {},
89 | versions = component.versions || {},
90 | latest = versions[distTags.latest] || {};
91 |
92 | if (!component.time) {
93 | component.created = '1980-01-01T00:00:00.000Z';
94 | component.modified = '1980-01-01T00:00:00.000Z';
95 |
96 | winston.warn(
97 | 'Component with name "' + component.name + '" has no time settings'
98 | );
99 | } else {
100 | component.created = component.time.created;
101 | component.modified = component.time.modified;
102 | }
103 |
104 | component.branch = latest.gitHead || 'master';
105 |
106 | return component;
107 | },
108 |
109 | addComponent: function(component) {
110 | this.components[component.name] = this.parseComponent(component);
111 |
112 | var existing = _.find(this.componentSummaries, { name: component.name });
113 | _.pull(this.componentSummaries, existing);
114 |
115 | this.componentSummaries.push(this.parseComponentSummary(component));
116 | }
117 | }));
118 |
119 | module.exports = ComponentStore;
120 |
--------------------------------------------------------------------------------
/app/pages/front.jsx:
--------------------------------------------------------------------------------
1 | /** @jsx React.DOM */
2 | 'use strict';
3 |
4 | var React = require('react');
5 | var Reflux = require('reflux');
6 | var Layout = require('app/components/layout.jsx');
7 | var ComponentList = require('app/components/component-list.jsx');
8 | var ComponentStore = require('app/stores/components-store');
9 |
10 | function getStateFromStores() {
11 | return {
12 | recentlyCreated: ComponentStore.getMostRecentlyCreated(),
13 | recentlyUpdated: ComponentStore.getMostRecentlyUpdated(),
14 | mostStarred: ComponentStore.getMostStarred()
15 | };
16 | }
17 |
18 | var FrontPage = React.createClass({
19 | displayName: 'FrontPage',
20 |
21 | mixins: [Reflux.ListenerMixin],
22 |
23 | getInitialState: function() {
24 | return getStateFromStores();
25 | },
26 |
27 | componentDidMount: function() {
28 | this.listenTo(ComponentStore, this.onComponentsChanged);
29 | },
30 |
31 | onComponentsChanged: function() {
32 | this.setState(getStateFromStores);
33 | },
34 |
35 | /* jshint trailing:false, quotmark:false, newcap:false */
36 | render: function() {
37 | return (
38 | 65 | Every module registered on NPM using the keyword react-component will show up in the list. 66 | It really is that simple. 67 |
68 | 69 |Let us know! We're always looking for ways to improve.
78 | 79 |81 | Developed and currently hosted by VaffelNinja, but it's an open-source, MIT-licensed solution. 82 |
83 |84 | Contributions are very welcome! 85 | Please make sure you read the contribution guidelines. 86 |
87 |f(h,y))&&((u||v)&&h.push(y),s.push(g))}return v?(l(h.k),c(h)):u&&l(h),s}function lt(n){return function(t,e,r){var u={};e=J.createCallback(e,r,3),r=-1;var o=t?t.length:0;if(typeof o=="number")for(;++r