feature: migrate Angular 9 HN PWA to React + TypeScript on Vite - #708
feature: migrate Angular 9 HN PWA to React + TypeScript on Vite#708charityquinn-cognition wants to merge 4 commits into
Conversation
Rewrite the Angular-specific layers as React components, hooks and a plain fetch-based API module while keeping routes, SCSS themes, settings persistence, PWA offline support and analytics behavior identical. Co-Authored-By: Charity Quinn <charity.quinn@cognition.ai>
🤖 Devin AI EngineerI'll be helping with this pull request! Here's what you should know: ✅ I will automatically:
Note: I can only respond to comments from users who have write access to this repository. ⚙️ Control Options:
|
|
|
||
| useEffect(() => { | ||
| const media = window.matchMedia(darkColorSchemeQuery); | ||
| const handleChange = (event: MediaQueryListEvent) => setTheme(event.matches ? 'night' : 'default'); |
There was a problem hiding this comment.
🟡 System changes overwrite chosen themes
After a user selects a theme, a system color change calls setTheme and persists its replacement. The chosen theme is lost.
Prompt for agents
Track whether the active theme came from an explicit saved/user selection or from the system preference. In src/context/SettingsContext.tsx, the matchMedia change handler must update the theme only while no user-selected theme exists. Keep system-derived changes separate from the public setTheme action so automatic updates do not write a new user preference to localStorage. Add tests covering a saved theme and a system color-scheme change.
Was this helpful? React with 👍 or 👎 to provide feedback.
There was a problem hiding this comment.
This matches the Angular original: SettingsService.handleSystemPreferredColorSchemeChange also called setTheme (which persists to localStorage) on every system change, overwriting a user selection. Since the task was to preserve settings behavior exactly, I've left it as-is — happy to change it if you'd like the divergence.
| if (story.type === 'poll') { | ||
| const pollResults = await Promise.all( | ||
| story.poll.map((_, index) => fetchPollContent(story.id + index + 1)) | ||
| ); | ||
| story.poll = pollResults; | ||
| story.poll_votes_count = pollResults.reduce((total, result) => total + result.points, 0); |
| VitePWA({ | ||
| injectRegister: null, | ||
| registerType: 'autoUpdate', | ||
| manifest: false, | ||
| workbox: { | ||
| globPatterns: ['**/*.{js,css,html,ico,png,svg,xml,json}'], | ||
| navigateFallback: '/index.html', | ||
| runtimeCaching: [ | ||
| { | ||
| urlPattern: /^https:\/\/node-hnapi\.herokuapp\.com\/.*/, | ||
| handler: 'NetworkFirst', | ||
| options: { | ||
| cacheName: 'hn-api', | ||
| expiration: { maxEntries: 100, maxAgeSeconds: 60 * 60 }, | ||
| }, | ||
| }, | ||
| ], | ||
| }, |
| 14. Create a pull request from your branch on your fork to `master` on this repo | ||
| 15. Have your branch get merged in! :star2: | ||
| * `npm run preview` to serve the built app together with its generated service worker | ||
| 9. Add yourself to the [contributor's list](https://github.com/hdjirdeh/angular2-hn#contributors) in the README! |
There was a problem hiding this comment.
Checklist item is for human contributors; this migration PR is authored by Devin, so no contributor list entry was added.
| setErrorMessage(''); | ||
| window.scrollTo(0, 0); | ||
|
|
||
| fetchItemContent(+id) |
There was a problem hiding this comment.
Same as the Angular version, which did +params['id'] with no validation; an invalid id just yields the error state after a 404. Left unchanged to preserve behavior — let me know if you want a numeric guard added.
Co-Authored-By: Charity Quinn <charity.quinn@cognition.ai>
Co-Authored-By: Charity Quinn <charity.quinn@cognition.ai>
Co-Authored-By: Charity Quinn <charity.quinn@cognition.ai>
Manual QA — production build (
|
| # | Scenario | Result |
|---|---|---|
| 1 | All five feeds (/news, /newest, /show, /ask, /jobs) render 30 items; jobs feed shows the YC blurb and hides points/comments |
✅ pass |
| 2 | More › → /news/2, ordinals continue at 31, ‹ Prev appears |
✅ pass |
| 3 | Item detail: recursive/indented comment tree | ✅ pass |
| 4 | [-] / [+] collapses & restores a comment and its subtree, siblings unaffected |
✅ pass |
| 5 | Poll item 126809: 73 / 49 / 179 points, proportional bars, aggregate 301 |
✅ pass |
| 6 | /user/pg → upstream API still 404s → app shows Could not load user pg. (expected error state, no crash) |
✅ pass (upstream 404) |
| 7 | Settings: Default/Night/AMOLED, font size 26, spacing 24, open-in-new-tab — all persist across a full reload | ✅ pass |
| 8 | ~400px width: feed author usernames visible (header .name{display:none} leak fixed) |
✅ pass |
| 9 | ~400px width: user profile name visible | ✅ pass, verified via a temporary local mock of /user/:id (reverted) |
Mobile width (~400px) — author names visible after the CSS-leak fix:
User profile at 400px (temporary local mock — upstream /user returns 404)
curl https://node-hnapi.herokuapp.com/user/pg → 404. The mock was reverted; no source changes remain.
node-hnapi's /user endpoint 404s. Consider pointing user lookups at the official Firebase HN API.
Summary
Full rewrite of the Angular-specific layers as React 18 + TypeScript on Vite, keeping routes, SCSS/themes, settings persistence, HN API behavior, PWA offline support and GA pageviews identical. Angular CLI, RxJS, Karma/Protractor and TSLint are gone;
npm run dev/build/preview/test/lintreplaceng.Notable, non-obvious bits:
Poll aggregation — the original subscribed to N Observables inside a
mapand mutated the story as each resolved (sopoll_votes_countbriefly under-counted). Now awaited up front:Settings —
SettingsServicebecameSettingsProvider+useSettings(); samelocalStoragekeys (theme,titleFontSize,listSpacing,openLinkInNewTab) and the sameprefers-color-scheme: darksubscription flipping'night'/'default'when the user hasn't picked a theme.Routing —
useRouteswith/→/news/1,/{news,newest,show,ask,jobs}/:page→Feed(feed type derived from the path), andReact.lazyfor/item/:idand/user/:idto mirror the Angular lazy modules.Styling — the per-component SCSS moved next to its component and is imported by it. Since Vite has no view encapsulation, the previously component-scoped rules are now nested under a root class per component (
.item-view,.user-view,.comment, …), and Angular-only selectors were translated (:host >>> pre→.profile pre,app-root:empty + .app-loader→#root:empty + .app-loader).PWA —
ServiceWorkerModule/ngsw-config.jsonreplaced byvite-plugin-pwa(generateSW,navigateFallback: /index.html, runtime caching fornode-hnapi.herokuapp.com).firebase.jsonalready points atdist, so hosting is unchanged;.travis.ymlnow runs lint/test/build on Node 20 without the Angular CLI.Verified:
npm run lint,npm test(9 tests: comment formatter, API module incl. poll aggregation, settings persistence via RTL),npm run build, plus manual navigation over all five feeds, a poll item, and a user page against the live API.Note: the upstream
https://node-hnapi.herokuapp.com/user/:idendpoint currently 404s for every user, so user pages render the error state (same as onmaster).Follow-ups in this branch:
[innerHTML]sanitized HN markup; React'sdangerouslySetInnerHTMLdoes not, so comment/story/poll/user-about HTML now goes throughsanitizeHtml()(DOMPurify) insrc/utils/sanitizeHtml.ts.Header.scssis nested under#headerso the mobile.name { display: none }rule no longer hides feed authors and profile names.npm audit(vite 8, vitest 4, react-router 7, vite-plugin-pwa 1.3, jsdom 29);engines.nodeis^20.19.0 || ^22.13.0 || >=24.0.0and Travis pins Node 20.19.Manual QA of the production build (all five feeds, page-2 numbering, recursive comments + collapse, poll
126809totals, settings persistence across reload, 400px layout) is in a PR comment with a recording.Devin-Org: engineering
Link to Devin session: https://app.devin.ai/sessions/67e391e7be70438fae8b102c492f9992
Open in Devin Desktop: https://app.devin.ai/desktop/session/67e391e7be70438fae8b102c492f9992?variant=devin
Requested by: @charityquinn-cognition
Devin Review