-
Notifications
You must be signed in to change notification settings - Fork 0
checkout, address, admin and orders screen UI completed #7
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,196 @@ | ||
| import { Ionicons } from "@expo/vector-icons"; | ||
| import React, { useEffect, useState } from "react"; | ||
| import { ScrollView, Text, TouchableOpacity, View, Modal, TextInput, ActivityIndicator } from "react-native"; | ||
| import { SafeAreaView } from "react-native-safe-area-context"; | ||
| import Header from "@/components/Header"; | ||
| import { COLORS } from "@/constants"; | ||
| import type { Address } from "@/constants/types"; | ||
| import { dummyAddress } from "@/assets/assets"; | ||
|
|
||
| export default function Addresses() { | ||
| const [addresses, setAddresses] = useState<Address[]>([]); | ||
| const [loading, setLoading] = useState(true); | ||
| const [modalVisible, setModalVisible] = useState(false); | ||
|
|
||
| // Form state | ||
| const [type, setType] = useState("Home"); | ||
| const [street, setStreet] = useState(""); | ||
| const [city, setCity] = useState(""); | ||
| const [state, setState] = useState(""); | ||
| const [zipCode, setZipCode] = useState(""); | ||
| const [country, setCountry] = useState(""); | ||
| const [isDefault, setIsDefault] = useState(false); | ||
| const [submitting, setSubmitting] = useState(false); | ||
|
|
||
| // Edit state | ||
| const [isEditing, setIsEditing] = useState(false); | ||
| const [editingId, setEditingId] = useState<string | null>(null); | ||
|
|
||
| useEffect(() => { | ||
| fetchAddresses(); | ||
| }, []); | ||
|
|
||
| const fetchAddresses = async () => { | ||
| setAddresses(dummyAddress as any); | ||
| setLoading(false); | ||
| }; | ||
|
|
||
| const handleEditSearch = (item: Address) => { | ||
| setIsEditing(true); | ||
| setEditingId(item._id); | ||
| setType(item.type); | ||
| setStreet(item.street); | ||
| setCity(item.city); | ||
| setState(item.state); | ||
| setZipCode(item.zipCode); | ||
| setCountry(item.country); | ||
| setIsDefault(item.isDefault); | ||
| setModalVisible(true); | ||
| }; | ||
|
|
||
| const handleSaveAddress = async () => { | ||
| setModalVisible(false); | ||
| resetForm(); | ||
| fetchAddresses(); | ||
| }; | ||
|
Comment on lines
+51
to
+55
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win Save action does not persist add/edit changes. Lines 51-55 only close/reset/refetch, so user edits are discarded and new addresses are never added. 🤖 Prompt for AI Agents |
||
|
|
||
| const handleDeleteAddress = async (id: string) => { | ||
|
|
||
| }; | ||
|
Comment on lines
+57
to
+59
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win Delete button is wired to a no-op handler. Line 57 defines an empty delete handler, but Line 111 invokes it from the UI, so delete never works. Also applies to: 111-113 🤖 Prompt for AI Agents |
||
|
|
||
| const resetForm = () => { | ||
| setStreet(""); | ||
| setCity(""); | ||
| setState(""); | ||
| setZipCode(""); | ||
| setCountry(""); | ||
| setType("Home"); | ||
| setIsDefault(false); | ||
| setIsEditing(false); | ||
| setEditingId(null); | ||
| }; | ||
|
|
||
| const openAddModal = () => { | ||
| resetForm(); | ||
| setModalVisible(true); | ||
| }; | ||
|
|
||
| return ( | ||
| <SafeAreaView className="flex-1 bg-surface" edges={['top']}> | ||
| <Header title="Shipping Addresses" showBack /> | ||
|
|
||
| {loading ? ( | ||
| <View className="flex-1 justify-center items-center"> | ||
| <ActivityIndicator size="large" color={COLORS.primary} /> | ||
| </View> | ||
| ) : ( | ||
| <ScrollView className="flex-1 px-4 pt-4"> | ||
| {addresses.length === 0 ? ( | ||
| <Text className="text-center text-secondary mt-10">No addresses found</Text> | ||
| ) : ( | ||
| addresses.map((item) => ( | ||
| <View key={item._id} className="bg-white p-4 rounded-xl mb-4 shadow-sm"> | ||
| <View className="flex-row items-center justify-between mb-2"> | ||
| <View className="flex-row items-center"> | ||
| <Ionicons | ||
| name={item.type === "Home" ? "home-outline" : "briefcase-outline"} | ||
| size={20} | ||
| color={COLORS.primary} | ||
| /> | ||
| <Text className="text-base font-bold text-primary ml-2">{item.type}</Text> | ||
| {item.isDefault && ( | ||
| <View className="bg-primary/10 px-2 py-1 rounded ml-2"> | ||
| <Text className="text-primary text-xs font-bold">Default</Text> | ||
| </View> | ||
| )} | ||
| </View> | ||
| <View className="flex-row items-center gap-4"> | ||
| <TouchableOpacity onPress={() => handleEditSearch(item)}> | ||
| <Ionicons name="pencil-outline" size={20} color={COLORS.secondary} /> | ||
| </TouchableOpacity> | ||
| <TouchableOpacity onPress={() => handleDeleteAddress(item._id)}> | ||
| <Ionicons name="trash-outline" size={20} color={COLORS.error || '#ff4444'} /> | ||
| </TouchableOpacity> | ||
| </View> | ||
| </View> | ||
| <Text className="text-secondary leading-5 ml-7"> | ||
| {item.street}, {item.city}, {item.state} {item.zipCode}, {item.country} | ||
| </Text> | ||
| </View> | ||
| )) | ||
| )} | ||
|
|
||
| <TouchableOpacity className="flex-row items-center justify-center p-4 border border-dashed border-gray-300 rounded-xl mt-2 mb-8" onPress={openAddModal}> | ||
| <Ionicons name="add" size={24} color={COLORS.secondary} /> | ||
| <Text className="text-secondary font-medium ml-2">Add New Address</Text> | ||
| </TouchableOpacity> | ||
| </ScrollView> | ||
| )} | ||
|
|
||
| {/* Add Address Modal */} | ||
| <Modal animationType="slide" transparent={true} visible={modalVisible} onRequestClose={() => setModalVisible(false)}> | ||
| <View className="flex-1 justify-end bg-black/50"> | ||
| <View className="bg-white rounded-t-3xl p-6 h-[85%]"> | ||
| <View className="flex-row justify-between items-center mb-6"> | ||
| <Text className="text-xl font-bold text-primary">{isEditing ? "Edit Address" : "Add New Address"}</Text> | ||
| <TouchableOpacity onPress={() => setModalVisible(false)}> | ||
| <Ionicons name="close" size={24} color={COLORS.primary} /> | ||
| </TouchableOpacity> | ||
| </View> | ||
|
|
||
| <ScrollView showsVerticalScrollIndicator={false}> | ||
| <Text className="text-primary font-medium mb-2">Label</Text> | ||
| <View className="flex-row gap-3 mb-4"> | ||
| {["Home", "Work", "Other"].map((t) => ( | ||
| <TouchableOpacity key={t} onPress={() => setType(t)} className={`px-4 py-2 rounded-full border ${type === t ? 'bg-primary border-primary' : 'bg-white border-gray-300'}`}> | ||
| <Text className={type === t ? 'text-white' : 'text-primary'}>{t}</Text> | ||
| </TouchableOpacity> | ||
| ))} | ||
| </View> | ||
|
|
||
| <Text className="text-primary font-medium mb-2">Street Address</Text> | ||
| <TextInput className="bg-surface p-4 rounded-xl text-primary mb-4" placeholder="123 Main St" value={street} onChangeText={setStreet} /> | ||
|
|
||
| <View className="flex-row gap-4 mb-4"> | ||
| <View className="flex-1"> | ||
| <Text className="text-primary font-medium mb-2">City</Text> | ||
| <TextInput className="bg-surface p-4 rounded-xl text-primary" placeholder="New York" value={city} onChangeText={setCity} /> | ||
| </View> | ||
| <View className="flex-1"> | ||
| <Text className="text-primary font-medium mb-2">State</Text> | ||
| <TextInput className="bg-surface p-4 rounded-xl text-primary" placeholder="NY" value={state} onChangeText={setState} /> | ||
| </View> | ||
| </View> | ||
|
|
||
| <View className="flex-row gap-4 mb-4"> | ||
| <View className="flex-1"> | ||
| <Text className="text-primary font-medium mb-2">Zip Code</Text> | ||
| <TextInput className="bg-surface p-4 rounded-xl text-primary" placeholder="10001" value={zipCode} onChangeText={setZipCode} keyboardType="numeric" /> | ||
| </View> | ||
| <View className="flex-1"> | ||
| <Text className="text-primary font-medium mb-2">Country</Text> | ||
| <TextInput className="bg-surface p-4 rounded-xl text-primary" placeholder="USA" value={country} onChangeText={setCountry} /> | ||
| </View> | ||
| </View> | ||
|
|
||
| <TouchableOpacity className="flex-row items-center mb-8" onPress={() => setIsDefault(!isDefault)}> | ||
| <View className={`w-5 h-5 border rounded mr-2 items-center justify-center ${isDefault ? 'bg-primary border-primary' : 'border-gray-300'}`}> | ||
| {isDefault && <Ionicons name="checkmark" size={14} color="white" />} | ||
| </View> | ||
| <Text className="text-primary">Set as default address</Text> | ||
| </TouchableOpacity> | ||
|
|
||
| <TouchableOpacity className="w-full bg-primary py-4 rounded-full items-center mb-10" onPress={handleSaveAddress} disabled={submitting} > | ||
| {submitting ? ( | ||
| <ActivityIndicator color="white" /> | ||
| ) : ( | ||
| <Text className="text-white font-bold text-lg">Save Address</Text> | ||
| )} | ||
| </TouchableOpacity> | ||
| </ScrollView> | ||
| </View> | ||
| </View> | ||
| </Modal> | ||
| </SafeAreaView> | ||
| ); | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,82 @@ | ||
| import { Tabs, useRouter } from "expo-router"; | ||
| import { useEffect } from "react"; | ||
| import { View, ActivityIndicator, TouchableOpacity, Text } from "react-native"; | ||
| import { Ionicons } from "@expo/vector-icons"; | ||
| import { COLORS } from "@/constants"; | ||
| import { dummyUser } from "@/assets/assets"; | ||
|
|
||
| export default function AdminLayout() { | ||
| const { user } = { user: dummyUser } | ||
| const isLoaded = true; | ||
| const router = useRouter(); | ||
|
|
||
| useEffect(() => { | ||
| if (isLoaded && (!user || user.publicMetadata?.role !== "admin")) { | ||
| router.replace("/(tabs)"); | ||
| } | ||
| }, [isLoaded, user]); | ||
|
|
||
| if (!isLoaded) { | ||
| return ( | ||
| <View className="flex-1 justify-center items-center bg-surface"> | ||
| <ActivityIndicator size="large" color={COLORS.primary} /> | ||
| </View> | ||
| ); | ||
| } | ||
|
|
||
| if (!user || user.publicMetadata?.role !== "admin") return null; | ||
|
|
||
| return ( | ||
| <Tabs | ||
| screenOptions={{ | ||
| headerStyle: { | ||
| backgroundColor: "#fff", | ||
| }, | ||
| headerTintColor: COLORS.primary, | ||
| headerTitleStyle: { | ||
| fontWeight: "bold", | ||
| }, | ||
| headerShadowVisible: false, | ||
| tabBarActiveTintColor: COLORS.primary, | ||
| tabBarInactiveTintColor: "gray", | ||
| headerRight: () => ( | ||
| <TouchableOpacity | ||
| onPress={() => router.replace("/(tabs)")} | ||
| className="mr-4 flex-row items-center" | ||
| > | ||
| <Ionicons name="log-out-outline" size={24} color={COLORS.primary} /> | ||
| <Text className="ml-1 text-primary font-medium">Exit</Text> | ||
| </TouchableOpacity> | ||
| ), | ||
| }} | ||
| > | ||
| <Tabs.Screen | ||
| name="index" | ||
| options={{ | ||
| title: "Dashboard", | ||
| tabBarIcon: ({ color, size }) => ( | ||
| <Ionicons name="grid-outline" size={size} color={color} /> | ||
| ) | ||
| }} | ||
| /> | ||
| <Tabs.Screen | ||
| name="products" | ||
| options={{ | ||
| title: "Products", | ||
| tabBarIcon: ({ color, size }) => ( | ||
| <Ionicons name="cube-outline" size={size} color={color} /> | ||
| ) | ||
| }} | ||
| /> | ||
| <Tabs.Screen | ||
| name="orders" | ||
| options={{ | ||
| title: "Orders", | ||
| tabBarIcon: ({ color, size }) => ( | ||
| <Ionicons name="receipt-outline" size={size} color={color} /> | ||
| ) | ||
| }} | ||
| /> | ||
| </Tabs> | ||
| ); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,107 @@ | ||
| import { useRouter } from "expo-router"; | ||
| import React, { useEffect, useState } from "react"; | ||
| import { ScrollView, Text, View, ActivityIndicator, RefreshControl } from "react-native"; | ||
| import { COLORS, getStatusColor } from "@/constants"; | ||
| import { dummyAdminStats } from "@/assets/assets"; | ||
|
|
||
| export default function AdminDashboard() { | ||
| const router = useRouter(); | ||
| const [loading, setLoading] = useState(true); | ||
| const [refreshing, setRefreshing] = useState(false); | ||
| const [stats, setStats] = useState({ | ||
| totalUsers: 0, | ||
| totalProducts: 0, | ||
| totalOrders: 0, | ||
| totalRevenue: 0, | ||
| recentOrders: [] | ||
| }); | ||
|
|
||
| const fetchStats = async () => { | ||
| setStats(dummyAdminStats as any); | ||
| setLoading(false); | ||
| setRefreshing(false); | ||
| }; | ||
|
|
||
| useEffect(() => { | ||
| fetchStats(); | ||
| }, []); | ||
|
|
||
| const onRefresh = () => { | ||
| setRefreshing(true); | ||
| fetchStats(); | ||
| }; | ||
|
|
||
| if (loading && !refreshing) { | ||
| return ( | ||
| <View className="flex-1 justify-center items-center bg-surface"> | ||
| <ActivityIndicator size="large" color={COLORS.primary} /> | ||
| </View> | ||
| ); | ||
| } | ||
|
|
||
| return ( | ||
| <ScrollView | ||
| className="flex-1 bg-surface p-4" | ||
| refreshControl={<RefreshControl refreshing={refreshing} onRefresh={onRefresh} />} | ||
| > | ||
| <View className="mb-8"> | ||
| <Text className="text-primary font-bold text-2xl mb-4 tracking-tight">Overview</Text> | ||
| <View className="flex-row flex-wrap justify-between"> | ||
| <StatCard label="Total Revenue" value={`$${stats.totalRevenue.toFixed(2)}`} /> | ||
| <StatCard label="Total Orders" value={stats.totalOrders.toString()} /> | ||
| <StatCard label="Products" value={stats.totalProducts.toString()} /> | ||
| <StatCard label="Users" value={stats.totalUsers.toString()} /> | ||
| </View> | ||
| </View> | ||
|
|
||
| <View className="mb-6"> | ||
| <Text className="text-primary font-bold text-2xl mb-4 tracking-tight">Recent Orders</Text> | ||
| {stats.recentOrders.length === 0 ? ( | ||
| <View className="bg-white p-6 rounded-2xl border border-gray-100 items-center"> | ||
| <Text className="text-secondary">No recent orders</Text> | ||
| </View> | ||
| ) : ( | ||
| stats.recentOrders.map((order: any) => ( | ||
| <View key={order._id} className="bg-white p-5 rounded-2xl border border-gray-100 mb-3"> | ||
| <View className="flex-row justify-between items-center mb-3"> | ||
| <View> | ||
| <Text className="font-bold text-primary text-base">Total Products : {order.items.reduce((acc: number, item: any) => acc + item.quantity, 0)}</Text> | ||
| <Text className="text-secondary text-xs mt-1">{new Date(order.createdAt).toLocaleDateString()}</Text> | ||
| </View> | ||
| <View className={`px-3 py-1.5 rounded-full ${getStatusColor(order.orderStatus)}`}> | ||
| <Text className="text-[10px] font-bold uppercase">{order.orderStatus}</Text> | ||
| </View> | ||
| </View> | ||
| <View className="pb-2"> | ||
| {order.items.map((item: any) => ( | ||
| <Text key={item._id} className="text-secondary text-xs mt-1">{item.name} x {item.quantity}</Text> | ||
| ))} | ||
| </View> | ||
|
|
||
| <View className="h-[1px] bg-gray-100 mb-3" /> | ||
|
|
||
| <View className="flex-row justify-between items-center"> | ||
| <View className="flex-row items-center"> | ||
| <View className="w-8 h-8 rounded-full bg-gray-100 items-center justify-center mr-2"> | ||
| <Text className="text-primary font-bold text-xs"> | ||
| {(order.user?.name || '?').charAt(0).toUpperCase()} | ||
| </Text> | ||
| </View> | ||
| <Text className="text-secondary text-sm">{order.user?.name || 'Unknown User'}</Text> | ||
| </View> | ||
| <Text className="text-primary font-bold text-lg">${order.totalAmount.toFixed(2)}</Text> | ||
| </View> | ||
| </View> | ||
| )) | ||
| )} | ||
| </View> | ||
| </ScrollView> | ||
| ); | ||
| } | ||
|
|
||
| const StatCard = ({ label, value }: { label: string, value: string }) => ( | ||
| <View className="bg-white p-5 rounded-2xl border border-gray-100 w-[48%] mb-4 justify-center"> | ||
| <Text className="text-xl font-bold text-primary mb-1">{value}</Text> | ||
| <Text className="text-secondary text-xs font-medium uppercase tracking-wide">{label}</Text> | ||
| </View> | ||
| ); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
Address type contract is bypassed with
as anyand can drift UI state.Line 34 suppresses a real contract mismatch: shared type allows
"Work"but dummy data includes"Office", which breaks the label-selection flow. Normalize the source values or align the union instead of casting.🤖 Prompt for AI Agents