Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
5 changes: 5 additions & 0 deletions .claude/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"enabledPlugins": {
"impeccable@impeccable": false
}
}
10 changes: 5 additions & 5 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@ module.exports = {
root: true,
extends: '@react-native',
rules: {
'comma-dangle': ['error', 'never']
},
"indent": ["error", 4],
"react/jsx-indent": ["error", 4],
"react/jsx-indent-props": ["error", 4]
'comma-dangle': ['error', 'never'],
'indent': ['error', 4],
'react/jsx-indent': ['error', 4],
'react/jsx-indent-props': ['error', 4]
}
};
11 changes: 6 additions & 5 deletions .prettierrc.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
module.exports = {
arrowParens: 'avoid',
bracketSameLine: true,
bracketSpacing: false,
singleQuote: true,
trailingComma: 'all',
arrowParens: 'avoid',
bracketSameLine: true,
bracketSpacing: false,
singleQuote: true,
trailingComma: 'none',
tabWidth: 4,
};
44 changes: 25 additions & 19 deletions App.tsx
Original file line number Diff line number Diff line change
@@ -1,33 +1,39 @@
import 'react-native-gesture-handler';
import React from 'react';
import { Provider } from 'react-redux';
import { PersistGate } from 'redux-persist/integration/react';
import { NavigationContainer } from '@react-navigation/native';
import { MainRoutes } from './routes';
import {Provider} from 'react-redux';
import {PersistGate} from 'redux-persist/integration/react';
import {NavigationContainer} from '@react-navigation/native';
import {GestureHandlerRootView} from 'react-native-gesture-handler';
import {MainRoutes} from './routes';
import * as Sentry from '@sentry/react-native';
import Config from 'react-native-config';
import configureAppStore from './store';
import { IS_PRODUCTION } from './actions/types';
import {IS_PRODUCTION} from './actions/types';
import setupAxiosInterceptors from './utils/setupAxiosInterceptors';
import './i18n';

const SENTRY_DSN = Config.SENTRY_DSN;
const { store, persistor } = configureAppStore();
const {store, persistor} = configureAppStore();

