Skip to content

Commit 189f752

Browse files
authored
Merge pull request #11 from maximcoding/feature/hn-news-reader
feat: HN news reader with offline-first feed and premium UI
2 parents 5f9803d + e6ab8cf commit 189f752

98 files changed

Lines changed: 1820 additions & 2098 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.claude/CLAUDE.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ src/
2929
├── shared/ # Cross-cutting code — must NOT import from features
3030
│ ├── components/ui/ # Button, Text, ScreenWrapper, …
3131
│ ├── hooks/
32-
│ ├── constants/ # shared non-config constants (not storage keys)
32+
│ ├── constants/ # shared non-config constants (not storage keys; tag arrays belong in feature api/keys.ts)
3333
│ ├── services/api|storage/
3434
│ ├── stores/
3535
│ ├── theme/

.claude/agents/code-reviewer.md

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -33,20 +33,28 @@ You are a senior code reviewer for this React Native TypeScript project. Your st
3333
- [ ] Strict mode compliance — no implicit `any`, no untyped function returns on exported APIs
3434
- [ ] Zod schemas validate every API response in feature services before returning domain models
3535

36+
### Code Quality
37+
- [ ] No magic numbers in logic — numeric literals with meaning (timeouts, limits, sizes, counts, offsets) must be named constants defined at module or config level
38+
- [ ] No magic strings in logic — non-i18n string literals used as identifiers, keys, or config values must be named constants (e.g. storage keys from `src/config/constants.ts`, route names from `src/navigation/routes.ts`)
39+
- [ ] Functions do one thing — no function exceeds ~40 lines or mixes concerns (data fetching + transformation + UI logic); extract helpers when a function grows beyond a single clear responsibility
40+
- [ ] No deeply nested callbacks or conditionals — flatten with early returns and extracted helpers
41+
3642
### UI & Styling
3743
- [ ] All screens use `ScreenWrapper` as root element
38-
- [ ] No raw hex colors, numeric spacing, or font sizes — `useTheme()` tokens only
39-
- [ ] `StyleSheet.create()` used; inline styles only for dynamically computed values
44+
- [ ] No raw hex colors, numeric spacing, or font sizes — `useTheme()` tokens only; brand colors via `theme.brand.*`
45+
- [ ] `StyleSheet.create()` used; inline styles only for values that are dynamically computed at render time — not for static overrides
46+
- [ ] Repeated style patterns extracted into shared `StyleSheet` entries or shared style constants — no copy-pasted style blocks across components
4047
- [ ] Shared UI components accept strings as props — no hardcoded user-facing copy
4148

4249
### i18n
43-
- [ ] All user-facing strings use `useT('<feature>')` with the correct per-feature namespace
44-
- [ ] Namespace matches the feature directory name (lowercase)
50+
- [ ] All user-facing strings use `useT()` (no argument) and access keys via `t('feature.key')`
51+
- [ ] Key prefix matches the feature directory name (lowercase)
4552

4653
### State & Data
4754
- [ ] Server state via React Query; local UI state via `useState`; global UI state via Zustand in `src/shared/stores/`
4855
- [ ] Query keys defined in feature `api/keys.ts` using `[feature, entity, id?, params?]` format
49-
- [ ] Mutations include `meta.tags` for targeted invalidation
56+
- [ ] Tag arrays (e.g. `AUTH_SESSION_TAGS`) exported from the feature's `api/keys.ts` — not defined inline in hooks or placed in `src/shared/constants/`
57+
- [ ] Mutations include `meta.tags` for targeted invalidation; invalidation uses `invalidateByTags` with the feature's own `tagMap` only — no cross-feature tagMap references
5058
- [ ] MMKV key strings imported from `src/config/constants.ts`, not hardcoded
5159

5260
### React Native Specifics

.claude/agents/rn-architect.md

Lines changed: 0 additions & 229 deletions
This file was deleted.

.claude/rules/navigation.md

