Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 41 additions & 0 deletions DealSwipe/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# Learn more https://docs.github.com/en/get-started/getting-started-with-git/ignoring-files

# dependencies
node_modules/

# Expo
.expo/
dist/
web-build/
expo-env.d.ts

# Native
.kotlin/
*.orig.*
*.jks
*.p8
*.p12
*.key
*.mobileprovision

# Metro
.metro-health-check*

# debug
npm-debug.*
yarn-debug.*
yarn-error.*

# macOS
.DS_Store
*.pem

# local env files
.env*.local

# typescript
*.tsbuildinfo

# generated native folders
/ios
/android
34 changes: 34 additions & 0 deletions DealSwipe/app.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
{
"expo": {
"name": "DealSwipe",
"slug": "dealswipe",
"version": "1.0.0",
"orientation": "portrait",
"icon": "./assets/icon.png",
"userInterfaceStyle": "dark",
"newArchEnabled": true,
"splash": {
"image": "./assets/splash-icon.png",
"resizeMode": "contain",
"backgroundColor": "#0D0D1A"
},
"ios": {
"supportsTablet": true
},
"android": {
"adaptiveIcon": {
"foregroundImage": "./assets/adaptive-icon.png",
"backgroundColor": "#0D0D1A"
},
"package": "com.dealswipe.app"
},
"web": {
"favicon": "./assets/favicon.png"
},
"plugins": [
"expo-router",
"expo-haptics"
],
"scheme": "dealswipe"
}
}
64 changes: 64 additions & 0 deletions DealSwipe/app/_layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { Tabs } from 'expo-router';
import { Ionicons } from '@expo/vector-icons';
import { View, StyleSheet, Platform } from 'react-native';
import { GestureHandlerRootView } from 'react-native-gesture-handler';
import '../global.css';

export default function AppLayout() {
return (
<GestureHandlerRootView style={{ flex: 1 }}>
<Tabs
screenOptions={{
headerShown: false,
tabBarStyle: styles.tabBar,
tabBarActiveTintColor: '#00F5A0',
tabBarInactiveTintColor: '#A0A0B0',
tabBarShowLabel: true,
tabBarLabelStyle: {
fontSize: 12,
fontWeight: '600',
marginBottom: Platform.OS === 'ios' ? 0 : 5,
},
}}
>
<Tabs.Screen
name="index"
options={{
title: 'Discover',
tabBarIcon: ({ color, focused }) => (
<Ionicons name={focused ? "flame" : "flame-outline"} size={26} color={color} />
),
}}
/>
<Tabs.Screen
name="wishlist"
options={{
title: 'Wishlist',
tabBarIcon: ({ color, focused }) => (
<Ionicons name={focused ? "heart" : "heart-outline"} size={26} color={color} />
),
}}
/>
</Tabs>
</GestureHandlerRootView>
);
}

const styles = StyleSheet.create({
tabBar: {
backgroundColor: '#1A1A2E',
borderTopWidth: 1,
borderTopColor: '#2A2A4A',
height: Platform.OS === 'ios' ? 85 : 65,
paddingTop: 5,
position: 'absolute',
bottom: 0,
left: 0,
right: 0,
elevation: 10,
shadowColor: '#000',
shadowOffset: { width: 0, height: -4 },
shadowOpacity: 0.3,
shadowRadius: 10,
},
});
210 changes: 210 additions & 0 deletions DealSwipe/app/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,210 @@
import React, { useMemo, useCallback } from 'react';
import { View, Text, StyleSheet, ScrollView, TouchableOpacity, SafeAreaView, Platform, StatusBar } from 'react-native';
import { useDealsStore, Deal } from '../store/dealsStore';
import { DealCard } from '../components/DealCard';
import { SwipeButtons } from '../components/SwipeButtons';
import { EmptyState } from '../components/EmptyState';
import { Ionicons } from '@expo/vector-icons';
import { Link } from 'expo-router';

const CATEGORIES = ['All', 'Electronics', 'Kitchen', 'Fitness', 'Home & Garden', 'Beauty', 'Books', 'Gaming', 'Clothing'];

