Compare commits
4 Commits
luggage-li
...
luggage-li
| Author | SHA1 | Date | |
|---|---|---|---|
| 063e5090ed | |||
| 354a13e9a9 | |||
| 4018e97476 | |||
| 1e0eb7aee9 |
@@ -14,7 +14,7 @@ import TripsTab from './tabs/TripsTab';
|
||||
import ItemsTab from './tabs/ItemsTab';
|
||||
import CheckupTab from './tabs/CheckupTab';
|
||||
import HistoryTab from './tabs/HistoryTab';
|
||||
import { emptyData, STORAGE_KEY } from './constants';
|
||||
import { emptyData, ITEM_PLACEMENTS, STORAGE_KEY } from './constants';
|
||||
import { findBestTripId, makeId, parseYMD, todayYMD } from './utils/date';
|
||||
import { styles } from './styles';
|
||||
|
||||
@@ -34,6 +34,7 @@ const emptyItemForm = () => ({
|
||||
category: '',
|
||||
status: 'unpacked',
|
||||
placement: 'suitcase',
|
||||
placementCustom: '',
|
||||
lentTo: '',
|
||||
imageUri: '',
|
||||
imageQuality: 'balanced',
|
||||
@@ -43,6 +44,7 @@ const emptyItemForm = () => ({
|
||||
const emptyCheckupNoForm = () => ({
|
||||
status: 'unpacked',
|
||||
placement: 'suitcase',
|
||||
placementCustom: '',
|
||||
lentTo: '',
|
||||
updateMasterList: false,
|
||||
});
|
||||
@@ -430,13 +432,17 @@ export default function AppRoot() {
|
||||
}
|
||||
|
||||
function openEditItemModal(item) {
|
||||
const existingPlacement = item.placement || 'suitcase';
|
||||
const hasPresetPlacement = ITEM_PLACEMENTS.includes(existingPlacement);
|
||||
|
||||
setItemForm({
|
||||
id: item.id,
|
||||
name: item.name || '',
|
||||
description: item.description || '',
|
||||
category: item.category || '',
|
||||
status: item.status || 'unpacked',
|
||||
placement: item.placement || 'suitcase',
|
||||
placement: hasPresetPlacement ? existingPlacement : 'other',
|
||||
placementCustom: hasPresetPlacement || existingPlacement === 'other' ? '' : existingPlacement,
|
||||
lentTo: item.lentTo || '',
|
||||
imageUri: item.imageUri || '',
|
||||
imageQuality: item.imageQuality || 'balanced',
|
||||
@@ -456,6 +462,12 @@ export default function AppRoot() {
|
||||
return;
|
||||
}
|
||||
|
||||
const resolvedPlacement = itemForm.placement === 'other' ? itemForm.placementCustom.trim() : itemForm.placement;
|
||||
if (!resolvedPlacement) {
|
||||
showAlert('Missing location', 'Please enter a custom location for "other".');
|
||||
return;
|
||||
}
|
||||
|
||||
const now = Date.now();
|
||||
|
||||
setData((prev) => {
|
||||
@@ -467,7 +479,7 @@ export default function AppRoot() {
|
||||
description: itemForm.description.trim(),
|
||||
category: itemForm.category.trim(),
|
||||
status: itemForm.status,
|
||||
placement: itemForm.placement,
|
||||
placement: resolvedPlacement,
|
||||
lentTo: itemForm.status === 'lent-to' ? itemForm.lentTo.trim() : '',
|
||||
imageUri: itemForm.imageUri,
|
||||
imageQuality: itemForm.imageQuality,
|
||||
@@ -650,9 +662,14 @@ export default function AppRoot() {
|
||||
function openCurrentCheckupNo() {
|
||||
const entry = checkupCurrentEntry;
|
||||
if (!entry) return;
|
||||
|
||||
const existingPlacement = entry.current.placement || 'suitcase';
|
||||
const hasPresetPlacement = ITEM_PLACEMENTS.includes(existingPlacement);
|
||||
|
||||
setCheckupNoForm({
|
||||
status: entry.current.status || 'unpacked',
|
||||
placement: entry.current.placement || 'suitcase',
|
||||
placement: hasPresetPlacement ? existingPlacement : 'other',
|
||||
placementCustom: hasPresetPlacement || existingPlacement === 'other' ? '' : existingPlacement,
|
||||
lentTo: entry.current.lentTo || '',
|
||||
updateMasterList: false,
|
||||
});
|
||||
@@ -663,9 +680,15 @@ export default function AppRoot() {
|
||||
const entry = checkupCurrentEntry;
|
||||
if (!entry) return;
|
||||
|
||||
const resolvedPlacement = checkupNoForm.placement === 'other' ? checkupNoForm.placementCustom.trim() : checkupNoForm.placement;
|
||||
if (!resolvedPlacement) {
|
||||
showAlert('Missing location', 'Please enter a custom location for "other".');
|
||||
return;
|
||||
}
|
||||
|
||||
const patch = {
|
||||
status: checkupNoForm.status,
|
||||
placement: checkupNoForm.placement,
|
||||
placement: resolvedPlacement,
|
||||
lentTo: checkupNoForm.status === 'lent-to' ? checkupNoForm.lentTo.trim() : '',
|
||||
};
|
||||
|
||||
@@ -877,7 +900,6 @@ export default function AppRoot() {
|
||||
createTrip={createTrip}
|
||||
trips={data.trips}
|
||||
selectedTripId={selectedTripId}
|
||||
chooseTrip={setSelectedTripId}
|
||||
setTripAsTemplate={setTripAsTemplate}
|
||||
saveTripEdits={saveTripEdits}
|
||||
setTripArchived={setTripArchived}
|
||||
|
||||
@@ -8,7 +8,7 @@ function statusAccent(status) {
|
||||
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 isUnpacked = item.status === 'unpacked';
|
||||
|
||||
@@ -18,7 +18,9 @@ export default function ItemCard({ item, onEdit, onDelete, onQuickPack, onQuickU
|
||||
|
||||
<View style={styles.itemThumbWrap}>
|
||||
{item.imageUri ? (
|
||||
<Pressable onPress={() => onOpenImage?.(item.imageUri)}>
|
||||
<Image source={{ uri: item.imageUri }} style={styles.itemThumbSmall} />
|
||||
</Pressable>
|
||||
) : (
|
||||
<View style={styles.itemThumbPlaceholder}>
|
||||
<Text style={styles.itemThumbPlaceholderText}>🧳</Text>
|
||||
@@ -29,11 +31,11 @@ export default function ItemCard({ item, onEdit, onDelete, onQuickPack, onQuickU
|
||||
<View style={styles.itemMain}>
|
||||
<View style={styles.cardRow}>
|
||||
<View style={styles.flex}>
|
||||
<Text style={styles.itemTitle}>{item.name}</Text>
|
||||
<Text style={styles.itemMeta}>{item.category || 'uncategorized'} · {formatStatusLabel(item.status, item.lentTo)}</Text>
|
||||
<Text style={styles.itemTitle} numberOfLines={1}>{item.name}</Text>
|
||||
<Text style={styles.itemMeta} numberOfLines={1}>{item.category || 'uncategorized'} · {formatStatusLabel(item.status, item.lentTo)}</Text>
|
||||
<Text style={styles.itemMeta}>Location: {item.placement}</Text>
|
||||
{item.status === 'lent-to' && !!item.lentTo ? <Text style={styles.itemMeta}>Borrower: {item.lentTo}</Text> : null}
|
||||
{!!item.description ? <Text style={styles.itemMeta}>{item.description}</Text> : null}
|
||||
{item.status === 'lent-to' && !!item.lentTo ? <Text style={styles.itemMeta} numberOfLines={1}>Borrower: {item.lentTo}</Text> : null}
|
||||
{!!item.description ? <Text style={styles.itemMeta} numberOfLines={2}>{item.description}</Text> : null}
|
||||
</View>
|
||||
<View style={styles.stackButtons}>
|
||||
<Pressable style={styles.miniBtn} onPress={() => onEdit(item)}>
|
||||
@@ -47,10 +49,10 @@ export default function ItemCard({ item, onEdit, onDelete, onQuickPack, onQuickU
|
||||
|
||||
<View style={styles.quickStatusRow}>
|
||||
<Pressable style={[styles.quickStatusBtn, isPacked && styles.quickStatusBtnActive]} onPress={onQuickPack}>
|
||||
<Text style={[styles.quickStatusBtnText, isPacked && styles.quickStatusBtnTextActive]}>Pack</Text>
|
||||
<Text style={[styles.quickStatusBtnText, isPacked && styles.quickStatusBtnTextActive]}>Packed</Text>
|
||||
</Pressable>
|
||||
<Pressable style={[styles.quickStatusBtn, isUnpacked && styles.quickStatusBtnActive]} onPress={onQuickUnpack}>
|
||||
<Text style={[styles.quickStatusBtnText, isUnpacked && styles.quickStatusBtnTextActive]}>Unpack</Text>
|
||||
<Text style={[styles.quickStatusBtnText, isUnpacked && styles.quickStatusBtnTextActive]}>Unpacked</Text>
|
||||
</Pressable>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
@@ -103,6 +103,18 @@ export default function CheckupFlowModal({
|
||||
/>
|
||||
</Field>
|
||||
|
||||
{noForm.placement === 'other' ? (
|
||||
<Field label="Custom location">
|
||||
<TextInput
|
||||
style={styles.input}
|
||||
value={noForm.placementCustom}
|
||||
onChangeText={(v) => setNoForm((prev) => ({ ...prev, placementCustom: v }))}
|
||||
placeholder="bath-kit"
|
||||
placeholderTextColor="#6b7280"
|
||||
/>
|
||||
</Field>
|
||||
) : null}
|
||||
|
||||
{noForm.status === 'lent-to' ? (
|
||||
<Field label="Lent to">
|
||||
<TextInput
|
||||
|
||||
@@ -81,6 +81,18 @@ export default function ItemModal({
|
||||
<ChipGroup options={ITEM_PLACEMENTS} value={itemForm.placement} onChange={(v) => updateItemForm('placement', v)} />
|
||||
</Field>
|
||||
|
||||
{itemForm.placement === 'other' ? (
|
||||
<Field label="Custom location">
|
||||
<TextInput
|
||||
style={styles.input}
|
||||
value={itemForm.placementCustom}
|
||||
onChangeText={(v) => updateItemForm('placementCustom', v)}
|
||||
placeholder="bath-kit"
|
||||
placeholderTextColor="#6b7280"
|
||||
/>
|
||||
</Field>
|
||||
) : null}
|
||||
|
||||
{itemForm.status === 'lent-to' ? (
|
||||
<Field label="Lent to">
|
||||
<TextInput
|
||||
|
||||
111
src/styles.js
111
src/styles.js
@@ -10,10 +10,10 @@ export const styles = StyleSheet.create({
|
||||
flex: 1,
|
||||
},
|
||||
content: {
|
||||
paddingHorizontal: 16,
|
||||
paddingTop: 12,
|
||||
paddingBottom: TAB_BAR_HEIGHT + 22,
|
||||
gap: 14,
|
||||
paddingHorizontal: 14,
|
||||
paddingTop: 10,
|
||||
paddingBottom: TAB_BAR_HEIGHT + 20,
|
||||
gap: 10,
|
||||
},
|
||||
statusSpacer: {
|
||||
height: Platform.OS === 'android' ? 8 : 0,
|
||||
@@ -86,7 +86,7 @@ export const styles = StyleSheet.create({
|
||||
},
|
||||
|
||||
section: {
|
||||
gap: 12,
|
||||
gap: 10,
|
||||
},
|
||||
sectionTitle: {
|
||||
color: '#f1f5f9',
|
||||
@@ -105,8 +105,8 @@ export const styles = StyleSheet.create({
|
||||
borderRadius: 16,
|
||||
borderWidth: 1,
|
||||
borderColor: '#1f2937',
|
||||
padding: 12,
|
||||
gap: 8,
|
||||
padding: 10,
|
||||
gap: 6,
|
||||
},
|
||||
cardActive: {
|
||||
borderColor: '#60a5fa',
|
||||
@@ -120,12 +120,12 @@ export const styles = StyleSheet.create({
|
||||
borderRadius: 16,
|
||||
borderWidth: 1,
|
||||
borderColor: '#1e293b',
|
||||
padding: 12,
|
||||
gap: 10,
|
||||
padding: 10,
|
||||
gap: 8,
|
||||
},
|
||||
cardRow: {
|
||||
flexDirection: 'row',
|
||||
gap: 10,
|
||||
gap: 8,
|
||||
},
|
||||
cardTitle: {
|
||||
color: '#f8fafc',
|
||||
@@ -134,8 +134,8 @@ export const styles = StyleSheet.create({
|
||||
},
|
||||
cardMeta: {
|
||||
color: '#94a3b8',
|
||||
marginTop: 3,
|
||||
fontSize: 13,
|
||||
marginTop: 2,
|
||||
fontSize: 12,
|
||||
},
|
||||
tripHistoryLabel: {
|
||||
color: '#93c5fd',
|
||||
@@ -148,8 +148,8 @@ export const styles = StyleSheet.create({
|
||||
borderRadius: 18,
|
||||
borderWidth: 1,
|
||||
borderColor: '#334155',
|
||||
padding: 12,
|
||||
gap: 8,
|
||||
padding: 10,
|
||||
gap: 6,
|
||||
},
|
||||
tripHeroImage: {
|
||||
width: '100%',
|
||||
@@ -160,7 +160,7 @@ export const styles = StyleSheet.create({
|
||||
tripHeroTitle: {
|
||||
color: '#f8fafc',
|
||||
fontWeight: '800',
|
||||
fontSize: 22,
|
||||
fontSize: 20,
|
||||
},
|
||||
tripListTitle: {
|
||||
color: '#cbd5e1',
|
||||
@@ -290,19 +290,19 @@ export const styles = StyleSheet.create({
|
||||
},
|
||||
|
||||
stackButtons: {
|
||||
gap: 7,
|
||||
gap: 6,
|
||||
},
|
||||
miniBtn: {
|
||||
backgroundColor: '#1e293b',
|
||||
borderRadius: 8,
|
||||
paddingVertical: 7,
|
||||
paddingHorizontal: 10,
|
||||
paddingVertical: 6,
|
||||
paddingHorizontal: 9,
|
||||
},
|
||||
miniBtnDanger: {
|
||||
backgroundColor: '#3b1d22',
|
||||
borderRadius: 8,
|
||||
paddingVertical: 7,
|
||||
paddingHorizontal: 10,
|
||||
paddingVertical: 6,
|
||||
paddingHorizontal: 9,
|
||||
},
|
||||
miniBtnText: {
|
||||
color: '#e2e8f0',
|
||||
@@ -324,18 +324,18 @@ export const styles = StyleSheet.create({
|
||||
alignSelf: 'stretch',
|
||||
},
|
||||
itemThumbWrap: {
|
||||
paddingTop: 10,
|
||||
paddingLeft: 10,
|
||||
paddingTop: 8,
|
||||
paddingLeft: 8,
|
||||
},
|
||||
itemThumbSmall: {
|
||||
width: 46,
|
||||
height: 46,
|
||||
width: 42,
|
||||
height: 42,
|
||||
borderRadius: 8,
|
||||
backgroundColor: '#0b1220',
|
||||
},
|
||||
itemThumbPlaceholder: {
|
||||
width: 46,
|
||||
height: 46,
|
||||
width: 42,
|
||||
height: 42,
|
||||
borderRadius: 8,
|
||||
backgroundColor: '#0b1220',
|
||||
alignItems: 'center',
|
||||
@@ -344,12 +344,12 @@ export const styles = StyleSheet.create({
|
||||
borderColor: '#243244',
|
||||
},
|
||||
itemThumbPlaceholderText: {
|
||||
fontSize: 18,
|
||||
fontSize: 16,
|
||||
},
|
||||
itemMain: {
|
||||
flex: 1,
|
||||
padding: 10,
|
||||
gap: 8,
|
||||
padding: 8,
|
||||
gap: 6,
|
||||
},
|
||||
itemTitle: {
|
||||
color: '#f8fafc',
|
||||
@@ -358,17 +358,17 @@ export const styles = StyleSheet.create({
|
||||
},
|
||||
itemMeta: {
|
||||
color: '#94a3b8',
|
||||
marginTop: 2,
|
||||
fontSize: 13,
|
||||
marginTop: 1,
|
||||
fontSize: 12,
|
||||
},
|
||||
quickStatusRow: {
|
||||
flexDirection: 'row',
|
||||
gap: 8,
|
||||
marginTop: 2,
|
||||
gap: 6,
|
||||
marginTop: 1,
|
||||
},
|
||||
quickStatusBtn: {
|
||||
paddingVertical: 6,
|
||||
paddingHorizontal: 12,
|
||||
paddingVertical: 5,
|
||||
paddingHorizontal: 10,
|
||||
borderRadius: 999,
|
||||
borderWidth: 1,
|
||||
borderColor: '#334155',
|
||||
@@ -381,7 +381,7 @@ export const styles = StyleSheet.create({
|
||||
quickStatusBtnText: {
|
||||
color: '#cbd5e1',
|
||||
fontWeight: '700',
|
||||
fontSize: 12,
|
||||
fontSize: 11,
|
||||
},
|
||||
quickStatusBtnTextActive: {
|
||||
color: '#dbeafe',
|
||||
@@ -517,6 +517,35 @@ export const styles = StyleSheet.create({
|
||||
borderRadius: 10,
|
||||
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: {
|
||||
position: 'absolute',
|
||||
@@ -547,8 +576,8 @@ export const styles = StyleSheet.create({
|
||||
tabItem: {
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
gap: 4,
|
||||
minWidth: 58,
|
||||
gap: 3,
|
||||
minWidth: 56,
|
||||
},
|
||||
tabAddBtn: {
|
||||
width: 54,
|
||||
@@ -566,7 +595,7 @@ export const styles = StyleSheet.create({
|
||||
},
|
||||
tabLabel: {
|
||||
color: '#94a3b8',
|
||||
fontSize: 12,
|
||||
fontSize: 11,
|
||||
fontWeight: '600',
|
||||
},
|
||||
tabLabelActive: {
|
||||
@@ -644,8 +673,8 @@ export const styles = StyleSheet.create({
|
||||
borderRadius: 20,
|
||||
borderWidth: 1,
|
||||
borderColor: '#1e293b',
|
||||
padding: 16,
|
||||
gap: 10,
|
||||
padding: 14,
|
||||
gap: 8,
|
||||
},
|
||||
closeText: {
|
||||
color: '#93c5fd',
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
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 { ITEM_STATUSES } from '../constants';
|
||||
import { styles } from '../styles';
|
||||
@@ -16,6 +16,7 @@ export default function ItemsTab({
|
||||
}) {
|
||||
const [statusFilter, setStatusFilter] = useState('all');
|
||||
const [categoryFilter, setCategoryFilter] = useState('all');
|
||||
const [imagePreviewUri, setImagePreviewUri] = useState('');
|
||||
|
||||
const categories = useMemo(
|
||||
() => 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}
|
||||
onQuickPack={() => quickSetItemStatus(item.id, 'packed')}
|
||||
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}
|
||||
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -28,7 +28,6 @@ export default function TripsTab({
|
||||
createTrip,
|
||||
trips,
|
||||
selectedTripId,
|
||||
chooseTrip,
|
||||
setTripAsTemplate,
|
||||
saveTripEdits,
|
||||
setTripArchived,
|
||||
@@ -137,12 +136,9 @@ export default function TripsTab({
|
||||
<View style={styles.flex}>
|
||||
<Text style={styles.cardTitle}>{trip.name}</Text>
|
||||
<Text style={styles.cardMeta}>{trip.location || 'No location'} · {trip.startDate} → {trip.endDate}</Text>
|
||||
<Text style={styles.cardMeta}>{defaultTemplateTripId === trip.id ? 'Default template' : ' '}</Text>
|
||||
{defaultTemplateTripId === trip.id ? <Text style={styles.cardMeta}>Default template</Text> : null}
|
||||
</View>
|
||||
<View style={styles.stackButtons}>
|
||||
<Pressable style={styles.miniBtn} onPress={() => chooseTrip(trip.id)}>
|
||||
<Text style={styles.miniBtnText}>Select</Text>
|
||||
</Pressable>
|
||||
<Pressable style={styles.miniBtn} onPress={() => openView(trip.id)}>
|
||||
<Text style={styles.miniBtnText}>View</Text>
|
||||
</Pressable>
|
||||
|
||||
Reference in New Issue
Block a user