Plan 1: Foundation + Home Loop

docs/superpowers/plans/2026-04-10-plan-1-foundation-home.md

← Zurück zu Knowledge

Plan 1: Foundation + Home Loop

docs/superpowers/plans/2026-04-10-plan-1-foundation-home.md · 59.1K

# Plan 1: Foundation + Home Loop

> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.

**Goal:** Build the foundation (types, storage, theming, mock data, providers) and a working Home tab with check-in flow, giving a bootable dummy app with realistic mock data and the core daily-use loop.

**Architecture:** AsyncStorage-backed providers hold all app state. Pure logic functions (xp, streaks) are isolated in `lib/`. Components consume providers via hooks. Theme uses React Context with light+dark color maps derived from `@carrot/shared`.

**Tech Stack:** Expo 54, React Native 0.81, expo-router 6, @react-native-async-storage/async-storage 2.2, react-native-reanimated 4.1, TypeScript 5.9 strict mode.

**Scope boundary:** This plan stops at a working Home tab. Progress, Squad, Profile tabs are stubbed. Onboarding is deferred to Plan 6. Goal detail screen (modal) is deferred to Plan 3. Testing: no Jest setup in this plan — validate visually via `npm run dev:mobile`. Tests added in a later plan if needed.

---

## File Structure

```
apps/mobile/
  types/
    index.ts                 → All domain types (Goal, Habit, Milestone, Todo, Squad, Bet, Challenge, Duel, Pact, Achievement, User)
  lib/
    storage.ts               → Typed AsyncStorage wrapper
    xp.ts                    → XP/level calculations
    streak.ts                → Streak logic
    date.ts                  → Date helpers (today key, day diff)
    mock-data.ts             → Seed data generator
  theme/
    colors.ts                → Light + dark color maps
    index.ts                 → Theme export
  providers/
    ThemeProvider.tsx        → Light/dark mode context
    AppDataProvider.tsx      → Root provider: loads AsyncStorage, seeds mock data, exposes state + actions
  hooks/
    useTheme.ts              → Access theme + toggle
    useAppData.ts            → Access all app data
    useActiveGoal.ts         → Currently active goal + derived data
  components/
    GoalHeader.tsx           → Top of Home: goal title + level badge + XP bar
    StatsRow.tsx             → Streak | XP | Today's completion
    HabitCard.tsx            → Single habit row with check-in
    XPPopup.tsx              → Animated +XP floating text
    CelebrationOverlay.tsx   → All-habits-done overlay
    FAB.tsx                  → Floating action button
  app/
    _layout.tsx              → MODIFY: wrap with providers
    (tabs)/
      _layout.tsx            → MODIFY: 4 tabs with icons
      index.tsx              → MODIFY: real Home tab
      progress.tsx           → MODIFY: stub with themed empty state
      squad.tsx              → CREATE: stub with themed empty state
      profile.tsx            → MODIFY: stub with themed empty state
```

---

## Task 1: Create directory structure

**Files:**
- Create: `apps/mobile/types/` (directory)
- Create: `apps/mobile/lib/` (directory)
- Create: `apps/mobile/theme/` (directory)
- Create: `apps/mobile/providers/` (directory)
- Create: `apps/mobile/hooks/` (directory)
- Create: `apps/mobile/components/` (directory)

- [ ] **Step 1: Verify directories exist (they were created empty earlier)**

Run: `ls apps/mobile/`
Expected output includes: `components`, `hooks`, `lib`, `providers`, `theme`, `types`

If any missing, create: `mkdir -p apps/mobile/{components,hooks,lib,providers,theme,types}`

- [ ] **Step 2: No commit (directories only become tracked when files are added)**

---

## Task 2: Define domain types

**Files:**
- Create: `apps/mobile/types/index.ts`

- [ ] **Step 1: Write the complete types file**

```ts
// apps/mobile/types/index.ts

export type ID = string;

export type ThemeMode = 'light' | 'dark' | 'system';

export interface User {
	id: ID;
	name: string;
	avatarColor: string; // hex color for initial avatar
	totalXP: number;
	currentStreak: number;
	longestStreak: number;
	createdAt: string; // ISO date
}

export type GoalStatus = 'active' | 'completed' | 'paused';

export interface Goal {
	id: ID;
	title: string;
	emoji: string;
	description?: string;
	targetDate: string; // ISO date
	status: GoalStatus;
	createdAt: string;
}

export interface Milestone {
	id: ID;
	goalId: ID;
	title: string;
	description?: string;
	xpReward: number;
	completed: boolean;
	completedAt?: string;
	order: number;
}

export interface Todo {
	id: ID;
	goalId: ID;
	milestoneId?: ID;
	title: string;
	xpReward: number;
	completed: boolean;
	completedAt?: string;
	dueDate?: string;
}

export type HabitFrequency = 'daily' | 'weekdays' | 'custom';

export interface Habit {
	id: ID;
	goalId: ID;
	title: string;
	emoji: string;
	xpReward: number;
	frequency: HabitFrequency;
	customDays?: number[]; // 0=Sun, 6=Sat
	currentStreak: number;
	createdAt: string;
}

// Daily completion record — one per habit per day
export interface HabitCompletion {
	id: ID;
	habitId: ID;
	dateKey: string; // YYYY-MM-DD
	xpEarned: number;
	completedAt: string; // ISO timestamp
}

export interface SquadMember {
	id: ID;
	name: string;
	avatarColor: string;
	initials: string;
	currentStreak: number;
	weeklyXP: number;
	totalXP: number;
}

export interface Squad {
	id: ID;
	name: string;
	emoji: string;
	level: number;
	currentStreak: number; // squad streak = min(all member streaks)
	members: SquadMember[];
}

export type BetStatus = 'active' | 'won' | 'lost';
export type BetSide = 'for' | 'against';

export interface BetSupport {
	memberId: ID;
	side: BetSide;
	stake: number;
}

export interface Bet {
	id: ID;
	creatorId: ID; // 'user' for the current user, or squad member id
	description: string;
	targetValue: number; // e.g., 14 days
	currentValue: number;
	stake: number; // creator's own XP on the line
	supports: BetSupport[]; // other members' wagers
	deadline: string; // ISO date
	status: BetStatus;
	createdAt: string;
}

export type ChallengeType = 'streak_sprint' | 'perfect_week' | 'xp_race';
export type ChallengeStatus = 'active' | 'won' | 'failed';

export interface ChallengeMemberProgress {
	memberId: ID; // 'user' for current user
	dailyStatus: Array<'done' | 'missed' | 'pending'>; // one per day
}

export interface Challenge {
	id: ID;
	type: ChallengeType;
	title: string;
	description: string;
	durationDays: number;
	startDate: string;
	progress: ChallengeMemberProgress[];
	rewardXP: number;
	status: ChallengeStatus;
}

export type DuelStatus = 'active' | 'won' | 'lost';

export interface Duel {
	id: ID;
	opponentId: ID;
	metric: 'checkins_this_week';
	stake: number;
	userScore: number;
	opponentScore: number;
	deadline: string;
	trashTalk: Array<{ from: 'user' | 'opponent'; message: string; timestamp: string }>;
	status: DuelStatus;
}

export type BattleStatus = 'active' | 'won' | 'lost';

export interface SquadBattle {
	id: ID;
	opponentSquad: {
		name: string;
		emoji: string;
		memberCount: number;
		weeklyXP: number;
	};
	ourSquadXP: number;
	deadline: string;
	status: BattleStatus;
}

export interface Pact {
	id: ID;
	partnerId: ID; // squad member id
	sharedStreak: number;
	multiplier: number; // 2 = 2x XP
	active: boolean;
	createdAt: string;
}

export interface Achievement {
	id: ID;
	key: string; // e.g., 'streak_7', 'level_5', 'first_milestone'
	title: string;
	emoji: string;
	description: string;
	unlockedAt?: string; // undefined = locked
}

// Root state shape stored in AsyncStorage
export interface AppData {
	schemaVersion: number;
	user: User;
	goals: Goal[];
	milestones: Milestone[];
	todos: Todo[];
	habits: Habit[];
	completions: HabitCompletion[];
	squad: Squad;
	bets: Bet[];
	challenges: Challenge[];
	duels: Duel[];
	battles: SquadBattle[];
	pacts: Pact[];
	achievements: Achievement[];
	settings: {
		themeMode: ThemeMode;
		remindersEnabled: boolean;
		language: 'de' | 'en';
	};
}
```

- [ ] **Step 2: Typecheck**

Run: `cd apps/mobile && npx tsc --noEmit`
Expected: No errors.

- [ ] **Step 3: Commit**

```bash
git add apps/mobile/types/index.ts
git commit -m "Add domain types for dummy app (Goal, Habit, Milestone, Todo, Squad, Bet, Challenge, Duel, Pact, Achievement)"
```

