Skip to content

feature: migrate Angular HN PWA to Vite + React + TypeScript#320

Open
devin-ai-integration[bot] wants to merge 1 commit into
masterfrom
devin/1781097687-migrate-to-react
Open

feature: migrate Angular HN PWA to Vite + React + TypeScript#320
devin-ai-integration[bot] wants to merge 1 commit into
masterfrom
devin/1781097687-migrate-to-react

Conversation

@devin-ai-integration

@devin-ai-integration devin-ai-integration Bot commented Jun 10, 2026

Copy link
Copy Markdown

Summary

Replaces the Angular 9 Hacker News PWA with an equivalent Vite + React 18 + TypeScript app, preserving all routes, theming, settings, PWA behavior, and the node-hnapi.herokuapp.com data source. All Angular scaffolding (angular.json, karma, protractor, src/app/**, ngsw-config.json, ejected build) is removed; icons/SVGs are kept (moved to public/assets).

Architecture mapping:

Angular React
HackerNewsAPIService (RxJS Observable + unfetch) src/api/hackernews.ts (async fetch) + @tanstack/react-query hooks in src/hooks/useHackerNews.ts
SettingsService (DI singleton) src/context/SettingsContext.tsx (SettingsProvider + useSettings)
app.routes.ts (modules, lazy loadChildren) src/router.tsx (React Router v6, React.lazy for item/user)
CommentPipe formatCommentCount in src/utils/index.ts
component .scss (:host, ngStyle) consolidated global SCSS in src/styles/

Behavior preserved deliberately:

  • Poll handling is ported verbatim — fetchItemContent sequentially fetches story.id + i for each option and sums poll_votes_count.
  • SettingsContext reads the same localStorage keys (openLinkInNewTab, titleFontSize, listSpacing, theme) and keeps the prefers-color-scheme: dark listener that flips theme to night/default when no theme is saved.
  • The theme engine (default / night / amoledblack) is kept as global descendant selectors (.<theme> .wrapper …), with the theme class applied to the root div in App.tsx from useSettings().theme — so CSS Modules were intentionally not used (they would break these selectors).
  • Routes mirror the original: //news/1, /:feed/:page for news/newest/show/ask/jobs, /item/:id, /user/:id.

PWA: @angular/service-worker + ngsw-config.json replaced by vite-plugin-pwa (Workbox generateSW) configured in vite.config.ts, including a NetworkFirst runtime cache for the HN API and a generated web manifest.

package.json scripts (dev/build/lint/typecheck/test) are kept to match the repo blueprint. build, lint, and typecheck pass; verified rendering against the live API (feed loads, theming applies).

Testing

  • npm run build ✅ (only Sass @import/darken deprecation warnings)
  • npm run typecheck
  • npm run lint
  • Loaded /news/1 in the browser against the live API — stories, header nav, and day theme render correctly.

Link to Devin session: https://app.devin.ai/sessions/5742358eeb0245c9823e9812addb0e55
Requested by: @schaudhry123


Devin Review

Status Commit
🟢 Reviewed 96cbb9a
Open in Devin Review (Staging)

Co-Authored-By: Samir Chaudhry <samir@cognition.ai>
@devin-ai-integration

Copy link
Copy Markdown
Author

🤖 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

@staging-devin-ai-integration staging-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.

Devin Review found 2 potential issues.

Open in Devin Review (Staging)
Debug

Playground

Comment on lines +29 to +35
return {
showSettings: false,
openLinkInNewTab: openLinkInNewTab ? JSON.parse(openLinkInNewTab) : false,
theme: 'default',
titleFontSize: titleFontSize ? titleFontSize : '16',
listSpacing: listSpacing ? listSpacing : '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.

🟡 Theme not read from localStorage synchronously, causing visible flash on page load

The readInitialSettings function at src/context/SettingsContext.tsx:24-36 reads openLinkInNewTab, titleFontSize, and listSpacing from localStorage synchronously, but hardcodes theme: 'default' (line 32). The actual saved theme is only restored later in a useEffect (line 47-65), which runs after the first paint. Since the App component at src/App.tsx:11 renders <div className={theme}> which controls all theming CSS, users who have selected 'night' or 'amoledblack' will see a flash of the light/default theme on every page load. This is a regression from the Angular version where the theme was initialized synchronously in the service constructor before rendering.

Suggested change
return {
showSettings: false,
openLinkInNewTab: openLinkInNewTab ? JSON.parse(openLinkInNewTab) : false,
theme: 'default',
titleFontSize: titleFontSize ? titleFontSize : '16',
listSpacing: listSpacing ? listSpacing : '0',
};
const savedTheme = localStorage.getItem('theme');
const systemDark = !savedTheme && window.matchMedia('(prefers-color-scheme: dark)').matches;
return {
showSettings: false,
openLinkInNewTab: openLinkInNewTab ? JSON.parse(openLinkInNewTab) : false,
theme: savedTheme || (systemDark ? 'night' : 'default'),
titleFontSize: titleFontSize ? titleFontSize : '16',
listSpacing: listSpacing ? listSpacing : '0',
};
Open in Devin Review (Staging)

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

Debug

Playground

Comment on lines +47 to +65
useEffect(() => {
const darkColorSchemeMedia = window.matchMedia('(prefers-color-scheme: dark)');

const handleChange = (event: MediaQueryListEvent | MediaQueryList) => {
setTheme(event.matches ? 'night' : 'default');
};

const savedTheme = localStorage.getItem('theme');
if (savedTheme) {
setSettings((prev) => ({ ...prev, theme: savedTheme }));
} else {
handleChange(darkColorSchemeMedia);
}

darkColorSchemeMedia.addEventListener('change', handleChange);
return () => {
darkColorSchemeMedia.removeEventListener('change', handleChange);
};
}, [setTheme]);

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 color scheme listener persists theme to localStorage on first detection

In src/context/SettingsContext.tsx:57-58, when no saved theme exists, handleChange(darkColorSchemeMedia) calls setTheme (line 51), which writes to localStorage (line 43). This means the first system-preference-based theme choice gets persisted, so subsequent loads will use the saved value rather than continuing to follow the system preference. The same setTheme call happens when the event listener fires (line 61), so any runtime system theme change also gets persisted. This behavior is inherited from the original Angular SettingsService which had the same pattern, so it's not a regression, but it's arguably unexpected — users who never explicitly choose a theme might expect the app to always follow their OS preference.

Open in Devin Review (Staging)

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

Debug

Playground

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