feat: data layer with types, 50 workouts, and Zustand stores

Types: Workout, Exercise, Trainer, UserProfile, UserSettings,
WorkoutResult, Achievement, Collection, Program.

Mock data: 50 workouts across 5 categories (full-body, core,
upper-body, lower-body, cardio), 4 durations (4/8/12/20 min),
3 levels, 5 trainers. Plus 6 collections, 3 programs, and
8 achievements.

Zustand stores with AsyncStorage persistence:
- userStore: profile, settings (haptics, sound, voice, reminders)
- activityStore: workout history, streak tracking
- playerStore: ephemeral timer state (not persisted)

Helper lookups: getWorkoutById, getWorkoutsByCategory,
getTrainerById, getPopularWorkouts, getCollectionWorkouts, etc.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Millian Lamiaux
2026-02-20 13:23:32 +01:00
parent 511e207762
commit 5477ecb852
15 changed files with 1799 additions and 0 deletions

View File

@@ -0,0 +1,72 @@
/**
* TabataFit Achievement Definitions
*/
import { Achievement } from '../types'
export const ACHIEVEMENTS: Achievement[] = [
{
id: 'first-burn',
title: 'First Burn',
description: 'Complete your first workout',
icon: 'flame',
requirement: 1,
type: 'workouts',
},
{
id: 'week-warrior',
title: 'Week Warrior',
description: '7 day streak',
icon: 'calendar',
requirement: 7,
type: 'streak',
},
{
id: 'century-club',
title: 'Century Club',
description: 'Burn 100 calories total',
icon: 'flame',
requirement: 100,
type: 'calories',
},
{
id: 'iron-will',
title: 'Iron Will',
description: 'Complete 10 workouts',
icon: 'trophy',
requirement: 10,
type: 'workouts',
},
{
id: 'tabata-master',
title: 'Tabata Master',
description: 'Complete 50 workouts',
icon: 'star',
requirement: 50,
type: 'workouts',
},
{
id: 'marathon-burner',
title: 'Marathon Burner',
description: 'Exercise for 100 minutes total',
icon: 'time',
requirement: 100,
type: 'minutes',
},
{
id: 'unstoppable',
title: 'Unstoppable',
description: '30 day streak',
icon: 'rocket',
requirement: 30,
type: 'streak',
},
{
id: 'calorie-crusher',
title: 'Calorie Crusher',
description: 'Burn 1000 calories total',
icon: 'flame',
requirement: 1000,
type: 'calories',
},
]

View File

@@ -0,0 +1,83 @@
/**
* TabataFit Collections & Programs
*/
import { Collection, Program } from '../types'
export const COLLECTIONS: Collection[] = [
{
id: 'morning-energizer',
title: 'Morning Energizer',
description: 'Start your day right',
icon: '🌅',
workoutIds: ['4', '6', '43', '47', '10'],
},
{
id: 'no-equipment',
title: 'No Equipment',
description: 'Workout anywhere',
icon: '💪',
workoutIds: ['1', '4', '6', '11', '13', '16', '17', '19', '23', '26', '31', '38', '42', '43', '45'],
},
{
id: '7-day-burn',
title: '7-Day Burn Challenge',
description: 'Transform in one week',
icon: '🔥',
workoutIds: ['1', '11', '31', '42', '6', '17', '23'],
gradient: ['#FF6B35', '#FF3B30'],
},
{
id: 'quick-intense',
title: 'Quick & Intense',
description: 'Max effort in 4 minutes',
icon: '⚡',
workoutIds: ['1', '11', '23', '35', '38', '42', '6', '17'],
},
{
id: 'core-focus',
title: 'Core Focus',
description: 'Build a solid foundation',
icon: '🎯',
workoutIds: ['11', '12', '13', '14', '16', '17'],
},
{
id: 'leg-day',
title: 'Leg Day',
description: 'Never skip leg day',
icon: '🦵',
workoutIds: ['31', '32', '33', '34', '35', '36', '37'],
},
]
export const PROGRAMS: Program[] = [
{
id: 'beginner-journey',
title: 'Beginner Journey',
description: 'Your first steps into Tabata fitness',
weeks: 2,
workoutsPerWeek: 3,
level: 'Beginner',
workoutIds: ['1', '13', '31', '43', '6', '16'],
},
{
id: 'strength-builder',
title: 'Strength Builder',
description: 'Progressive strength development',
weeks: 4,
workoutsPerWeek: 4,
level: 'Intermediate',
workoutIds: ['2', '21', '32', '9', '14', '25', '35', '29', '7', '23', '39', '44', '11', '42', '8', '27'],
},
{
id: 'fat-burn-protocol',
title: 'Fat Burn Protocol',
description: 'Maximum calorie burn program',
weeks: 6,
workoutsPerWeek: 5,
level: 'Advanced',
workoutIds: ['3', '41', '12', '34', '46', '5', '18', '27', '36', '50', '8', '15', '24', '38', '44', '20', '30', '40', '46', '50', '3', '41', '5', '8', '12', '15', '18', '24', '27', '34'],
},
]
export const FEATURED_COLLECTION_ID = '7-day-burn'