---

## Task 3: Date helpers

**Files:**
- Create: `apps/mobile/lib/date.ts`

- [ ] **Step 1: Write the file**

```ts
// apps/mobile/lib/date.ts

/** Returns YYYY-MM-DD key for a given date (local time) */
export function dateKey(date: Date = new Date()): string {
	const y = date.getFullYear();
	const m = String(date.getMonth() + 1).padStart(2, '0');
	const d = String(date.getDate()).padStart(2, '0');
	return `${y}-${m}-${d}`;
}

/** Today's date key */
export function todayKey(): string {
	return dateKey(new Date());
}

/** Yesterday's date key */
export function yesterdayKey(): string {
	const d = new Date();
	d.setDate(d.getDate() - 1);
	return dateKey(d);
}

/** Offset today by n days (negative = past) */
export function offsetKey(daysOffset: number): string {
	const d = new Date();
	d.setDate(d.getDate() + daysOffset);
	return dateKey(d);
}

/** Parse YYYY-MM-DD into a local Date at 00:00 */
export function keyToDate(key: string): Date {
	const parts = key.split('-').map(Number);
	const y = parts[0] ?? 1970;
	const m = parts[1] ?? 1;
	const d = parts[2] ?? 1;
	return new Date(y, m - 1, d);
}

/** Number of days between two keys (b - a) */
export function daysBetween(a: string, b: string): number {
	const ms = keyToDate(b).getTime() - keyToDate(a).getTime();
	return Math.round(ms / (1000 * 60 * 60 * 24));
}

/** Add n months to today, return ISO date string */
export function addMonths(n: number): string {
	const d = new Date();
	d.setMonth(d.getMonth() + n);
	return d.toISOString();
}

/** Short random ID (not cryptographically secure, fine for mock data) */
export function randomId(): string {
	return Math.random().toString(36).slice(2, 10);
}
```

- [ ] **Step 2: Typecheck**

Run: `cd apps/mobile && npx tsc --noEmit`
Expected: No errors.

- [ ] **Step 3: Commit**

```bash
git add apps/mobile/lib/date.ts
git commit -m "Add date helpers (today key, offset, days between)"
```

---

## Task 4: XP and level calculations

**Files:**
- Create: `apps/mobile/lib/xp.ts`

- [ ] **Step 1: Write the file**

```ts
// apps/mobile/lib/xp.ts

/**
 * Level formula: floor(sqrt(totalXP / 100)) + 1
 * Every user starts at Level 1 with 0 XP.
 *
 * Level 1: 0 XP, Level 2: 100, Level 5: 1,600, Level 10: 8,100, Level 25: 57,600
 */
export function levelFromXP(totalXP: number): number {
	if (totalXP < 0) return 1;
	return Math.floor(Math.sqrt(totalXP / 100)) + 1;
}

/** Total XP required to reach a given level */
export function xpForLevel(level: number): number {
	if (level <= 1) return 0;
	return Math.pow(level - 1, 2) * 100;
}

/** Progress within current level, 0..1 */
export function levelProgress(totalXP: number): number {
	const level = levelFromXP(totalXP);
	const currentLevelXP = xpForLevel(level);
	const nextLevelXP = xpForLevel(level + 1);
	const range = nextLevelXP - currentLevelXP;
	if (range <= 0) return 1;
	return Math.max(0, Math.min(1, (totalXP - currentLevelXP) / range));
}

/** XP remaining until next level */
export function xpToNextLevel(totalXP: number): number {
	const level = levelFromXP(totalXP);
	return xpForLevel(level + 1) - totalXP;
}
```

- [ ] **Step 2: Sanity-check the formula mentally (no test framework yet)**

Reference values:
- `levelFromXP(0)` → 1
- `levelFromXP(100)` → 2
- `levelFromXP(1600)` → 5
- `levelFromXP(1800)` → 5 (since floor(sqrt(18)) + 1 = 5)
- `levelFromXP(8100)` → 10
- `xpForLevel(5)` → 1600
- `xpForLevel(10)` → 8100

- [ ] **Step 3: Typecheck**

Run: `cd apps/mobile && npx tsc --noEmit`
Expected: No errors.

- [ ] **Step 4: Commit**

```bash
git add apps/mobile/lib/xp.ts
git commit -m "Add XP and level calculation utilities"
```

---

## Task 5: Streak logic

**Files:**
- Create: `apps/mobile/lib/streak.ts`

- [ ] **Step 1: Write the file**

```ts
// apps/mobile/lib/streak.ts

import type { Habit, HabitCompletion } from '../types';
import { todayKey, yesterdayKey, dateKey, offsetKey } from './date';

/**
 * Did the user complete ALL active habits on a given day?
 * A day counts as "complete" if every daily habit has a completion record.
 */
export function allHabitsCompletedOn(
	habits: Habit[],
	completions: HabitCompletion[],
	dayKey: string,
): boolean {
	const activeHabits = habits.filter((h) => h.frequency === 'daily');
	if (activeHabits.length === 0) return false;
	return activeHabits.every((h) =>
		completions.some((c) => c.habitId === h.id && c.dateKey === dayKey),
	);
}

/**
 * Compute the user's current streak by walking back from today.
 * Today counts if all habits done today; otherwise start from yesterday.
 * Stops at the first incomplete day.
 */
export function computeCurrentStreak(
	habits: Habit[],
	completions: HabitCompletion[],
): number {
	let streak = 0;
	let offset = 0;
	// If today is incomplete, start counting from yesterday
	if (!allHabitsCompletedOn(habits, completions, todayKey())) {
		offset = -1;
	}
	while (true) {
		const key = offsetKey(offset);
		if (allHabitsCompletedOn(habits, completions, key)) {
			streak++;
			offset--;
		} else {
			break;
		}
	}
	return streak;
}

/** Count habits completed today */
export function completedTodayCount(
	habits: Habit[],
	completions: HabitCompletion[],
): number {
	const today = todayKey();
	return habits.filter((h) =>
		completions.some((c) => c.habitId === h.id && c.dateKey === today),
	).length;
}

/** Is a specific habit completed today? */
export function isHabitDoneToday(
	habit: Habit,
	completions: HabitCompletion[],
): boolean {
	const today = todayKey();
	return completions.some((c) => c.habitId === habit.id && c.dateKey === today);
}

/** Compute per-habit streak (consecutive days back from today) */
export function computeHabitStreak(
	habit: Habit,
	completions: HabitCompletion[],
): number {
	let streak = 0;
	let offset = 0;
	const doneToday = completions.some(
		(c) => c.habitId === habit.id && c.dateKey === todayKey(),
	);
	if (!doneToday) offset = -1;
	while (true) {
		const key = offsetKey(offset);
		const done = completions.some((c) => c.habitId === habit.id && c.dateKey === key);
		if (done) {
			streak++;
			offset--;
		} else {
			break;
		}
	}
	return streak;
}
```

- [ ] **Step 2: Typecheck**

Run: `cd apps/mobile && npx tsc --noEmit`
Expected: No errors.

- [ ] **Step 3: Commit**

```bash
git add apps/mobile/lib/streak.ts
git commit -m "Add streak logic (current streak, per-habit streak, completion counting)"
```

---

## Task 6: Storage wrapper

**Files:**
- Create: `apps/mobile/lib/storage.ts`

- [ ] **Step 1: Write the file**

```ts
// apps/mobile/lib/storage.ts

import AsyncStorage from '@react-native-async-storage/async-storage';
import type { AppData } from '../types';

const STORAGE_KEY = '@carrot/app-data/v1';
export const CURRENT_SCHEMA_VERSION = 1;

export async function loadAppData(): Promise<AppData | null> {
	try {
		const raw = await AsyncStorage.getItem(STORAGE_KEY);
		if (!raw) return null;
		const parsed = JSON.parse(raw) as AppData;
		if (parsed.schemaVersion !== CURRENT_SCHEMA_VERSION) {
			// Schema mismatch — treat as fresh install for dummy app
			return null;
		}
		return parsed;
	} catch (err) {
		console.warn('[storage] failed to load, resetting:', err);
		return null;
	}
}

export async function saveAppData(data: AppData): Promise<void> {
	await AsyncStorage.setItem(STORAGE_KEY, JSON.stringify(data));
}

export async function clearAppData(): Promise<void> {
	await AsyncStorage.removeItem(STORAGE_KEY);
}
```

- [ ] **Step 2: Typecheck**

Run: `cd apps/mobile && npx tsc --noEmit`
Expected: No errors.

- [ ] **Step 3: Commit**

```bash
git add apps/mobile/lib/storage.ts
git commit -m "Add AsyncStorage wrapper for AppData persistence"
```