const App = () => {
if (IS_PRODUCTION) {
Sentry.init({
dsn: SENTRY_DSN
});
}
setupAxiosInterceptors(store);

if (IS_PRODUCTION) {
Sentry.init({
dsn: SENTRY_DSN
});
}

const App = () => {
return (
<NavigationContainer>
<Provider store={store}>
<PersistGate loading={null} persistor={persistor}>
<MainRoutes />
</PersistGate>
</Provider>
</NavigationContainer>
<GestureHandlerRootView style={{flex: 1}}>
<NavigationContainer>
<Provider store={store}>
<PersistGate loading={null} persistor={persistor}>
<MainRoutes />
</PersistGate>
</Provider>
</NavigationContainer>
</GestureHandlerRootView>
);
};

Expand Down
121 changes: 121 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## Project Overview

OpenLitterMap is a React Native mobile app (iOS & Android) for crowdsourced litter mapping. Users photograph litter, tag it by category, and upload geotagged data to the OpenLitterMap Laravel backend API. Authentication uses Laravel Sanctum token-based auth (login with email or username).

## Common Commands

```bash
# Install dependencies
npm install

# Start Metro bundler
npm start

# Run on iOS / Android
npm run ios
npm run android

# Install iOS native dependencies
cd ios && bundle exec pod install && cd ..

# Lint
npm run lint

# Run tests
npm test

# Run a single test file
npx jest path/to/test.js
```

Runtime: **Node v20.20.0**, **npm 10.8.2**

Package manager: **npm** (v10.8.2). Both `yarn.lock` and `package-lock.json` exist; prefer npm.

## Architecture

### State Management

Redux Toolkit with `createSlice` and `createAsyncThunk`. The store is configured in `store/index.js` with `redux-persist` (AsyncStorage backend, `auth` and `images` slices persisted). The `images` transform only persists `imagesArray` — upload counters reset on relaunch. In dev mode, `redux-immutable-state-invariant` middleware is included.

Reducers in `reducers/`:
- `auth_reducer` - Authentication, user profile, Sanctum token management
- `tags_reducer` - Tag data fetched from API, search index (objectEntries, categoriesById, entriesByCloId)
- `gallery_reducer` / `images_reducer` - Photo selection, v5 tagging (tagsV5), swiperIndex, GPS/EXIF handling
- `shared_reducer` - Cross-feature shared state (upload modal, app version)
- `settings_reducer` - User preferences
- `stats_reducer` / `leaderboards_reducer` - Statistics and rankings
- `team_reducer` - Team features
- `my_uploads_reducer` - Upload history and management
- `locations_reducer` - Location hierarchy data

API calls use **axios** with Bearer token auth, hitting endpoints on the `URL` from `actions/types.js`.

### Navigation

React Navigation v6 with `@react-navigation/stack` and `@react-navigation/material-top-tabs`.

- `routes/MainRoutes.js` - Root navigator. Shows `AuthStack` when no token, otherwise the main app stack.
- `routes/AuthStack.js` - Welcome → Auth (login/signup) flow.
- `routes/TabRoutes.tsx` - Bottom tab navigator: Home, Team, Global Data, Leaderboards, User Stats.
- `routes/PermissionStack.tsx` - Camera/gallery permission request flow.
- Modal screens: AddTagScreen, Album, Settings, Update, MyUploads.

### Screen Organization

Each screen lives in `screens/<feature>/` with a main screen component and a subdirectory for sub-components (e.g., `screens/home/homeComponents/`, `screens/addTag/components/`, `screens/userStats/userComponents/`).

Shared/reusable components are in `screens/components/` with barrel exports via `index.ts`:
- `theme/colors.ts` - App color palette (`Colors.accent`, `Colors.error`, etc.)
- `theme/fonts.ts` - Font definitions
- `typography/` - Styled text components (Title, SubTitle, Body, Caption, StyledText)
- `Button.tsx`, `Header.js`, `AnimatedCircle.tsx`, `StatsGrid`, `IconStatsCard`, `CustomTextInput`

### Environment Configuration

Uses `react-native-config` to load `.env` variables. Key env vars defined in `actions/types.js`:
- `CURRENT_ENVIRONMENT` - `"production"` or `"local"`
- `OLM_ENDPOINT` - Production API URL
- `LOCAL_OLM_ENDPOINT` - Local dev API URL
- `SENTRY_DSN` - Error tracking (only initialized in production)

### Internationalization

i18next with `react-i18next`. Configured in `i18n.js`. Translation keys are **full British English string literals** (key = English display text). All translation files use a single flat JSON structure with keys sorted alphabetically A-Z. See `Translations.md` for full architecture details.

Translation files live in `assets/langs/` with 8 languages: en, ar, de, es, fr, ie, nl, pt. Each language directory contains `{lang}.json` (flat UI strings), `litter.json` (nested litter taxonomy), and `index.js`.

The `litter.json` files use a nested structure with 15 sections (categories, 12 litter categories, materials, types) derived from the backend `TagsConfig`. Keys are snake_case identifiers matching the backend. Access via `t('litter.smoking.butts')`, `t('litter.materials.plastic')`, etc. Unlike UI strings, litter keys are **translated per language** — each language has its own `litter.json` with identical keys but translated values (174 key-value pairs per language).

**When adding a new user-facing string:** Add the key to `en/en.json` (key = value for English), then translate and add it to ALL other language files (`ar.json`, `de.json`, `es.json`, `fr.json`, `ie.json`, `nl.json`, `pt.json`). Keep all files sorted alphabetically A-Z by key. Use `t('Your new string')` or `dictionary="Your new string"` in code.

**When adding a new litter key:** Add it to `en/litter.json` in the appropriate section, then translate and add it to ALL other `litter.json` files. Keep keys sorted alphabetically within each section.

### Litter Data Model

Tag data is fetched from the API (`GET /api/tags/all`) and cached in AsyncStorage (7-day TTL) by `tags_reducer.js`. Per-image tags are stored as `tagsV5: [{ cloId, quantity, materials, brands, customTags }]` in `images_reducer`. The `cloId` (category_litter_object_id) uniquely identifies an (object, category) pair. Display names are resolved at render time from `entriesByCloId`. Materials and brands are indexed by ID in `materialsById` and `brandsById`. Image-level custom tags (`image.customTags`) are stored separately and merged into the first tag's `custom_tags` on upload.

## Code Style

- ESLint extends `@react-native` with 4-space indentation and no trailing commas
- Prettier: single quotes, no bracket spacing, arrow parens avoided, trailing commas
- Mixed JS/TS codebase (newer files tend to be TypeScript)
- Screens export via barrel files (`index.js` or `index.ts`)

## Key Dependencies

- `@shopify/flash-list` for performant lists
- `formik` + `yup` for form handling/validation
- `lottie-react-native` for animations
- `react-native-permissions` for camera/location/photo library permissions (iOS permissions listed in `reactNativePermissionsIOS` in package.json)
- `@sentry/react-native` for error tracking (production only). Sentry Cocoa SDK version is overridden to 8.46.0+ via `postinstall` script for Xcode 26 compatibility
- `dayjs` for date formatting

## Build Notes

- **Xcode 26+/macOS Tahoe**: Sentry Cocoa SDK < 8.46.0 fails to compile (`std::allocator does not support const types`). The `postinstall` script in package.json patches the RNSentry podspec to use 8.46.0. After `npm install`, run `cd ios && pod update Sentry && cd ..` if the `Podfile.lock` still references an older version.
- After modifying native dependencies: clean Xcode build folder (Cmd+Shift+K) and rebuild
65 changes: 37 additions & 28 deletions Gemfile.lock
Original file line number Diff line number Diff line change
@@ -1,23 +1,28 @@
GEM
remote: https://rubygems.org/
specs:
CFPropertyList (3.0.7)
CFPropertyList (3.0.8)
activesupport (7.0.10)
base64
nkf
rexml
activesupport (6.1.7.8)
benchmark (>= 0.3)
bigdecimal
concurrent-ruby (~> 1.0, >= 1.0.2)
drb
i18n (>= 1.6, < 2)
logger (>= 1.4.2)
minitest (>= 5.1)
mutex_m
securerandom (>= 0.3)
tzinfo (~> 2.0)
zeitwerk (~> 2.3)
addressable (2.8.7)
public_suffix (>= 2.0.2, < 7.0)
addressable (2.8.9)
public_suffix (>= 2.0.2, < 8.0)
algoliasearch (1.27.5)
httpclient (~> 2.8, >= 2.8.3)
json (>= 1.5.1)
atomos (0.1.3)
base64 (0.2.0)
base64 (0.3.0)
benchmark (0.5.0)
bigdecimal (4.0.1)
claide (1.1.0)
cocoapods (1.14.3)
addressable (~> 2.8)
Expand Down Expand Up @@ -57,41 +62,45 @@ GEM
netrc (~> 0.11)
cocoapods-try (1.2.0)
colored2 (3.1.2)
concurrent-ruby (1.3.3)
concurrent-ruby (1.3.6)
drb (2.2.3)
escape (0.0.4)
ethon (0.16.0)
ethon (0.15.0)
ffi (>= 1.15.0)
ffi (1.17.0)
ffi (1.17.3)
fourflusher (2.3.1)
fuzzy_match (2.0.4)
gh_inspector (1.1.3)
httpclient (2.8.3)
i18n (1.14.5)
httpclient (2.9.0)
mutex_m
i18n (1.14.8)
concurrent-ruby (~> 1.0)
json (2.7.2)
minitest (5.24.1)
json (2.18.1)
logger (1.7.0)
minitest (6.0.2)
drb (~> 2.0)
prism (~> 1.5)
molinillo (0.8.0)
nanaimo (0.3.0)
mutex_m (0.3.0)
nanaimo (0.4.0)
nap (1.1.0)
netrc (0.11.0)
nkf (0.2.0)
prism (1.9.0)
public_suffix (4.0.7)
rexml (3.2.9)
strscan
rexml (3.4.4)
ruby-macho (2.5.1)
strscan (3.1.0)
typhoeus (1.4.1)
ethon (>= 0.9.0)
securerandom (0.4.1)
typhoeus (1.5.0)
ethon (>= 0.9.0, < 0.16.0)
tzinfo (2.0.6)
concurrent-ruby (~> 1.0)
xcodeproj (1.24.0)
xcodeproj (1.27.0)
CFPropertyList (>= 2.3.3, < 4.0)
atomos (~> 0.1.3)
claide (>= 1.0.2, < 2.0)
colored2 (~> 3.1)
nanaimo (~> 0.3.0)
rexml (~> 3.2.4)
zeitwerk (2.6.16)
nanaimo (~> 0.4.0)
rexml (>= 3.3.6, < 4.0)

PLATFORMS
ruby
Expand All @@ -101,7 +110,7 @@ DEPENDENCIES
cocoapods (>= 1.13, < 1.15)

RUBY VERSION
ruby 2.6.10p210
ruby 3.3.6p108

BUNDLED WITH
1.17.2
2.5.22
71 changes: 71 additions & 0 deletions TODO.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
# TODO — Dependency Upgrades

Last reviewed: 2026-03-01

## Quick Wins (low risk, no blockers)

- [ ] `react-native-safe-area-context` 4.10 → 5.x — No API changes, only requires RN ≥ 0.74
- [ ] `react-native-pager-view` 6.3 → 8.x — API-compatible, iOS rewritten in SwiftUI
- [ ] `lottie-react-native` 6.7 → 7.x — No code changes from 6.7, adds Fabric support
- [ ] `i18next` 23 → 25 — `initImmediate` renamed to `initAsync`, check `i18n.js`
- [ ] `typescript` 5.0 → 5.9 — Smooth incremental path, may surface new type errors
- [ ] `prettier` 2 → 3 — `trailingComma` default changes to `"all"`, pin it to match ESLint config (`"none"`)

## Upgrade With Care (individual PRs, test each)

- [ ] `react-native-permissions` 4 → 5 — Requires Xcode 16, Android codebase rewritten in Kotlin. No API changes to call sites.
- [ ] `react-native-device-info` 11 → 15 — `getUniqueId()` changed from per-user to per-device in v12 (audit usage). Requires `compileSdk 34+` in v15.
- [ ] `react-native-actions-sheet` 0.9 → 10 — `payload` → `returnValue`, `onChange` signature changed. Peer deps (reanimated, safe-area-context) already present.
- [ ] `eslint` 8 → 9 — Must migrate `.eslintrc` to flat config (`eslint.config.js`). Migration CLI tool available (`@eslint/migrate-config`).

## React Native Upgrade (big coordinated project)

Upgrade path: **0.74 → 0.76 → 0.78 → 0.82 → 0.84**

### What we get
- New Architecture (Fabric/TurboModules) — enabled by default from 0.76
- React 19 — ships from 0.78 (hooks changes, `forwardRef` no longer needed, new `use()` API)
- Hermes V1 — 10-15% faster Time to Interactive, smaller memory footprint (default from 0.84)
- Precompiled iOS builds — up to 10x faster build times (default from 0.84)
- Metro v0.82 — up to 3x faster startup via deferred hashing (from 0.79)
- Legacy architecture fully removed in 0.82

### Platform version changes
| RN Version | Android min | iOS min |
|------------|-------------|---------|
| 0.74 (current) | API 23 (Android 6) | 13.4 |
| 0.76+ | **API 24 (Android 7)** | **15.1** |

### Packages unlocked by the RN upgrade
These cannot be upgraded until React Native is upgraded:

- [ ] `react-native` 0.74 → 0.84
- [ ] `react` 18.3 → 19.x (required from RN 0.78)
- [ ] `@react-native/*` (babel-preset, eslint-config, metro-config, typescript-config) 0.74 → 0.84
- [ ] `@react-navigation/*` v6 → v7 — `navigate()` no longer pops back (use `popTo()`), nested navigation requires explicit parent screen, option renames (`headerBackTitleVisible` → `headerBackButtonDisplayMode`, `animationEnabled` → `animation`, etc.), custom themes need `fonts` property
- [ ] `react-native-screens` 3 → 4 — Coupled with React Navigation v7
- [ ] `react-native-reanimated` 3 → 4 — New Architecture only. `react-native-worklets` becomes required peer dep, `useAnimatedGestureHandler` removed, `withSpring` behavior changed
- [ ] `@shopify/flash-list` 1 → 2 — New Architecture only. Ground-up rewrite, `estimatedItemSize` removed (auto-sizing), `MasonryFlashList` replaced by `<FlashList masonry>`, ref type changed
- [ ] `@sentry/react-native` 5 → 8 — Requires iOS 15+. Would remove the postinstall Cocoa SDK hack. JS SDK jumps v7→v10, `tracePropagationTargets` default changes (may inject headers into axios requests — set explicitly after upgrade)
- [ ] `lottie-react-native` 7 → latest with React 19 support
- [ ] `react-native-pager-view` 8 → latest
- [ ] `@react-native-async-storage/async-storage` 1 → 3

## Audit Status

- 8 low severity vulnerabilities remaining (all `fast-xml-parser` in RN CLI — only fixable by upgrading react-native)
- All semver-compatible updates applied as of 2026-03-01

## Other Notes

- `moment` is referenced in CLAUDE.md but not in package.json — was replaced by `dayjs`
- `packageManager` field in package.json still references `yarn@3.6.4` — using npm locally

---

# TODO — Phase 4 Mobile Features

- [ ] **Public profile screen** — View other users' profiles (stats, recent uploads, level)
- [ ] **Location-scoped leaderboards** — Leaderboards filtered by city/country/region
- [ ] **Achievements display** — Show earned achievements/badges on profile
- [ ] **User photo map** — Map view of the current user's uploaded photos
Loading