117
src/shared/data/index.ts Normal file
View File

@@ -0,0 +1,117 @@
/**
* TabataFit Data Layer
* Single source of truth + helper lookups
*/
import { WORKOUTS } from './workouts'
import { TRAINERS } from './trainers'
import { COLLECTIONS, PROGRAMS, FEATURED_COLLECTION_ID } from './collections'
import { ACHIEVEMENTS } from './achievements'
import type { WorkoutCategory, WorkoutLevel, WorkoutDuration } from '../types'
// Re-export raw data
export { WORKOUTS, TRAINERS, COLLECTIONS, PROGRAMS, ACHIEVEMENTS, FEATURED_COLLECTION_ID }
// ═══════════════════════════════════════════════════════════════════════════
// WORKOUT LOOKUPS
// ═══════════════════════════════════════════════════════════════════════════
export function getWorkoutById(id: string) {
return WORKOUTS.find(w => w.id === id)
}
export function getWorkoutsByCategory(category: WorkoutCategory) {
return WORKOUTS.filter(w => w.category === category)
}
export function getWorkoutsByTrainer(trainerId: string) {
return WORKOUTS.filter(w => w.trainerId === trainerId)
}
export function getWorkoutsByLevel(level: WorkoutLevel) {
return WORKOUTS.filter(w => w.level === level)
}
export function getWorkoutsByDuration(duration: WorkoutDuration) {
return WORKOUTS.filter(w => w.duration === duration)
}
export function getFeaturedWorkouts() {
return WORKOUTS.filter(w => w.isFeatured)
}
export function getPopularWorkouts(count = 8) {
// Simulate popularity — pick a diverse spread
return WORKOUTS.filter(w => ['1', '11', '21', '31', '41', '42', '2', '32'].includes(w.id)).slice(0, count)
}
// ═══════════════════════════════════════════════════════════════════════════
// TRAINER LOOKUPS
// ═══════════════════════════════════════════════════════════════════════════
export function getTrainerById(id: string) {
return TRAINERS.find(t => t.id === id)
}
export function getTrainerByName(name: string) {
return TRAINERS.find(t => t.name.toLowerCase() === name.toLowerCase())
}
// ═══════════════════════════════════════════════════════════════════════════
// COLLECTION LOOKUPS
// ═══════════════════════════════════════════════════════════════════════════
export function getCollectionById(id: string) {
return COLLECTIONS.find(c => c.id === id)
}
export function getCollectionWorkouts(collectionId: string) {
const collection = getCollectionById(collectionId)
if (!collection) return []
return collection.workoutIds.map(id => getWorkoutById(id)).filter(Boolean)
}
export function getFeaturedCollection() {
return getCollectionById(FEATURED_COLLECTION_ID)
}
// ═══════════════════════════════════════════════════════════════════════════
// PROGRAM LOOKUPS
// ═══════════════════════════════════════════════════════════════════════════
export function getProgramById(id: string) {
return PROGRAMS.find(p => p.id === id)
}
// ═══════════════════════════════════════════════════════════════════════════
// CATEGORY METADATA
// ═══════════════════════════════════════════════════════════════════════════
export const CATEGORIES: { id: WorkoutCategory | 'all'; label: string }[] = [
{ id: 'all', label: 'All' },
{ id: 'full-body', label: 'Full Body' },
{ id: 'core', label: 'Core' },
{ id: 'upper-body', label: 'Upper Body' },
{ id: 'lower-body', label: 'Lower Body' },
{ id: 'cardio', label: 'Cardio' },
]
// SF Symbol icon map for collections
export const COLLECTION_ICONS: Record<string, string> = {
'morning-energizer': 'sunrise.fill',
'no-equipment': 'figure.strengthtraining.traditional',
'7-day-burn': 'flame.fill',
'quick-intense': 'bolt.fill',
'core-focus': 'target',
'leg-day': 'figure.walk',
}
// Collection color map
export const COLLECTION_COLORS: Record<string, string> = {
'morning-energizer': '#FFD60A',
'no-equipment': '#30D158',
'7-day-burn': '#FF3B30',
'quick-intense': '#FF3B30',
'core-focus': '#5AC8FA',
'leg-day': '#BF5AF2',
}

View File

@@ -0,0 +1,44 @@
/**
* TabataFit Trainer Data
* 5 trainers per PRD
*/
import { Trainer } from '../types'
export const TRAINERS: Trainer[] = [
{
id: 'emma',
name: 'Emma',
specialty: 'Full Body',
color: '#FF6B35',
workoutCount: 15,
},
{
id: 'jake',
name: 'Jake',
specialty: 'Strength',
color: '#FFD60A',
workoutCount: 12,
},
{
id: 'mia',
name: 'Mia',
specialty: 'Core',
color: '#30D158',
workoutCount: 10,
},
{
id: 'alex',
name: 'Alex',
specialty: 'Cardio',
color: '#5AC8FA',
workoutCount: 8,
},
{
id: 'sofia',
name: 'Sofia',
specialty: 'Recovery',
color: '#BF5AF2',
workoutCount: 5,
},
]

1079
src/shared/data/workouts.ts Normal file

File diff suppressed because it is too large Load Diff