import { Text } from '@/aesthetics/Text'; import { font, radius, shadows, spacing, useColors } from '@/aesthetics/styles'; import { useStrings } from '@/common/hooks/useStrings'; import { IngredientChips } from '@/see-log/components/IngredientChips'; import { useFoodLogPhoto } from '@/see-log/hooks/useFoodLogPhoto'; import type { BoundingBox, FoodLog, StagedFoodLog } from '@/types/foodlog'; import { X } from 'lucide-react-native'; import { useState, type ReactNode } from 'react'; import { ActivityIndicator, Image, Pressable, StyleSheet, View } from 'react-native'; type FoodLogCardProps = { log: FoodLog; onPress: () => void; }; type StagedFoodLogCardProps = { staged: StagedFoodLog; onPress: () => void; onDelete: () => void; header?: ReactNode; children?: ReactNode; }; export function FoodLogCard({ log, onPress }: FoodLogCardProps) { const c = useColors(); const strings = useStrings(); const { uri: photoUri, loading: photoLoading } = useFoodLogPhoto(log); const isCompleted = log.processingStatus === 'completed'; const isFailed = log.processingStatus === 'failed'; const displayName = !isCompleted ? (log.userInputFoodName ?? log.foodName) : log.foodName; const statusLabel = isFailed ? strings.seeLog.foodLog.needsAttention : log.processingStatus === 'pending_ai_food_reco' ? strings.seeLog.foodLog.identifying : log.processingStatus === 'pending_ai_nutrition_reco' ? strings.seeLog.foodLog.analyzing : ''; return ( [ styles.card, shadows.sm, { backgroundColor: c.surface, borderColor: c.borderSubtle }, pressed && { opacity: 0.9 }, ]} onPress={onPress} > {photoUri ? ( ) : photoLoading ? ( ) : log.remotePhotoPath ? ( ) : null} {displayName} {!isCompleted && ( {!isFailed && } {statusLabel} )} {log.ingredients.length > 0 && isCompleted && ( )} ); } const PHOTO_SIZE = 88; function PhotoWithBox({ uri, box }: { uri: string; box?: BoundingBox }) { const [aspect, setAspect] = useState(0); return ( { const { width, height } = e.nativeEvent.source; if (height > 0) setAspect(width / height); }} /> {box && aspect > 0 && } ); } function coverBox(box: BoundingBox, aspect: number) { let sx = 1, sy = 1, ox = 0, oy = 0; if (aspect > 1) { sx = aspect; ox = (aspect - 1) / 2; } else { sy = 1 / aspect; oy = (1 / aspect - 1) / 2; } return { left: (box.x * sx - ox) * PHOTO_SIZE, top: (box.y * sy - oy) * PHOTO_SIZE, width: box.width * sx * PHOTO_SIZE, height: box.height * sy * PHOTO_SIZE, }; } export function StagedFoodLogCard({ staged, onPress, onDelete, header, children, }: StagedFoodLogCardProps) { const c = useColors(); const strings = useStrings(); const phase = staged.detectionPhase; const isDetecting = !!phase && phase !== 'streaming' && phase !== 'complete' && phase !== 'error'; const disabled = isDetecting || staged.regeneratingIngredients; const phaseStrings: Partial> = { capturing: strings.addLog.detection.capturingPhoto, compressing: strings.addLog.detection.compressingPhoto, connecting: strings.addLog.detection.connectingServer, uploading: strings.addLog.detection.uploadingPhoto, identifying: strings.addLog.detection.identifyingFood, ingredients: strings.addLog.detection.studyingIngredients, error: strings.addLog.detection.retry, }; const statusText = phase ? (phaseStrings[phase] ?? null) : null; return ( [ styles.card, shadows.sm, { backgroundColor: c.surface, borderColor: c.borderSubtle }, pressed && { opacity: 0.9 }, ]} onPress={onPress} disabled={disabled} > {staged.photoUri && } {statusText ? ( {phase !== 'error' && } {statusText} ) : header ? ( header ) : ( {staged.foodName} )} {children} ); } const styles = StyleSheet.create({ card: { flexDirection: 'row', padding: spacing.lg, borderRadius: radius.lg, borderWidth: 1, }, photoWrapper: { position: 'relative', width: PHOTO_SIZE, height: PHOTO_SIZE, borderRadius: radius.md, overflow: 'hidden', marginRight: spacing.lg, }, photo: { width: PHOTO_SIZE, height: PHOTO_SIZE, borderRadius: radius.md, }, boundingBox: { position: 'absolute', borderWidth: 2, borderColor: '#ffffff', borderRadius: 4, }, photoMargin: { marginRight: spacing.lg, }, photoPlaceholder: { alignItems: 'center', justifyContent: 'center', marginRight: spacing.lg, }, content: { flex: 1, justifyContent: 'center', gap: spacing.sm, }, headerRow: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', }, name: { fontSize: font.lg, fontWeight: '600', flex: 1, }, deleteButton: { padding: spacing.xs, }, statusRow: { flexDirection: 'row', alignItems: 'center', gap: spacing.sm, }, statusText: { fontSize: font.xs, fontWeight: '500', }, });