export default function SwipeScreen() {
const {
deals,
currentIndex,
selectedCategory,
setSelectedCategory,
nextDeal,
addToWishlist,
undoLastSwipe,
wishlist
} = useDealsStore();

const filteredDeals = useMemo(() => {
return selectedCategory === 'All'
? deals
: deals.filter(d => d.category === selectedCategory);
}, [deals, selectedCategory]);

const dealsRemaining = Math.max(0, filteredDeals.length - currentIndex);

const handleSwipeLeft = useCallback(() => {
nextDeal();
}, [nextDeal]);

const handleSwipeRight = useCallback(() => {
const currentDeal = filteredDeals[currentIndex];
if (currentDeal) {
addToWishlist(currentDeal);
}
nextDeal();
}, [filteredDeals, currentIndex, addToWishlist, nextDeal]);

if (dealsRemaining === 0) {
return <EmptyState />;
}

// We want to render up to 3 cards for the stack effect
const visibleDeals = filteredDeals.slice(currentIndex, currentIndex + 3).reverse();

return (
<SafeAreaView style={styles.container}>
<StatusBar barStyle="light-content" />

{/* Header */}
<View style={styles.header}>
<Text style={styles.logo}>DealSwipe 🔥</Text>
<Link href="/wishlist" asChild>
<TouchableOpacity style={styles.wishlistIcon}>
<Ionicons name="heart" size={28} color="#00F5A0" />
{wishlist.length > 0 && (
<View style={styles.badge}>
<Text style={styles.badgeText}>{wishlist.length}</Text>
</View>
)}
</TouchableOpacity>
</Link>
</View>

{/* Category Bar */}
<View style={styles.categoryContainer}>
<ScrollView
horizontal
showsHorizontalScrollIndicator={false}
contentContainerStyle={styles.categoryContent}
>
{CATEGORIES.map(cat => {
const count = cat === 'All' ? deals.length : deals.filter(d => d.category === cat).length;
if (count === 0 && cat !== 'All') return null;

return (
<TouchableOpacity
key={cat}
style={[styles.categoryPill, selectedCategory === cat && styles.categoryPillActive]}
onPress={() => setSelectedCategory(cat)}
>
<Text style={[styles.categoryText, selectedCategory === cat && styles.categoryTextActive]}>
{cat} ({count})
</Text>
</TouchableOpacity>
);
})}
</ScrollView>
</View>

{/* Card Stack */}
<View style={styles.stackContainer}>
{visibleDeals.map((deal, index) => {
// The top card is the last one in the reversed visibleDeals array
const isTop = index === visibleDeals.length - 1;
const stackIndex = (visibleDeals.length - 1) - index;

return (
<DealCard
key={deal.asin}
deal={deal}
isTop={isTop}
stackIndex={stackIndex}
onSwipeLeft={handleSwipeLeft}
onSwipeRight={handleSwipeRight}
/>
);
})}
</View>

{/* Footer / Actions */}
<View style={styles.footer}>
<SwipeButtons
onPressLeft={handleSwipeLeft}
onPressRight={handleSwipeRight}
onPressUndo={undoLastSwipe}
/>
<Text style={styles.counter}>{dealsRemaining} deals left</Text>
</View>
</SafeAreaView>
);
}

const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#0D0D1A',
paddingTop: Platform.OS === 'android' ? StatusBar.currentHeight : 0,
},
header: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
paddingHorizontal: 20,
paddingVertical: 15,
},
logo: {
fontSize: 24,
fontWeight: 'bold',
color: '#FFF',
},
wishlistIcon: {
position: 'relative',
padding: 5,
},
badge: {
position: 'absolute',
top: 0,
right: 0,
backgroundColor: '#FF4757',
borderRadius: 10,
minWidth: 18,
height: 18,
justifyContent: 'center',
alignItems: 'center',
paddingHorizontal: 4,
},
badgeText: {
color: '#FFF',
fontSize: 10,
fontWeight: 'bold',
},
categoryContainer: {
height: 50,
marginBottom: 10,
},
categoryContent: {
paddingHorizontal: 15,
alignItems: 'center',
},
categoryPill: {
paddingHorizontal: 16,
paddingVertical: 8,
borderRadius: 20,
backgroundColor: '#1A1A2E',
marginHorizontal: 5,
borderWidth: 1,
borderColor: '#2A2A4A',
},
categoryPillActive: {
backgroundColor: '#00F5A0',
borderColor: '#00F5A0',
},
categoryText: {
color: '#A0A0B0',
fontSize: 14,
fontWeight: '600',
},
categoryTextActive: {
color: '#0D0D1A',
},
stackContainer: {
flex: 1,
marginVertical: 10,
},
footer: {
paddingBottom: 20,
alignItems: 'center',
},
counter: {
color: '#A0A0B0',
fontSize: 14,
fontWeight: '500',
},
});
Loading