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:
72
src/shared/data/achievements.ts
Normal file
72
src/shared/data/achievements.ts
Normal 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',
|
||||
},
|
||||
]
|
||||
83
src/shared/data/collections.ts
Normal file
83
src/shared/data/collections.ts
Normal 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
117
src/shared/data/index.ts
Normal 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',
|
||||
}
|
||||
44
src/shared/data/trainers.ts
Normal file
44
src/shared/data/trainers.ts
Normal 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
1079
src/shared/data/workouts.ts
Normal file
File diff suppressed because it is too large
Load Diff
13
src/shared/stores/CLAUDE.md
Normal file
13
src/shared/stores/CLAUDE.md
Normal file
@@ -0,0 +1,13 @@
|
||||
<claude-mem-context>
|
||||
# Recent Activity
|
||||
|
||||
<!-- This section is auto-generated by claude-mem. Edit content outside the tags. -->
|
||||
|
||||
### Feb 20, 2026
|
||||
|
||||
| ID | Time | T | Title | Read |
|
||||
|----|------|---|-------|------|
|
||||
| #5177 | 11:54 AM | ✅ | Exported getWeeklyActivity from stores index | ~158 |
|
||||
| #5171 | 11:53 AM | 🔄 | Refactored activityStore to remove computed helpers from state | ~256 |
|
||||
| #5170 | " | 🔵 | Activity store implementation with persistence | ~220 |
|
||||
</claude-mem-context>
|
||||
109
src/shared/stores/activityStore.ts
Normal file
109
src/shared/stores/activityStore.ts
Normal file
@@ -0,0 +1,109 @@
|
||||
/**
|
||||
* TabataFit Activity Store
|
||||
* Workout history, streak, stats — persisted via AsyncStorage
|
||||
*/
|
||||
|
||||
import { create } from 'zustand'
|
||||
import { persist, createJSONStorage } from 'zustand/middleware'
|
||||
import AsyncStorage from '@react-native-async-storage/async-storage'
|
||||
import type { WorkoutResult, DayActivity } from '../types'
|
||||
|
||||
interface ActivityState {
|
||||
history: WorkoutResult[]
|
||||
streak: { current: number; longest: number }
|
||||
|
||||
// Actions
|
||||
addWorkoutResult: (result: WorkoutResult) => void
|
||||
}
|
||||
|
||||
function getDateString(timestamp: number) {
|
||||
return new Date(timestamp).toISOString().split('T')[0]
|
||||
}
|
||||
|
||||
function calculateStreak(history: WorkoutResult[]): { current: number; longest: number } {
|
||||
if (history.length === 0) return { current: 0, longest: 0 }
|
||||
|
||||
const uniqueDays = new Set(history.map(r => getDateString(r.completedAt)))
|
||||
const sortedDays = Array.from(uniqueDays).sort().reverse()
|
||||
|
||||
const today = getDateString(Date.now())
|
||||
const yesterday = getDateString(Date.now() - 86400000)
|
||||
const hasRecentActivity = sortedDays[0] === today || sortedDays[0] === yesterday
|
||||
|
||||
if (!hasRecentActivity) return { current: 0, longest: calculateLongest(sortedDays) }
|
||||
|
||||
let current = 1
|
||||
for (let i = 0; i < sortedDays.length - 1; i++) {
|
||||
const diff = new Date(sortedDays[i]).getTime() - new Date(sortedDays[i + 1]).getTime()
|
||||
if (diff <= 86400000) {
|
||||
current++
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return { current, longest: Math.max(current, calculateLongest(sortedDays)) }
|
||||
}
|
||||
|
||||
function calculateLongest(sortedDays: string[]): number {
|
||||
if (sortedDays.length === 0) return 0
|
||||
let longest = 1
|
||||
let streak = 1
|
||||
for (let i = 0; i < sortedDays.length - 1; i++) {
|
||||
const diff = new Date(sortedDays[i]).getTime() - new Date(sortedDays[i + 1]).getTime()
|
||||
if (diff <= 86400000) {
|
||||
streak++
|
||||
longest = Math.max(longest, streak)
|
||||
} else {
|
||||
streak = 1
|
||||
}
|
||||
}
|
||||
return longest
|
||||
}
|
||||
|
||||
export const useActivityStore = create<ActivityState>()(
|
||||
persist(
|
||||
(set) => ({
|
||||
history: [],
|
||||
streak: { current: 0, longest: 0 },
|
||||
|
||||
addWorkoutResult: (result) =>
|
||||
set((state) => {
|
||||
const newHistory = [result, ...state.history]
|
||||
const newStreak = calculateStreak(newHistory)
|
||||
return {
|
||||
history: newHistory,
|
||||
streak: newStreak,
|
||||
}
|
||||
}),
|
||||
}),
|
||||
{
|
||||
name: 'tabatafit-activity',
|
||||
storage: createJSONStorage(() => AsyncStorage),
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
// Standalone helper functions (use outside selectors)
|
||||
export function getWeeklyActivity(history: WorkoutResult[]): DayActivity[] {
|
||||
const days: DayActivity[] = []
|
||||
const dayNames = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']
|
||||
const today = new Date()
|
||||
const startOfWeek = new Date(today)
|
||||
startOfWeek.setDate(today.getDate() - today.getDay())
|
||||
|
||||
for (let i = 0; i < 7; i++) {
|
||||
const date = new Date(startOfWeek)
|
||||
date.setDate(startOfWeek.getDate() + i)
|
||||
const dateStr = getDateString(date.getTime())
|
||||
const workoutsOnDay = history.filter(
|
||||
r => getDateString(r.completedAt) === dateStr
|
||||
)
|
||||
days.push({
|
||||
date: dayNames[i],
|
||||
completed: workoutsOnDay.length > 0,
|
||||
workoutCount: workoutsOnDay.length,
|
||||
})
|
||||
}
|
||||
return days
|
||||
}
|
||||
7
src/shared/stores/index.ts
Normal file
7
src/shared/stores/index.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
/**
|
||||
* TabataFit Stores
|
||||
*/
|
||||
|
||||
export { useUserStore } from './userStore'
|
||||
export { useActivityStore, getWeeklyActivity } from './activityStore'
|
||||
export { usePlayerStore } from './playerStore'
|
||||
75
src/shared/stores/playerStore.ts
Normal file
75
src/shared/stores/playerStore.ts
Normal file
@@ -0,0 +1,75 @@
|
||||
/**
|
||||
* TabataFit Player Store
|
||||
* Current workout state — ephemeral (not persisted)
|
||||
*/
|
||||
|
||||
import { create } from 'zustand'
|
||||
import type { Workout } from '../types'
|
||||
|
||||
type TimerPhase = 'PREP' | 'WORK' | 'REST' | 'COMPLETE'
|
||||
|
||||
interface PlayerState {
|
||||
workout: Workout | null
|
||||
phase: TimerPhase
|
||||
timeRemaining: number
|
||||
currentRound: number
|
||||
isPaused: boolean
|
||||
isRunning: boolean
|
||||
calories: number
|
||||
startedAt: number | null
|
||||
|
||||
// Actions
|
||||
loadWorkout: (workout: Workout) => void
|
||||
setPhase: (phase: TimerPhase) => void
|
||||
setTimeRemaining: (time: number) => void
|
||||
setCurrentRound: (round: number) => void
|
||||
setPaused: (paused: boolean) => void
|
||||
setRunning: (running: boolean) => void
|
||||
addCalories: (amount: number) => void
|
||||
reset: () => void
|
||||
}
|
||||
|
||||
export const usePlayerStore = create<PlayerState>((set) => ({
|
||||
workout: null,
|
||||
phase: 'PREP',
|
||||
timeRemaining: 10,
|
||||
currentRound: 1,
|
||||
isPaused: false,
|
||||
isRunning: false,
|
||||
calories: 0,
|
||||
startedAt: null,
|
||||
|
||||
loadWorkout: (workout) =>
|
||||
set({
|
||||
workout,
|
||||
phase: 'PREP',
|
||||
timeRemaining: workout.prepTime,
|
||||
currentRound: 1,
|
||||
isPaused: false,
|
||||
isRunning: false,
|
||||
calories: 0,
|
||||
startedAt: null,
|
||||
}),
|
||||
|
||||
setPhase: (phase) => set({ phase }),
|
||||
setTimeRemaining: (time) => set({ timeRemaining: time }),
|
||||
setCurrentRound: (round) => set({ currentRound: round }),
|
||||
setPaused: (paused) => set({ isPaused: paused }),
|
||||
setRunning: (running) =>
|
||||
set((state) => ({
|
||||
isRunning: running,
|
||||
startedAt: running && !state.startedAt ? Date.now() : state.startedAt,
|
||||
})),
|
||||
addCalories: (amount) => set((state) => ({ calories: state.calories + amount })),
|
||||
reset: () =>
|
||||
set({
|
||||
workout: null,
|
||||
phase: 'PREP',
|
||||
timeRemaining: 10,
|
||||
currentRound: 1,
|
||||
isPaused: false,
|
||||
isRunning: false,
|
||||
calories: 0,
|
||||
startedAt: null,
|
||||
}),
|
||||
}))
|
||||
57
src/shared/stores/userStore.ts
Normal file
57
src/shared/stores/userStore.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
/**
|
||||
* TabataFit User Store
|
||||
* Profile, settings, subscription — persisted via AsyncStorage
|
||||
*/
|
||||
|
||||
import { create } from 'zustand'
|
||||
import { persist, createJSONStorage } from 'zustand/middleware'
|
||||
import AsyncStorage from '@react-native-async-storage/async-storage'
|
||||
import type { UserProfile, UserSettings, SubscriptionPlan } from '../types'
|
||||
|
||||
interface UserState {
|
||||
profile: UserProfile
|
||||
settings: UserSettings
|
||||
// Actions
|
||||
updateProfile: (updates: Partial<UserProfile>) => void
|
||||
updateSettings: (updates: Partial<UserSettings>) => void
|
||||
setSubscription: (plan: SubscriptionPlan) => void
|
||||
}
|
||||
|
||||
export const useUserStore = create<UserState>()(
|
||||
persist(
|
||||
(set) => ({
|
||||
profile: {
|
||||
name: 'Alex',
|
||||
email: 'alex@example.com',
|
||||
joinDate: 'January 2026',
|
||||
subscription: 'premium-monthly',
|
||||
},
|
||||
settings: {
|
||||
haptics: true,
|
||||
soundEffects: true,
|
||||
voiceCoaching: true,
|
||||
reminders: false,
|
||||
reminderTime: '09:00',
|
||||
},
|
||||
|
||||
updateProfile: (updates) =>
|
||||
set((state) => ({
|
||||
profile: { ...state.profile, ...updates },
|
||||
})),
|
||||
|
||||
updateSettings: (updates) =>
|
||||
set((state) => ({
|
||||
settings: { ...state.settings, ...updates },
|
||||
})),
|
||||
|
||||
setSubscription: (plan) =>
|
||||
set((state) => ({
|
||||
profile: { ...state.profile, subscription: plan },
|
||||
})),
|
||||
}),
|
||||
{
|
||||
name: 'tabatafit-user',
|
||||
storage: createJSONStorage(() => AsyncStorage),
|
||||
}
|
||||
)
|
||||
)
|
||||
32
src/shared/types/activity.ts
Normal file
32
src/shared/types/activity.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
/**
|
||||
* TabataFit Activity Types
|
||||
*/
|
||||
|
||||
export interface WorkoutResult {
|
||||
id: string
|
||||
workoutId: string
|
||||
completedAt: number
|
||||
calories: number
|
||||
durationMinutes: number
|
||||
rounds: number
|
||||
/** 0..1 */
|
||||
completionRate: number
|
||||
}
|
||||
|
||||
export interface DayActivity {
|
||||
date: string
|
||||
completed: boolean
|
||||
workoutCount: number
|
||||
}
|
||||
|
||||
export interface WeeklyStats {
|
||||
days: DayActivity[]
|
||||
totalWorkouts: number
|
||||
totalMinutes: number
|
||||
totalCalories: number
|
||||
}
|
||||
|
||||
export interface Streak {
|
||||
current: number
|
||||
longest: number
|
||||
}
|
||||
8
src/shared/types/index.ts
Normal file
8
src/shared/types/index.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
/**
|
||||
* TabataFit Shared Types
|
||||
*/
|
||||
|
||||
export * from './workout'
|
||||
export * from './trainer'
|
||||
export * from './user'
|
||||
export * from './activity'
|
||||
12
src/shared/types/trainer.ts
Normal file
12
src/shared/types/trainer.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
/**
|
||||
* TabataFit Trainer Types
|
||||
*/
|
||||
|
||||
export interface Trainer {
|
||||
id: string
|
||||
name: string
|
||||
specialty: string
|
||||
color: string
|
||||
avatarUrl?: string
|
||||
workoutCount: number
|
||||
}
|
||||
29
src/shared/types/user.ts
Normal file
29
src/shared/types/user.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
/**
|
||||
* TabataFit User Types
|
||||
*/
|
||||
|
||||
export type SubscriptionPlan = 'free' | 'premium-monthly' | 'premium-yearly'
|
||||
|
||||
export interface UserSettings {
|
||||
haptics: boolean
|
||||
soundEffects: boolean
|
||||
voiceCoaching: boolean
|
||||
reminders: boolean
|
||||
reminderTime: string
|
||||
}
|
||||
|
||||
export interface UserProfile {
|
||||
name: string
|
||||
email: string
|
||||
joinDate: string
|
||||
subscription: SubscriptionPlan
|
||||
}
|
||||
|
||||
export interface Achievement {
|
||||
id: string
|
||||
title: string
|
||||
description: string
|
||||
icon: string
|
||||
requirement: number
|
||||
type: 'workouts' | 'streak' | 'minutes' | 'calories'
|
||||
}
|
||||
62
src/shared/types/workout.ts
Normal file
62
src/shared/types/workout.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
/**
|
||||
* TabataFit Workout Types
|
||||
*/
|
||||
|
||||
export type WorkoutCategory = 'full-body' | 'core' | 'upper-body' | 'lower-body' | 'cardio'
|
||||
|
||||
export type WorkoutLevel = 'Beginner' | 'Intermediate' | 'Advanced'
|
||||
|
||||
export type WorkoutDuration = 4 | 8 | 12 | 20
|
||||
|
||||
export type MusicVibe = 'electronic' | 'hip-hop' | 'pop' | 'rock' | 'chill'
|
||||
|
||||
export interface Exercise {
|
||||
name: string
|
||||
/** Work duration in seconds */
|
||||
duration: number
|
||||
}
|
||||
|
||||
export interface Workout {
|
||||
id: string
|
||||
title: string
|
||||
trainerId: string
|
||||
category: WorkoutCategory
|
||||
level: WorkoutLevel
|
||||
/** Duration in minutes */
|
||||
duration: WorkoutDuration
|
||||
/** Estimated calories burned */
|
||||
calories: number
|
||||
exercises: Exercise[]
|
||||
/** Total rounds (work+rest cycles) */
|
||||
rounds: number
|
||||
/** Prep time in seconds */
|
||||
prepTime: number
|
||||
/** Work interval in seconds */
|
||||
workTime: number
|
||||
/** Rest interval in seconds */
|
||||
restTime: number
|
||||
equipment: string[]
|
||||
musicVibe: MusicVibe
|
||||
thumbnailUrl?: string
|
||||
videoUrl?: string
|
||||
isFeatured?: boolean
|
||||
}
|
||||
|
||||
export interface Collection {
|
||||
id: string
|
||||
title: string
|
||||
description: string
|
||||
icon: string
|
||||
workoutIds: string[]
|
||||
gradient?: [string, string]
|
||||
}
|
||||
|
||||
export interface Program {
|
||||
id: string
|
||||
title: string
|
||||
description: string
|
||||
weeks: number
|
||||
workoutsPerWeek: number
|
||||
level: WorkoutLevel
|
||||
workoutIds: string[]
|
||||
}
|
||||
Reference in New Issue
Block a user