---

## Task 7: Mock data generator

**Files:**
- Create: `apps/mobile/lib/mock-data.ts`

- [ ] **Step 1: Write the file**

```ts
// apps/mobile/lib/mock-data.ts

import type {
	AppData,
	Goal,
	Habit,
	HabitCompletion,
	Milestone,
	Todo,
	Squad,
	SquadMember,
	Bet,
	Challenge,
	Duel,
	SquadBattle,
	Pact,
	Achievement,
} from '../types';
import { addMonths, offsetKey, randomId } from './date';
import { CURRENT_SCHEMA_VERSION } from './storage';

/**
 * Creates a full pre-seeded AppData for the dummy app.
 * This is what a user sees on first launch after onboarding.
 * Hard-coded for "Fitter werden" goal — Plan 6 will make onboarding configurable.
 */
export function createMockAppData(name = 'Du'): AppData {
	const goalId = 'goal-fitness';
	const habitRun = 'habit-run';
	const habitWater = 'habit-water';
	const habitStretch = 'habit-stretch';
	const habitFood = 'habit-food';

	const goal: Goal = {
		id: goalId,
		title: 'Fitter werden',
		emoji: '🏃',
		description: 'Marathon in 6 Monaten',
		targetDate: addMonths(6),
		status: 'active',
		createdAt: offsetKey(-10) + 'T00:00:00.000Z',
	};

	const milestones: Milestone[] = [
		{
			id: 'mile-1',
			goalId,
			title: 'Erste 5km ohne Pause laufen',
			xpReward: 200,
			completed: true,
			completedAt: offsetKey(-5) + 'T12:00:00.000Z',
			order: 1,
		},
		{
			id: 'mile-2',
			goalId,
			title: '30 Tage Streak',
			xpReward: 300,
			completed: false,
			order: 2,
		},
		{
			id: 'mile-3',
			goalId,
			title: '10km in unter 60 Min',
			xpReward: 400,
			completed: false,
			order: 3,
		},
		{
			id: 'mile-4',
			goalId,
			title: 'Halbmarathon finishen',
			xpReward: 500,
			completed: false,
			order: 4,
		},
	];

	const todos: Todo[] = [
		{
			id: 'todo-1',
			goalId,
			milestoneId: 'mile-1',
			title: 'Laufschuhe kaufen',
			xpReward: 20,
			completed: true,
			completedAt: offsetKey(-8) + 'T10:00:00.000Z',
		},
		{
			id: 'todo-2',
			goalId,
			milestoneId: 'mile-2',
			title: 'Trainingsplan recherchieren',
			xpReward: 30,
			completed: false,
			dueDate: offsetKey(3),
		},
		{
			id: 'todo-3',
			goalId,
			milestoneId: 'mile-3',
			title: 'Beim 10km-Lauf anmelden',
			xpReward: 40,
			completed: false,
			dueDate: offsetKey(14),
		},
	];

	const habits: Habit[] = [
		{
			id: habitRun,
			goalId,
			title: '30 Min Bewegung',
			emoji: '🏃',
			xpReward: 50,
			frequency: 'daily',
			currentStreak: 12,
			createdAt: offsetKey(-15) + 'T00:00:00.000Z',
		},
		{
			id: habitWater,
			goalId,
			title: '2L Wasser trinken',
			emoji: '💧',
			xpReward: 25,
			frequency: 'daily',
			currentStreak: 8,
			createdAt: offsetKey(-15) + 'T00:00:00.000Z',
		},
		{
			id: habitStretch,
			goalId,
			title: '10 Min Stretching',
			emoji: '🧘',
			xpReward: 25,
			frequency: 'daily',
			currentStreak: 5,
			createdAt: offsetKey(-10) + 'T00:00:00.000Z',
		},
		{
			id: habitFood,
			goalId,
			title: 'Ernährungsplan',
			emoji: '🥗',
			xpReward: 30,
			frequency: 'daily',
			currentStreak: 3,
			createdAt: offsetKey(-8) + 'T00:00:00.000Z',
		},
	];

	// 10 days of historical completions (not today — user should check in today)
	const completions: HabitCompletion[] = [];
	for (let d = 10; d >= 1; d--) {
		const key = offsetKey(-d);
		const iso = key + 'T20:00:00.000Z';
		// All 4 habits done every past day to give current 10-day streak
		for (const h of habits) {
			completions.push({
				id: randomId(),
				habitId: h.id,
				dateKey: key,
				xpEarned: h.xpReward,
				completedAt: iso,
			});
		}
	}

	const squadMembers: SquadMember[] = [
		{
			id: 'member-anna',
			name: 'Anna K.',
			initials: 'AK',
			avatarColor: '#4CAF50',
			currentStreak: 14,
			weeklyXP: 380,
			totalXP: 4200,
		},
		{
			id: 'member-max',
			name: 'Max L.',
			initials: 'ML',
			avatarColor: '#2196F3',
			currentStreak: 9,
			weeklyXP: 340,
			totalXP: 3800,
		},
		{
			id: 'member-sarah',
			name: 'Sarah B.',
			initials: 'SB',
			avatarColor: '#9C27B0',
			currentStreak: 4,
			weeklyXP: 290,
			totalXP: 2100,
		},
	];

	const squad: Squad = {
		id: 'squad-1',
		name: 'Fitness Crew',
		emoji: '💪',
		level: 3,
		currentStreak: 4, // min of member streaks
		members: squadMembers,
	};

	const bets: Bet[] = [
		{
			id: 'bet-1',
			creatorId: 'user',
			description: '14 Tage Lauf-Streak',
			targetValue: 14,
			currentValue: 7,
			stake: 200,
			supports: [
				{ memberId: 'member-anna', side: 'for', stake: 100 },
				{ memberId: 'member-max', side: 'against', stake: 100 },
			],
			deadline: offsetKey(7),
			status: 'active',
			createdAt: offsetKey(-7) + 'T00:00:00.000Z',
		},
	];

	const challenges: Challenge[] = [
		{
			id: 'challenge-1',
			type: 'streak_sprint',
			title: '7 Tage Streak Sprint 🔥',
			description: 'Alle 4 Members müssen 7 Tage Streak halten.',
			durationDays: 7,
			startDate: offsetKey(-3),
			rewardXP: 500,
			status: 'active',
			progress: [
				{
					memberId: 'user',
					dailyStatus: ['done', 'done', 'done', 'pending', 'pending', 'pending', 'pending'],
				},
				{
					memberId: 'member-anna',
					dailyStatus: ['done', 'done', 'done', 'pending', 'pending', 'pending', 'pending'],
				},
				{
					memberId: 'member-max',
					dailyStatus: ['done', 'done', 'missed', 'pending', 'pending', 'pending', 'pending'],
				},
				{
					memberId: 'member-sarah',
					dailyStatus: ['done', 'done', 'done', 'pending', 'pending', 'pending', 'pending'],
				},
			],
		},
	];

	const duels: Duel[] = [
		{
			id: 'duel-1',
			opponentId: 'member-anna',
			metric: 'checkins_this_week',
			stake: 100,
			userScore: 5,
			opponentScore: 4,
			deadline: offsetKey(3),
			trashTalk: [
				{ from: 'opponent', message: 'Morgen zieh ich vorbei 😏', timestamp: offsetKey(-1) + 'T18:00:00.000Z' },
				{ from: 'user', message: 'Träum weiter 💪', timestamp: offsetKey(-1) + 'T18:05:00.000Z' },
			],
			status: 'active',
		},
	];

	const battles: SquadBattle[] = [
		{
			id: 'battle-1',
			opponentSquad: { name: 'Morning Runners', emoji: '🌅', memberCount: 3, weeklyXP: 1620 },
			ourSquadXP: 1840,
			deadline: offsetKey(3),
			status: 'active',
		},
	];

	const pacts: Pact[] = [
		{
			id: 'pact-1',
			partnerId: 'member-anna',
			sharedStreak: 12,
			multiplier: 2,
			active: true,
			createdAt: offsetKey(-12) + 'T00:00:00.000Z',
		},
	];

	const achievements: Achievement[] = [
		{
			id: 'ach-streak-7',
			key: 'streak_7',
			title: '7-Tage Streak',
			emoji: '🔥',
			description: '7 Tage am Stück alle Habits erledigt',
			unlockedAt: offsetKey(-3) + 'T20:00:00.000Z',
		},
		{
			id: 'ach-perfect-day',
			key: 'perfect_day',
			title: 'Perfekter Tag',
			emoji: '⚡',
			description: 'Alle Habits an einem Tag erledigt',
			unlockedAt: offsetKey(-10) + 'T20:00:00.000Z',
		},
		{
			id: 'ach-first-milestone',
			key: 'first_milestone',
			title: 'Erster Meilenstein',
			emoji: '🥕',
			description: 'Deinen ersten Meilenstein erreicht',
			unlockedAt: offsetKey(-5) + 'T12:00:00.000Z',
		},
		{
			id: 'ach-streak-30',
			key: 'streak_30',
			title: '30-Tage Streak',
			emoji: '💎',
			description: '30 Tage am Stück alle Habits erledigt',
		},
		{
			id: 'ach-level-5',
			key: 'level_5',
			title: 'Level 5',
			emoji: '🏆',
			description: 'Erreiche Level 5',
		},
		{
			id: 'ach-first-bet',
			key: 'first_bet',
			title: 'Erste Wette',
			emoji: '🎲',
			description: 'Gewinne deine erste XP-Wette',
		},
	];

	const data: AppData = {
		schemaVersion: CURRENT_SCHEMA_VERSION,
		user: {
			id: 'user',
			name,
			avatarColor: '#FA7000',
			totalXP: 1800,
			currentStreak: 10,
			longestStreak: 10,
			createdAt: offsetKey(-15) + 'T00:00:00.000Z',
		},
		goals: [goal],
		milestones,
		todos,
		habits,
		completions,
		squad,
		bets,
		challenges,
		duels,
		battles,
		pacts,
		achievements,
		settings: {
			themeMode: 'system',
			remindersEnabled: true,
			language: 'de',
		},
	};

	return data;
}
```

- [ ] **Step 2: Typecheck**

Run: `cd apps/mobile && npx tsc --noEmit`
Expected: No errors.

- [ ] **Step 3: Commit**

```bash
git add apps/mobile/lib/mock-data.ts
git commit -m "Add mock data generator with full seed (goal, habits, squad, bets, challenges, pacts, achievements)"
```

---

## Task 8: Theme tokens (light + dark)

**Files:**
- Create: `apps/mobile/theme/colors.ts`
- Create: `apps/mobile/theme/index.ts`

- [ ] **Step 1: Write `apps/mobile/theme/colors.ts`**

```ts
// apps/mobile/theme/colors.ts

import { colors as brand } from '@carrot/shared/constants';

export interface ThemeColors {
	background: string;
	surface: string;
	surfaceElevated: string;
	surfaceHighlight: string;
	border: string;
	text: {
		primary: string;
		secondary: string;
		muted: string;
		inverse: string;
	};
	brand: string; // #FA7000
	brandMuted: string;
	success: string;
	warning: string;
	error: string;
}

export const lightColors: ThemeColors = {
	background: '#FFFFFF',
	surface: '#F8F8F8',
	surfaceElevated: '#FFFFFF',
	surfaceHighlight: '#FFF8F2',
	border: '#E5E5E5',
	text: {
		primary: '#171717',
		secondary: '#525252',
		muted: '#A3A3A3',
		inverse: '#FFFFFF',
	},
	brand: brand.orange,
	brandMuted: '#FFD4A8',
	success: brand.success,
	warning: brand.warning,
	error: brand.error,
};

export const darkColors: ThemeColors = {
	background: '#1A1A1A',
	surface: '#252525',
	surfaceElevated: '#2A2A2A',
	surfaceHighlight: '#2A2215',
	border: '#333333',
	text: {
		primary: '#FFFFFF',
		secondary: '#BBBBBB',
		muted: '#888888',
		inverse: '#171717',
	},
	brand: brand.orange,
	brandMuted: '#8A4500',
	success: brand.success,
	warning: brand.warning,
	error: brand.error,
};
```

- [ ] **Step 2: Write `apps/mobile/theme/index.ts`**

```ts
// apps/mobile/theme/index.ts

export { lightColors, darkColors } from './colors';
export type { ThemeColors } from './colors';
```

- [ ] **Step 3: Typecheck**

Run: `cd apps/mobile && npx tsc --noEmit`
Expected: No errors.

- [ ] **Step 4: Commit**

```bash
git add apps/mobile/theme/
git commit -m "Add theme colors for light + dark mode"
```

---

## Task 9: ThemeProvider

**Files:**
- Create: `apps/mobile/providers/ThemeProvider.tsx`
- Create: `apps/mobile/hooks/useTheme.ts`

- [ ] **Step 1: Write `apps/mobile/providers/ThemeProvider.tsx`**

```tsx
// apps/mobile/providers/ThemeProvider.tsx

import { createContext, useMemo, useState, useCallback, type ReactNode } from 'react';
import { useColorScheme } from 'react-native';
import { darkColors, lightColors, type ThemeColors } from '../theme';
import type { ThemeMode } from '../types';

export interface ThemeContextValue {
	mode: ThemeMode;
	isDark: boolean;
	colors: ThemeColors;
	setMode: (mode: ThemeMode) => void;
	toggle: () => void;
}

export const ThemeContext = createContext<ThemeContextValue | null>(null);

interface ThemeProviderProps {
	children: ReactNode;
	initialMode?: ThemeMode;
}

export function ThemeProvider({ children, initialMode = 'system' }: ThemeProviderProps) {
	const systemScheme = useColorScheme();
	const [mode, setMode] = useState<ThemeMode>(initialMode);

	const isDark = mode === 'system' ? systemScheme === 'dark' : mode === 'dark';
	const colors = isDark ? darkColors : lightColors;

	const toggle = useCallback(() => {
		setMode((prev) => (prev === 'dark' ? 'light' : 'dark'));
	}, []);

	const value = useMemo<ThemeContextValue>(
		() => ({ mode, isDark, colors, setMode, toggle }),
		[mode, isDark, colors, toggle],
	);

	return <ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>;
}
```

- [ ] **Step 2: Write `apps/mobile/hooks/useTheme.ts`**

```ts
// apps/mobile/hooks/useTheme.ts

import { useContext } from 'react';
import { ThemeContext, type ThemeContextValue } from '../providers/ThemeProvider';

export function useTheme(): ThemeContextValue {
	const ctx = useContext(ThemeContext);
	if (!ctx) throw new Error('useTheme must be used inside ThemeProvider');
	return ctx;
}
```

- [ ] **Step 3: Typecheck**

Run: `cd apps/mobile && npx tsc --noEmit`
Expected: No errors.

- [ ] **Step 4: Commit**

```bash
git add apps/mobile/providers/ThemeProvider.tsx apps/mobile/hooks/useTheme.ts
git commit -m "Add ThemeProvider with light/dark mode and useTheme hook"
```

---

## Task 10: AppDataProvider

**Files:**
- Create: `apps/mobile/providers/AppDataProvider.tsx`
- Create: `apps/mobile/hooks/useAppData.ts`

- [ ] **Step 1: Write `apps/mobile/providers/AppDataProvider.tsx`**

