├── .eslintignore
├── .eslintrc.js
├── .gitignore
├── .prettierrc
├── README.md
├── REQUIREMENTS.md
├── image1.gif
├── index.html
├── manifest.webmanifest
├── package.json
├── src
├── api.ts
├── index.ts
├── listItem.ts
├── styles.scss
├── sw.js
└── types.d.ts
├── tsconfig.json
└── yarn.lock
/.eslintignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | dist
3 |
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | root: true,
3 |
4 | parser: '@typescript-eslint/parser',
5 | plugins: ['@typescript-eslint'],
6 |
7 | extends: [
8 | 'eslint:recommended',
9 | 'plugin:@typescript-eslint/eslint-recommended',
10 | 'plugin:@typescript-eslint/recommended',
11 | 'prettier/@typescript-eslint',
12 | ],
13 | rules: {
14 | '@typescript-eslint/no-use-before-define': 'off',
15 | '@typescript-eslint/explicit-function-return-type': 'off',
16 | },
17 | };
18 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | dist
2 | .cache
3 | node_modules
4 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "printWidth": 80,
3 | "tabWidth": 2,
4 | "useTabs": false,
5 | "semi": true,
6 | "singleQuote": true,
7 | "trailingComma": "all",
8 | "bracketSpacing": true,
9 | "jsxBracketSameLine": false,
10 | "fluid": true
11 | }
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Hacker News List Viewer
2 |
3 | ## Usage:
4 |
5 | To install (feel free to use `npm`):
6 |
7 | ```sh
8 | yarn install
9 | yarn start
10 | ```
11 |
12 | If you want to run the app in production mode use `yarn build` to build the production bundle and `npx serve dist` (or some other HTTP server) to serve the `dist` directory locally. The app can also be quickly edited on https://githubbox.com/ivancuric/hn-scroll.
13 |
14 | ## Config
15 |
16 | You can set `DEBUG_BATCHES = true` to see the batched updates visually in the DOM. `STORIES_UNDER_FOLD` controls the stories fetched outside the viewport. You can uncomment the background of the `#sentinel` in styles.scss to see the behaviour of the `intersectionObserver`.
17 |
18 | ## Choice of tools and technology
19 |
20 | The app was built in [CodeSandbox](https://codesandbox.io/s/github/ivancuric/hn-scroll) and then finished up locally to get the service worker up and running. The repo is available on [https://github.com/ivancuric/hn-scroll](https://github.com/ivancuric/hn-scroll).
21 |
22 | Taking into account the requirements and the evaluation criteria for the task I've decided to avoid using a JS framework and have written it in "vanilla" TypeScript.
23 |
24 | The app is written in a way to minimize re-rendering and to display content as soon as possible without the need to reorder it. The responses are batched by network responses, animation frames and finally batched into a single DOM update.
25 |
26 | I've also written it as an IIFE instead of a `class` to make it more readable with less `this.x` going around.
27 |
28 | The app is using [Workbox](https://developers.google.com/web/tools/workbox) and service workers to cache responses and provide offline functionality.
29 |
30 | The app has zero dependencies (without the service worker) and is under 2KB before gzipping, a lot of it being [Parcel](https://parceljs.org/)'s module loader. It also scores 100 on Lighthouse.
31 |
32 | ## Thoughts and potential for upgrades
33 |
34 | Since most of the logic is fetching data and batching network responses, it's easy to move to a JS framework like React.
35 |
36 | The same data fetching could be done in a service worker, but since the JS thread is doing very little work it would be overkill.
37 |
38 | I've thought about implementing a DOM node recycler, but there is no need in this case, since the entire DOM is taking a minimal amount of memory. In case one was needed a premade solution like [react-window](https://github.com/bvaughn/react-window) could be used.
39 |
40 | The app is built as a single component with an internal state and methods. The current implementation pipeline is built as a chain of side-effects working on the internal state:
41 | `fetchNextBatch => addToBatchPipeline => pushToRenderQueue => render => fetchNextBatch (optional)`
42 |
43 | Most of this could be decoupled but would add a lot of additional complexity.
44 |
45 | The design is very rudimentary (true to the original 😅). I didn't want to spend a lot of time desiging as it's not a part of the evaluation criteria, so I went with a GitHub markdown-like look.
46 |
47 | The UX is also a bit rough since there are no helpful error messages or loading indicators presented to the user. This is the area where most work could be done.
48 |
49 | The scrolling could be made non-blocking by using tombstones/placeholders. This would also allow for viewport-dependent fetching in batches and non-sequential fetches. This would also require compensation for viewport shifting, ideally using [FLIP](https://aerotwist.com/blog/flip-your-animations/).
50 |
51 | Aditionally, the fetching/rendering could potentially be a bit faster by listening to `scroll` events instead of `intersectionObserver`. Even though `scroll` events are passive, `intersectionObserver` uses `requestIdleCallback` under the hood, meaning that it's throttled down even more. This approach would also require additional work, like reacting to `resize` events.
52 |
--------------------------------------------------------------------------------
/REQUIREMENTS.md:
--------------------------------------------------------------------------------
1 | # Web Developer Test Project - Hacker News Reader
2 |
3 | You will be building a news reader app which displays the latest[ Hacker News](https://news.ycombinator.com/) stories. Your app will leverage the[ Hacker News API](https://github.com/HackerNews/API) to fetch data it requires.
4 |
5 | _Note: We will abbreviate Hacker News as HN for this project._
6 |
7 | ## Requirements
8 |
9 | The app will be a single page web app will the following features:
10 |
11 | - The app will display a list of the latest Hacker News stories in descending order from newest to oldest. Each list item should show the title (which should link to the story), author name, and posted time.
12 | - The targeted users for this app are exceptionally inpatient, so the app needs to display each HN list item _as soon as it has been fetched_. The resulting list will look like it is populating new items one-by-one, as seen below.
13 |
14 | 
15 |
16 | - The app should support infinite scroll (like Facebook News Feed). Specifically, when the user reaches the bottom of the page, the app should fetch earlier items and display them.
17 | - The app should support offline capability. Specifically, the user should be able to use the app offline to check out the list.
18 | - You can assume that the app will only be run on modern browsers, hence feel free to use latest web spec if needed.
19 |
20 | ## Evaluation Criteria
21 |
22 | - App performance. We will be looking at the wait times for reader to see the content — the shorter, the better.
23 | - We will evaluate your code’s quality. Does your code have good modular design and testability? Is it easy to read?
24 | - We prefer the project to be lightweight, and all dependencies should be well justified.
25 |
26 | ## Deliverable
27 |
28 | - Upload your test project as a public repository on Github. Please make sure to include a readme file with instructions on how to run your app.
29 | - Make sure your project is easy to setup. Your app should be ready to run after npm install.
30 | - Please spend no more than 12 hours on this task.
31 | - Test project will be compensated
32 |
--------------------------------------------------------------------------------
/image1.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ivancuric/hn-scroll/26099ba93463ede927034166db46badf9236649d/image1.gif
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |