Skip to content

feat: migrate from Angular 9 to React 19 infrastructure#274

Open
devin-ai-integration[bot] wants to merge 7 commits into
masterfrom
devin/1779402204-react-migration-infra
Open

feat: migrate from Angular 9 to React 19 infrastructure#274
devin-ai-integration[bot] wants to merge 7 commits into
masterfrom
devin/1779402204-react-migration-infra

Conversation

@devin-ai-integration
Copy link
Copy Markdown

@devin-ai-integration devin-ai-integration Bot commented May 21, 2026

Summary

Complete Angular 9 → React 19 infrastructure migration. Replaces the entire Angular frontend with:

  • React 19 + TypeScript 5.8 + Vite 6 (build tool)
  • React Router v7 — all 8 routes (news, newest, show, ask, jobs, item, user, root redirect)
  • TanStack Query v5 — data fetching with caching
  • React Context + useReducer — settings state management (theme, font size, link behavior)
  • SCSS Modules — preserves existing 3-theme system (default, night, AMOLED black)
  • Official HN Firebase API (https://hacker-news.firebaseio.com/v0/) — replaces defunct Heroku proxy
  • GitHub Actions CI — build + lint + typecheck + test (replaces Travis CI)

Key fixes from Devin Review:

  • Poll data fetching via item.parts field (commit 042629d)
  • Default theme set to 'default' instead of empty string to prevent FOUC (commit 46175fe)
  • Pagination "More" button uses hasMore flag from total IDs count instead of items.length === 30 (commit 46175fe)

Review & Testing Checklist for Human

  • Navigate to /news/1 and verify 30 stories load with live HN data (titles, points, usernames, time ago, domains)
  • Click a story's comment count — verify item detail page shows nested comments
  • Switch to Night theme in settings (cog icon) — verify dark background applies and persists after reload
  • Navigate to root / — verify redirect to /news/1
  • Check pagination on last page of a feed — verify "More ›" button hides correctly when no more stories exist

Notes

  • E2E testing completed with all 8 tests passing — see test results comment with screenshots and recording
  • Poll rendering code is in place but could not be verified end-to-end as no active poll items appeared on the HN front page during testing
  • Angular source files, Travis CI config, and Webpack config have been fully removed

Link to Devin session: https://app.devin.ai/sessions/a443e4df1fb14856b719f2d703bcb65c
Requested by: @matthewguerra-cog


Devin Review

Status Commit
🟢 Reviewed b6f4137
Open in Devin Review (Staging)

- Replace Angular 9 with React 19, Vite 6, TypeScript 5.8
- Add React Router v7 for client-side routing
- Add TanStack Query v5 for data fetching (replaces RxJS)
- Create API service layer using official HN Firebase API
- Port TypeScript interfaces from Angular models
- Create SettingsContext (replaces SettingsService)
- Port SCSS theme system (default, night, AMOLED black)
- Implement all page components (Feed, ItemDetails, User)
- Add shared components (Header, Footer, Settings, Loader, ErrorMessage)
- Add ESLint 9 + Prettier for code quality
- Add GitHub Actions CI (replaces Travis CI)
- Update Firebase config for Vite output
- Remove all Angular-specific files and dependencies

Co-Authored-By: Matthew Guerra <matthew.guerra@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 and CI monitoring

Co-Authored-By: Matthew Guerra <matthew.guerra@cognition.ai>
devin-ai-integration[bot]

This comment was marked as resolved.

Co-Authored-By: Matthew Guerra <matthew.guerra@cognition.ai>
@devin-ai-integration
Copy link
Copy Markdown
Author

E2E Test Results — React HN Infrastructure Migration

Ran the app locally (npm run dev on port 4200) against the live HN Firebase API, testing all core flows end-to-end via browser interactions.

Result: All 8 tests passed.

Core Flow Tests
Test Result
Feed page loads with 30 live HN stories passed
Feed navigation between types (news → show) passed
Pagination to page 2 (numbering starts at 31) passed
Item detail page with deeply nested comments passed
User profile page (username, karma★, created date) passed
Settings panel — theme switching (Default → Night) passed
Settings persistence after page reload passed
Root / redirect to /news/1 passed
Screenshots

Feed Page — 30 stories with live data

Feed

Pagination — page 2, <ol start="31">

Pagination

Item Detail — nested comments (3+ levels)

Item Detail

User Profile

User Profile

Theme Switching (Default → Night)

Default Night
Default Night

Settings Persistence After Reload

Persisted Night Theme

Root Redirect → /news/1

Root Redirect

Notes
  • Tested against live HN Firebase API (https://hacker-news.firebaseio.com/v0/)
  • Poll data fetching fix (commit 042629d) was verified at the code level; no active poll items appeared on the front page during testing
  • Root div className verified via browser console for theme tests
  • <ol start> attribute verified via browser console for pagination test
  • Devin session

devin-ai-integration[bot]

This comment was marked as resolved.

- Set theme default to 'default' instead of '' to avoid flash of
  unstyled content on first visit (Devin Review)
- Return hasMore flag from fetchFeed based on total IDs count instead
  of relying on items.length === 30, which breaks when deleted items
  are filtered out (Devin Review)

Co-Authored-By: Matthew Guerra <matthew.guerra@cognition.ai>
devin-ai-integration[bot]

This comment was marked as resolved.

The useEffect guard checked settings.theme which is always truthy
('default'), making the system color scheme detection dead code.
Check localStorage.getItem('theme') directly so first-time users
with no saved preference get the system dark/light mode applied.

Co-Authored-By: Matthew Guerra <matthew.guerra@cognition.ai>
devin-ai-integration[bot]

This comment was marked as resolved.

…division by zero

- Add APPLY_SYSTEM_THEME reducer action that updates state without
  writing to localStorage, so system dark mode detection stays active
  across page reloads until the user explicitly picks a theme
- Guard poll bar width calculation against division by zero when
  poll_votes_count is 0

Co-Authored-By: Matthew Guerra <matthew.guerra@cognition.ai>
devin-ai-integration[bot]

This comment was marked as resolved.

…ar 'second'

- Check localStorage inside media query change handler so an explicit
  user theme choice stops system theme from overriding it mid-session
- Fix '1 seconds ago' → '1 second ago' for consistency with other units

Co-Authored-By: Matthew Guerra <matthew.guerra@cognition.ai>
Copy link
Copy Markdown

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

Choose a reason for hiding this comment

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

Devin Review found 8 new potential issues.

View 4 additional findings in Devin Review.

Open in Devin Review (Staging)
Debug

Playground

<span className={styles.time}> {comment.time_ago}</span>
</div>
<div className={styles.commentTree}>
<p className={styles.commentText} dangerouslySetInnerHTML={{ __html: comment.content }} />
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔴 Untrusted Hacker News HTML is injected without sanitization

The React port renders Hacker News comment text with dangerouslySetInnerHTML, and the same pattern is used for poll/story text and user profiles (src/pages/ItemDetailsPage.tsx:136, src/pages/ItemDetailsPage.tsx:149, src/pages/UserPage.tsx:37). Those fields come from remote user-generated content via fetchItemContent/fetchUser, and React does not sanitize dangerouslySetInnerHTML, so unsafe markup such as event handlers or unsafe links returned by the API can execute in this app’s origin. The previous Angular [innerHTML] bindings were sanitized by Angular by default, so this is a security regression introduced by the migration.

Prompt for agents
Sanitize all remote HTML before rendering it in React. The affected sinks are CommentItem in src/pages/ItemDetailsPage.tsx, poll result content, story/item content, and user about text in src/pages/UserPage.tsx. Introduce a shared sanitizer/HTML rendering helper that whitelists the small subset of HTML needed for Hacker News content, and use it consistently anywhere dangerouslySetInnerHTML is currently used. Alternatively, render these fields as escaped text if preserving formatting is not required.
Open in Devin Review (Staging)

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

Debug

Playground

Comment on lines +143 to +151
const items = await Promise.all(ids.map((id) => fetchItem(id)));

const comments = await Promise.all(
items
.filter((item): item is HNItem => item !== null)
.map(async (item) => {
const childComments =
item.kids && item.kids.length > 0
? await fetchComments(item.kids, level + 1)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔴 Item pages block on fetching every descendant comment

Opening any item with comments now waits for fetchComments to recursively fetch every child thread via nested Promise.all calls before the query resolves. For popular Hacker News posts this can fan out into hundreds or thousands of browser requests; a single failed descendant request rejects the whole item query, so the item page remains on the loader for a long time or falls back to “Could not load item comments” even though the story itself loaded. This is a regression from the previous single aggregated item API call, and it is triggered by fetchItemContent awaiting fetchComments(item.kids) before returning the story at src/services/hackernews-api.ts:124-125.

Prompt for agents
The React migration switched item details from the aggregated node-hnapi endpoint to recursively fetching every Hacker News Firebase comment in src/services/hackernews-api.ts. This makes ItemDetailsPage wait for all descendants before rendering and creates unbounded concurrent request fan-out. Rework the item-details data path so the story can render without waiting for the entire comment tree and so comment fetching is bounded/resilient. Possible approaches include using an aggregated API endpoint again, fetching top-level comments first and loading children progressively, limiting concurrency, and handling individual comment fetch failures without rejecting the whole item query.
Open in Devin Review (Staging)

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

Debug

Playground

Comment thread src/app/app.module.ts
Comment on lines -25 to -27
ServiceWorkerModule.register('ngsw-worker.js', {
enabled: environment.production,
}),
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 Offline/PWA support was removed without a replacement

The deleted Angular bootstrap registered the service worker in production, and the PR also deletes ngsw-config.json without adding any Vite/React service-worker registration. The repository advertises offline support in README.md:44-46, so production builds will no longer install a service worker or cache the app shell/content for offline use.

Prompt for agents
Restore the production PWA/offline behavior that was provided by ServiceWorkerModule.register and ngsw-config.json. In the React/Vite app, add an equivalent service worker/PWA setup, include the manifest and static assets, and register it only for production builds. Verify that the Firebase-hosted build still loads previously visited pages/assets offline as described in README.md.
Open in Devin Review (Staging)

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

Debug

Playground

Comment thread src/styles/globals.scss
Comment on lines +14 to +16
pre {
white-space: pre-wrap;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 Responsive mobile/laptop sections are never hidden

The React pages still render elements with global mobile and laptop classes (src/pages/ItemDetailsPage.tsx:72-94, src/pages/UserPage.tsx:24-28), but the PR deleted the old component SCSS rules that hid .mobile on desktop and .laptop on mobile, and the new global stylesheet only defines pre. As a result, item pages render both the mobile header and the desktop header on all viewport sizes, and user pages show the mobile back header on desktop.

Suggested change
pre {
white-space: pre-wrap;
}
pre {
white-space: pre-wrap;
}
@media only screen and (max-width: 768px) {
.laptop {
display: none;
}
}
@media only screen and (min-width: 769px) {
.mobile {
display: none;
}
}
Open in Devin Review (Staging)

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

Debug

Playground

Comment thread .travis.yml
Comment on lines -16 to -18
after_success:
- firebase use default
- firebase deploy --token $FIREBASE_TOKEN
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 Replacing Travis CI drops Firebase deployment

The removed Travis job deployed to Firebase after a successful master build using firebase deploy --token $FIREBASE_TOKEN, but the new GitHub Actions workflow only runs typecheck/lint/build/test (.github/workflows/ci.yml:21-33) and has no deployment step. After this PR, pushes to master can pass CI but the Firebase-hosted app will never be updated automatically.

Prompt for agents
If GitHub Actions is replacing Travis for this repository, port the existing Firebase deployment behavior into the new workflow instead of dropping it. Add an authenticated deploy job or step that runs only after successful master builds, using GitHub Actions secrets for the Firebase token/service account and preserving the previous hosting target.
Open in Devin Review (Staging)

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

Debug

Playground

Comment thread src/pages/FeedPage.tsx
Comment on lines +123 to +130
))}
</ol>
<div className="nav">
{listStart !== 1 && (
<Link to={`/${feedType}/${pageNum - 1}`} className={styles.prev}>
‹ Prev
</Link>
)}
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 changes no longer reset scroll position

The old feed component scrolled to the top after each feed fetch (src/app/feeds/feed/feed.component.ts:42-45) and item details did so on initialization (src/app/item-details/item-details.component.ts:32-39). The new pagination links only change the React Router location, and there is no ScrollRestoration/scroll-to-top handler anywhere in the new app, so clicking “More” from the bottom of a feed or opening an item can leave the user at the old scroll offset on the new page.

Prompt for agents
Add scroll restoration for SPA route changes in the React app. A small component mounted under BrowserRouter can call window.scrollTo(0, 0) whenever the pathname/search changes, or the feed pagination/item navigation handlers can explicitly reset scroll. Ensure the behavior matches the old Angular pages, especially feed pagination and item/user detail navigation.
Open in Devin Review (Staging)

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

Debug

Playground

Comment thread package.json
Comment on lines +7 to +13
"dev": "vite",
"build": "tsc -b && vite build",
"preview": "vite preview",
"lint": "eslint .",
"typecheck": "tsc -b --noEmit",
"test": "vitest run",
"test:watch": "vitest"
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔴 npm start no longer works despite contributor setup requiring it

CONTRIBUTING.md:14 explicitly tells contributors to run npm start and open localhost:4200, but the rewritten scripts only expose dev, build, preview, etc. Running npm start now fails with npm's “Missing script: start”, so the repository's mandatory setup instructions are broken by this PR.

Suggested change
"dev": "vite",
"build": "tsc -b && vite build",
"preview": "vite preview",
"lint": "eslint .",
"typecheck": "tsc -b --noEmit",
"test": "vitest run",
"test:watch": "vitest"
"dev": "vite",
"start": "vite",
"build": "tsc -b && vite build",
"preview": "vite preview",
"lint": "eslint .",
"typecheck": "tsc -b --noEmit",
"test": "vitest run",
"test:watch": "vitest"
Open in Devin Review (Staging)

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

Debug

Playground

Comment on lines +74 to +105
function mapItemToStory(item: HNItem): Story {
return {
id: item.id,
title: item.title ?? '',
points: item.score ?? 0,
user: item.by ?? '',
time: item.time ?? 0,
time_ago: item.time ? timeAgo(item.time) : '',
type: item.type === 'job' ? 'job' : item.type === 'poll' ? 'poll' : 'story',
url: item.url ?? '',
domain: extractDomain(item.url),
comments: [],
comments_count: item.descendants ?? 0,
poll: [],
poll_votes_count: 0,
content: item.text ?? '',
deleted: item.deleted ?? false,
dead: item.dead ?? false,
};
}

export interface FeedResult {
items: Story[];
hasMore: boolean;
}

export async function fetchFeed(feedType: string, page: number): Promise<FeedResult> {
const endpoint = FEED_ENDPOINTS[feedType] ?? 'topstories';
const ids = await fetchJSON<number[]>(`${BASE_URL}/${endpoint}.json`);

const start = (page - 1) * ITEMS_PER_PAGE;
const end = start + ITEMS_PER_PAGE;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🚩 Direct Firebase API changes the data contract from the previous HN API

The old service used https://node-hnapi.herokuapp.com, which returned hydrated feed/item/user objects, while the new service maps raw Firebase REST records into the old app types (src/services/hackernews-api.ts:74-92). This is intentional for the migration and most fields are covered, but behavior differs in subtle ways: deleted/dead stories are not filtered out, time_ago is computed only at fetch time and then cached by React Query, and comments_count depends on Firebase descendants. These are not clear bugs without product expectations, but reviewers should be aware that the API swap is not just a transport change.

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