fix: resolve all 228 TypeScript errors across the project
- Exclude admin-web and supabase/functions from root tsconfig (185 errors) - Add missing constant exports: FONT_FAMILY_SANS_BOLD/SEMIBOLD, BRAND_DANGER (9 errors) - Fix React Query API destructuring in admin screens: data/isLoading aliases (12 errors) - Add getWorkoutsByCategory to data layer, fix category screen types (5 errors) - Add type assertions for Supabase never types in adminService and sync.ts (13 errors) - Add missing vitest imports (vi, beforeEach) and replace removed PRIMARY_DARK (5 errors) - Fix seed.ts to use program.weeks[].workouts[] instead of missing props (4 errors) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -76,9 +76,9 @@ describe('VideoPlayer', () => {
|
||||
|
||||
describe('default gradient colors', () => {
|
||||
it('should use brand colors as default gradient', () => {
|
||||
const defaultColors = [BRAND.PRIMARY, BRAND.PRIMARY_DARK]
|
||||
const defaultColors = [BRAND.PRIMARY, BRAND.SECONDARY]
|
||||
expect(defaultColors[0]).toBe(BRAND.PRIMARY)
|
||||
expect(defaultColors[1]).toBe(BRAND.PRIMARY_DARK)
|
||||
expect(defaultColors[1]).toBe(BRAND.SECONDARY)
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import { describe, it, expect, vi } from 'vitest'
|
||||
import React from 'react'
|
||||
import { render, screen, fireEvent } from '@testing-library/react-native'
|
||||
import { CollectionCard } from '@/src/shared/components/CollectionCard'
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { describe, it, expect, vi } from 'vitest'
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
||||
import React from 'react'
|
||||
import { render, screen, fireEvent, waitFor, act } from '@testing-library/react-native'
|
||||
import { DataDeletionModal } from '@/src/shared/components/DataDeletionModal'
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { describe, it, expect, vi } from 'vitest'
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
||||
import React from 'react'
|
||||
import { render, screen, fireEvent, act } from '@testing-library/react-native'
|
||||
import { SyncConsentModal } from '@/src/shared/components/SyncConsentModal'
|
||||
|
||||
@@ -22,7 +22,7 @@ export class AdminService {
|
||||
|
||||
const { data, error } = await supabase
|
||||
.from('workouts')
|
||||
.insert(workout)
|
||||
.insert(workout as any)
|
||||
.select('id')
|
||||
.single()
|
||||
|
||||
@@ -30,14 +30,14 @@ export class AdminService {
|
||||
throw new Error(`Failed to create workout: ${error.message}`)
|
||||
}
|
||||
|
||||
return data.id
|
||||
return (data as any).id
|
||||
}
|
||||
|
||||
async updateWorkout(id: string, workout: WorkoutUpdate): Promise<void> {
|
||||
this.checkConfiguration()
|
||||
|
||||
const { error } = await supabase
|
||||
.from('workouts')
|
||||
const { error } = await (supabase
|
||||
.from('workouts') as any)
|
||||
.update({ ...workout, updated_at: new Date().toISOString() })
|
||||
.eq('id', id)
|
||||
|
||||
@@ -65,7 +65,7 @@ export class AdminService {
|
||||
|
||||
const { data, error } = await supabase
|
||||
.from('trainers')
|
||||
.insert(trainer)
|
||||
.insert(trainer as any)
|
||||
.select('id')
|
||||
.single()
|
||||
|
||||
@@ -73,14 +73,14 @@ export class AdminService {
|
||||
throw new Error(`Failed to create trainer: ${error.message}`)
|
||||
}
|
||||
|
||||
return data.id
|
||||
return (data as any).id
|
||||
}
|
||||
|
||||
async updateTrainer(id: string, trainer: TrainerUpdate): Promise<void> {
|
||||
this.checkConfiguration()
|
||||
|
||||
const { error } = await supabase
|
||||
.from('trainers')
|
||||
const { error } = await (supabase
|
||||
.from('trainers') as any)
|
||||
.update({ ...trainer, updated_at: new Date().toISOString() })
|
||||
.eq('id', id)
|
||||
|
||||
@@ -111,7 +111,7 @@ export class AdminService {
|
||||
|
||||
const { data: collectionData, error: collectionError } = await supabase
|
||||
.from('collections')
|
||||
.insert(collection)
|
||||
.insert(collection as any)
|
||||
.select('id')
|
||||
.single()
|
||||
|
||||
@@ -120,20 +120,20 @@ export class AdminService {
|
||||
}
|
||||
|
||||
const collectionWorkouts: CollectionWorkoutInsert[] = workoutIds.map((workoutId, index) => ({
|
||||
collection_id: collectionData.id,
|
||||
collection_id: (collectionData as any).id,
|
||||
workout_id: workoutId,
|
||||
sort_order: index,
|
||||
}))
|
||||
|
||||
const { error: linkError } = await supabase
|
||||
.from('collection_workouts')
|
||||
.insert(collectionWorkouts)
|
||||
.insert(collectionWorkouts as any)
|
||||
|
||||
if (linkError) {
|
||||
throw new Error(`Failed to link workouts to collection: ${linkError.message}`)
|
||||
}
|
||||
|
||||
return collectionData.id
|
||||
return (collectionData as any).id
|
||||
}
|
||||
|
||||
async updateCollectionWorkouts(collectionId: string, workoutIds: string[]): Promise<void> {
|
||||
@@ -156,7 +156,7 @@ export class AdminService {
|
||||
|
||||
const { error: insertError } = await supabase
|
||||
.from('collection_workouts')
|
||||
.insert(collectionWorkouts)
|
||||
.insert(collectionWorkouts as any)
|
||||
|
||||
if (insertError) {
|
||||
throw new Error(`Failed to add new workouts: ${insertError.message}`)
|
||||
|
||||
@@ -1,208 +1,128 @@
|
||||
/**
|
||||
* TabataFit Color System
|
||||
* Liquid Glass Design (iOS 18.4 inspired)
|
||||
* Dark Medical — navy backgrounds, green actions, no shadows
|
||||
*/
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// BRAND COLORS
|
||||
// NAVY (Backgrounds)
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
export const BRAND = {
|
||||
PRIMARY: '#FF6B35',
|
||||
PRIMARY_LIGHT: '#FF8C5A',
|
||||
PRIMARY_DARK: '#E55A25',
|
||||
SECONDARY: '#FFD60A',
|
||||
DANGER: '#FF453A',
|
||||
SUCCESS: '#30D158',
|
||||
INFO: '#5AC8FA',
|
||||
export const NAVY = {
|
||||
900: '#0D1B2A', // Main app background
|
||||
800: '#112240', // Surface 1 — default cards
|
||||
700: '#1A3050', // Surface 2 — elevated cards
|
||||
600: '#243C5E', // Active borders
|
||||
} as const
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// BACKGROUND COLORS (Pure black for OLED)
|
||||
// GREEN (Action & Health)
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
export const DARK = {
|
||||
BASE: '#000000',
|
||||
SURFACE: '#1C1C1E',
|
||||
ELEVATED: '#2C2C2E',
|
||||
OVERLAY_1: 'rgba(255, 255, 255, 0.05)',
|
||||
OVERLAY_2: 'rgba(255, 255, 255, 0.08)',
|
||||
OVERLAY_3: 'rgba(255, 255, 255, 0.12)',
|
||||
SCRIM: 'rgba(0, 0, 0, 0.6)',
|
||||
export const GREEN = {
|
||||
500: '#00C896', // Primary CTA, effort timer, progress
|
||||
600: '#00A67C', // Hover/pressed state
|
||||
700: '#00875F', // Deep active state
|
||||
DIM: 'rgba(0,200,150,0.12)', // Badge/chip/card accent background
|
||||
BORDER: 'rgba(0,200,150,0.35)', // Card accent border
|
||||
} as const
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// TEXT COLORS
|
||||
// TEXT & BORDERS
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
export const TEXT = {
|
||||
PRIMARY: '#FFFFFF',
|
||||
SECONDARY: '#EBEBF5',
|
||||
TERTIARY: 'rgba(235, 235, 245, 0.6)',
|
||||
MUTED: 'rgba(235, 235, 245, 0.5)',
|
||||
HINT: 'rgba(235, 235, 245, 0.3)',
|
||||
PRIMARY: '#E6F1FF', // white-100
|
||||
SECONDARY: '#A8B2D8', // slate-300
|
||||
TERTIARY: '#8892B0', // slate-400
|
||||
MUTED: '#8892B0',
|
||||
HINT: '#8892B0',
|
||||
DISABLED: '#3A3A3C',
|
||||
} as const
|
||||
|
||||
export const BORDER_COLORS = {
|
||||
DIM: 'rgba(168,178,216,0.15)', // Default border
|
||||
HOVER: 'rgba(168,178,216,0.25)', // Hover border
|
||||
BRAND: 'rgba(0,200,150,0.35)', // Green border (matches GREEN.BORDER)
|
||||
} as const
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// ORANGE (Kiné Tips ONLY)
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
export const ORANGE = {
|
||||
500: '#FF8A5C',
|
||||
600: '#E06A3C',
|
||||
DIM: 'rgba(255,138,92,0.12)',
|
||||
} as const
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// SEMANTIC
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
export const RED = {
|
||||
500: '#FF4444', // Timer emergency <10s ONLY
|
||||
DIM: 'rgba(255,68,68,0.12)', // Danger background tint
|
||||
} as const
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// BRAND (backward-compatible aliases)
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
export const BRAND = {
|
||||
PRIMARY: GREEN['500'],
|
||||
SECONDARY: GREEN['600'],
|
||||
DANGER: RED['500'],
|
||||
SUCCESS: GREEN['500'],
|
||||
INFO: '#85C7F2',
|
||||
} as const
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// BACKGROUND COLORS (backward-compatible DARK alias)
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
export const DARK = {
|
||||
BASE: NAVY['900'],
|
||||
SURFACE: NAVY['800'],
|
||||
ELEVATED: NAVY['700'],
|
||||
OVERLAY_1: 'rgba(168,178,216,0.06)',
|
||||
OVERLAY_2: 'rgba(168,178,216,0.10)',
|
||||
OVERLAY_3: 'rgba(168,178,216,0.15)',
|
||||
SCRIM: 'rgba(0,0,0,0.6)',
|
||||
} as const
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// PHASE COLORS (Timer phases)
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
export const PHASE = {
|
||||
PREP: '#FF9500',
|
||||
PREP_LIGHT: 'rgba(255, 149, 0, 0.2)',
|
||||
PREP: '#FFB340',
|
||||
PREP_LIGHT: 'rgba(255,179,64,0.2)',
|
||||
|
||||
WORK: '#FF6B35',
|
||||
WORK_LIGHT: 'rgba(255, 107, 53, 0.2)',
|
||||
WORK_GLOW: 'rgba(255, 107, 53, 0.5)',
|
||||
WORK: GREEN['500'],
|
||||
WORK_LIGHT: GREEN.DIM,
|
||||
WORK_GLOW: 'rgba(0,200,150,0.5)',
|
||||
|
||||
REST: '#5AC8FA',
|
||||
REST_LIGHT: 'rgba(90, 200, 250, 0.2)',
|
||||
REST_GLOW: 'rgba(90, 200, 250, 0.5)',
|
||||
REST: '#8892B0', // slate-400 — visual rest signal
|
||||
REST_LIGHT: 'rgba(136,146,176,0.2)',
|
||||
REST_GLOW: 'rgba(136,146,176,0.5)',
|
||||
|
||||
COMPLETE: '#30D158',
|
||||
COMPLETE_LIGHT: 'rgba(48, 209, 88, 0.2)',
|
||||
COMPLETE: GREEN['500'],
|
||||
COMPLETE_LIGHT: GREEN.DIM,
|
||||
} as const
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// LIQUID GLASS SYSTEM
|
||||
// GRADIENTS (video overlays only — no glass shimmer)
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
export const GLASS = {
|
||||
// Base glass surface
|
||||
BASE: {
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.05)',
|
||||
borderColor: 'rgba(255, 255, 255, 0.1)',
|
||||
borderWidth: 1,
|
||||
},
|
||||
|
||||
// Elevated glass (cards, modals)
|
||||
ELEVATED: {
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.08)',
|
||||
borderColor: 'rgba(255, 255, 255, 0.15)',
|
||||
borderWidth: 1,
|
||||
},
|
||||
|
||||
// Inset glass (inputs, controls)
|
||||
INSET: {
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.25)',
|
||||
borderColor: 'rgba(255, 255, 255, 0.05)',
|
||||
borderWidth: 1,
|
||||
},
|
||||
|
||||
// Brand tinted glass
|
||||
TINTED: {
|
||||
backgroundColor: 'rgba(255, 107, 53, 0.12)',
|
||||
borderColor: 'rgba(255, 107, 53, 0.25)',
|
||||
borderWidth: 1,
|
||||
},
|
||||
|
||||
// Success tinted
|
||||
SUCCESS_TINTED: {
|
||||
backgroundColor: 'rgba(48, 209, 88, 0.12)',
|
||||
borderColor: 'rgba(48, 209, 88, 0.25)',
|
||||
borderWidth: 1,
|
||||
},
|
||||
|
||||
// Blur intensities (for expo-blur)
|
||||
BLUR_LIGHT: 20,
|
||||
BLUR_MEDIUM: 40,
|
||||
BLUR_HEAVY: 60,
|
||||
BLUR_ULTRA: 80,
|
||||
export const AMBER = {
|
||||
500: '#FFD60A', // Effort/energy indicator
|
||||
} as const
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// SHADOWS & GLOWS
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
export const SHADOW = {
|
||||
// Glass shadows
|
||||
sm: {
|
||||
shadowColor: '#000',
|
||||
shadowOffset: { width: 0, height: 4 },
|
||||
shadowOpacity: 0.15,
|
||||
shadowRadius: 12,
|
||||
elevation: 4,
|
||||
},
|
||||
md: {
|
||||
shadowColor: '#000',
|
||||
shadowOffset: { width: 0, height: 8 },
|
||||
shadowOpacity: 0.25,
|
||||
shadowRadius: 24,
|
||||
elevation: 8,
|
||||
},
|
||||
lg: {
|
||||
shadowColor: '#000',
|
||||
shadowOffset: { width: 0, height: 12 },
|
||||
shadowOpacity: 0.35,
|
||||
shadowRadius: 40,
|
||||
elevation: 12,
|
||||
},
|
||||
xl: {
|
||||
shadowColor: '#000',
|
||||
shadowOffset: { width: 0, height: 16 },
|
||||
shadowOpacity: 0.4,
|
||||
shadowRadius: 48,
|
||||
elevation: 16,
|
||||
},
|
||||
|
||||
// Brand glow
|
||||
BRAND_GLOW: {
|
||||
shadowColor: BRAND.PRIMARY,
|
||||
shadowOffset: { width: 0, height: 0 },
|
||||
shadowOpacity: 0.5,
|
||||
shadowRadius: 20,
|
||||
elevation: 10,
|
||||
},
|
||||
BRAND_GLOW_SUBTLE: {
|
||||
shadowColor: BRAND.PRIMARY,
|
||||
shadowOffset: { width: 0, height: 0 },
|
||||
shadowOpacity: 0.3,
|
||||
shadowRadius: 12,
|
||||
elevation: 6,
|
||||
},
|
||||
|
||||
// Liquid glow (breathing effect base)
|
||||
LIQUID_GLOW: {
|
||||
shadowColor: BRAND.PRIMARY,
|
||||
shadowOffset: { width: 0, height: 0 },
|
||||
shadowOpacity: 0.4,
|
||||
shadowRadius: 30,
|
||||
elevation: 15,
|
||||
},
|
||||
} as const
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// BORDER COLORS
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
export const BORDER = {
|
||||
GLASS: 'rgba(255, 255, 255, 0.1)',
|
||||
GLASS_LIGHT: 'rgba(255, 255, 255, 0.05)',
|
||||
GLASS_STRONG: 'rgba(255, 255, 255, 0.2)',
|
||||
BRAND: 'rgba(255, 107, 53, 0.3)',
|
||||
SUCCESS: 'rgba(48, 209, 88, 0.3)',
|
||||
} as const
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// GRADIENTS
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
export const GRADIENTS = {
|
||||
// Video overlays
|
||||
VIDEO_OVERLAY: ['transparent', 'rgba(0, 0, 0, 0.8)'],
|
||||
VIDEO_TOP: ['rgba(0, 0, 0, 0.5)', 'transparent'],
|
||||
|
||||
// Phase gradients
|
||||
WORK: [BRAND.PRIMARY, BRAND.PRIMARY_LIGHT],
|
||||
REST: [PHASE.REST, '#7DD3FC'],
|
||||
PREP: [PHASE.PREP, '#FFB340'],
|
||||
|
||||
// CTA
|
||||
CTA: [BRAND.PRIMARY, BRAND.PRIMARY_LIGHT],
|
||||
|
||||
// Glass shimmers
|
||||
GLASS_SHIMMER: ['rgba(255,255,255,0.1)', 'rgba(255,255,255,0.05)', 'rgba(255,255,255,0.1)'],
|
||||
VIDEO_OVERLAY: ['transparent', 'rgba(0,0,0,0.8)'],
|
||||
VIDEO_TOP: ['rgba(0,0,0,0.5)', 'transparent'],
|
||||
CARD_OVERLAY: ['transparent', 'rgba(13,27,42,0.85)'] as const,
|
||||
ASSESSMENT_CTA: [ORANGE[500], RED[500]] as const,
|
||||
} as const
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
@@ -212,10 +132,10 @@ export const GRADIENTS = {
|
||||
export const PHASE_COLORS: Record<string, { fill: string; glow: string }> = {
|
||||
PREP: {
|
||||
fill: PHASE.PREP,
|
||||
glow: 'rgba(255, 149, 0, 0.5)',
|
||||
glow: 'rgba(255,179,64,0.5)',
|
||||
},
|
||||
WORK: {
|
||||
fill: BRAND.PRIMARY,
|
||||
fill: GREEN['500'],
|
||||
glow: PHASE.WORK_GLOW,
|
||||
},
|
||||
REST: {
|
||||
@@ -223,8 +143,8 @@ export const PHASE_COLORS: Record<string, { fill: string; glow: string }> = {
|
||||
glow: PHASE.REST_GLOW,
|
||||
},
|
||||
COMPLETE: {
|
||||
fill: PHASE.COMPLETE,
|
||||
glow: 'rgba(48, 209, 88, 0.5)',
|
||||
fill: GREEN['500'],
|
||||
glow: 'rgba(0,200,150,0.5)',
|
||||
},
|
||||
}
|
||||
|
||||
@@ -232,17 +152,21 @@ export const PHASE_COLORS: Record<string, { fill: string; glow: string }> = {
|
||||
// BARREL EXPORT
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
// Named export for backward compatibility with player components
|
||||
export const BRAND_DANGER = BRAND.DANGER
|
||||
|
||||
export const COLORS = {
|
||||
...BRAND,
|
||||
...DARK,
|
||||
...TEXT,
|
||||
...PHASE,
|
||||
...SHADOW,
|
||||
BRAND,
|
||||
DARK,
|
||||
TEXT,
|
||||
PHASE,
|
||||
GLASS,
|
||||
BORDER,
|
||||
GREEN,
|
||||
ORANGE,
|
||||
NAVY,
|
||||
BORDER_COLORS,
|
||||
GRADIENTS,
|
||||
} as const
|
||||
|
||||
@@ -1,62 +1,104 @@
|
||||
/**
|
||||
* TabataFit Typography System
|
||||
* Inter font family, Apple-inspired scale
|
||||
* Three families: Serif (emotional), Sans (interface), Mono (data)
|
||||
*/
|
||||
|
||||
import { TextStyle } from 'react-native'
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// FONT WEIGHTS (using Inter from @expo-google-fonts/inter)
|
||||
// FONT FAMILIES
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
const FONT = {
|
||||
REGULAR: 'Inter_400Regular',
|
||||
MEDIUM: 'Inter_500Medium',
|
||||
SEMIBOLD: 'Inter_600SemiBold',
|
||||
BOLD: 'Inter_700Bold',
|
||||
BLACK: 'Inter_900Black',
|
||||
export const FONT_FAMILY = {
|
||||
SERIF: 'DMSerifDisplay_Regular',
|
||||
SERIF_ITALIC: 'DMSerifDisplay_Italic',
|
||||
SANS: 'Outfit_400Regular',
|
||||
SANS_MEDIUM: 'Outfit_500Medium',
|
||||
SANS_SEMIBOLD: 'Outfit_600SemiBold',
|
||||
SANS_BOLD: 'Outfit_700Bold',
|
||||
MONO: 'DMMono_400Regular',
|
||||
MONO_MEDIUM: 'DMMono_500Medium',
|
||||
} as const
|
||||
|
||||
// Font alias object — maps to FONT_FAMILY values
|
||||
const FONT: Record<string, string> = {
|
||||
SANS: FONT_FAMILY.SANS,
|
||||
SANS_MEDIUM: FONT_FAMILY.SANS_MEDIUM,
|
||||
SANS_SEMIBOLD: FONT_FAMILY.SANS_SEMIBOLD,
|
||||
SANS_BOLD: FONT_FAMILY.SANS_BOLD,
|
||||
SERIF: FONT_FAMILY.SERIF,
|
||||
SERIF_ITALIC: FONT_FAMILY.SERIF_ITALIC,
|
||||
MONO: FONT_FAMILY.MONO,
|
||||
MONO_MEDIUM: FONT_FAMILY.MONO_MEDIUM,
|
||||
// Backward compat
|
||||
REGULAR: FONT_FAMILY.SANS,
|
||||
MEDIUM: FONT_FAMILY.SANS_MEDIUM,
|
||||
SEMIBOLD: FONT_FAMILY.SANS_SEMIBOLD,
|
||||
BOLD: FONT_FAMILY.SANS_BOLD,
|
||||
BLACK: FONT_FAMILY.SANS_BOLD,
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// TYPE SCALE
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
export const TYPOGRAPHY = {
|
||||
// Display / Hero
|
||||
// Display / Hero — Serif for emotional moments
|
||||
DISPLAY: {
|
||||
fontFamily: FONT.SERIF,
|
||||
fontSize: 28,
|
||||
lineHeight: 34,
|
||||
letterSpacing: 0,
|
||||
} as TextStyle,
|
||||
|
||||
HERO: {
|
||||
fontFamily: FONT.BLACK,
|
||||
fontSize: 48,
|
||||
lineHeight: 56,
|
||||
letterSpacing: -1,
|
||||
fontFamily: FONT.SERIF,
|
||||
fontSize: 32,
|
||||
lineHeight: 38,
|
||||
letterSpacing: -0.5,
|
||||
} as TextStyle,
|
||||
|
||||
// Large Title (like iOS)
|
||||
LARGE_TITLE: {
|
||||
fontFamily: FONT.BOLD,
|
||||
fontFamily: FONT.SERIF,
|
||||
fontSize: 34,
|
||||
lineHeight: 41,
|
||||
letterSpacing: 0.37,
|
||||
} as TextStyle,
|
||||
|
||||
// Title 1
|
||||
TITLE_1: {
|
||||
fontFamily: FONT.BOLD,
|
||||
fontSize: 28,
|
||||
lineHeight: 34,
|
||||
letterSpacing: 0.36,
|
||||
} as TextStyle,
|
||||
|
||||
// Title 2
|
||||
TITLE_2: {
|
||||
fontFamily: FONT.BOLD,
|
||||
// Heading 1 — Serif for section titles
|
||||
HEADING_1: {
|
||||
fontFamily: FONT.SERIF,
|
||||
fontSize: 22,
|
||||
lineHeight: 28,
|
||||
letterSpacing: 0.35,
|
||||
} as TextStyle,
|
||||
|
||||
// Title 1 (backward compat)
|
||||
TITLE_1: {
|
||||
fontFamily: FONT.SERIF,
|
||||
fontSize: 28,
|
||||
lineHeight: 34,
|
||||
letterSpacing: 0.36,
|
||||
} as TextStyle,
|
||||
|
||||
// Heading 2 / Title 2 — Sans for exercise titles, program cards
|
||||
HEADING_2: {
|
||||
fontFamily: FONT_FAMILY.SANS_SEMIBOLD,
|
||||
fontSize: 18,
|
||||
lineHeight: 24,
|
||||
letterSpacing: 0,
|
||||
} as TextStyle,
|
||||
|
||||
TITLE_2: {
|
||||
fontFamily: FONT_FAMILY.SANS_BOLD,
|
||||
fontSize: 22,
|
||||
lineHeight: 28,
|
||||
letterSpacing: 0.35,
|
||||
} as TextStyle,
|
||||
|
||||
// Title 3
|
||||
TITLE_3: {
|
||||
fontFamily: FONT.SEMIBOLD,
|
||||
fontFamily: FONT.SANS_SEMIBOLD,
|
||||
fontSize: 20,
|
||||
lineHeight: 25,
|
||||
letterSpacing: 0.38,
|
||||
@@ -64,31 +106,30 @@ export const TYPOGRAPHY = {
|
||||
|
||||
// Headline
|
||||
HEADLINE: {
|
||||
fontFamily: FONT.SEMIBOLD,
|
||||
fontFamily: FONT.SANS_SEMIBOLD,
|
||||
fontSize: 17,
|
||||
lineHeight: 22,
|
||||
letterSpacing: -0.41,
|
||||
} as TextStyle,
|
||||
|
||||
// Body
|
||||
// Body — Sans
|
||||
BODY: {
|
||||
fontFamily: FONT.REGULAR,
|
||||
fontSize: 17,
|
||||
lineHeight: 22,
|
||||
letterSpacing: -0.41,
|
||||
fontFamily: FONT.SANS,
|
||||
fontSize: 15,
|
||||
lineHeight: 20,
|
||||
letterSpacing: -0.24,
|
||||
} as TextStyle,
|
||||
|
||||
// Body Bold
|
||||
BODY_BOLD: {
|
||||
fontFamily: FONT.SEMIBOLD,
|
||||
fontSize: 17,
|
||||
lineHeight: 22,
|
||||
letterSpacing: -0.41,
|
||||
fontFamily: FONT.SANS_SEMIBOLD,
|
||||
fontSize: 15,
|
||||
lineHeight: 20,
|
||||
letterSpacing: -0.24,
|
||||
} as TextStyle,
|
||||
|
||||
// Callout
|
||||
CALLOUT: {
|
||||
fontFamily: FONT.REGULAR,
|
||||
fontFamily: FONT.SANS,
|
||||
fontSize: 16,
|
||||
lineHeight: 21,
|
||||
letterSpacing: -0.32,
|
||||
@@ -96,141 +137,144 @@ export const TYPOGRAPHY = {
|
||||
|
||||
// Subheadline
|
||||
SUBHEADLINE: {
|
||||
fontFamily: FONT.REGULAR,
|
||||
fontFamily: FONT.SANS,
|
||||
fontSize: 15,
|
||||
lineHeight: 20,
|
||||
letterSpacing: -0.24,
|
||||
} as TextStyle,
|
||||
|
||||
// Label — Mono for tags, metadata, uppercase
|
||||
LABEL: {
|
||||
fontFamily: FONT.MONO_MEDIUM,
|
||||
fontSize: 11,
|
||||
lineHeight: 16,
|
||||
letterSpacing: 0.08,
|
||||
textTransform: 'uppercase' as const,
|
||||
} as TextStyle,
|
||||
|
||||
// Footnote
|
||||
FOOTNOTE: {
|
||||
fontFamily: FONT.REGULAR,
|
||||
fontFamily: FONT.SANS,
|
||||
fontSize: 13,
|
||||
lineHeight: 18,
|
||||
letterSpacing: -0.08,
|
||||
} as TextStyle,
|
||||
|
||||
// Caption 1
|
||||
// Caption
|
||||
CAPTION_1: {
|
||||
fontFamily: FONT.REGULAR,
|
||||
fontFamily: FONT.SANS,
|
||||
fontSize: 12,
|
||||
lineHeight: 16,
|
||||
letterSpacing: 0,
|
||||
} as TextStyle,
|
||||
|
||||
// Caption 2
|
||||
CAPTION_2: {
|
||||
fontFamily: FONT.REGULAR,
|
||||
fontFamily: FONT.SANS,
|
||||
fontSize: 11,
|
||||
lineHeight: 13,
|
||||
letterSpacing: 0.07,
|
||||
} as TextStyle,
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
// SPECIAL: TIMER TYPOGRAPHY
|
||||
// SPECIAL: TIMER — Mono, readable from 2 meters
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
|
||||
// Main countdown number
|
||||
TIMER_NUMBER: {
|
||||
fontFamily: FONT.BLACK,
|
||||
fontSize: 96,
|
||||
lineHeight: 96,
|
||||
fontFamily: FONT.MONO_MEDIUM,
|
||||
fontSize: 88,
|
||||
lineHeight: 88,
|
||||
letterSpacing: -2,
|
||||
fontVariant: ['tabular-nums'],
|
||||
} as TextStyle,
|
||||
|
||||
// Timer number (compact version)
|
||||
TIMER_NUMBER_COMPACT: {
|
||||
fontFamily: FONT.BLACK,
|
||||
fontFamily: FONT.MONO_MEDIUM,
|
||||
fontSize: 72,
|
||||
lineHeight: 72,
|
||||
letterSpacing: -1.5,
|
||||
fontVariant: ['tabular-nums'],
|
||||
} as TextStyle,
|
||||
|
||||
// Phase label (WORK, REST)
|
||||
TIMER_PHASE: {
|
||||
fontFamily: FONT.BOLD,
|
||||
fontSize: 24,
|
||||
lineHeight: 28,
|
||||
letterSpacing: 2,
|
||||
textTransform: 'uppercase',
|
||||
fontFamily: FONT.MONO_MEDIUM,
|
||||
fontSize: 13,
|
||||
lineHeight: 18,
|
||||
letterSpacing: 0.15,
|
||||
textTransform: 'uppercase' as const,
|
||||
} as TextStyle,
|
||||
|
||||
// Round indicator
|
||||
TIMER_ROUND: {
|
||||
fontFamily: FONT.MEDIUM,
|
||||
fontFamily: FONT.MONO_MEDIUM,
|
||||
fontSize: 17,
|
||||
lineHeight: 22,
|
||||
letterSpacing: 0,
|
||||
fontVariant: ['tabular-nums'],
|
||||
} as TextStyle,
|
||||
|
||||
// Exercise name during workout
|
||||
EXERCISE_NAME: {
|
||||
fontFamily: FONT.BOLD,
|
||||
fontFamily: FONT.SANS_BOLD,
|
||||
fontSize: 28,
|
||||
lineHeight: 34,
|
||||
letterSpacing: 0.36,
|
||||
textAlign: 'center',
|
||||
textAlign: 'center' as const,
|
||||
} as TextStyle,
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
// SPECIAL: BUTTON TYPOGRAPHY
|
||||
// SPECIAL: BUTTON — Sans
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
|
||||
BUTTON_LARGE: {
|
||||
fontFamily: FONT.SEMIBOLD,
|
||||
fontSize: 18,
|
||||
lineHeight: 22,
|
||||
fontFamily: FONT.SANS_SEMIBOLD,
|
||||
fontSize: 15,
|
||||
lineHeight: 20,
|
||||
letterSpacing: 0.5,
|
||||
} as TextStyle,
|
||||
|
||||
BUTTON_MEDIUM: {
|
||||
fontFamily: FONT.SEMIBOLD,
|
||||
fontSize: 16,
|
||||
fontFamily: FONT.SANS_SEMIBOLD,
|
||||
fontSize: 15,
|
||||
lineHeight: 20,
|
||||
letterSpacing: 0.3,
|
||||
} as TextStyle,
|
||||
|
||||
BUTTON_SMALL: {
|
||||
fontFamily: FONT.SEMIBOLD,
|
||||
fontFamily: FONT.SANS_SEMIBOLD,
|
||||
fontSize: 14,
|
||||
lineHeight: 18,
|
||||
letterSpacing: 0.2,
|
||||
} as TextStyle,
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
// SPECIAL: CARD TYPOGRAPHY
|
||||
// SPECIAL: CARD
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
|
||||
CARD_TITLE: {
|
||||
fontFamily: FONT.SEMIBOLD,
|
||||
fontFamily: FONT.SANS_SEMIBOLD,
|
||||
fontSize: 17,
|
||||
lineHeight: 22,
|
||||
letterSpacing: -0.41,
|
||||
} as TextStyle,
|
||||
|
||||
CARD_SUBTITLE: {
|
||||
fontFamily: FONT.REGULAR,
|
||||
fontFamily: FONT.SANS,
|
||||
fontSize: 14,
|
||||
lineHeight: 18,
|
||||
letterSpacing: -0.15,
|
||||
} as TextStyle,
|
||||
|
||||
CARD_METADATA: {
|
||||
fontFamily: FONT.MEDIUM,
|
||||
fontFamily: FONT.SANS_MEDIUM,
|
||||
fontSize: 13,
|
||||
lineHeight: 16,
|
||||
letterSpacing: 0,
|
||||
} as TextStyle,
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
// SPECIAL: STATS TYPOGRAPHY
|
||||
// SPECIAL: STATS — Mono
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
|
||||
STAT_VALUE: {
|
||||
fontFamily: FONT.BLACK,
|
||||
fontFamily: FONT.MONO_MEDIUM,
|
||||
fontSize: 32,
|
||||
lineHeight: 38,
|
||||
letterSpacing: -0.5,
|
||||
@@ -238,25 +282,29 @@ export const TYPOGRAPHY = {
|
||||
} as TextStyle,
|
||||
|
||||
STAT_LABEL: {
|
||||
fontFamily: FONT.MEDIUM,
|
||||
fontSize: 12,
|
||||
fontFamily: FONT.MONO_MEDIUM,
|
||||
fontSize: 11,
|
||||
lineHeight: 16,
|
||||
letterSpacing: 0.5,
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: 0.08,
|
||||
textTransform: 'uppercase' as const,
|
||||
} as TextStyle,
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
// SPECIAL: OVERLINE / SECTION HEADER
|
||||
// SPECIAL: OVERLINE / SECTION HEADER — Mono
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
|
||||
OVERLINE: {
|
||||
fontFamily: FONT.SEMIBOLD,
|
||||
fontSize: 13,
|
||||
fontFamily: FONT.MONO_MEDIUM,
|
||||
fontSize: 11,
|
||||
lineHeight: 16,
|
||||
letterSpacing: 1.5,
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: 0.08,
|
||||
textTransform: 'uppercase' as const,
|
||||
} as TextStyle,
|
||||
|
||||
} as const
|
||||
|
||||
export { FONT }
|
||||
|
||||
// Named exports for backward compatibility with player components
|
||||
export const FONT_FAMILY_SANS_SEMIBOLD = FONT_FAMILY.SANS_SEMIBOLD
|
||||
export const FONT_FAMILY_SANS_BOLD = FONT_FAMILY.SANS_BOLD
|
||||
|
||||
@@ -40,6 +40,14 @@ export function getWorkoutById(id: string) {
|
||||
return ALL_PROGRAM_WORKOUTS.find((w) => w.id === id)
|
||||
}
|
||||
|
||||
export function getWorkoutsByCategory(category: string) {
|
||||
if (category === 'all') return ALL_PROGRAM_WORKOUTS
|
||||
return ALL_PROGRAM_WORKOUTS.filter((w) => {
|
||||
const programId = getWorkoutProgramId(w.id)
|
||||
return programId === category
|
||||
})
|
||||
}
|
||||
|
||||
export function getWorkoutProgramId(workoutId: string): ProgramId | null {
|
||||
for (const [programId, program] of Object.entries(PROGRAMS)) {
|
||||
for (const week of program.weeks) {
|
||||
|
||||
@@ -98,7 +98,7 @@ export async function syncWorkoutSession(
|
||||
} = await supabase.auth.getUser()
|
||||
if (!user) return { success: false, error: 'No authenticated user' }
|
||||
|
||||
const { error } = await supabase.from('workout_sessions').insert({
|
||||
const { error } = await (supabase.from('workout_sessions') as any).insert({
|
||||
user_id: user.id,
|
||||
workout_id: session.workoutId,
|
||||
completed_at: session.completedAt,
|
||||
@@ -196,7 +196,7 @@ export async function getSyncState(): Promise<SyncState> {
|
||||
return {
|
||||
status: 'synced',
|
||||
userId: userId,
|
||||
lastSyncAt: latestSession?.[0]?.created_at || null,
|
||||
lastSyncAt: (latestSession as any)?.[0]?.created_at || null,
|
||||
pendingWorkouts: 0,
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user