Skip to content

feature: repurpose Hacker News PWA into a 4% cashback card UI on Angular 20 - #516

Open
devin-ai-integration[bot] wants to merge 2 commits into
masterfrom
devin/1785347839-cashback-ui
Open

feature: repurpose Hacker News PWA into a 4% cashback card UI on Angular 20#516
devin-ai-integration[bot] wants to merge 2 commits into
masterfrom
devin/1785347839-cashback-ui

Conversation

@devin-ai-integration

@devin-ai-integration devin-ai-integration Bot commented Jul 29, 2026

Copy link
Copy Markdown

Summary

Replaces the Hacker News domain with a credit card cashback product ("Vantage 4% Cash Card") and moves the app from Angular 9 + modules + TSLint + Protractor to Angular 20 standalone/signals + ESLint + Playwright. The app shell that was worth keeping — lazy feature routes, the SettingsService theme engine, the centralized data service, and @angular/service-worker — is preserved; everything HN-specific (feeds/, item-details/, user/, hackernews-api.service.ts, unfetch/lazyFetch, rxjs-compat, the ga(...) calls) is gone.

The 4% rate exists in exactly one place and everything else derives from it, so the ledger, per-transaction amounts and the rewards breakdown can't disagree:

// shared/data/mock-cashback-data.ts
export const CASHBACK_RATE = 0.04;
export const calculateCashback = (amount: number, rate = CASHBACK_RATE) => Math.round(amount * rate * 100) / 100;

MOCK_TRANSACTIONS = MOCK_SPEND.map((s) => ({ ...s, cashbackRate: CASHBACK_RATE, cashbackEarned: calculateCashback(s.amount) }));

Data service — mocked today, HTTP tomorrow, without touching callers. environment.apiBaseUrl is '', so every method falls through to in-memory fixtures; set it and the same methods issue HttpClient requests:

private request<T>(path: string, mock: T): Observable<T> {
    return this.baseUrl ? this.http.get<T>(`${this.baseUrl}${path}`) : of(mock).pipe(delay(150));
}
// fetchCashbackRate() · fetchCardAccounts() · fetchTransactions() · fetchTransaction(id) · fetchRewardsSummary()

fetchRewardsSummary() is derived rather than stored: summarizeRewards(transactions) groups by SpendCategory, sums spend/cashback and computes each category's share of total cashback.

Signals over subscriptions. Components hold no RxJS subscriptions; shared/util/load-state.ts folds an observable into a three-state signal so every view gets loading/error handling for free:

type LoadState<T> = { status: 'loading' } | { status: 'loaded'; value: T } | { status: 'error'; error: unknown };

private readonly rewardsState = toLoadState(this.api.fetchRewardsSummary());
readonly loading = computed(() => isLoading(this.rewardsState(), this.rateState()));
readonly rewards = computed(() => loadedValue(this.rewardsState()));

SettingsService is now a single signal<Settings> with computed projections (theme, maskAmounts, titleFontSize, rowPadding), still persisted to localStorage and still honouring prefers-color-scheme. It gained maskAmounts — a card-appropriate privacy toggle that blanks every balance to •••• via the new amount pipe.

Routes (//dashboard, wildcard → /dashboard), each feature lazily loaded:

Route Content
/dashboard 4% headline, cash back earned, available to redeem, current balance, available credit, recent transactions
/transactions Full ledger with per-transaction cashback + rate, category chips filtering both the list and the totals
/transactions/:id $54.72 × 4% = $2.19 calculation breakdown (:id bound via withComponentInputBinding())
/rewards Category breakdown with share bars, total spend, redeemed vs. available
/account Card accounts, utilization, program terms, and the theme/font/spacing/masking controls

The settings overlay and the account page share one SettingsControlsComponent, so the theme engine has a single implementation.

Toolchain: Angular 20.3 / TypeScript 5.9 / RxJS 7.8; provideHttpClient(withFetch()), provideRouter(...), provideServiceWorker(...) in app.config.ts; SCSS migrated off deprecated @import to @use; assets moved to public/; Travis → GitHub Actions running lint + unit + e2e + build; Firebase hosting now points at dist/vantage-cashback/browser with no-cache on the ngsw files. Icons/favicon are generated from scripts/generate-icons.py so the brand mark has one source.

Verification

  • ng build (clean, no warnings), ng lint, 13 Karma/Jasmine specs, 5 Playwright specs — all green.
  • Verified in the browser on localhost:4200: every route renders the mocked data with 4% applied (e.g. $1,879.36 spend → $75.19 cash back), and the Default/Night/AMOLED themes still work.

Dashboard

Transactions

Rewards

Night theme

Link to Devin session: https://app.devin.ai/sessions/702cce9e35ca4feb869e1e2d4dffb55c
Requested by: @Colhodm


Devin Review

Status Commit
⚪ Not started

Run Devin Review

Open in Devin Review (Staging)

…on Angular 20

Co-Authored-By: Arjun Mishra <arjunsaxmishra@gmail.com>
@Colhodm Colhodm self-assigned this Jul 29, 2026
@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

…s are readable on light themes

Co-Authored-By: Arjun Mishra <arjunsaxmishra@gmail.com>
@devin-ai-integration

Copy link
Copy Markdown
Author

🧪 End-to-end test results — green after one fix

Drove the real UI on a local ng serve. Every dollar figure was recomputed independently from MOCK_SPEND rather than read back from the description.

ng build → exit 0, bundle in dist/vantage-cashback. ng serve → app on :4200.

✅ The 4% rate is applied correctly everywhere (14/14 rows verified)

/transactions renders all 14 rows and both totals ($1,879.36 spend / $75.19 cash back); every row equals round(amount × 0.04, 2) with zero mismatches. The Dining chip filters to 3 rows and recomputes both totals to $66.33 / $2.66, so the stat block genuinely follows the filter.

Transactions

/dashboard — $75.19 earned, $30.19 available ($45.00 redeemed), $1,602.95 balance, $14,397.05 credit:

Dashboard

/rewards — the 7 category rows sum to exactly $75.19 and the bars are proportional (Travel 100%, Shopping ~76%, Groceries ~29%):

Rewards

Also passing: /transactions/txn-1041 shows "$143.28 × 4% = $5.73"; /account cards with 11% / 8% utilization; Default → Night → AMOLED re-themes everything incl. the 4% hero and survives a reload; "Hide balances" masks all amounts to •••• while leaving the rate visible; font size / list spacing change row pitch ~42px → ~82px; / and unknown paths redirect to /dashboard; no console errors on any route.

🔴 Found and fixed: settings overlay was white-on-white in the Default theme

settings.component.scss hard-coded color: #fff on .popup h1 / .popup app-settings-controls, while the theme engine gives .popup the theme's $panel-background-color — white in Default. Every label in the dialog was invisible:

Broken overlay

Fixed in c14edff by dropping the hard-coded white so the popup inherits the themed $wrapper-color (the close button now uses color: inherit + opacity). Added a Playwright regression test that opens the overlay in all three themes and asserts its computed text colour differs from its background.

Fixed overlay

Not covered

Production bundle was built but not served/clicked through; no mobile-viewport pass; the environment.apiBaseUrl HTTP path is untested (no backend exists); only the Dining/All chips were clicked; the prefers-color-scheme auto-night path wasn't exercised.

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