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',
},
});