feat(#13): open item images in full-screen preview modal
All checks were successful
Luggage List Build / build-web (push) Successful in 29s
Luggage List Build / build-android (push) Successful in 5m42s
Luggage List Build / release (push) Successful in 11s

This commit is contained in:
2026-04-18 19:45:53 +02:00
parent 1e0eb7aee9
commit 4018e97476
3 changed files with 45 additions and 3 deletions

View File

@@ -8,7 +8,7 @@ function statusAccent(status) {
return STATUS_COLORS[status] || '#64748b'; return STATUS_COLORS[status] || '#64748b';
} }
export default function ItemCard({ item, onEdit, onDelete, onQuickPack, onQuickUnpack }) { export default function ItemCard({ item, onEdit, onDelete, onQuickPack, onQuickUnpack, onOpenImage }) {
const isPacked = item.status === 'packed'; const isPacked = item.status === 'packed';
const isUnpacked = item.status === 'unpacked'; const isUnpacked = item.status === 'unpacked';
@@ -18,7 +18,9 @@ export default function ItemCard({ item, onEdit, onDelete, onQuickPack, onQuickU
<View style={styles.itemThumbWrap}> <View style={styles.itemThumbWrap}>
{item.imageUri ? ( {item.imageUri ? (
<Image source={{ uri: item.imageUri }} style={styles.itemThumbSmall} /> <Pressable onPress={() => onOpenImage?.(item.imageUri)}>
<Image source={{ uri: item.imageUri }} style={styles.itemThumbSmall} />
</Pressable>
) : ( ) : (
<View style={styles.itemThumbPlaceholder}> <View style={styles.itemThumbPlaceholder}>
<Text style={styles.itemThumbPlaceholderText}>🧳</Text> <Text style={styles.itemThumbPlaceholderText}>🧳</Text>

View File

@@ -517,6 +517,35 @@ export const styles = StyleSheet.create({
borderRadius: 10, borderRadius: 10,
backgroundColor: '#111827', backgroundColor: '#111827',
}, },
imagePreviewBackdrop: {
flex: 1,
backgroundColor: 'rgba(2,6,23,0.88)',
alignItems: 'center',
justifyContent: 'center',
paddingHorizontal: 14,
},
imagePreviewCard: {
width: '100%',
maxWidth: 460,
borderRadius: 14,
borderWidth: 1,
borderColor: '#334155',
backgroundColor: '#0f172a',
padding: 10,
gap: 8,
},
imagePreviewImage: {
width: '100%',
height: 360,
borderRadius: 10,
backgroundColor: '#0b1220',
},
imagePreviewHint: {
color: '#93c5fd',
textAlign: 'center',
fontSize: 12,
fontWeight: '600',
},
tabBarWrap: { tabBarWrap: {
position: 'absolute', position: 'absolute',

View File

@@ -1,5 +1,5 @@
import React, { useMemo, useState } from 'react'; import React, { useMemo, useState } from 'react';
import { Pressable, Text, View } from 'react-native'; import { Image, Modal, Pressable, Text, View } from 'react-native';
import ItemCard from '../components/ItemCard'; import ItemCard from '../components/ItemCard';
import { ITEM_STATUSES } from '../constants'; import { ITEM_STATUSES } from '../constants';
import { styles } from '../styles'; import { styles } from '../styles';
@@ -16,6 +16,7 @@ export default function ItemsTab({
}) { }) {
const [statusFilter, setStatusFilter] = useState('all'); const [statusFilter, setStatusFilter] = useState('all');
const [categoryFilter, setCategoryFilter] = useState('all'); const [categoryFilter, setCategoryFilter] = useState('all');
const [imagePreviewUri, setImagePreviewUri] = useState('');
const categories = useMemo( const categories = useMemo(
() => Array.from(new Set(selectedTripItems.map((item) => item.category?.trim()).filter(Boolean))).sort((a, b) => a.localeCompare(b)), () => Array.from(new Set(selectedTripItems.map((item) => item.category?.trim()).filter(Boolean))).sort((a, b) => a.localeCompare(b)),
@@ -100,10 +101,20 @@ export default function ItemsTab({
onDelete={deleteItem} onDelete={deleteItem}
onQuickPack={() => quickSetItemStatus(item.id, 'packed')} onQuickPack={() => quickSetItemStatus(item.id, 'packed')}
onQuickUnpack={() => quickSetItemStatus(item.id, 'unpacked')} onQuickUnpack={() => quickSetItemStatus(item.id, 'unpacked')}
onOpenImage={(uri) => setImagePreviewUri(uri)}
/> />
))} ))}
{selectedTripItems.length > 0 && filteredItems.length === 0 ? <Text style={styles.muted}>No items match the current filters.</Text> : null} {selectedTripItems.length > 0 && filteredItems.length === 0 ? <Text style={styles.muted}>No items match the current filters.</Text> : null}
<Modal visible={!!imagePreviewUri} transparent animationType="fade">
<Pressable style={styles.imagePreviewBackdrop} onPress={() => setImagePreviewUri('')}>
<Pressable style={styles.imagePreviewCard} onPress={() => {}}>
{imagePreviewUri ? <Image source={{ uri: imagePreviewUri }} style={styles.imagePreviewImage} resizeMode="contain" /> : null}
<Text style={styles.imagePreviewHint}>Tap outside to close</Text>
</Pressable>
</Pressable>
</Modal>
</View> </View>
); );
} }