Skip to content

feature: migrate Angular 9 HN PWA to React + TypeScript on Vite - #708

Open
charityquinn-cognition wants to merge 4 commits into
masterfrom
devin/1788163101-react-migration
Open

feature: migrate Angular 9 HN PWA to React + TypeScript on Vite#708
charityquinn-cognition wants to merge 4 commits into
masterfrom
devin/1788163101-react-migration

Conversation

@charityquinn-cognition

@charityquinn-cognition charityquinn-cognition commented Aug 31, 2026

Copy link
Copy Markdown

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/lint replace ng.

Notable, non-obvious bits:

Poll aggregation — the original subscribed to N Observables inside a map and mutated the story as each resolved (so poll_votes_count briefly under-counted). Now awaited up front:

if (story.type === 'poll') {
    const results = await Promise.all(story.poll.map((_, i) => fetchPollContent(story.id + i + 1)));
    story.poll = results;
    story.poll_votes_count = results.reduce((t, r) => t + r.points, 0);
}

SettingsSettingsService became SettingsProvider + useSettings(); same localStorage keys (theme, titleFontSize, listSpacing, openLinkInNewTab) and the same prefers-color-scheme: dark subscription flipping 'night'/'default' when the user hasn't picked a theme.

RoutinguseRoutes with //news/1, /{news,newest,show,ask,jobs}/:pageFeed (feed type derived from the path), and React.lazy for /item/:id and /user/:id to 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).

PWAServiceWorkerModule/ngsw-config.json replaced by vite-plugin-pwa (generateSW, navigateFallback: /index.html, runtime caching for node-hnapi.herokuapp.com). firebase.json already points at dist, so hosting is unchanged; .travis.yml now 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.

news feed

poll item

Note: the upstream https://node-hnapi.herokuapp.com/user/:id endpoint currently 404s for every user, so user pages render the error state (same as on master).

Follow-ups in this branch:

  • Sanitization — Angular's [innerHTML] sanitized HN markup; React's dangerouslySetInnerHTML does not, so comment/story/poll/user-about HTML now goes through sanitizeHtml() (DOMPurify) in src/utils/sanitizeHtml.ts.
  • Header CSS scopingHeader.scss is nested under #header so the mobile .name { display: none } rule no longer hides feed authors and profile names.
  • Dependencies — bumped to versions clean under Snyk/npm audit (vite 8, vitest 4, react-router 7, vite-plugin-pwa 1.3, jsdom 29); engines.node is ^20.19.0 || ^22.13.0 || >=24.0.0 and Travis pins Node 20.19.

Manual QA of the production build (all five feeds, page-2 numbering, recursive comments + collapse, poll 126809 totals, 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

Status Commit
⚪ Not started

Run Devin Review

Devin Review (Staging)
Devin Review

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-integration

Copy link
Copy Markdown

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR. Add '(aside)' to your comment to have me ignore it.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment, CI, and merge conflict monitoring

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note

This report is out of date. Scroll down for Devin Review's latest report on this PR.

Devin Review found 7 potential issues.

Devin Review


useEffect(() => {
const media = window.matchMedia(darkColorSchemeQuery);
const handleChange = (event: MediaQueryListEvent) => setTheme(event.matches ? 'night' : 'default');

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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.
Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread src/components/core/Header.tsx
Comment on lines +23 to +28
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);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📝 Info: Poll aggregation preserves prior behavior

Live poll options still map to adjacent item IDs. Awaiting all option requests preserves order and avoids exposing partially aggregated totals.

Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment thread vite.config.ts
Comment on lines +8 to +25
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 },
},
},
],
},

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📝 Info: Offline shell includes route chunks

The production service worker precaches both lazy route chunks and static assets. Firebase and Workbox route deep-link navigations to the app shell.

Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment thread CONTRIBUTING.md
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!

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔍 Contributor credit remains missing

The contribution checklist requires adding the contributor to the README list. This pull request updates that checklist without adding its contributor.

Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Checklist item is for human contributors; this migration PR is authored by Devin, so no contributor list entry was added.

Comment thread src/components/item-details/Comment.tsx Outdated
setErrorMessage('');
window.scrollTo(0, 0);

fetchItemContent(+id)

@devin-ai-integration devin-ai-integration Bot Aug 31, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟨 Route parameters lack validation

Arbitrary item identifiers reach fetchItemContent after unchecked numeric conversion. Invalid routes trigger unintended remote requests and cached error responses.

Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>
devin-ai-integration[bot]

This comment was marked as resolved.

Co-Authored-By: Charity Quinn <charity.quinn@cognition.ai>
devin-ai-integration[bot]

This comment was marked as resolved.

Co-Authored-By: Charity Quinn <charity.quinn@cognition.ai>
@devin-ai-integration

Copy link
Copy Markdown

Manual QA — production build (npm run build + vite preview on :4200)

Navigated the built bundle in a real browser against the live node-hnapi API. No functional regressions found.

# 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:

Mobile feed with author names

Poll rendering (item 126809)

Poll options with points and proportional bars

Settings persistence after full reload

Settings persisted after reload

User profile at 400px (temporary local mock — upstream /user returns 404)

Profile pg at mobile width

curl https://node-hnapi.herokuapp.com/user/pg404. The mock was reverted; no source changes remain.

Full run recording

Production build walkthrough

⚠️ Note for reviewers: the user profile happy path cannot be exercised against production data while node-hnapi's /user endpoint 404s. Consider pointing user lookups at the official Firebase HN API.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant