feat(#13): open item images in full-screen preview modal
This commit is contained in:
@@ -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>
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user