diff --git a/src/shared/components/CollectionCard.tsx b/src/shared/components/CollectionCard.tsx new file mode 100644 index 0000000..dceee84 --- /dev/null +++ b/src/shared/components/CollectionCard.tsx @@ -0,0 +1,133 @@ +/** + * CollectionCard - Premium collection card with glassmorphism + * Used in Home and Browse screens + */ + +import { useMemo } from 'react' +import { + View, + StyleSheet, + Pressable, + ImageBackground, + Dimensions, + Text as RNText, +} from 'react-native' +import { LinearGradient } from 'expo-linear-gradient' +import { BlurView } from 'expo-blur' + +import { useThemeColors, BRAND, GRADIENTS } from '@/src/shared/theme' +import type { ThemeColors } from '@/src/shared/theme/types' +import { RADIUS } from '@/src/shared/constants/borderRadius' +import { SPACING } from '@/src/shared/constants/spacing' +import { StyledText } from './StyledText' +import type { Collection } from '@/src/shared/types' + +const { width: SCREEN_WIDTH } = Dimensions.get('window') + +interface CollectionCardProps { + collection: Collection + onPress?: () => void + imageUrl?: string +} + +export function CollectionCard({ collection, onPress, imageUrl }: CollectionCardProps) { + const colors = useThemeColors() + const styles = useMemo(() => createStyles(colors), [colors]) + + return ( + + {/* Background Image or Gradient */} + {imageUrl ? ( + + + + ) : ( + + )} + + {/* Glassmorphism Overlay */} + + + + + {/* Content */} + + + {collection.icon} + + + + {collection.title} + + + + {collection.workoutIds.length} workouts + + + + ) +} + +function createStyles(colors: ThemeColors) { + const cardWidth = (SCREEN_WIDTH - SPACING[6] * 2 - SPACING[3]) / 2 + + return StyleSheet.create({ + container: { + width: cardWidth, + aspectRatio: 1, + borderRadius: RADIUS.XL, + overflow: 'hidden', + ...colors.shadow.md, + }, + overlay: { + ...StyleSheet.absoluteFillObject, + backgroundColor: 'rgba(0,0,0,0.3)', + }, + content: { + flex: 1, + padding: SPACING[4], + justifyContent: 'flex-end', + }, + iconContainer: { + width: 48, + height: 48, + borderRadius: 14, + backgroundColor: 'rgba(255,255,255,0.15)', + justifyContent: 'center', + alignItems: 'center', + marginBottom: SPACING[3], + borderWidth: 1, + borderColor: 'rgba(255,255,255,0.2)', + }, + icon: { + fontSize: 24, + }, + title: { + marginBottom: SPACING[1], + }, + }) +} diff --git a/src/shared/components/WorkoutCard.tsx b/src/shared/components/WorkoutCard.tsx new file mode 100644 index 0000000..92dba16 --- /dev/null +++ b/src/shared/components/WorkoutCard.tsx @@ -0,0 +1,200 @@ +/** + * WorkoutCard - Premium workout card with glassmorphism + * Used in Home and Browse screens + */ + +import { useMemo } from 'react' +import { + View, + StyleSheet, + Pressable, + ImageBackground, + Dimensions, + Text as RNText, + ViewStyle, +} from 'react-native' +import { LinearGradient } from 'expo-linear-gradient' +import { BlurView } from 'expo-blur' +import Ionicons from '@expo/vector-icons/Ionicons' + +import { useThemeColors, BRAND, GRADIENTS } from '@/src/shared/theme' +import type { ThemeColors } from '@/src/shared/theme/types' +import { RADIUS } from '@/src/shared/constants/borderRadius' +import { SPACING } from '@/src/shared/constants/spacing' +import { StyledText } from './StyledText' +import type { Workout, WorkoutCategory } from '@/src/shared/types' + +const { width: SCREEN_WIDTH } = Dimensions.get('window') + +export type WorkoutCardVariant = 'horizontal' | 'grid' | 'featured' + +interface WorkoutCardProps { + workout: Workout + variant?: WorkoutCardVariant + onPress?: () => void + title?: string + metadata?: string +} + +const CATEGORY_COLORS: Record = { + 'full-body': BRAND.PRIMARY, + 'core': '#5AC8FA', + 'upper-body': '#BF5AF2', + 'lower-body': '#30D158', + 'cardio': '#FF9500', +} + +const CATEGORY_LABELS: Record = { + 'full-body': 'Full Body', + 'core': 'Core', + 'upper-body': 'Upper Body', + 'lower-body': 'Lower Body', + 'cardio': 'Cardio', +} + +function getVariantDimensions(variant: WorkoutCardVariant): ViewStyle { + switch (variant) { + case 'featured': + return { + width: SCREEN_WIDTH - SPACING[6] * 2, + height: 320, + } + case 'horizontal': + return { + width: 200, + height: 280, + } + case 'grid': + default: + return { + flex: 1, + aspectRatio: 0.75, + } + } +} + +export function WorkoutCard({ + workout, + variant = 'horizontal', + onPress, + title, + metadata, +}: WorkoutCardProps) { + const colors = useThemeColors() + const styles = useMemo(() => createStyles(colors), [colors]) + const dimensions = useMemo(() => getVariantDimensions(variant), [variant]) + + const displayTitle = title ?? workout.title + const displayMetadata = metadata ?? `${workout.duration} min • ${workout.calories} cal` + const categoryColor = CATEGORY_COLORS[workout.category] + + return ( + + {/* Background Image */} + + {/* Gradient Overlay */} + + + + {/* Category Badge */} + + + + {CATEGORY_LABELS[workout.category]} + + + + {/* Play Button */} + + + + + + + + {/* Content at Bottom */} + + + {displayTitle} + + + {displayMetadata} + + + + ) +} + +function createStyles(colors: ThemeColors) { + return StyleSheet.create({ + container: { + borderRadius: RADIUS.XL, + overflow: 'hidden', + ...colors.shadow.lg, + }, + categoryBadge: { + position: 'absolute', + top: SPACING[3], + left: SPACING[3], + flexDirection: 'row', + alignItems: 'center', + paddingHorizontal: SPACING[3], + paddingVertical: SPACING[1], + borderRadius: RADIUS.SM, + borderWidth: 1, + overflow: 'hidden', + }, + categoryText: { + fontSize: 11, + fontWeight: '600', + }, + playButtonContainer: { + ...StyleSheet.absoluteFillObject, + justifyContent: 'center', + alignItems: 'center', + }, + playButton: { + width: 56, + height: 56, + borderRadius: 28, + justifyContent: 'center', + alignItems: 'center', + overflow: 'hidden', + borderWidth: 1, + borderColor: 'rgba(255,255,255,0.2)', + backgroundColor: 'rgba(255,255,255,0.1)', + }, + content: { + position: 'absolute', + bottom: 0, + left: 0, + right: 0, + padding: SPACING[4], + }, + title: { + marginBottom: SPACING[1], + }, + }) +}