Files
Sauci/apps/mobile/src/components/discovery/ContentTile.tsx
T
jjbruntonandClaude Fable 5 6bc4e31719 chore(mobile): upgrade to Expo SDK 56
Bump expo, react, react-native, and all expo-* / react-native-* peers to
the SDK 56 line via expo install --fix, adding expo-splash-screen and
expo-system-ui for config plugins the SDK split out of core. Bump
@bacons/apple-targets, react-native-view-shot, and other native modules
to versions compatible with React Native 0.85 and the new architecture.

Remove @react-navigation/* direct dependencies and route their hooks
through expo-router, which is no longer compatible with react-navigation
as of SDK 56. Fix RN 0.85's removed StyleSheet.absoluteFillObject (use
absoluteFill) and react-native-view-shot 5.x's ViewShotRef type across
call sites. Downgrade eslint-plugin-react-hooks v7's new React Compiler
era rules to warnings pending a dedicated refactor, and repair Jest
fallout from the jest-expo/reanimated/worklets/expo-media-library bumps.

Regenerate the committed ios/ and android/ native projects with
`expo prebuild --clean` (required for @bacons/apple-targets 5.x) against
the production release channel, and document that requirement.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-31 11:11:37 +01:00

159 lines
4.4 KiB
TypeScript

import React from 'react';
import { StyleSheet, View, Text, TouchableOpacity, Dimensions } from 'react-native';
import { Ionicons } from '@expo/vector-icons';
import * as Haptics from 'expo-haptics';
import { colors, spacing, radius, typography, shadows, getCategoryColor } from '../../theme';
import { CircularProgress } from './CircularProgress';
import type { QuestionPack } from '@/types';
import type { PackProgressData } from '../../store/packsStore';
const SCREEN_WIDTH = Dimensions.get('window').width;
const TILE_GAP = spacing.sm;
const HORIZONTAL_PADDING = spacing.lg;
// Calculate tile width: 2 tiles per screen with gap and padding, plus peek of next
export const TILE_WIDTH = (SCREEN_WIDTH - HORIZONTAL_PADDING * 2 - TILE_GAP) / 2 + 8;
export const TILE_HEIGHT = 170;
interface ContentTileProps {
pack: QuestionPack;
isLocked: boolean;
isNew?: boolean;
progress?: PackProgressData;
onPress: () => void;
}
export function ContentTile({ pack, isLocked, isNew = false, progress, onPress }: ContentTileProps) {
const handlePress = () => {
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
onPress();
};
const backgroundColor = getCategoryColor(pack.category);
const icon = pack.icon || pack.category?.icon || 'layers';
// Calculate progress percentage
const progressPercent = progress && progress.totalQuestions > 0
? progress.answeredQuestions / progress.totalQuestions
: 0;
// Always show progress ring when progress data exists (even at 0% or 0 questions)
const showProgress = !!progress;
return (
<TouchableOpacity
onPress={handlePress}
activeOpacity={0.85}
style={styles.container}
>
<View style={[styles.tile, { backgroundColor }, shadows.md]}>
{/* Top row: icon and badges */}
<View style={styles.topRow}>
<View style={styles.iconContainer}>
<Ionicons
name={icon as any}
size={20}
color="rgba(255, 255, 255, 0.9)"
/>
</View>
<View style={styles.badges}>
{isNew && (
<View style={styles.newBadge}>
<Text style={styles.newBadgeText}>NEW</Text>
</View>
)}
{isLocked && (
<View style={styles.lockBadge}>
<Ionicons name="lock-closed" size={12} color={colors.premium.gold} />
</View>
)}
{showProgress && (
<CircularProgress
progress={progressPercent}
size={20}
strokeWidth={2}
/>
)}
</View>
</View>
{/* Content */}
<View style={styles.content}>
<Text style={styles.titleText} numberOfLines={2}>
{pack.name}
</Text>
</View>
{/* Decorative overlay for depth */}
<View style={styles.overlay} />
</View>
</TouchableOpacity>
);
}
const styles = StyleSheet.create({
container: {
width: TILE_WIDTH,
height: TILE_HEIGHT,
},
tile: {
flex: 1,
borderRadius: radius.lg,
padding: spacing.md,
overflow: 'hidden',
position: 'relative',
},
overlay: {
...StyleSheet.absoluteFill,
backgroundColor: 'rgba(0, 0, 0, 0.05)',
borderRadius: radius.lg,
pointerEvents: 'none',
},
topRow: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'flex-start',
},
iconContainer: {
width: 36,
height: 36,
borderRadius: 18,
backgroundColor: 'rgba(255, 255, 255, 0.2)',
justifyContent: 'center',
alignItems: 'center',
},
badges: {
flexDirection: 'row',
gap: spacing.xs,
},
newBadge: {
backgroundColor: 'rgba(255, 255, 255, 0.95)',
paddingHorizontal: spacing.sm,
paddingVertical: 2,
borderRadius: radius.xs,
},
newBadgeText: {
...typography.caption2,
fontWeight: '700',
color: colors.background,
letterSpacing: 0.5,
},
lockBadge: {
width: 24,
height: 24,
borderRadius: 12,
backgroundColor: 'rgba(0, 0, 0, 0.3)',
justifyContent: 'center',
alignItems: 'center',
},
content: {
flex: 1,
justifyContent: 'center',
marginTop: spacing.sm,
},
titleText: {
...typography.headline,
color: colors.text,
fontWeight: '700',
},
});
export default ContentTile;