Lines changed: 63 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,19 @@ Global rules: [AGENTS.md](../../AGENTS.md). Claude stack summary: [CLAUDE.md](..
77
# Rules — navigation
88

99
## Structure
10-
- **Root stack** (`src/navigation/root/root-navigator.tsx`): `ROOT_ONBOARDING`, `ROOT_AUTH`, `ROOT_APP`.
11-
- **Bootstrap:** `src/session/useBootstrapRoute.ts` determines the initial route.
12-
- **Routes:** all constants in `src/navigation/routes.ts` — never inline route strings.
13-
- **ParamLists:** per-feature at `src/features/<name>/navigation/param-list.ts`; root shell at `src/navigation/root-param-list.ts`.
14-
- **`NavigationContainer`** lives inside `src/navigation/NavigationRoot.tsx` — do not add a second one.
10+
- **Navigator** (`src/navigation/root/root-navigator.tsx`): JSX `<Stack.Navigator>` + `<Stack.Screen>` pattern. Export `RootNavigator` (the root stack component) and `HomeTabs` (tab navigator component). Create navigators at module level: `const Stack = createNativeStackNavigator<RootStackParamList>()`.
11+
- **Root entry** `src/navigation/NavigationRoot.tsx`: single `<NavigationContainer>` with `ref={navigationRef}` — render exactly once, no second instance.
12+
- **Initial route:** `useInitialRoute()` from `src/session/useInitialRoute.ts` — sync `useState` initializer that reads MMKV once, resolves `ROOT_ONBOARDING | ROOT_AUTH | ROOT_APP`.
13+
- **Routes:** all constants in `src/navigation/routes.ts` — never inline route string literals anywhere.
14+
- **ParamLists:** `RootStackParamList` and `HomeTabParamList` in `src/navigation/root-param-list.ts`; declared manually to avoid circular deps. Global augmentation declared once there:
15+
```ts
16+
declare global {
17+
namespace ReactNavigation {
18+
interface RootParamList extends RootStackParamList {}
19+
}
20+
}
21+
```
22+
- **Imperative navigation** outside the React tree: use `navigationRef` from `src/navigation/helpers/navigation-helpers.ts`.
1523

1624
## Provider order (must match `App.tsx`)
1725
```
@@ -22,18 +30,61 @@ GestureHandlerRootView
2230
ErrorBoundary
2331
QueryProvider
2432
OfflineBanner
25-
NavigationRoot
33+
NavigationRoot ← contains NavigationContainer + RootNavigator
2634
```
2735

36+
## Dynamic navigator rules (React Navigation v7)
37+
- Create navigator instances (`const Stack = createNativeStackNavigator<T>()`) at module scope — not inside components.
38+
- Add new screens as `<Stack.Screen name={ROUTES.X} component={ScreenComponent} />` inside the appropriate navigator JSX.
39+
- Share `screenOptions` via the `screenOptions` prop on `<Stack.Navigator>` — do not repeat options on individual screens if they apply to all.
40+
- For auth gating or feature flags: conditionally render `<Stack.Screen>` in JSX — do not call `navigation.navigate()` after auth state changes.
41+
- Nested navigators (e.g. tabs inside a stack screen): create a dedicated component (e.g. `HomeTabs`) and register it as the screen component.
42+
43+
## Params
44+
- Params must be **JSON-serializable** — required for state persistence and deep linking.
45+
- Pass **IDs only**, never full data objects. Fetch data from React Query cache using the ID inside the screen.
46+
- Do not use reserved param keys: `screen`, `params`, `initial`, `state`.
47+
- To update params from within a screen: `navigation.setParams({...})` (merge) or `navigation.replaceParams({...})` (replace).
48+
- To pass data back to a previous screen: `navigation.popTo('ScreenName', { result })`.
49+
- To navigate to a nested screen with params: `navigation.navigate('Parent', { screen: 'Child', params: { id } })`.
50+
51+
## Navigation actions
52+
- `navigate()` — standard transition; no-op if already on that screen (safe, no duplication).
53+
- `push()` — forces a new instance of the same screen; use when multiple instances are needed.
54+
- `goBack()` — standard back; hardware back and swipe gestures call this automatically.
55+
- `popTo('ScreenName')` — jump back to a specific screen, skipping intermediates.
56+
- `popToTop()` — reset a stack to its root screen.
57+
58+
## Screen lifecycle
59+
- Use `useFocusEffect(useCallback(() => { ... return cleanup }, []))` for side effects that must run on focus (data refresh, analytics). Always return a cleanup function.
60+
- Use `useIsFocused()` when a component must re-render on focus state change.
61+
- Use `navigation.addListener('focus' | 'blur', cb)` inside `useEffect` — always return the unsubscribe result.
62+
- Do not use plain `useEffect` for focus-aware operations without listener subscriptions.
63+
64+
## TypeScript
65+
- ParamList entries must use `type`, never `interface`.
66+
- Do not annotate `useNavigation<T>()` with a specific type — declare the global `RootParamList` instead.
67+
- Do not annotate `useRoute<T>()` — use `route` from screen props (`NativeStackScreenProps<RootStackParamList, 'RouteName'>`).
68+
- Use `CompositeScreenProps` for screens inside nested navigators that need access to parent navigation.
69+
- Use `NavigatorScreenParams<ChildParamList>` when a parent ParamList entry wraps a nested navigator.
70+
2871
## Half-sheet modals
29-
Use `presentation: 'transparentModal'`, `animation: 'none'`, `gestureEnabled: false` (see `HALF_SHEET_OPTIONS` in `root-navigator.tsx`).
72+
- Register with `presentation: 'transparentModal'`, `animation: 'none'`, `gestureEnabled: false` (see `HALF_SHEET_OPTIONS` in `root-navigator.tsx`).
73+
- Use `HalfSheet` from `src/shared/components/ui/HalfSheet.tsx` as the content wrapper.
3074

3175
## Must
32-
- All route constants from `src/navigation/routes.ts` — never use inline string literals for routes.
33-
- New screens: register in `routes.ts` and add the ParamList entry to the feature's `navigation/param-list.ts`.
34-
- Use `navigationRef` from `src/navigation/helpers/navigation-helpers.ts` for imperative navigation outside React tree.
76+
- All route constants from `src/navigation/routes.ts` — never inline strings.
77+
- New screens: add constant to `routes.ts`, add `ParamList` entry to `root-param-list.ts`, register in `root-navigator.tsx`.
78+
- `navigationRef` for imperative navigation outside the React tree.
79+
- `useFocusEffect` + `useCallback` for focus-scoped side effects.
80+
- `InteractionManager.runAfterInteractions` for heavy non-UI work triggered by navigation.
3581

3682
## Must not
37-
- Do not nest a second `NavigationContainer`.
38-
- Do not navigate from `src/shared/**` directly — navigate only via helpers or callbacks passed in as props.
83+
- Do not render a second `<NavigationContainer>` — one root only.
84+
- Do not call `navigation.navigate()` after an auth state change — conditional screen rendering handles it.
85+
- Do not nest navigators of the same type (tabs-in-tabs, stack-in-stack at the same level).
86+
- Do not create a nested navigator solely for code organization — group screens under a shared `screenOptions` prop instead.
87+
- Do not navigate from `src/shared/**` directly — pass callbacks as props or use `navigationRef` helpers.
3988
- Do not add navigation logic inside `src/shared/components/ui/` components.
89+
- Do not pass full data objects as params — pass IDs and fetch data inside the screen.
90+
- Do not leave empty directories under `src/navigation/` — remove the folder when the last file is deleted.

.claude/rules/react-query.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ Never hardcode numeric stale/gc values — always reference a `Freshness` profil
3838

3939
## Must not
4040
- No React Query keys inline in components — always use `api/keys.ts`.
41+
- No tag arrays (e.g. `['auth:me', 'auth:session']`) inline in hooks — export named arrays from the feature's `api/keys.ts` and import from there.
4142
- No `invalidateQueries()` without a targeted key — use tag-based invalidation.
4243
- No magic `staleTime` / `gcTime` numbers — use `Freshness` profiles.
4344
- No server data duplicated in Zustand.

.claude/rules/shared-services.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ shared/services/
3535
- All HTTP calls must go through `http/http.client.ts` (exported `httpClient`) / `http/api.ts` helpers or a `transport/` adapter — never bare `fetch`.
3636
- Every adapter must pass responses through `src/shared/utils/normalize-error.ts` for consistent error shapes.
3737
- React Query client configuration (staleTime, retry, persistence) must live in `query/policy/`, `query/client/`, or `query/persistence/` — not scattered across feature hooks.
38-
- Tag-based cache invalidation must use `query/helpers/invalidate-by-tags.ts` and tag constants from `query/tags.ts`.
38+
- Tag-based cache invalidation must use `query/helpers/invalidate-by-tags.ts`. The `Tag`, `TagMap`, and `KeyGetter` types live in `query/tags.ts`; feature tag arrays (e.g. `AUTH_SESSION_TAGS`) and tagMaps belong in each feature's `api/keys.ts`.
3939
- MMKV key strings must be imported from `src/config/constants.ts`.
4040
- Sentry calls (`captureException`, `captureMessage`) must go through `monitoring/sentry.ts` helpers — do not call the Sentry SDK directly in feature code.
4141
- The mock transport adapter (`transport/adapters/mock.adapter.ts`) is dev-only; it must be gated by `flags.USE_MOCK` from `src/config/constants.ts`.
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
# RN Starter Review Checklist
2+
3+
Use this as a concise pass/fail checklist during reviews.
4+
5+
## Correctness
6+
- [ ] Logic changes handle happy path and edge cases.
7+
- [ ] Async flows handle loading, success, and error states.
8+
- [ ] Errors are not swallowed; context is preserved.
9+
10+
## Architecture and imports
11+
- [ ] `src/shared` does not import feature code.
12+
- [ ] Imports use `@/` aliases, not deep relative paths.
13+
- [ ] New feature code is placed under `src/features/<feature>/...`.
14+
15+
## RN UI and theming
16+
- [ ] No raw colors/spacing/fonts in UI code.
17+
- [ ] Styling uses theme tokens and `StyleSheet.create()`.
18+
- [ ] Platform-specific behavior is handled where required.
19+
20+
## Data and state
21+
- [ ] Server state uses React Query patterns from feature `api/keys.ts`.
22+
- [ ] Mutations include targeted invalidation patterns.
23+
- [ ] No server/domain data is moved into Zustand global stores.
24+
- [ ] No direct `fetch` usage; transport/API layer is used.
25+
26+
## Tests
27+
- [ ] Non-trivial behavior changes include tests.
28+
- [ ] Missing tests are called out with exact suggested scenarios.
29+
- [ ] Residual risk and untested paths are explicitly noted.
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
---
2+
name: rn-code-reviewer
3+
description: Reviews React Native bare-workflow changes in this starter for correctness, regressions, architecture boundaries, React Query usage, theming, and test coverage. Use when reviewing PRs, diffs, or requested code reviews.
4+
---
5+
6+
# RN Code Reviewer
7+
8+
Project-specific code review workflow for this React Native starter.
9+
10+
## When to apply
11+
12+
Use this skill when:
13+
- Reviewing pull requests or local diffs
14+
- Asked to "review" code quality, bugs, or regressions
15+
- Validating RN architecture and repo conventions before merge
16+
17+
## Review priorities
18+
19+
Always prioritize findings in this order:
20+
1. Correctness and behavioral regressions
21+
2. Security/privacy risks and error handling gaps
22+
3. Architecture and boundary violations
23+
4. Performance concerns with real user impact
24+
5. Missing or weak tests
25+
26+
## Required checks
27+
28+
### 1) Correctness and risk
29+
- Verify changed logic matches intended behavior and handles edge cases.
30+
- Flag silent failures, swallowed errors, or lossy error handling.
31+
- Check async flows for loading, success, and error-state handling.
32+
33+
### 2) Repository architecture constraints
34+
- Enforce feature/shared boundaries (`src/shared` must not import from features).
35+
- Enforce alias imports (`@/`); flag deep relative imports.
36+
- Verify feature code stays under `src/features/<feature>/...` with expected folders.
37+
38+
### 3) React Native UI conventions
39+
- Flag raw colors/spacing/fonts in UI; require theme tokens.
40+
- Prefer `StyleSheet.create()` over broad inline styles (except dynamic values).
41+
- Verify platform-specific behavior is handled where needed (`Platform.select`, native differences).
42+
43+
### 4) Data/state conventions
44+
- Server state should use React Query (feature-level `api/keys.ts` key patterns).
45+
- Mutations should use targeted invalidation (`meta.tags` / invalidation paths).
46+
- Flag server data leaking into Zustand stores.
47+
- Flag direct `fetch` usage (project transport layer should be used).
48+
49+
### 5) Tests and verification
50+
- Ensure non-trivial logic changes include tests or justify why not.
51+
- Suggest concrete missing tests (what behavior, where to add).
52+
- Highlight risky untested paths before merge.
53+
54+
## Output format
55+
56+
Return findings first, ordered by severity:
57+
58+
1. `Critical` — must fix before merge
59+
2. `Major` — high-impact issues, should fix
60+
3. `Minor` — quality/maintainability improvements
61+
62+
For each finding include:
63+
- File path
64+
- Why it is a problem (risk/regression)
65+
- Specific fix direction
66+
67+
Then include:
68+
- Open questions/assumptions
69+
- Brief change summary (secondary)
70+
- Residual testing gaps
71+
72+
## Review behavior rules
73+
74+
- Do not lead with a broad summary before findings.
75+
- Be explicit about potential regressions and user impact.
76+
- Prefer actionable, minimal suggestions over large refactors.
77+
- If no issues are found, explicitly state "No findings" and note residual risks or test gaps.
78+
79+
## Additional reference
80+
81+
Use this checklist for quick pass/fail scanning:
82+
- [CHECKLIST.md](CHECKLIST.md)

AGENTS.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ When changing SVGs, run `npm run gen:icons`. When changing i18n keys, run `npm r
3131
- **src/i18n/** — useT, locales, extraction.
3232
- **src/shared/components/ui/** — Reusable UI primitives (Button, Text, ScreenWrapper, …). If a component needs sharing, it either belongs here (truly generic) or stays in the feature that owns it — there is no intermediate category.
3333
- **src/shared/hooks/** — Shared hooks.
34-
- **src/shared/constants/** — Shared non-config constants (e.g. query invalidation tag lists). Storage key names and env-backed flags stay in **`src/config/`**.
34+
- **src/shared/constants/** — Shared non-config constants. Storage key names and env-backed flags stay in **`src/config/`**. Tag arrays for cache invalidation (e.g. `AUTH_SESSION_TAGS`) belong in each feature's `api/keys.ts` — not here.
3535
- **src/shared/services/api/** — HTTP, transport, query client, network, offline.
3636
- **src/shared/services/monitoring/** — Optional crash reporting (e.g. Sentry init); see `docs/OPERATIONS.md#sentry`.
3737
- **src/shared/services/storage/** — MMKV, cache, Zustand persistence adapter.
@@ -73,6 +73,7 @@ Use path alias `@/` only (e.g. `@/navigation/`, `@/session/`, `@/config/`, `@/i1
7373
- No server data in Zustand; no feature logic in `src/shared/components/ui/` or `src/shared/stores/`.
7474
- No deep relative imports; path aliases only (`check:imports`).
7575
- When adding/changing SVGs: run `npm run gen:icons` and keep `check:icons` passing.
76+
- No empty directories in `src/`. When moving or deleting files, remove the containing folder if it becomes empty. Empty folders signal dead code and confuse codebase navigation.
7677

7778
## When adding
7879

App.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,6 @@ export default function App() {
3737
// Android: exit app from root-level leaves (main tabs, login, onboarding).
3838
useBackButtonHandler(
3939
routeName =>
40-
routeName === ROUTES.HOME_TABS ||
4140
routeName === ROUTES.TAB_HOME ||
4241
routeName === ROUTES.TAB_SETTINGS ||
4342
routeName === ROUTES.AUTH_LOGIN ||

0 commit comments

Comments
 (0)