Kids Reading helps families build healthy reading habits. Create kid profiles, set daily goals, track progress & streaks, and (optionally) get gentle daily reminders — all stored on‑device (no accounts, no cloud).
Built with Expo, React Native, Expo Router, and Zustand. Offline‑first by design.
- Features
- Tech Stack
- Project Structure
- Getting Started
- Configuration
- First‑Run & Navigation
- Notifications
- Internationalization
- Testing & Linting
- Known Warnings
- Troubleshooting
- Privacy
- License
- Multi‑Kid Profiles — Add, rename, make primary, remove (cap configurable).
- Reading Timer — Start / Pause / Resume / Hold to Save & End.
- Daily Goal Ring — Progress toward the current child’s goal.
- Totals & Streaks — Today / Week / Month totals, current & best streaks.
- Recent Sessions — Review and delete with confirmation.
- Celebration UX — Confetti + (native) haptics when the goal is reached (once/kid/day).
- Local Notifications —
- “Goal reached” instant notification (iOS/Android).
- Optional daily reminder at HH:MM (iOS/Android).
- Web falls back to a short chime (no scheduled notifications).
- Multilingual — English, Ελληνικά, हिन्दी.
- Digit localization (e.g., Hindi numerals where it makes sense).
- Smart Greek uppercase (accent stripping in headings).
- Offline‑first — All data lives on device (AsyncStorage / localStorage).
- Consistent UI — Small design‑system components (Button, Card, Pill, etc.).
| Layer | Technology |
|---|---|
| Framework | Expo SDK 54, React Native 0.81, React 19 |
| Navigation | Expo Router v6 (typed routes, tabs) |
| State | Zustand v5 |
| i18n | i18next + react‑i18next (+ helpers) |
| Notifications | expo‑notifications |
| Haptics / Celebration | expo‑haptics, react‑native‑confetti‑cannon |
| Animations / SVG | react‑native‑reanimated, react‑native‑svg |
| Storage | AsyncStorage (native) / localStorage (web) |
| Lint / Types | ESLint, TypeScript 5 |
| Tests | Jest + jest‑expo |
kids-reading/
├─ app/
│ ├─ (tabs)/
│ │ ├─ _layout.tsx # Tab bar: Home, Kids, Dashboard, Settings, About
│ │ ├─ index.tsx # Home/Timer (confetti, haptics, goal notifications, web chime)
│ │ ├─ kids.tsx # Manage kids
│ │ ├─ dashboard.tsx # Totals, streaks, recent sessions
│ │ ├─ settings.tsx # Per‑kid goal, language, daily reminder
│ │ └─ explore.tsx # “About” tab (UI label = About)
│ ├─ onboarding.tsx # First‑run setup
│ └─ modal.tsx # Example modal
│
├─ src/
│ ├─ config/ # App constants & theme
│ │ └─ constants.ts
│ ├─ hooks/
│ │ └─ useFocusSession.ts # Keep‑awake + end‑reminder scheduling (with channel name sync)
│ ├─ lib/ # Utilities
│ │ ├─ i18n.ts # i18next init
│ │ ├─ i18n-helpers.ts # useLabel(), tSafe(), etc.
│ │ ├─ notifications.ts # Daily reminder + notifyGoalReached()
│ │ ├─ time.ts # Time helpers (windows, formatters)
│ │ ├─ text.ts # Localized digits, Greek uppercase
│ │ ├─ sound.ts # Web chime (prepareChime/playChime)
│ │ ├─ appInit.ts # One‑time app init (migrations + Android channel bootstrap)
│ │ └─ storage.ts # AsyncStorage/localStorage abstraction
│ ├─ store/
│ │ ├─ useReadingStore.ts # Kids, sessions, parent prefs, active timer
│ │ └─ useCelebration.ts # Goal celebration guard
│ ├─ components/
│ │ ├─ CelebrationOverlay.tsx
│ │ ├─ timer/TimerRing.tsx
│ │ └─ ui/{Button,Card,Pill,Row,SectionTitle,ConfirmDialog,ActionMenu}.tsx
│ └─ locales/ # en/el/hi translations
│
├─ assets/
│ ├─ images/{icon.png, favicon.png, splash-icon.png, android-icon-*.png, notification-icon.png}
│ └─ sounds/chime.mp3
│
├─ app.json # Expo config
├─ package.json # Scripts, deps, Jest config
├─ tsconfig.json # Path aliases (@"/*")
├─ babel.config.js # Module resolver for "@/"
├─ .eslintrc.* # ESLint config
└─ README.md
- Node.js ≥ 20.19, npm ≥ 10
- Android Studio / Xcode for emulators (or real device + Expo Go)
npm installnpx expo-doctornpm run android # Android
npm run ios # iOS
npm run web # Web (no scheduled notifications; chime fallback)On first launch with no kids, you’ll be redirected to /onboarding. After completion (first kid + language + reminder), the app goes to the tab bar.
- Android status‑bar notification icon (
assets/images/notification-icon.png): solid white on transparent, ~96×96. - Adaptive icon (Android):
android-icon-foreground.png,android-icon-background.png,android-icon-monochrome.png. - Splash:
splash-icon.png; Favicon:favicon.png. - Web chime:
assets/sounds/chime.mp3.
app/_layout.tsxdeclares the root stack and callsensureAppInit()once (migrations + Android channel bootstrap).- If the store has no kids,
app/index.tsxredirects to/onboarding. - Tabs live in
app/(tabs)/_layout.tsxand render Home, Kids, Dashboard, Settings, About.
- Triggered in Home when progress hits the daily goal:
- Haptic feedback → Confetti overlay →
notifyGoalReached(). - Tapping the notification routes back to Home.
- Haptic feedback → Confetti overlay →
- Toggle + time in Settings; saved to local state then scheduled via
setupDailyReminder(HH:MM). - Cancelling uses
cancelDailyReminders().
- Web can’t schedule local notifications; when a goal is reached we play a short chime (
playChime()).
Make sure to callprepareChime()on a user gesture first (e.g., Start button) to unlock audio.
useFocusSession.syncSessionChannelName(label("channel_sessions_name", "Reading Sessions"))keeps the “sessions” channel display name in sync with the current language.- Called after language changes in Settings.
- All UI strings go through
useLabel()ortSafe()to ensure strong fallbacks. - To add a key:
- Add to
src/locales/{en,el,hi}.json. - Reference via
label("your_key", "Fallback").
- Add to
- Greek headings use
toUpperLocalized()to remove accents where appropriate. - Numbers shown to the user can use
formatNumberForLang(value, lang)to localize digits (e.g., Hindi).
npm run test # Jest
npm run lint # ESLint
npm run lint:fix # ESLint with --fix
npm run typecheck # TypeScript
npm run check # typecheck + lintIf you see describe/test/expect “not defined”, ensure your ESLint has a Jest override:
// .eslintrc.json (example)
{
"extends": ["expo", "plugin:react-hooks/recommended", "plugin:@typescript-eslint/recommended"],
"overrides": [
{
"files": ["**/__tests__/**/*.{js,jsx,ts,tsx}"],
"env": { "jest": true }
}
]
}(Alternatively install and enable plugin:jest/recommended.)
- React Native Web:
props.pointerEvents is deprecated. Use style.pointerEvents
We’ve updated overlays (e.g.,CelebrationOverlay) to usestyle={{ pointerEvents: "none" }}. - expo-notifications on web: “Listening to push token changes is not yet fully supported on web” — informational only.
- Audio on web: Browsers block autoplay — call
prepareChime()from a user gesture beforeplayChime().
- Doctor:
npx expo-doctorto spot version/config mismatches. - Notification icon: Must be solid white on transparent (Android), or notifications may render as a gray square.
- Multiple React copies / invalid hooks: Ensure only one
reactis installed and hoisted; clear caches withnpm run reset-project(seescripts/reset-project.js).
- All data is stored locally on the device. No accounts. No analytics. No tracking.
MIT — see LICENSE. Contributions welcome!
{ "expo": { "ios": { "infoPlist": { "NSUserNotificationUsageDescription": "We use notifications to remind you about reading goals." } }, "android": { "permissions": ["POST_NOTIFICATIONS"], "edgeToEdgeEnabled": true, "predictiveBackGestureEnabled": false }, "notification": { "icon": "./assets/images/notification-icon.png", "color": "#3B82F6" }, "plugins": [ "expo-router", ["expo-splash-screen", { "image": "./assets/images/splash-icon.png", "imageWidth": 200 }], "expo-notifications" ], "experiments": { "typedRoutes": true, "reactCompiler": true } } }