```tsx
// apps/mobile/providers/AppDataProvider.tsx

import {
	createContext,
	useEffect,
	useMemo,
	useState,
	useCallback,
	type ReactNode,
} from 'react';
import type { AppData, Habit, HabitCompletion } from '../types';
import { loadAppData, saveAppData } from '../lib/storage';
import { createMockAppData } from '../lib/mock-data';
import { todayKey, randomId } from '../lib/date';
import {
	allHabitsCompletedOn,
	computeCurrentStreak,
	isHabitDoneToday,
} from '../lib/streak';

export interface AppDataContextValue {
	ready: boolean;
	data: AppData | null;
	/** Complete a habit for today. Awards XP, updates streak if all done, unlocks achievements. */
	checkInHabit: (habitId: string) => Promise<{ xpEarned: number; allDone: boolean }>;
	/** Uncheck a habit completed today (removes completion, refunds XP). */
	uncheckHabit: (habitId: string) => Promise<void>;
	/** Replace the full data object (used by onboarding / reset). */
	replaceData: (next: AppData) => Promise<void>;
	/** Wipe storage and reseed with mock data (dev helper). */
	reset: () => Promise<void>;
}

export const AppDataContext = createContext<AppDataContextValue | null>(null);

export function AppDataProvider({ children }: { children: ReactNode }) {
	const [data, setData] = useState<AppData | null>(null);
	const [ready, setReady] = useState(false);

	useEffect(() => {
		(async () => {
			let loaded = await loadAppData();
			if (!loaded) {
				loaded = createMockAppData();
				await saveAppData(loaded);
			}
			setData(loaded);
			setReady(true);
		})();
	}, []);

	const persist = useCallback(async (next: AppData) => {
		setData(next);
		await saveAppData(next);
	}, []);

	const checkInHabit = useCallback<AppDataContextValue['checkInHabit']>(
		async (habitId) => {
			if (!data) return { xpEarned: 0, allDone: false };
			const habit = data.habits.find((h) => h.id === habitId);
			if (!habit) return { xpEarned: 0, allDone: false };
			if (isHabitDoneToday(habit, data.completions)) {
				return { xpEarned: 0, allDone: allHabitsCompletedOn(data.habits, data.completions, todayKey()) };
			}

			const newCompletion: HabitCompletion = {
				id: randomId(),
				habitId,
				dateKey: todayKey(),
				xpEarned: habit.xpReward,
				completedAt: new Date().toISOString(),
			};

			const newCompletions = [...data.completions, newCompletion];
			let xpEarned = habit.xpReward;

			// Bonus XP if this completes ALL today's habits
			const allDoneNow = allHabitsCompletedOn(data.habits, newCompletions, todayKey());
			const bonus = allDoneNow ? 50 : 0;
			xpEarned += bonus;

			const nextUser = {
				...data.user,
				totalXP: data.user.totalXP + xpEarned,
			};

			// Recompute streak
			const newStreak = computeCurrentStreak(data.habits, newCompletions);
			nextUser.currentStreak = newStreak;
			nextUser.longestStreak = Math.max(data.user.longestStreak, newStreak);

			const next: AppData = {
				...data,
				completions: newCompletions,
				user: nextUser,
			};

			await persist(next);
			return { xpEarned, allDone: allDoneNow };
		},
		[data, persist],
	);

	const uncheckHabit = useCallback<AppDataContextValue['uncheckHabit']>(
		async (habitId) => {
			if (!data) return;
			const today = todayKey();
			const completion = data.completions.find(
				(c) => c.habitId === habitId && c.dateKey === today,
			);
			if (!completion) return;
			const allDoneBefore = allHabitsCompletedOn(data.habits, data.completions, today);
			const newCompletions = data.completions.filter((c) => c.id !== completion.id);
			let refund = completion.xpEarned;
			if (allDoneBefore) refund += 50; // remove the all-done bonus

			const nextUser = {
				...data.user,
				totalXP: Math.max(0, data.user.totalXP - refund),
				currentStreak: computeCurrentStreak(data.habits, newCompletions),
			};
			await persist({ ...data, completions: newCompletions, user: nextUser });
		},
		[data, persist],
	);

	const replaceData = useCallback(
		async (next: AppData) => {
			await persist(next);
		},
		[persist],
	);

	const reset = useCallback(async () => {
		const fresh = createMockAppData(data?.user.name ?? 'Du');
		await persist(fresh);
	}, [data, persist]);

	const value = useMemo<AppDataContextValue>(
		() => ({ ready, data, checkInHabit, uncheckHabit, replaceData, reset }),
		[ready, data, checkInHabit, uncheckHabit, replaceData, reset],
	);

	return <AppDataContext.Provider value={value}>{children}</AppDataContext.Provider>;
}
```

- [ ] **Step 2: Write `apps/mobile/hooks/useAppData.ts`**

```ts
// apps/mobile/hooks/useAppData.ts

import { useContext } from 'react';
import { AppDataContext, type AppDataContextValue } from '../providers/AppDataProvider';

export function useAppData(): AppDataContextValue {
	const ctx = useContext(AppDataContext);
	if (!ctx) throw new Error('useAppData must be used inside AppDataProvider');
	return ctx;
}
```

- [ ] **Step 3: Typecheck**

Run: `cd apps/mobile && npx tsc --noEmit`
Expected: No errors.

- [ ] **Step 4: Commit**

```bash
git add apps/mobile/providers/AppDataProvider.tsx apps/mobile/hooks/useAppData.ts
git commit -m "Add AppDataProvider with check-in, uncheck, and persistence"
```

---

## Task 11: useActiveGoal hook

**Files:**
- Create: `apps/mobile/hooks/useActiveGoal.ts`

- [ ] **Step 1: Write the file**

```ts
// apps/mobile/hooks/useActiveGoal.ts

import { useMemo } from 'react';
import { useAppData } from './useAppData';
import type { Goal, Habit, HabitCompletion, Milestone, Todo } from '../types';
import { completedTodayCount, isHabitDoneToday } from '../lib/streak';

export interface ActiveGoalSnapshot {
	goal: Goal | null;
	habits: Habit[];
	milestones: Milestone[];
	todos: Todo[];
	completions: HabitCompletion[];
	todayCompletedCount: number;
	totalHabitsToday: number;
	allDoneToday: boolean;
	progressPercent: number; // 0..100
}

export function useActiveGoal(): ActiveGoalSnapshot {
	const { data } = useAppData();

	return useMemo(() => {
		if (!data) {
			return {
				goal: null,
				habits: [],
				milestones: [],
				todos: [],
				completions: [],
				todayCompletedCount: 0,
				totalHabitsToday: 0,
				allDoneToday: false,
				progressPercent: 0,
			};
		}
		const goal = data.goals.find((g) => g.status === 'active') ?? data.goals[0] ?? null;
		if (!goal) {
			return {
				goal: null,
				habits: [],
				milestones: [],
				todos: [],
				completions: [],
				todayCompletedCount: 0,
				totalHabitsToday: 0,
				allDoneToday: false,
				progressPercent: 0,
			};
		}
		const habits = data.habits.filter((h) => h.goalId === goal.id);
		const milestones = data.milestones
			.filter((m) => m.goalId === goal.id)
			.sort((a, b) => a.order - b.order);
		const todos = data.todos.filter((t) => t.goalId === goal.id);
		const dailyHabits = habits.filter((h) => h.frequency === 'daily');
		const todayCompletedCount = completedTodayCount(dailyHabits, data.completions);
		const totalHabitsToday = dailyHabits.length;
		const allDoneToday = totalHabitsToday > 0 && dailyHabits.every((h) => isHabitDoneToday(h, data.completions));

		// Progress: 60% milestones + 40% habit consistency (last 10 days)
		const milestoneProgress =
			milestones.length === 0 ? 0 : milestones.filter((m) => m.completed).length / milestones.length;
		const last10DaysCompletions = data.completions.filter((c) => {
			const d = new Date(c.completedAt);
			const diff = (Date.now() - d.getTime()) / (1000 * 60 * 60 * 24);
			return diff <= 10;
		});
		const expected = dailyHabits.length * 10;
		const habitConsistency = expected === 0 ? 0 : Math.min(1, last10DaysCompletions.length / expected);
		const progressPercent = Math.round((milestoneProgress * 0.6 + habitConsistency * 0.4) * 100);

		return {
			goal,
			habits,
			milestones,
			todos,
			completions: data.completions,
			todayCompletedCount,
			totalHabitsToday,
			allDoneToday,
			progressPercent,
		};
	}, [data]);
}
```

- [ ] **Step 2: Typecheck**

Run: `cd apps/mobile && npx tsc --noEmit`
Expected: No errors.

- [ ] **Step 3: Commit**

```bash
git add apps/mobile/hooks/useActiveGoal.ts
git commit -m "Add useActiveGoal hook for derived goal/habit state"
```

---

## Task 12: GoalHeader component

**Files:**
- Create: `apps/mobile/components/GoalHeader.tsx`

- [ ] **Step 1: Write the file**

```tsx
// apps/mobile/components/GoalHeader.tsx

import { View, Text, StyleSheet } from 'react-native';
import { spacing, fontSize, radii } from '@carrot/shared/constants';
import { useTheme } from '../hooks/useTheme';
import { useAppData } from '../hooks/useAppData';
import { levelFromXP, levelProgress } from '../lib/xp';
import type { Goal } from '../types';

interface GoalHeaderProps {
	goal: Goal;
}

export function GoalHeader({ goal }: GoalHeaderProps) {
	const { colors } = useTheme();
	const { data } = useAppData();
	const totalXP = data?.user.totalXP ?? 0;
	const level = levelFromXP(totalXP);
	const progress = levelProgress(totalXP);

	return (
		<View style={styles.container}>
			<View style={styles.row}>
				<View style={styles.titleBlock}>
					<Text style={[styles.label, { color: colors.text.muted }]}>Dein Ziel</Text>
					<Text style={[styles.title, { color: colors.text.primary }]} numberOfLines={1}>
						{goal.title} {goal.emoji}
					</Text>
				</View>
				<View style={[styles.levelBadge, { backgroundColor: colors.brand }]}>
					<Text style={styles.levelText}>L{level}</Text>
				</View>
			</View>
			<View style={[styles.progressTrack, { backgroundColor: colors.border }]}>
				<View
					style={[
						styles.progressFill,
						{ backgroundColor: colors.brand, width: `${progress * 100}%` },
					]}
				/>
			</View>
		</View>
	);
}

const styles = StyleSheet.create({
	container: {
		marginBottom: spacing.md,
	},
	row: {
		flexDirection: 'row',
		alignItems: 'center',
		justifyContent: 'space-between',
		marginBottom: spacing.sm,
	},
	titleBlock: {
		flex: 1,
		marginRight: spacing.md,
	},
	label: {
		fontSize: fontSize.xs,
	},
	title: {
		fontSize: fontSize.lg,
		fontWeight: '700',
		marginTop: 2,
	},
	levelBadge: {
		width: 40,
		height: 40,
		borderRadius: radii.full,
		alignItems: 'center',
		justifyContent: 'center',
	},
	levelText: {
		color: '#FFFFFF',
		fontSize: fontSize.sm,
		fontWeight: '700',
	},
	progressTrack: {
		height: 6,
		borderRadius: radii.sm,
		overflow: 'hidden',
	},
	progressFill: {
		height: '100%',
		borderRadius: radii.sm,
	},
});
```

- [ ] **Step 2: Typecheck**

Run: `cd apps/mobile && npx tsc --noEmit`
Expected: No errors.

- [ ] **Step 3: Commit**

```bash
git add apps/mobile/components/GoalHeader.tsx
git commit -m "Add GoalHeader component (goal title + level + XP progress)"
```

---

## Task 13: StatsRow component

**Files:**
- Create: `apps/mobile/components/StatsRow.tsx`

- [ ] **Step 1: Write the file**

```tsx
// apps/mobile/components/StatsRow.tsx

import { View, Text, StyleSheet } from 'react-native';
import { spacing, fontSize, radii } from '@carrot/shared/constants';
import { useTheme } from '../hooks/useTheme';

interface StatsRowProps {
	streak: number;
	xp: number;
	todayCount: number;
	todayTotal: number;
}

export function StatsRow({ streak, xp, todayCount, todayTotal }: StatsRowProps) {
	const { colors } = useTheme();

	return (
		<View style={styles.row}>
			<Stat label="Streak 🔥" value={String(streak)} surface={colors.surface} brand={colors.brand} mutedText={colors.text.muted} />
			<Stat label="XP" value={xp.toLocaleString('de-DE')} surface={colors.surface} brand={colors.brand} mutedText={colors.text.muted} />
			<Stat label="Heute" value={`${todayCount}/${todayTotal}`} surface={colors.surface} brand={colors.brand} mutedText={colors.text.muted} />
		</View>
	);
}

interface StatProps {
	label: string;
	value: string;
	surface: string;
	brand: string;
	mutedText: string;
}

function Stat({ label, value, surface, brand, mutedText }: StatProps) {
	return (
		<View style={[styles.stat, { backgroundColor: surface }]}>
			<Text style={[styles.value, { color: brand }]}>{value}</Text>
			<Text style={[styles.label, { color: mutedText }]}>{label}</Text>
		</View>
	);
}

const styles = StyleSheet.create({
	row: {
		flexDirection: 'row',
		gap: spacing.sm,
		marginBottom: spacing.lg,
	},
	stat: {
		flex: 1,
		borderRadius: radii.lg,
		padding: spacing.md,
		alignItems: 'center',
	},
	value: {
		fontSize: fontSize.xl,
		fontWeight: '800',
	},
	label: {
		fontSize: fontSize.xs,
		marginTop: 2,
	},
});
```

- [ ] **Step 2: Typecheck**

Run: `cd apps/mobile && npx tsc --noEmit`
Expected: No errors.

- [ ] **Step 3: Commit**

```bash
git add apps/mobile/components/StatsRow.tsx
git commit -m "Add StatsRow component (streak, XP, today count)"
```

---

## Task 14: HabitCard component

**Files:**
- Create: `apps/mobile/components/HabitCard.tsx`

- [ ] **Step 1: Write the file**

```tsx
// apps/mobile/components/HabitCard.tsx

import { Pressable, View, Text, StyleSheet } from 'react-native';
import Animated, {
	useSharedValue,
	useAnimatedStyle,
	withSpring,
	withSequence,
} from 'react-native-reanimated';
import { spacing, fontSize, radii, springs } from '@carrot/shared/constants';
import { useTheme } from '../hooks/useTheme';
import type { Habit } from '../types';

interface HabitCardProps {
	habit: Habit;
	done: boolean;
	streak: number;
	onPress: () => void;
}

export function HabitCard({ habit, done, streak, onPress }: HabitCardProps) {
	const { colors } = useTheme();
	const scale = useSharedValue(1);

	const animatedStyle = useAnimatedStyle(() => ({
		transform: [{ scale: scale.value }],
	}));

	const handlePress = () => {
		scale.value = withSequence(
			withSpring(0.95, springs.snappy),
			withSpring(1, springs.bouncy),
		);
		onPress();
	};

	const bg = done ? colors.surface : colors.surfaceHighlight;
	const borderColor = done ? colors.success : colors.brand;

	return (
		<Animated.View style={animatedStyle}>
			<Pressable onPress={handlePress} style={[styles.card, { backgroundColor: bg, borderLeftColor: borderColor }]}>
				<View style={[styles.check, done ? { backgroundColor: colors.success } : { borderColor: colors.brand, borderWidth: 2 }]}>
					{done ? <Text style={styles.checkMark}>✓</Text> : null}
				</View>
				<View style={styles.body}>
					<Text style={[styles.title, { color: colors.text.primary }]}>
						{habit.title} {habit.emoji}
					</Text>
					<Text style={[styles.meta, { color: colors.text.muted }]}>
						+{habit.xpReward} XP · Streak: {streak}
					</Text>
				</View>
			</Pressable>
		</Animated.View>
	);
}

const styles = StyleSheet.create({
	card: {
		flexDirection: 'row',
		alignItems: 'center',
		padding: spacing.md,
		borderRadius: radii.lg,
		borderLeftWidth: 3,
		marginBottom: spacing.sm,
	},
	check: {
		width: 28,
		height: 28,
		borderRadius: radii.full,
		marginRight: spacing.md,
		alignItems: 'center',
		justifyContent: 'center',
	},
	checkMark: {
		color: '#FFFFFF',
		fontSize: fontSize.sm,
		fontWeight: '700',
	},
	body: {
		flex: 1,
	},
	title: {
		fontSize: fontSize.md,
		fontWeight: '600',
	},
	meta: {
		fontSize: fontSize.xs,
		marginTop: 2,
	},
});
```

- [ ] **Step 2: Typecheck**

Run: `cd apps/mobile && npx tsc --noEmit`
Expected: No errors.

- [ ] **Step 3: Commit**

```bash
git add apps/mobile/components/HabitCard.tsx
git commit -m "Add HabitCard component with spring tap animation"
```

---

## Task 15: XPPopup component

**Files:**
- Create: `apps/mobile/components/XPPopup.tsx`

- [ ] **Step 1: Write the file**

```tsx
// apps/mobile/components/XPPopup.tsx

import { useEffect } from 'react';
import { StyleSheet, Text } from 'react-native';
import Animated, {
	useSharedValue,
	useAnimatedStyle,
	withTiming,
	withDelay,
	runOnJS,
} from 'react-native-reanimated';
import { fontSize, radii, spacing, durations } from '@carrot/shared/constants';
import { useTheme } from '../hooks/useTheme';

interface XPPopupProps {
	xp: number;
	onDone: () => void;
}

export function XPPopup({ xp, onDone }: XPPopupProps) {
	const { colors } = useTheme();
	const opacity = useSharedValue(0);
	const translateY = useSharedValue(0);

	useEffect(() => {
		opacity.value = withTiming(1, { duration: durations.fast });
		translateY.value = withTiming(-60, { duration: durations.slow });
		opacity.value = withDelay(
			durations.slow,
			withTiming(0, { duration: durations.fast }, (finished) => {
				if (finished) runOnJS(onDone)();
			}),
		);
	}, [opacity, translateY, onDone]);

	const animStyle = useAnimatedStyle(() => ({
		opacity: opacity.value,
		transform: [{ translateY: translateY.value }],
	}));

	return (
		<Animated.View style={[styles.popup, animStyle, { backgroundColor: colors.brand }]} pointerEvents="none">
			<Text style={styles.text}>+{xp} XP</Text>
		</Animated.View>
	);
}

const styles = StyleSheet.create({
	popup: {
		position: 'absolute',
		top: '40%',
		alignSelf: 'center',
		paddingHorizontal: spacing.lg,
		paddingVertical: spacing.sm,
		borderRadius: radii.xl,
		zIndex: 100,
	},
	text: {
		color: '#FFFFFF',
		fontSize: fontSize.xl,
		fontWeight: '800',
	},
});
```

- [ ] **Step 2: Typecheck**

Run: `cd apps/mobile && npx tsc --noEmit`
Expected: No errors.

- [ ] **Step 3: Commit**

```bash
git add apps/mobile/components/XPPopup.tsx
git commit -m "Add XPPopup component with float-up animation"
```

