Building Components: From Boring Buttons to Receipt-Chomping Cards π§±π¨
Time to add some meat to our React Native skeleton! Today we're building the core components that'll make our Purchase Tracker actually track purchases (revolutionary, I know). Think of this as interior decorating, but for apps β and way more fun than picking out throw pillows.
The Component Cast: Meet Your New Best Friends π
We're building three main characters for our app:
- PurchaseCard π³ β The star of the show, displays purchase info
- CameraView πΈ β The receipt scanner (currently fake, but it has dreams)
- Navigation π§ β The GPS for our app (gets you where you need to go)
Let's dive in!
PurchaseCard: The Receipt Whisperer π§Ύβ¨
This little beauty turns boring purchase data into eye candy. It's like Marie Kondo for your receipts β everything has its place, and it sparks joy (or at least doesn't spark rage).
// src/components/PurchaseCard.tsx
import React from 'react';
import { View, Text, StyleSheet, TouchableOpacity, Alert } from 'react-native';
import { Purchase } from '../types';
interface PurchaseCardProps {
purchase: Purchase;
}
export const PurchaseCard: React.FC<PurchaseCardProps> = ({ purchase }) => {
const formatDate = (dateString: string) => {
const date = new Date(dateString);
return date.toLocaleDateString('en-US', {
month: 'short',
day: 'numeric',
year: 'numeric',
hour: '2-digit',
minute: '2-digit'
});
};
const handlePress = () => {
// Show purchase details in a popup
const itemsList = purchase.items
.map(item => `${item.quantity}x ${item.name} - $${item.price.toFixed(2)}`)
.join('\n');
Alert.alert(
`${purchase.storeName} - $${purchase.total.toFixed(2)}`,
`Date: ${formatDate(purchase.date)}\n\nItems:\n${itemsList}`,
[{ text: 'OK' }]
);
};
return (
<TouchableOpacity style={styles.card} onPress={handlePress}>
<View style={styles.header}>
<Text style={styles.storeName}>{purchase.storeName}</Text>
<Text style={styles.total}>${purchase.total.toFixed(2)}</Text>
</View>
<Text style={styles.date}>{formatDate(purchase.date)}</Text>
<Text style={styles.itemCount}>
{purchase.items.length} item{purchase.items.length !== 1 ? 's' : ''}
</Text>
</TouchableOpacity>
);
};
const styles = StyleSheet.create({
card: {
backgroundColor: '#fff',
padding: 16,
marginHorizontal: 16,
marginVertical: 8,
borderRadius: 12,
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.1,
shadowRadius: 4,
elevation: 3, // Android shadow
},
header: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: 8,
},
storeName: {
fontSize: 18,
fontWeight: 'bold',
color: '#333',
},
total: {
fontSize: 18,
fontWeight: 'bold',
color: '#2196F3',
},
date: {
fontSize: 14,
color: '#666',
marginBottom: 4,
},
itemCount: {
fontSize: 12,
color: '#999',
},
});
What makes this card special?
- Tappable! (Shows purchase details)
- Pretty shadows (because flat is boring)
- Smart date formatting (no more ISO timestamp nightmares)
- Responsive design (works on tiny and huge screens)
CameraView: The Fake-It-Till-You-Make-It Scanner πΈπ¬
This component pretends to be a camera until we hook up the real thing. It's like a stunt double for our actual camera functionality β looks the part, does the job (sort of).
// src/components/CameraView.tsx
import React from 'react';
import { View, Text, StyleSheet, TouchableOpacity } from 'react-native';
interface CameraViewProps {
onTakePhoto: () => void;
isLoading?: boolean;
}
export const CameraView: React.FC<CameraViewProps> = ({
onTakePhoto,
isLoading = false
}) => {
return (
<View style={styles.container}>
<View style={styles.cameraPlaceholder}>
<Text style={styles.cameraText}>πΈ</Text>
<Text style={styles.instructionText}>Point camera at receipt</Text>
{/* The viewfinder frame */}
<View style={styles.frame}>
<View style={styles.corner} />
<View style={[styles.corner, styles.topRight]} />
<View style={[styles.corner, styles.bottomLeft]} />
<View style={[styles.corner, styles.bottomRight]} />
</View>
</View>
<TouchableOpacity
style={[styles.captureButton, isLoading && styles.captureButtonDisabled]}
onPress={onTakePhoto}
disabled={isLoading}
>
<Text style={styles.captureButtonText}>
{isLoading ? 'Processing...' : 'Take Photo'}
</Text>
</TouchableOpacity>
</View>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#000',
justifyContent: 'center',
alignItems: 'center',
},
cameraPlaceholder: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
width: '100%',
},
cameraText: {
fontSize: 64,
marginBottom: 16,
},
instructionText: {
color: '#fff',
fontSize: 16,
marginBottom: 32,
},
frame: {
width: 250,
height: 350,
position: 'relative',
},
corner: {
position: 'absolute',
width: 30,
height: 30,
borderColor: '#2196F3',
borderWidth: 3,
// Each corner has different border sides
},
// ... more corner styles for positioning
captureButton: {
backgroundColor: '#2196F3',
paddingHorizontal: 40,
paddingVertical: 16,
borderRadius: 50,
marginBottom: 50,
},
captureButtonText: {
color: '#fff',
fontSize: 18,
fontWeight: 'bold',
},
});
Why this works:
- Looks legit (corners like a real camera app)
- Loading states (because users need feedback)
- Accessible (clear instructions and big buttons)
- Dark theme (because cameras are moody)
Navigation: The App's GPS System πΊοΈ
Bottom tabs are the SUV of mobile navigation β practical, reliable, and everyone knows how to use them.
// src/navigation/AppNavigator.tsx
import React from 'react';
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
import { Ionicons } from '@expo/vector-icons';
import { HomeScreen } from '../screens/HomeScreen';
import { ScanScreen } from '../screens/ScanScreen';
import { ProfileScreen } from '../screens/ProfileScreen';
const Tab = createBottomTabNavigator();
export const AppNavigator: React.FC = () => {
return (
<Tab.Navigator
screenOptions={({ route }) => ({
tabBarIcon: ({ focused, color, size }) => {
let iconName: keyof typeof Ionicons.glyphMap;
if (route.name === 'Home') {
iconName = focused ? 'list' : 'list-outline';
} else if (route.name === 'Scan') {
iconName = focused ? 'camera' : 'camera-outline';
} else if (route.name === 'Profile') {
iconName = focused ? 'person' : 'person-outline';
}
return <Ionicons name={iconName} size={size} color={color} />;
},
tabBarActiveTintColor: '#2196F3',
tabBarInactiveTintColor: 'gray',
tabBarStyle: {
backgroundColor: '#fff',
borderTopColor: '#e0e0e0',
height: 60,
},
headerStyle: {
backgroundColor: '#2196F3',
},
headerTintColor: '#fff',
})}
>
<Tab.Screen name="Home" component={HomeScreen} />
<Tab.Screen name="Scan" component={ScanScreen} />
<Tab.Screen name="Profile" component={ProfileScreen} />
</Tab.Navigator>
);
};
The Mock Data Factory: Fake It Till You Make It π
We need some fake purchases to test our components. Let's create a data service that pretends to be an API:
// src/services/mockData.ts
import { Purchase } from '../types';
export const mockPurchases: Purchase[] = [
{
id: '1',
date: '2025-08-27T14:30:00Z',
storeName: 'Carrefour',
total: 15.67,
items: [
{ name: 'Milk 1L', price: 1.20, quantity: 1 },
{ name: 'Bread', price: 0.80, quantity: 2 },
{ name: 'Apples 1kg', price: 2.50, quantity: 1 },
{ name: 'Chicken breast', price: 8.97, quantity: 1 }
]
},
// More mock data...
];
// Simulate API delay because real APIs aren't instant
export const fetchPurchases = (): Promise<Purchase[]> => {
return new Promise((resolve) => {
setTimeout(() => {
resolve([...mockPurchases]);
}, 800); // Fake loading time
});
};
Component Design Philosophy: KISS (Keep It Simple, Smarty) π
Our component rules:
- Single responsibility β Each component does one thing well
- Predictable props β Clear interfaces, no surprises
- Accessible β Works for everyone, not just developers
- Testable β Easy to test, easy to trust
- Reusable β Write once, use everywhere
Testing Your Components (The Reality Check) β
Here's how to make sure your components actually work:
// Quick manual test checklist:
// β
PurchaseCard displays all info correctly
// β
Tapping shows purchase details
// β
CameraView looks like a camera
// β
"Take Photo" button responds
// β
Navigation switches between screens
// β
Loading states show properly
What We Built vs. What We'll Build ποΈ
Current status (MVP vibes):
- Mock camera (looks real, acts fake)
- Static purchase data (no backend yet)
- Basic interactions (taps and alerts)
- Pretty UI (because ugly apps make people sad)
Future upgrades (when we level up):
- Real camera integration
- Actual OCR processing
- Cloud data storage
- Push notifications
- Dark mode (because we're fancy)
Pro Tips for Component Success π―
- Start with mock data β Build the UI first, worry about APIs later
- Use TypeScript β Catch bugs before they catch you
- Think mobile-first β Thumbs are bigger than cursors
- Test on real devices β Simulators lie sometimes
- Keep it simple β Complex components are hard to debug
What's Next? π
We've got our building blocks ready! Next up:
- Putting these components together into real screens
- Adding state management (when things get stateful)
- Connecting to a real backend (when we're feeling brave)
Remember: great apps are just collections of well-designed components that play nice together. We're building the Avengers of components here! π¦ΈββοΈ
Component Status: β Built and ready to assemble!
Next post: We'll explore the magical world of Expo and how to test this masterpiece on every device you own (and some you don't). Stay tuned! π±β¨
Top comments (0)