├── .gitignore ├── MaintainerInfo.md ├── README.md ├── css ├── NewsHeader.css ├── NewsItem.css ├── NewsList.css └── app.css ├── html ├── NewsHeader.html ├── NewsItem.html ├── NewsList.html └── app.html ├── img ├── DeveloperConsole.png ├── NewsHeader@2x.png ├── NewsHeaderLogin.png ├── NewsHeaderLogoTitle.png ├── NewsHeaderNav.png ├── NewsItem@2x.png ├── NewsItemDomain.png ├── NewsItemRankVote.png ├── NewsItemSubtext.png ├── NewsItemTitle.png ├── NewsList@2x.png ├── NewsListHeaderItems.png ├── NewsListMore.png ├── grayarrow2x.gif └── y18.gif ├── js ├── NewsHeader.js ├── NewsHeaderTest.js ├── NewsItem.js ├── NewsItemTest.js ├── NewsList.js ├── NewsListTest.js └── app.js ├── json └── items.json ├── package.json └── sketch └── figures.sketch /.gitignore: -------------------------------------------------------------------------------- 1 | # alphabetized 2 | .DS_Store 3 | build 4 | node_modules 5 | -------------------------------------------------------------------------------- /MaintainerInfo.md: -------------------------------------------------------------------------------- 1 | - Last full run through: 12/28/14. 2 | 3 | TODO 4 | --- 5 | - Build task for GitHub pages. 6 | 7 | Run Through Checklist 8 | --- 9 | The table of contents should be visual. This makes it easy to understand how all the pieces fit together. 10 | 11 | Instruction blocks should be single level lists. This is a readability issue. 12 | 13 | Instruction blocks should have previous/next links. 14 | 15 | Instruction blocks should be separated by horizontal rules. 16 | 17 | Instruction blocks should end with a "check your work" screenshot. 18 | 19 | Code excerpts should not have leading, trailing, or unnecessary ellipses. 20 | 21 | Links should work (table of contents, previous/next links, localhost links). 22 | 23 | Images should be in sync. 24 | 25 | Dependencies should be minimized. We can't assume everyone knows SCSS or Bluebird, even though I would use those if this were a production app. 26 | 27 | Alphabetize JS methods and requires. 28 | 29 | Alphabetize CSS rules and declarations. 30 | 31 | The code in the repo should exactly match the code in the tutorial. 32 | 33 | We use `1.` only for Markdown lists. 34 | 35 | Remove unused requires. 36 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | React HN 2 | === 3 | This is a visual React tutorial. This tutorial should give you a feel for "growing" a React UI from small, modular parts. By the end of this tutorial, you will have built the [HN front page in React](https://mking.github.io/react-hn). 4 | 5 | > Note: This tutorial covers React, Browserify, and CSS. It does not cover event handling (not needed for the HN front page), state (not needed for the HN front page), or Flux. 6 | 7 | This tutorial has five parts: 8 | 9 | 1. [Setup](#setup) 10 | 11 | 1. [NewsItem component](#newsitem) 12 | 13 | 14 | 15 | 1. [NewsHeader component](#newsheader) 16 | 17 | 18 | 19 | 1. [NewsList component](#newslist) 20 | 21 | 22 | 23 | 1. [Display live data from the Hacker News API](#hacker-news-api) 24 | 25 | --- 26 | 27 | Setup 28 | --- 29 | 1. Create the project directory structure. 30 | ```bash 31 | mkdir -p hn/{build/js,css,html,img,js,json} 32 | cd hn 33 | ``` 34 | 35 | > Note: We will be building the project from scratch. The solution in this repo is meant primarily to be a reference. 36 | 37 | 1. [Download the sample data](https://raw.githubusercontent.com/mking/react-hn/master/json/items.json) into /json. 38 | 39 | 1. Download [y18.gif](https://news.ycombinator.com/y18.gif) and [grayarrow2x.gif](https://news.ycombinator.com/grayarrow2x.gif) into /img. 40 | 41 | 1. Create /package.json. 42 | ```json 43 | { 44 | "name": "hn", 45 | "version": "0.1.0", 46 | "private": true, 47 | "browserify": { 48 | "transform": [ 49 | ["reactify"] 50 | ] 51 | } 52 | } 53 | ``` 54 | 55 | 1. Install Browserify, React, and tools. 56 | ```bash 57 | # These dependencies are required for running the app. 58 | npm install --save react jquery lodash moment 59 | 60 | # These dependencies are required for building the app. 61 | npm install --save-dev browserify watchify reactify 62 | 63 | # These dependencies are globally installed command line tools. 64 | npm install -g browserify watchify http-server 65 | ``` 66 | 67 | [Next](#newsitem) 68 | 69 | --- 70 | 71 | NewsItem 72 | --- 73 | 1. [Display the title.](#newsitem-title) 74 | 75 | 76 | 77 | 1. [Add the domain.](#newsitem-domain) 78 | 79 | 80 | 81 | 1. [Add the subtext.](#newsitem-subtext) 82 | 83 | 84 | 85 | 1. [Add the rank and vote.](#newsitem-rank-and-vote) 86 | 87 | 88 | 89 | [Previous](#setup) · [Next](#newsitem-title) 90 | 91 | --- 92 | 93 | NewsItem Title 94 | --- 95 | 1. Create a new JS file: /js/NewsItem.js. 96 | ```javascript 97 | var $ = require('jquery'); 98 | var React = require('react'); 99 | 100 | var NewsItem = React.createClass({ 101 | render: function () { 102 | return ( 103 |
104 | {this.props.item.title} 105 |
106 | ); 107 | } 108 | }); 109 | 110 | module.exports = NewsItem; 111 | ``` 112 | 113 | > Note: You should be able to paste this code directly into your JS file. 114 | 115 | 1. Create a new JS file: /js/NewsItemTest.js. 116 | ```javascript 117 | var $ = require('jquery'); 118 | var NewsItem = require('./NewsItem'); 119 | var React = require('react'); 120 | 121 | $.ajax({ 122 | url: '/json/items.json' 123 | }).then(function (items) { 124 | // Log the data so we can inspect it in the developer console. 125 | console.log('items', items); 126 | // Use a fake rank for now. 127 | React.render(, $('#content')[0]); 128 | }); 129 | ``` 130 | 131 | > Note: This lets us develop the NewsItem component in isolation, rather than requiring it to be hooked into the full app. 132 | 133 | 1. Create a new CSS file: /css/NewsItem.css. We are following [Jacob Thornton's CSS style guide](https://medium.com/@fat/mediums-css-is-actually-pretty-fucking-good-b8e2a6c78b06). 134 | ```css 135 | .newsItem { 136 | color: #828282; 137 | margin-top: 5px; 138 | } 139 | 140 | .newsItem-titleLink { 141 | color: black; 142 | font-size: 10pt; 143 | text-decoration: none; 144 | } 145 | ``` 146 | 147 | 1. Create a new CSS file: /css/app.css. 148 | ```css 149 | body { 150 | font-family: Verdana, sans-serif; 151 | } 152 | ``` 153 | 154 | 1. Create a new HTML file: /html/NewsItem.html. 155 | ```html 156 | 157 | 158 | 159 | 160 | NewsItem 161 | 162 | 163 | 164 | 165 |
166 | 167 | 168 | 169 | ``` 170 | 171 | 1. Start Watchify. This compiles your React (JSX) components into ordinary JavaScript. 172 | ```bash 173 | watchify -v -o build/js/NewsItemTest.js js/NewsItemTest.js 174 | ``` 175 | 176 | 1. Start the HTTP server. 177 | ```bash 178 | http-server -p 8888 179 | ``` 180 | 181 | 1. Visit [http://localhost:8888/html/NewsItem.html](http://localhost:8888/html/NewsItem.html). You should see the following. 182 | 183 | 184 | 185 | 186 | 187 | [Previous](#newsitem) · [Next](#newsitem-domain) 188 | 189 | NewsItem Domain 190 | --- 191 | 1. Update the JS. 192 | ```javascript 193 | // ... 194 | var url = require('url'); 195 | 196 | var NewsItem = React.createClass({ 197 | // ... 198 | 199 | getDomain: function () { 200 | return url.parse(this.props.item.url).hostname; 201 | }, 202 | 203 | render: function () { 204 | return ( 205 |
206 | ... 207 | 208 | ({this.getDomain()}) 209 | 210 |
211 | ); 212 | } 213 | ``` 214 | 215 | > Note: This code should be added onto the existing code in /js/NewsItem.js. 216 | 217 | 1. Update the CSS. 218 | ```css 219 | .newsItem-domain { 220 | font-size: 8pt; 221 | margin-left: 5px; 222 | } 223 | ``` 224 | 225 | > Note: This code should be added onto the existing code in /css/NewsItem.css. 226 | 227 | 1. Refresh the browser. You should see the following. 228 | 229 | 230 | 231 | [Previous](#newsitem-title) · [Next](#newsitem-subtext) 232 | 233 | --- 234 | 235 | NewsItem Subtext 236 | --- 237 | 1. Update the JS. Note: We are factoring out the title part into its own method. 238 | ```javascript 239 | // ... 240 | var moment = require('moment'); 241 | 242 | var NewsItem = React.createClass({ 243 | // ... 244 | 245 | getCommentLink: function () { 246 | var commentText = 'discuss'; 247 | if (this.props.item.kids && this.props.item.kids.length) { 248 | // This only counts top-level comments. 249 | // To get the full count, recursively get item details for this news item. 250 | commentText = this.props.item.kids.length + ' comments'; 251 | } 252 | 253 | return ( 254 | {commentText} 255 | ); 256 | }, 257 | 258 | getSubtext: function () { 259 | return ( 260 |
261 | {this.props.item.score} points by {this.props.item.by} {moment.utc(this.props.item.time * 1000).fromNow()} | {this.getCommentLink()} 262 |
263 | ); 264 | }, 265 | 266 | getTitle: function () { 267 | return ( 268 |
269 | ... 270 |
271 | ); 272 | }, 273 | 274 | render: function () { 275 | return ( 276 |
277 | {this.getTitle()} 278 | {this.getSubtext()} 279 |
280 | ); 281 | } 282 | ``` 283 | 284 | 1. Update the CSS. 285 | ```css 286 | .newsItem-subtext { 287 | font-size: 7pt; 288 | } 289 | 290 | .newsItem-subtext > a { 291 | color: #828282; 292 | text-decoration: none; 293 | } 294 | 295 | .newsItem-subtext > a:hover { 296 | text-decoration: underline; 297 | } 298 | ``` 299 | 300 | 1. Refresh the browser. You should see the following. 301 | 302 | 303 | 304 | [Previous](#newsitem-domain) · [Next](#newsitem-rank-and-vote) 305 | 306 | --- 307 | 308 | NewsItem Rank and Vote 309 | --- 310 | 1. Update the JS. 311 | ```javascript 312 | var NewsItem = React.createClass({ 313 | // ... 314 | 315 | getRank: function () { 316 | return ( 317 |
318 | {this.props.rank}. 319 |
320 | ); 321 | }, 322 | 323 | getVote: function () { 324 | return ( 325 |
326 | 327 | 328 | 329 |
330 | ); 331 | }, 332 | 333 | render: function () { 334 | return ( 335 |
336 | {this.getRank()} 337 | {this.getVote()} 338 |
339 | {this.getTitle()} 340 | {this.getSubtext()} 341 |
342 |
343 | ); 344 | } 345 | ``` 346 | 347 | 1. Update the CSS. 348 | ```css 349 | .newsItem { 350 | /* ... */ 351 | align-items: baseline; 352 | display: flex; 353 | } 354 | 355 | .newsItem-itemText { 356 | flex-grow: 1; 357 | } 358 | 359 | .newsItem-rank { 360 | flex-basis: 25px; 361 | font-size: 10pt; 362 | text-align: right; 363 | } 364 | 365 | .newsItem-vote { 366 | flex-basis: 15px; 367 | text-align: center; 368 | } 369 | ``` 370 | 371 | 1. Refresh the browser. You should see the following. 372 | 373 | 374 | 375 | You have now implemented an HN news item in React. 376 | 377 | 378 | 379 | [Previous](#newsitem-subtext) · [Next](#newsheader) 380 | 381 | --- 382 | 383 | NewsHeader 384 | --- 385 | 1. [Display the logo and title.](#newsheader-logo-and-title) 386 | 387 | 388 | 389 | 1. [Add the nav links.](#newsheader-nav) 390 | 391 | 392 | 393 | 1. [Add the login link.](#newsheader-login) 394 | 395 | 396 | 397 | [Previous](#newsitem-rank-and-vote) · [Next](#newsheader-logo-and-title) 398 | 399 | --- 400 | 401 | NewsHeader Logo and Title 402 | --- 403 | 1. Create a new JS file: /js/NewsHeader.js. 404 | ```javascript 405 | var $ = require('jquery'); 406 | var React = require('react'); 407 | 408 | var NewsHeader = React.createClass({ 409 | getLogo: function () { 410 | return ( 411 |
412 | 413 |
414 | ); 415 | }, 416 | 417 | getTitle: function () { 418 | return ( 419 |
420 | Hacker News 421 |
422 | ); 423 | }, 424 | 425 | render: function () { 426 | return ( 427 |
428 | {this.getLogo()} 429 | {this.getTitle()} 430 |
431 | ); 432 | } 433 | }); 434 | 435 | module.exports = NewsHeader; 436 | ``` 437 | 438 | 1. Create a new JS file: /js/NewsHeaderTest.js. 439 | ```javascript 440 | var $ = require('jquery'); 441 | var NewsHeader = require('./NewsHeader'); 442 | var React = require('react'); 443 | 444 | React.render(, $('#content')[0]); 445 | ``` 446 | 447 | 1. Create a new CSS file: /css/NewsHeader.css. 448 | ```css 449 | .newsHeader { 450 | align-items: center; 451 | background: #ff6600; 452 | color: black; 453 | display: flex; 454 | font-size: 10pt; 455 | padding: 2px; 456 | } 457 | 458 | .newsHeader-logo { 459 | border: 1px solid white; 460 | flex-basis: 18px; 461 | height: 18px; 462 | } 463 | 464 | .newsHeader-textLink { 465 | color: black; 466 | text-decoration: none; 467 | } 468 | 469 | .newsHeader-title { 470 | font-weight: bold; 471 | margin-left: 4px; 472 | } 473 | ``` 474 | 475 | 1. Create a new HTML file: /html/NewsHeader.html. 476 | ```html 477 | 478 | 479 | 480 | 481 | NewsHeader 482 | 483 | 484 | 485 | 486 |
487 | 488 | 489 | 490 | ``` 491 | 492 | 1. Start Watchify. 493 | ```bash 494 | watchify -v -o build/js/NewsHeaderTest.js js/NewsHeaderTest.js 495 | ``` 496 | 497 | 1. Start the HTTP server if necessary. 498 | ```bash 499 | http-server -p 8888 500 | ``` 501 | 502 | 1. Visit [http://localhost:8888/html/NewsHeader.html](http://localhost:8888/html/NewsHeader.html). You should see the following. 503 | 504 | 505 | 506 | [Previous](#newsheader) · [Next](#newsheader-nav) 507 | 508 | --- 509 | 510 | NewsHeader Nav 511 | --- 512 | 1. Update the JS. 513 | ```javascript 514 | // ... 515 | var _ = require('lodash'); 516 | 517 | var NewsHeader = React.createClass({ 518 | // ... 519 | 520 | getNav: function () { 521 | var navLinks = [ 522 | { 523 | name: 'new', 524 | url: 'newest' 525 | }, 526 | { 527 | name: 'comments', 528 | url: 'newcomments' 529 | }, 530 | { 531 | name: 'show', 532 | url: 'show' 533 | }, 534 | { 535 | name: 'ask', 536 | url: 'ask' 537 | }, 538 | { 539 | name: 'jobs', 540 | url: 'jobs' 541 | }, 542 | { 543 | name: 'submit', 544 | url: 'submit' 545 | } 546 | ]; 547 | 548 | return ( 549 |
550 | {_(navLinks).map(function (navLink) { 551 | return ( 552 | 553 | {navLink.name} 554 | 555 | ); 556 | }).value()} 557 |
558 | ); 559 | }, 560 | 561 | render: function () { 562 | return ( 563 |
564 | ... 565 | {this.getNav()} 566 |
567 | ); 568 | } 569 | ``` 570 | 571 | 1. Update the CSS. 572 | ```css 573 | .newsHeader-nav { 574 | flex-grow: 1; 575 | margin-left: 10px; 576 | } 577 | 578 | .newsHeader-navLink:not(:first-child)::before { 579 | content: ' | '; 580 | } 581 | ``` 582 | 583 | 1. Refresh the browser. You should see the following. 584 | 585 | 586 | 587 | [Previous](#newsheader-logo-and-title) · [Next](#newsheader-login) 588 | 589 | --- 590 | 591 | NewsHeader Login 592 | --- 593 | 1. Update the JS. 594 | ```javascript 595 | var NewsHeader = React.createClass({ 596 | // ... 597 | 598 | getLogin: function () { 599 | return ( 600 |
601 | login 602 |
603 | ); 604 | }, 605 | 606 | render: function () { 607 | return ( 608 |
609 | ... 610 | {this.getLogin()} 611 |
612 | ); 613 | } 614 | ``` 615 | 616 | 1. Update the CSS. 617 | ```css 618 | .newsHeader-login { 619 | margin-right: 5px; 620 | } 621 | ``` 622 | 623 | 1. Refresh the browser. You should see the following. 624 | 625 | 626 | 627 | You have now implemented the HN header in React. 628 | 629 | 630 | 631 | [Previous](#newsheader-nav) · [Next](#newslist) 632 | 633 | --- 634 | 635 | NewsList 636 | --- 637 | 1. [Display the header and items.](#newslist-header-and-items) 638 | 639 | 640 | 641 | 1. [Add the more link.](#newslist-more) 642 | 643 | 644 | 645 | [Previous](#newsheader-login) · [Next](#newslist-header-and-items) 646 | 647 | --- 648 | 649 | NewsList Header and Items 650 | --- 651 | 1. Create a new JS file: /js/NewsList.js. 652 | ```javascript 653 | var _ = require('lodash'); 654 | var NewsHeader = require('./NewsHeader'); 655 | var NewsItem = require('./NewsItem'); 656 | var React = require('react'); 657 | 658 | var NewsList = React.createClass({ 659 | render: function () { 660 | return ( 661 |
662 | 663 |
664 | {_(this.props.items).map(function (item, index) { 665 | return ; 666 | }.bind(this)).value()} 667 |
668 |
669 | ); 670 | } 671 | }); 672 | 673 | module.exports = NewsList; 674 | ``` 675 | 676 | 1. Create a new JS file: /js/NewsListTest.js. 677 | ```javascript 678 | var $ = require('jquery'); 679 | var NewsList = require('./NewsList'); 680 | var React = require('react'); 681 | 682 | $.ajax({ 683 | url: '/json/items.json' 684 | }).then(function (items) { 685 | React.render(, $('#content')[0]); 686 | }); 687 | ``` 688 | 689 | 1. Create a new CSS file: /css/NewsList.css. 690 | ```css 691 | .newsList { 692 | background: #f6f6ef; 693 | margin-left: auto; 694 | margin-right: auto; 695 | width: 85%; 696 | } 697 | ``` 698 | 699 | 1. Create a new HTML file: /html/NewsList.html. 700 | ```html 701 | 702 | 703 | 704 | 705 | NewsList 706 | 707 | 708 | 709 | 710 | 711 | 712 |
713 | 714 | 715 | 716 | ``` 717 | 718 | 1. Start Watchify. 719 | ```bash 720 | watchify -v -o build/js/NewsListTest.js js/NewsListTest.js 721 | ``` 722 | 723 | 1. Start the HTTP server if necessary. 724 | ```bash 725 | http-server -p 8888 726 | ``` 727 | 728 | 1. Visit [http://localhost:8888/html/NewsList.html](http://localhost:8888/html/NewsList.html). You should see the following. 729 | 730 | 731 | 732 | [Previous](#newslist) · [Next](#newslist-more) 733 | 734 | --- 735 | 736 | NewsList More 737 | --- 738 | 1. Update the JS. 739 | ```javascript 740 | var NewsList = React.createClass({ 741 | // ... 742 | 743 | getMore: function () { 744 | return ( 745 |
746 | More 747 |
748 | ); 749 | }, 750 | 751 | render: function () { 752 | return ( 753 |
754 | ... 755 | {this.getMore()} 756 |
757 | ); 758 | } 759 | ``` 760 | 761 | 1. Update the CSS. 762 | ```css 763 | .newsList-more { 764 | margin-left: 40px; /* matches NewsItem rank and vote */ 765 | margin-top: 10px; 766 | padding-bottom: 10px; 767 | } 768 | 769 | .newsList-moreLink { 770 | color: black; 771 | font-size: 10pt; 772 | text-decoration: none; 773 | } 774 | ``` 775 | 776 | 1. Refresh the browser. You should see the following. 777 | 778 | 779 | 780 | You have now implemented the HN item list in React. 781 | 782 | 783 | 784 | [Previous](#newslist-header-and-items) · [Next](#hacker-news-api) 785 | 786 | --- 787 | 788 | Hacker News API 789 | --- 790 | 1. Create a new JS file: /js/app.js. 791 | ```javascript 792 | var _ = require('lodash'); 793 | var $ = require('jquery'); 794 | var NewsList = require('./NewsList'); 795 | var React = require('react'); 796 | 797 | // Get the top item ids 798 | $.ajax({ 799 | url: 'https://hacker-news.firebaseio.com/v0/topstories.json', 800 | dataType: 'json' 801 | }).then(function (stories) { 802 | // Get the item details in parallel 803 | var detailDeferreds = _(stories.slice(0, 30)).map(function (itemId) { 804 | return $.ajax({ 805 | url: 'https://hacker-news.firebaseio.com/v0/item/' + itemId + '.json', 806 | dataType: 'json' 807 | }); 808 | }).value(); 809 | return $.when.apply($, detailDeferreds); 810 | }).then(function () { 811 | // Extract the response JSON 812 | var items = _(arguments).map(function (argument) { 813 | return argument[0]; 814 | }).value(); 815 | 816 | // Render the items 817 | React.render(, $('#content')[0]); 818 | }); 819 | 820 | ``` 821 | 822 | 1. Create a new HTML file: /html/app.html. 823 | ```html 824 | 825 | 826 | 827 | 828 | Hacker News 829 | 830 | 831 | 832 | 833 | 834 | 835 |
836 | 837 | 838 | 839 | ``` 840 | 841 | 1. Start Watchify. 842 | ```bash 843 | watchify -v -o build/js/app.js js/app.js 844 | ``` 845 | 846 | 1. Start the HTTP server if necessary. 847 | ```bash 848 | http-server -p 8888 849 | ``` 850 | 851 | 1. Visit [http://localhost:8888/html/app.html](http://localhost:8888/html/app.html). 852 | 853 | You have now implemented the [HN front page](https://news.ycombinator.com) in React. 854 | 855 | [Previous](#newslist-more) 856 | -------------------------------------------------------------------------------- /css/NewsHeader.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: Verdana, sans-serif; 3 | } 4 | 5 | .newsHeader { 6 | align-items: center; 7 | background: #ff6600; 8 | color: black; 9 | display: flex; 10 | font-size: 10pt; 11 | padding: 2px; 12 | } 13 | 14 | .newsHeader-login { 15 | margin-right: 5px; 16 | } 17 | 18 | .newsHeader-logo { 19 | border: 1px solid white; 20 | flex-basis: 18px; 21 | height: 18px; 22 | } 23 | 24 | .newsHeader-nav { 25 | flex-grow: 1; 26 | margin-left: 10px; 27 | } 28 | 29 | .newsHeader-navLink:not(:first-child)::before { 30 | content: ' | '; 31 | } 32 | 33 | .newsHeader-textLink { 34 | color: black; 35 | text-decoration: none; 36 | } 37 | 38 | .newsHeader-title > a { 39 | font-weight: bold; 40 | margin-left: 4px; 41 | } 42 | -------------------------------------------------------------------------------- /css/NewsItem.css: -------------------------------------------------------------------------------- 1 | .newsItem { 2 | align-items: baseline; 3 | color: #828282; 4 | display: flex; 5 | margin-top: 5px; 6 | } 7 | 8 | .newsItem-domain { 9 | font-size: 8pt; 10 | margin-left: 5px; 11 | } 12 | 13 | .newsItem-itemText { 14 | flex-grow: 1; 15 | } 16 | 17 | .newsItem-rank { 18 | flex-basis: 25px; 19 | font-size: 10pt; 20 | text-align: right; 21 | } 22 | 23 | .newsItem-subtext { 24 | font-size: 7pt; 25 | } 26 | 27 | .newsItem-subtext > a { 28 | color: #828282; 29 | text-decoration: none; 30 | } 31 | 32 | .newsItem-subtext > a:hover { 33 | text-decoration: underline; 34 | } 35 | 36 | .newsItem-titleLink { 37 | color: black; 38 | font-size: 10pt; 39 | text-decoration: none; 40 | } 41 | 42 | .newsItem-vote { 43 | flex-basis: 15px; 44 | text-align: center; 45 | } 46 | -------------------------------------------------------------------------------- /css/NewsList.css: -------------------------------------------------------------------------------- 1 | .newsList { 2 | background: #f6f6ef; 3 | margin-left: auto; 4 | margin-right: auto; 5 | width: 85%; 6 | } 7 | 8 | .newsList-more { 9 | margin-left: 40px; /* matches NewsItem rank and vote */ 10 | margin-top: 10px; 11 | padding-bottom: 10px; 12 | } 13 | 14 | .newsList-moreLink { 15 | color: black; 16 | font-size: 10pt; 17 | text-decoration: none; 18 | } 19 | -------------------------------------------------------------------------------- /css/app.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: Verdana, sans-serif; 3 | } 4 | -------------------------------------------------------------------------------- /html/NewsHeader.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | NewsHeader 6 | 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /html/NewsItem.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | NewsItem 6 | 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /html/NewsList.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | NewsList 6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /html/app.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Hacker News 6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /img/DeveloperConsole.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mking/react-hn/4afe96b6e73b6c4a600bd9a61471c41deb95139c/img/DeveloperConsole.png -------------------------------------------------------------------------------- /img/NewsHeader@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mking/react-hn/4afe96b6e73b6c4a600bd9a61471c41deb95139c/img/NewsHeader@2x.png -------------------------------------------------------------------------------- /img/NewsHeaderLogin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mking/react-hn/4afe96b6e73b6c4a600bd9a61471c41deb95139c/img/NewsHeaderLogin.png -------------------------------------------------------------------------------- /img/NewsHeaderLogoTitle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mking/react-hn/4afe96b6e73b6c4a600bd9a61471c41deb95139c/img/NewsHeaderLogoTitle.png -------------------------------------------------------------------------------- /img/NewsHeaderNav.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mking/react-hn/4afe96b6e73b6c4a600bd9a61471c41deb95139c/img/NewsHeaderNav.png -------------------------------------------------------------------------------- /img/NewsItem@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mking/react-hn/4afe96b6e73b6c4a600bd9a61471c41deb95139c/img/NewsItem@2x.png -------------------------------------------------------------------------------- /img/NewsItemDomain.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mking/react-hn/4afe96b6e73b6c4a600bd9a61471c41deb95139c/img/NewsItemDomain.png -------------------------------------------------------------------------------- /img/NewsItemRankVote.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mking/react-hn/4afe96b6e73b6c4a600bd9a61471c41deb95139c/img/NewsItemRankVote.png -------------------------------------------------------------------------------- /img/NewsItemSubtext.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mking/react-hn/4afe96b6e73b6c4a600bd9a61471c41deb95139c/img/NewsItemSubtext.png -------------------------------------------------------------------------------- /img/NewsItemTitle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mking/react-hn/4afe96b6e73b6c4a600bd9a61471c41deb95139c/img/NewsItemTitle.png -------------------------------------------------------------------------------- /img/NewsList@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mking/react-hn/4afe96b6e73b6c4a600bd9a61471c41deb95139c/img/NewsList@2x.png -------------------------------------------------------------------------------- /img/NewsListHeaderItems.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mking/react-hn/4afe96b6e73b6c4a600bd9a61471c41deb95139c/img/NewsListHeaderItems.png -------------------------------------------------------------------------------- /img/NewsListMore.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mking/react-hn/4afe96b6e73b6c4a600bd9a61471c41deb95139c/img/NewsListMore.png -------------------------------------------------------------------------------- /img/grayarrow2x.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mking/react-hn/4afe96b6e73b6c4a600bd9a61471c41deb95139c/img/grayarrow2x.gif -------------------------------------------------------------------------------- /img/y18.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mking/react-hn/4afe96b6e73b6c4a600bd9a61471c41deb95139c/img/y18.gif -------------------------------------------------------------------------------- /js/NewsHeader.js: -------------------------------------------------------------------------------- 1 | var _ = require('lodash'); 2 | var $ = require('jquery'); 3 | var React = require('react'); 4 | 5 | var NewsHeader = React.createClass({ 6 | getLogin: function () { 7 | return ( 8 |
9 | login 10 |
11 | ); 12 | }, 13 | 14 | getLogo: function () { 15 | return ( 16 |
17 | 18 |
19 | ); 20 | }, 21 | 22 | getNav: function () { 23 | var navLinks = [ 24 | { 25 | name: 'new', 26 | url: 'newest' 27 | }, 28 | { 29 | name: 'comments', 30 | url: 'newcomments' 31 | }, 32 | { 33 | name: 'show', 34 | url: 'show' 35 | }, 36 | { 37 | name: 'ask', 38 | url: 'ask' 39 | }, 40 | { 41 | name: 'jobs', 42 | url: 'jobs' 43 | }, 44 | { 45 | name: 'submit', 46 | url: 'submit' 47 | } 48 | ]; 49 | 50 | return ( 51 |
52 | {_(navLinks).map(function (navLink) { 53 | return ( 54 | 55 | {navLink.name} 56 | 57 | ); 58 | }).value()} 59 |
60 | ); 61 | }, 62 | 63 | getTitle: function () { 64 | return ( 65 |
66 | Hacker News 67 |
68 | ); 69 | }, 70 | 71 | render: function () { 72 | return ( 73 |
74 | {this.getLogo()} 75 | {this.getTitle()} 76 | {this.getNav()} 77 | {this.getLogin()} 78 |
79 | ); 80 | } 81 | }); 82 | 83 | module.exports = NewsHeader; 84 | -------------------------------------------------------------------------------- /js/NewsHeaderTest.js: -------------------------------------------------------------------------------- 1 | var $ = require('jquery'); 2 | var NewsHeader = require('./NewsHeader'); 3 | var React = require('react'); 4 | 5 | React.render(, $('#content')[0]); 6 | -------------------------------------------------------------------------------- /js/NewsItem.js: -------------------------------------------------------------------------------- 1 | var $ = require('jquery'); 2 | var moment = require('moment'); 3 | var React = require('react'); 4 | var url = require('url'); 5 | 6 | var NewsItem = React.createClass({ 7 | getCommentLink: function () { 8 | var commentText = 'discuss'; 9 | if (this.props.item.kids && this.props.item.kids.length) { 10 | commentText = this.props.item.kids.length + ' comments'; 11 | } 12 | 13 | return ( 14 | {commentText} 15 | ); 16 | }, 17 | 18 | getDomain: function () { 19 | return url.parse(this.props.item.url).hostname; 20 | }, 21 | 22 | getRank: function () { 23 | return ( 24 |
25 | {this.props.rank}. 26 |
27 | ); 28 | }, 29 | 30 | getSubtext: function () { 31 | return ( 32 |
33 | {this.props.item.score} points by {this.props.item.by} {moment.utc(this.props.item.time * 1000).fromNow()} | {this.getCommentLink()} 34 |
35 | ); 36 | }, 37 | 38 | getTitle: function () { 39 | return ( 40 |
41 | {this.props.item.title} 42 | 43 | ({this.getDomain()}) 44 | 45 |
46 | ); 47 | }, 48 | 49 | getVote: function () { 50 | return ( 51 |
52 | 53 | 54 | 55 |
56 | ); 57 | }, 58 | 59 | render: function () { 60 | return ( 61 |
62 | {this.getRank()} 63 | {this.getVote()} 64 |
65 | {this.getTitle()} 66 | {this.getSubtext()} 67 |
68 |
69 | ); 70 | } 71 | }); 72 | 73 | module.exports = NewsItem; 74 | -------------------------------------------------------------------------------- /js/NewsItemTest.js: -------------------------------------------------------------------------------- 1 | var $ = require('jquery'); 2 | var NewsItem = require('./NewsItem'); 3 | var React = require('react'); 4 | 5 | $.ajax({ 6 | url: '/json/items.json' 7 | }).then(function (items) { 8 | // Log the data so we can inspect it in the developer console. 9 | console.log('items', items); 10 | // Use a fake rank for now. 11 | React.render(, $('#content')[0]); 12 | }); 13 | -------------------------------------------------------------------------------- /js/NewsList.js: -------------------------------------------------------------------------------- 1 | var _ = require('lodash'); 2 | var NewsHeader = require('./NewsHeader'); 3 | var NewsItem = require('./NewsItem'); 4 | var React = require('react'); 5 | 6 | var NewsList = React.createClass({ 7 | getMore: function () { 8 | return ( 9 |
10 | More 11 |
12 | ); 13 | }, 14 | 15 | render: function () { 16 | return ( 17 |
18 | 19 |
20 | {_(this.props.items).map(function (item, index) { 21 | return ; 22 | }.bind(this)).value()} 23 |
24 | {this.getMore()} 25 |
26 | ); 27 | } 28 | }); 29 | 30 | module.exports = NewsList; 31 | -------------------------------------------------------------------------------- /js/NewsListTest.js: -------------------------------------------------------------------------------- 1 | var $ = require('jquery'); 2 | var NewsList = require('./NewsList'); 3 | var React = require('react'); 4 | 5 | $.ajax({ 6 | url: '/json/items.json' 7 | }).then(function (items) { 8 | React.render(, $('#content')[0]); 9 | }); 10 | -------------------------------------------------------------------------------- /js/app.js: -------------------------------------------------------------------------------- 1 | var _ = require('lodash'); 2 | var $ = require('jquery'); 3 | var NewsList = require('./NewsList'); 4 | var React = require('react'); 5 | 6 | // Get the top item ids 7 | $.ajax({ 8 | url: 'https://hacker-news.firebaseio.com/v0/topstories.json', 9 | dataType: 'json' 10 | }).then(function (stories) { 11 | // Get the item details in parallel 12 | var detailDeferreds = _(stories.slice(0, 30)).map(function (itemId) { 13 | return $.ajax({ 14 | url: 'https://hacker-news.firebaseio.com/v0/item/' + itemId + '.json', 15 | dataType: 'json' 16 | }); 17 | }).value(); 18 | return $.when.apply($, detailDeferreds); 19 | }).then(function () { 20 | // Extract the response JSON 21 | var items = _(arguments).map(function (argument) { 22 | return argument[0]; 23 | }).value(); 24 | React.render(, $('#content')[0]); 25 | }); 26 | -------------------------------------------------------------------------------- /json/items.json: -------------------------------------------------------------------------------- 1 | [{"by":"jamesisaac","id":8805053,"kids":[8805325,8805679,8805195,8805408,8805681,8805650,8805643,8805577,8805469,8805204,8805222,8805498,8805190,8805304,8805569,8805328,8805478,8805350,8805186,8805432,8805318,8805480,8805320,8805601,8805106],"score":167,"text":"","time":1419737448,"title":"Look, no hands","type":"story","url":"http://looknohands.me/"},{"by":"jcr","id":8805606,"score":10,"text":"","time":1419750879,"title":"Psychologists Strike a Blow for Reproducibility","type":"story","url":"http://www.nature.com/news/psychologists-strike-a-blow-for-reproducibility-1.14232/"},{"by":"lelf","id":8802424,"kids":[8802475,8804091,8802511,8802500,8805677,8802481,8804771,8802746,8804040,8803150,8805382,8802739,8802597,8802644,8802987,8802722,8805123,8805545,8802484,8802716,8802505,8802687,8805135,8803538,8804569,8804707,8802504,8802499,8803836,8803653,8802781,8802613,8803595,8803216,8804402,8802626,8803875,8804191,8802491,8804898,8804004,8803701,8804551,8802522,8802570,8802877,8802657,8802533,8802636,8802659,8802537,8802827,8802774,8803511,8802905,8802550,8802601,8803205,8804487,8802554,8803290,8803152,8804425,8802686,8802557,8802478,8803004,8802480,8804231,8802494,8803114,8802488,8803737,8802553,8804442,8802816,8804240,8803715,8803112,8802831,8803018,8802498,8803555,8803592,8803231],"score":608,"text":"","time":1419685280,"title":"Adblock Plus is probably the reason Firefox and Chrome are such memory hogs","type":"story","url":"http://www.extremetech.com/computing/182428-ironic-iframes-adblock-plus-is-probably-the-reason-firefox-and-chrome-are-such-memory-hogs"},{"by":"gscott","id":8804127,"kids":[8805695,8804783,8804448,8804580,8805479,8805141,8804363,8804561,8805227,8804582,8804937,8804696],"score":120,"text":"","time":1419717862,"title":"How Colonel Sanders Became Father Christmas in Japan","type":"story","url":"http://talkingpointsmemo.com/ts/kfc-christmas-in-japan-colonel-sanders-history-12-23-2014"},{"by":"getdavidhiggins","id":8805039,"kids":[8805113,8805501,8805231],"score":34,"text":"","time":1419736952,"title":"Redesiging a Broken Internet: Cory Doctorow [video]","type":"story","url":"https://www.youtube.com/watch?v=_J_9EFGFR-Y"},{"by":"davidbarker","id":8804691,"kids":[8805070,8805429,8805035,8804964],"score":54,"text":"","time":1419729238,"title":"A Primer on Bézier Curves (2013)","type":"story","url":"http://pomax.github.io/bezierinfo/"},{"by":"nkurz","id":8804961,"kids":[8805214,8805634,8805248,8805541],"score":27,"text":"","time":1419734983,"title":"How Google Cracked House Number Identification in Street View","type":"story","url":"http://www.technologyreview.com/view/523326/how-google-cracked-house-number-identification-in-street-view/"},{"by":"superfx","id":8804824,"score":10,"text":"","time":1419731802,"title":"Jeff Hawkins: Brains, Data, and Machine Intelligence [video]","type":"story","url":"https://www.youtube.com/watch?v=cz-3WDdqbj0"},{"by":"danso","id":8802676,"kids":[8803169,8803141,8804121,8803753,8803115,8803523,8804504,8804102,8803157,8803733],"score":240,"text":"","time":1419691704,"title":"The Wire in HD","type":"story","url":"http://davidsimon.com/the-wire-hd-with-videos/"},{"by":"theoutlander","id":8804909,"kids":[8805693,8805550],"score":23,"text":"","time":1419733513,"title":"Chicago gave hundreds of high-risk kids a job; violent crime arrests plummeted","type":"story","url":"http://www.washingtonpost.com/blogs/wonkblog/wp/2014/12/26/chicago-gave-hun…t-crime-arrests-plummeted/?Post+generic=%3Ftid%3Dsm_twitter_washingtonpost"},{"by":"moe","id":8803998,"kids":[8804028,8804859,8804528,8804521,8804591],"score":63,"text":"Tobias Engel demonstrates (amongst other things):

* How to find out the phone numbers of nearby cellphones

* How to track the location of a cellphone that you only know the phone number of

* How intercept outgoing calls of nearby cellphones","time":1419715606,"title":"SS7: Locate. Track. Manipulate [video]","type":"story","url":"http://streaming.media.ccc.de/relive/6249/"},{"by":"jwatte","id":8803235,"kids":[8803652,8804123,8804262,8804421,8804287,8804144,8803910],"score":114,"text":"","time":1419702205,"title":"The Real-time Web: How to Get Millisecond Updates with REST","type":"story","url":"http://engineering.imvu.com/2014/12/27/the-real-time-web-in-rest-services-at-imvu/"},{"by":"pmoriarty","id":8803498,"kids":[8804214,8805055,8804229,8804041,8804008,8804753,8804990,8804173,8804042,8804997,8803959,8804450],"score":91,"text":"","time":1419706531,"title":"Masscan: Scan the entire Internet in under 5 minutes","type":"story","url":"https://github.com/robertdavidgraham/masscan"},{"by":"Thevet","id":8804296,"kids":[8804610,8805631,8804717,8804483,8804874,8804748,8804475,8804914,8804447,8804790,8805257,8804623,8805051],"score":41,"text":"","time":1419721241,"title":"Home for the holidays, and for a 20-year-old issue of PC Magazine","type":"story","url":"http://www.theverge.com/tldr/2014/12/26/7451295/home-for-the-holidays-and-for-a-20-year-old-issue-of-pc-magazine"},{"by":"lkrubner","id":8803101,"kids":[8805105,8805658,8804495,8803794,8804347,8805289,8805524,8804793,8804247,8804968,8804106,8805271,8804665,8805031,8804642,8805434,8804814,8803946,8804259,8804826,8804875,8804013,8803833],"score":89,"text":"","time":1419699685,"title":"When rational thinking is correlated with intelligence the correlation is modest","type":"story","url":"http://www.scientificamerican.com/article/rational-and-irrational-thought-the-thinking-that-iq-tests-miss/"},{"by":"ColinWright","id":8803138,"kids":[8805684,8803829,8805200,8803761,8804051,8804965,8804311],"score":96,"time":1419700378,"title":"Apollo 11 Flight Plan – Final [pdf]","type":"story","url":"https://www.hq.nasa.gov/alsj/a11/a11fltpln_final_reformat.pdf"},{"by":"r0h1n","id":8805394,"score":7,"text":"","time":1419744647,"title":"The Slow Death of ‘Do Not Track’","type":"story","url":"http://nytimes.com/2014/12/27/opinion/the-slow-death-of-do-not-track.html"},{"by":"waffle_ss","id":8804624,"kids":[8804895,8804788,8804923,8804784,8805077,8805691,8804729,8804989,8804931,8804763,8804940,8804780,8804764,8804868,8804956,8804982,8804935],"score":150,"text":"","time":1419727453,"title":"James Golick has died","type":"story","url":"https://twitter.com/jill380/status/548978785404874753"},{"by":"lelf","id":8802290,"kids":[8805536,8803376,8803757,8803307,8803438,8804667,8803619,8803647,8804264,8804609,8804218,8804222,8803633],"score":91,"time":1419678666,"title":"Patients do better when cardiologists are away at academic meetings","type":"story","url":"http://theincidentaleconomist.com/wordpress/patients-do-better-when-cardiologists-are-away-at-academic-meetings/"},{"by":"MichaelAO","id":8801616,"kids":[8801839,8803710,8802569,8801827,8802483,8804177,8801852,8801823,8802186,8802059,8802072,8803818,8802058,8801819,8802178,8801828,8801973],"score":240,"text":"","time":1419652028,"title":"Slow, flexible and cheap: Six years of development to create a rubber hexagon","type":"story","url":"https://medium.com/dome-kit/slow-flexible-cheap-5598ca91fb38"},{"by":"frostmatthew","id":8804934,"kids":[8805042,8805075,8805120,8805591,8805509,8805056,8805084,8805556,8805224,8805089,8805187,8805061,8805274,8805101,8805193,8805463,8805064,8805338,8805109,8805063,8805071,8805067],"score":165,"text":"","time":1419734117,"title":"On Immigration, Engineers Simply Don’t Trust VCs","type":"story","url":"http://techcrunch.com/2014/12/27/on-immigration-engineers-simply-dont-trust-vcs/"},{"by":"sanxiyn","id":8802425,"kids":[8805417,8802961,8803240],"score":116,"text":"","time":1419685288,"title":"Robdns \u2013 A fast DNS server based on C10M principles","type":"story","url":"https://github.com/robertdavidgraham/robdns"},{"by":"Petiver","id":8805487,"score":3,"text":"","time":1419747147,"title":"The Treasure of Nagyszentmiklós","type":"story","url":"http://en.wikipedia.org/wiki/Treasure_of_Nagyszentmikl%C3%B3s"},{"by":"carljoseph","id":8802485,"kids":[8803927,8805337,8803869,8803739,8804304,8804966,8804031,8803901,8804886,8803763,8805515,8803577,8804668,8804224],"score":76,"text":"","time":1419687337,"title":"The Software Scientist","type":"story","url":"http://www.evanmiller.org/the-software-scientist.html"},{"by":"Vigier","id":8804752,"score":7,"text":"","time":1419730398,"title":"House Perfect: Is the Ikea Ethos Comfy or Creepy? (2011)","type":"story","url":"http://www.newyorker.com/magazine/2011/10/03/house-perfect"},{"by":"sethbannon","id":8803844,"kids":[8805605,8805100,8804211,8804986,8804727,8804055,8804361,8805340,8804301,8804680,8804268,8804225,8804058,8804305,8804379],"score":59,"text":"","time":1419712871,"title":"When you have to wake up earlier than usual","type":"story","url":"http://blog.42floors.com/waking-up-early/"},{"by":"wslh","id":8804979,"score":5,"text":"","time":1419735352,"title":"How movies embraced Hinduism","type":"story","url":"http://www.theguardian.com/film/2014/dec/25/movies-embraced-hinduism"},{"by":"aestetix","id":8802138,"kids":[8802393,8802343,8802215,8802437,8802493,8803188,8802357,8804415,8802383,8802319,8802397,8803608,8802610,8802286,8802197,8802329,8802285,8802271,8802330],"score":157,"text":"","time":1419671428,"title":"31C3 Streaming: Saal 1 \u2013 SD Video","type":"story","url":"http://streaming.media.ccc.de/saal1/"},{"by":"diafygi","id":8803118,"kids":[8803351,8804151,8803613,8803234,8803409,8803466,8803454,8803396,8803395,8803413,8803381,8803427,8803926,8803416,8803369,8803587,8803402,8803974,8804261,8804322,8804245,8803140,8803340,8803424,8803897,8803357,8803363,8804917],"score":241,"text":"","time":1419700057,"title":"Many Android bugs with 500+ stars closed as obsolete on December 25","type":"story","url":"https://code.google.com/p/android/issues/list?can=1&q=status:Obsolete&sort=-stars"},{"by":"mrry","id":8802392,"kids":[8802474],"score":98,"text":"","time":1419684021,"title":"Functional Operating System and Security Protocol Engineering","type":"story","url":"http://decks.openmirage.org/31c3#/"}] 2 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-hn", 3 | "version": "0.1.0", 4 | "private": true, 5 | "devDependencies": { 6 | "browserify": "^8.1.3", 7 | "reactify": "^1.0.0", 8 | "watchify": "^2.3.0" 9 | }, 10 | "dependencies": { 11 | "jquery": "^2.1.3", 12 | "lodash": "^3.2.0", 13 | "moment": "^2.9.0", 14 | "react": "^0.12.2" 15 | }, 16 | "browserify": { 17 | "transform": [ 18 | [ 19 | "reactify" 20 | ] 21 | ] 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /sketch/figures.sketch: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mking/react-hn/4afe96b6e73b6c4a600bd9a61471c41deb95139c/sketch/figures.sketch --------------------------------------------------------------------------------