---

## Task 16: CelebrationOverlay component

**Files:**
- Create: `apps/mobile/components/CelebrationOverlay.tsx`

- [ ] **Step 1: Write the file**

```tsx
// apps/mobile/components/CelebrationOverlay.tsx

import { useEffect } from 'react';
import { StyleSheet, Text, View, Pressable } from 'react-native';
import Animated, {
	useSharedValue,
	useAnimatedStyle,
	withSpring,
	withTiming,
} from 'react-native-reanimated';
import { fontSize, radii, spacing, springs } from '@carrot/shared/constants';
import { useTheme } from '../hooks/useTheme';

interface CelebrationOverlayProps {
	onDismiss: () => void;
}

export function CelebrationOverlay({ onDismiss }: CelebrationOverlayProps) {
	const { colors } = useTheme();
	const scale = useSharedValue(0.5);
	const opacity = useSharedValue(0);

	useEffect(() => {
		scale.value = withSpring(1, springs.bouncy);
		opacity.value = withTiming(1, { duration: 200 });
	}, [scale, opacity]);

	const animStyle = useAnimatedStyle(() => ({
		transform: [{ scale: scale.value }],
		opacity: opacity.value,
	}));

	return (
		<Pressable style={styles.backdrop} onPress={onDismiss}>
			<Animated.View style={[styles.card, animStyle, { backgroundColor: colors.brand }]}>
				<Text style={styles.emoji}>🎉</Text>
				<Text style={styles.title}>Alle Habits erledigt!</Text>
				<Text style={styles.subtitle}>+50 XP Bonus</Text>
				<View style={styles.tapHint}>
					<Text style={styles.hintText}>Tap zum Schließen</Text>
				</View>
			</Animated.View>
		</Pressable>
	);
}

const styles = StyleSheet.create({
	backdrop: {
		...StyleSheet.absoluteFillObject,
		backgroundColor: 'rgba(0,0,0,0.4)',
		alignItems: 'center',
		justifyContent: 'center',
		zIndex: 200,
	},
	card: {
		borderRadius: radii.xl,
		padding: spacing['2xl'],
		alignItems: 'center',
		minWidth: 260,
	},
	emoji: {
		fontSize: 64,
		marginBottom: spacing.md,
	},
	title: {
		color: '#FFFFFF',
		fontSize: fontSize['2xl'],
		fontWeight: '800',
		textAlign: 'center',
	},
	subtitle: {
		color: 'rgba(255,255,255,0.9)',
		fontSize: fontSize.md,
		marginTop: spacing.xs,
	},
	tapHint: {
		marginTop: spacing.lg,
	},
	hintText: {
		color: 'rgba(255,255,255,0.7)',
		fontSize: fontSize.xs,
	},
});
```

- [ ] **Step 2: Typecheck**

Run: `cd apps/mobile && npx tsc --noEmit`
Expected: No errors.

- [ ] **Step 3: Commit**

```bash
git add apps/mobile/components/CelebrationOverlay.tsx
git commit -m "Add CelebrationOverlay for all-habits-done moment"
```

---

## Task 17: Wire providers into root _layout

**Files:**
- Modify: `apps/mobile/app/_layout.tsx`

- [ ] **Step 1: Replace the file content**

```tsx
// apps/mobile/app/_layout.tsx

import { Stack } from 'expo-router';
import { StatusBar } from 'expo-status-bar';
import { GestureHandlerRootView } from 'react-native-gesture-handler';
import { ThemeProvider } from '../providers/ThemeProvider';
import { AppDataProvider } from '../providers/AppDataProvider';
import { useTheme } from '../hooks/useTheme';

function StatusBarThemed() {
	const { isDark } = useTheme();
	return <StatusBar style={isDark ? 'light' : 'dark'} />;
}

export default function RootLayout() {
	return (
		<GestureHandlerRootView style={{ flex: 1 }}>
			<ThemeProvider>
				<AppDataProvider>
					<StatusBarThemed />
					<Stack screenOptions={{ headerShown: false }} />
				</AppDataProvider>
			</ThemeProvider>
		</GestureHandlerRootView>
	);
}
```

- [ ] **Step 2: Typecheck**

Run: `cd apps/mobile && npx tsc --noEmit`
Expected: No errors.

- [ ] **Step 3: Commit**

```bash
git add apps/mobile/app/_layout.tsx
git commit -m "Wire ThemeProvider and AppDataProvider into root layout"
```

---

## Task 18: Create Squad stub screen

**Files:**
- Create: `apps/mobile/app/(tabs)/squad.tsx`

- [ ] **Step 1: Write the file**

```tsx
// apps/mobile/app/(tabs)/squad.tsx

import { View, Text, StyleSheet } from 'react-native';
import { fontSize } from '@carrot/shared/constants';
import { useTheme } from '../../hooks/useTheme';

export default function SquadScreen() {
	const { colors } = useTheme();
	return (
		<View style={[styles.container, { backgroundColor: colors.background }]}>
			<Text style={[styles.title, { color: colors.text.primary }]}>Squad</Text>
			<Text style={[styles.hint, { color: colors.text.muted }]}>Coming soon in Plan 5</Text>
		</View>
	);
}

const styles = StyleSheet.create({
	container: { flex: 1, alignItems: 'center', justifyContent: 'center' },
	title: { fontSize: fontSize['2xl'], fontWeight: '800' },
	hint: { fontSize: fontSize.sm, marginTop: 8 },
});
```

- [ ] **Step 2: Commit**

```bash
git add apps/mobile/app/\(tabs\)/squad.tsx
git commit -m "Add Squad tab stub"
```

---

## Task 19: Update tab navigator to 4 tabs with icons

**Files:**
- Modify: `apps/mobile/app/(tabs)/_layout.tsx`

- [ ] **Step 1: Replace the file content**

```tsx
// apps/mobile/app/(tabs)/_layout.tsx

import { Tabs } from 'expo-router';
import { Text } from 'react-native';
import { useTheme } from '../../hooks/useTheme';

function TabIcon({ emoji, color }: { emoji: string; color: string }) {
	return <Text style={{ fontSize: 22, color }}>{emoji}</Text>;
}

export default function TabLayout() {
	const { colors } = useTheme();
	return (
		<Tabs
			screenOptions={{
				headerShown: false,
				tabBarActiveTintColor: colors.brand,
				tabBarInactiveTintColor: colors.text.muted,
				tabBarStyle: {
					backgroundColor: colors.background,
					borderTopColor: colors.border,
				},
			}}
		>
			<Tabs.Screen
				name="index"
				options={{
					title: 'Home',
					tabBarIcon: ({ color }) => <TabIcon emoji="🏠" color={color} />,
				}}
			/>
			<Tabs.Screen
				name="progress"
				options={{
					title: 'Progress',
					tabBarIcon: ({ color }) => <TabIcon emoji="📊" color={color} />,
				}}
			/>
			<Tabs.Screen
				name="squad"
				options={{
					title: 'Squad',
					tabBarIcon: ({ color }) => <TabIcon emoji="💪" color={color} />,
				}}
			/>
			<Tabs.Screen
				name="profile"
				options={{
					title: 'Profile',
					tabBarIcon: ({ color }) => <TabIcon emoji="👤" color={color} />,
				}}
			/>
		</Tabs>
	);
}
```

- [ ] **Step 2: Typecheck**

Run: `cd apps/mobile && npx tsc --noEmit`
Expected: No errors.

- [ ] **Step 3: Commit**

```bash
git add apps/mobile/app/\(tabs\)/_layout.tsx
git commit -m "Update tab layout to 4 tabs with emoji icons and themed colors"
```

---

## Task 20: Update Progress and Profile stubs to be themed

**Files:**
- Modify: `apps/mobile/app/(tabs)/progress.tsx`
- Modify: `apps/mobile/app/(tabs)/profile.tsx`

- [ ] **Step 1: Replace `apps/mobile/app/(tabs)/progress.tsx`**

```tsx
// apps/mobile/app/(tabs)/progress.tsx

import { View, Text, StyleSheet } from 'react-native';
import { fontSize } from '@carrot/shared/constants';
import { useTheme } from '../../hooks/useTheme';

export default function ProgressScreen() {
	const { colors } = useTheme();
	return (
		<View style={[styles.container, { backgroundColor: colors.background }]}>
			<Text style={[styles.title, { color: colors.text.primary }]}>Progress</Text>
			<Text style={[styles.hint, { color: colors.text.muted }]}>Coming soon in Plan 4</Text>
		</View>
	);
}

const styles = StyleSheet.create({
	container: { flex: 1, alignItems: 'center', justifyContent: 'center' },
	title: { fontSize: fontSize['2xl'], fontWeight: '800' },
	hint: { fontSize: fontSize.sm, marginTop: 8 },
});
```

- [ ] **Step 2: Replace `apps/mobile/app/(tabs)/profile.tsx`**

```tsx
// apps/mobile/app/(tabs)/profile.tsx

import { View, Text, StyleSheet } from 'react-native';
import { fontSize } from '@carrot/shared/constants';
import { useTheme } from '../../hooks/useTheme';

export default function ProfileScreen() {
	const { colors } = useTheme();
	return (
		<View style={[styles.container, { backgroundColor: colors.background }]}>
			<Text style={[styles.title, { color: colors.text.primary }]}>Profile</Text>
			<Text style={[styles.hint, { color: colors.text.muted }]}>Coming soon in Plan 4</Text>
		</View>
	);
}

const styles = StyleSheet.create({
	container: { flex: 1, alignItems: 'center', justifyContent: 'center' },
	title: { fontSize: fontSize['2xl'], fontWeight: '800' },
	hint: { fontSize: fontSize.sm, marginTop: 8 },
});
```

- [ ] **Step 3: Typecheck**

Run: `cd apps/mobile && npx tsc --noEmit`
Expected: No errors.

- [ ] **Step 4: Commit**

```bash
git add apps/mobile/app/\(tabs\)/progress.tsx apps/mobile/app/\(tabs\)/profile.tsx
git commit -m "Theme Progress and Profile tab stubs"
```

---

## Task 21: Build the Home tab

**Files:**
- Modify: `apps/mobile/app/(tabs)/index.tsx`

- [ ] **Step 1: Replace the file content**

```tsx
// apps/mobile/app/(tabs)/index.tsx

import { useState, useCallback } from 'react';
import { View, Text, StyleSheet, ScrollView, ActivityIndicator, SafeAreaView } from 'react-native';
import { spacing, fontSize } from '@carrot/shared/constants';
import { useTheme } from '../../hooks/useTheme';
import { useAppData } from '../../hooks/useAppData';
import { useActiveGoal } from '../../hooks/useActiveGoal';
import { GoalHeader } from '../../components/GoalHeader';
import { StatsRow } from '../../components/StatsRow';
import { HabitCard } from '../../components/HabitCard';
import { XPPopup } from '../../components/XPPopup';
import { CelebrationOverlay } from '../../components/CelebrationOverlay';
import { computeHabitStreak, isHabitDoneToday } from '../../lib/streak';

export default function HomeScreen() {
	const { colors } = useTheme();
	const { ready, data, checkInHabit, uncheckHabit } = useAppData();
	const { goal, habits, todayCompletedCount, totalHabitsToday } = useActiveGoal();
	const [popupXP, setPopupXP] = useState<number | null>(null);
	const [showCelebration, setShowCelebration] = useState(false);

	const handlePress = useCallback(
		async (habitId: string) => {
			if (!data) return;
			const habit = data.habits.find((h) => h.id === habitId);
			if (!habit) return;
			if (isHabitDoneToday(habit, data.completions)) {
				await uncheckHabit(habitId);
				return;
			}
			const result = await checkInHabit(habitId);
			if (result.xpEarned > 0) setPopupXP(result.xpEarned);
			if (result.allDone) setShowCelebration(true);
		},
		[data, checkInHabit, uncheckHabit],
	);

	if (!ready || !data || !goal) {
		return (
			<View style={[styles.loading, { backgroundColor: colors.background }]}>
				<ActivityIndicator color={colors.brand} />
			</View>
		);
	}

	const dailyHabits = habits.filter((h) => h.frequency === 'daily');

	return (
		<SafeAreaView style={[styles.safe, { backgroundColor: colors.background }]}>
			<ScrollView contentContainerStyle={styles.container}>
				<GoalHeader goal={goal} />
				<StatsRow
					streak={data.user.currentStreak}
					xp={data.user.totalXP}
					todayCount={todayCompletedCount}
					todayTotal={totalHabitsToday}
				/>
				<Text style={[styles.sectionLabel, { color: colors.text.muted }]}>HEUTE</Text>
				{dailyHabits.map((habit) => {
					const done = isHabitDoneToday(habit, data.completions);
					const streak = computeHabitStreak(habit, data.completions);
					return (
						<HabitCard
							key={habit.id}
							habit={habit}
							done={done}
							streak={streak}
							onPress={() => handlePress(habit.id)}
						/>
					);
				})}
			</ScrollView>
			{popupXP !== null && <XPPopup xp={popupXP} onDone={() => setPopupXP(null)} />}
			{showCelebration && <CelebrationOverlay onDismiss={() => setShowCelebration(false)} />}
		</SafeAreaView>
	);
}

const styles = StyleSheet.create({
	safe: { flex: 1 },
	container: {
		padding: spacing.lg,
	},
	loading: {
		flex: 1,
		alignItems: 'center',
		justifyContent: 'center',
	},
	sectionLabel: {
		fontSize: fontSize.xs,
		fontWeight: '700',
		letterSpacing: 1,
		marginBottom: spacing.sm,
		marginTop: spacing.xs,
	},
});
```

- [ ] **Step 2: Typecheck**

Run: `cd apps/mobile && npx tsc --noEmit`
Expected: No errors.

- [ ] **Step 3: Commit**

```bash
git add apps/mobile/app/\(tabs\)/index.tsx
git commit -m "Build Home tab with goal header, stats, habits, and check-in flow"
```

---

## Task 22: Manual end-to-end validation

**No file changes — purely validation.**

- [ ] **Step 1: Start the dev server**

Run: `npm run dev:mobile`
Expected: Expo dev server starts and shows QR code.

- [ ] **Step 2: Open on device or simulator**

- Scan QR with Expo Go (iOS/Android) OR press `i` for iOS simulator / `a` for Android emulator.

- [ ] **Step 3: Verify first-launch experience**

Expected on Home tab:
- Goal header: "Fitter werden 🏃" and level badge (should be L5, since 1800 XP → floor(sqrt(18)) + 1 = 5)
- XP progress bar visible
- Stats row: Streak 10, XP 1,800, Heute 0/4
- 4 habit cards all unchecked, each showing emoji, XP, and its per-habit streak
- Tab bar shows 4 tabs: Home 🏠, Progress 📊, Squad 💪, Profile 👤

- [ ] **Step 4: Verify check-in flow**

- Tap the first habit card.
- Expected: spring bounce animation on card, checkmark appears, "+50 XP" popup floats up, stats row "Heute" increments to 1/4, XP total increases.
- Tap the same card again. Expected: card un-checks, XP refunds, stats update.
- Tap all 4 habits in sequence.
- Expected on the last tap: XP popup shows the habit's own XP + 50 bonus (e.g., "+80 XP" for Ernährungsplan with 30 XP + 50 bonus), then celebration overlay appears.
- Dismiss the overlay by tapping it.

- [ ] **Step 5: Verify persistence**

- Close and reopen the app.
- Expected: Home tab shows the same checked-in state; stats are preserved.

- [ ] **Step 6: Verify other tabs load**

- Tap each of Progress, Squad, Profile.
- Expected: Each shows title and "Coming soon in Plan X" text with correct theme colors.

- [ ] **Step 7: Verify theme follows system**

- Switch device to dark mode.
- Expected: App re-renders with dark theme (dark background, light text, dark surface colors).
- Switch back to light. App returns to light theme.

- [ ] **Step 8: If everything looks good, no commit needed. If issues found, fix them and commit fixes.**

---

## Self-review checklist (for plan author after writing)

1. **Spec coverage (Plan 1 scope only):**
   - Architecture — data layer (AsyncStorage, providers, typed interfaces) ✓ Tasks 6, 10
   - 4 tabs navigation ✓ Tasks 18, 19
   - Theming (light/dark) ✓ Tasks 8, 9, 17
   - Home tab layout (goal header, stats row, habit list) ✓ Tasks 12, 13, 14, 21
   - Check-in flow (animation, XP popup, celebration) ✓ Tasks 14, 15, 16, 21
   - Mock data seed ✓ Task 7
   - Deferred: onboarding (Plan 6), progress/squad/profile content (Plans 4-5), goal detail (Plan 3)

2. **Placeholder scan:** No TBDs. Every step has complete code. File paths exact.

3. **Type consistency:**
   - `AppData` fields match across mock-data, storage, AppDataProvider, useActiveGoal ✓
   - Hook names: `useTheme`, `useAppData`, `useActiveGoal` ✓
   - `checkInHabit` returns `{ xpEarned, allDone }` and is called consistently in Home tab ✓

4. **Known quirk:** Task 7 mock-data imports `todayKey` but only uses it via `void` to satisfy strict import rules — this is addressed in Step 2 (remove the import if tsc complains about unused). Prefer removing the import cleanly.