diff --git a/client/app/addresses/index.tsx b/client/app/addresses/index.tsx new file mode 100644 index 0000000..9d3d9ff --- /dev/null +++ b/client/app/addresses/index.tsx @@ -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([]); + 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(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(); + }; + + const handleDeleteAddress = async (id: string) => { + + }; + + const resetForm = () => { + setStreet(""); + setCity(""); + setState(""); + setZipCode(""); + setCountry(""); + setType("Home"); + setIsDefault(false); + setIsEditing(false); + setEditingId(null); + }; + + const openAddModal = () => { + resetForm(); + setModalVisible(true); + }; + + return ( + +
+ + {loading ? ( + + + + ) : ( + + {addresses.length === 0 ? ( + No addresses found + ) : ( + addresses.map((item) => ( + + + + + {item.type} + {item.isDefault && ( + + Default + + )} + + + handleEditSearch(item)}> + + + handleDeleteAddress(item._id)}> + + + + + + {item.street}, {item.city}, {item.state} {item.zipCode}, {item.country} + + + )) + )} + + + + Add New Address + + + )} + + {/* Add Address Modal */} + setModalVisible(false)}> + + + + {isEditing ? "Edit Address" : "Add New Address"} + setModalVisible(false)}> + + + + + + Label + + {["Home", "Work", "Other"].map((t) => ( + setType(t)} className={`px-4 py-2 rounded-full border ${type === t ? 'bg-primary border-primary' : 'bg-white border-gray-300'}`}> + {t} + + ))} + + + Street Address + + + + + City + + + + State + + + + + + + Zip Code + + + + Country + + + + + setIsDefault(!isDefault)}> + + {isDefault && } + + Set as default address + + + + {submitting ? ( + + ) : ( + Save Address + )} + + + + + + + ); +} diff --git a/client/app/admin/_layout.tsx b/client/app/admin/_layout.tsx new file mode 100644 index 0000000..c23ca10 --- /dev/null +++ b/client/app/admin/_layout.tsx @@ -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 ( + + + + ); + } + + if (!user || user.publicMetadata?.role !== "admin") return null; + + return ( + ( + router.replace("/(tabs)")} + className="mr-4 flex-row items-center" + > + + Exit + + ), + }} + > + ( + + ) + }} + /> + ( + + ) + }} + /> + ( + + ) + }} + /> + + ); +} diff --git a/client/app/admin/index.tsx b/client/app/admin/index.tsx new file mode 100644 index 0000000..3503fbd --- /dev/null +++ b/client/app/admin/index.tsx @@ -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 ( + + + + ); + } + + return ( + } + > + + Overview + + + + + + + + + + Recent Orders + {stats.recentOrders.length === 0 ? ( + + No recent orders + + ) : ( + stats.recentOrders.map((order: any) => ( + + + + Total Products : {order.items.reduce((acc: number, item: any) => acc + item.quantity, 0)} + {new Date(order.createdAt).toLocaleDateString()} + + + {order.orderStatus} + + + + {order.items.map((item: any) => ( + {item.name} x {item.quantity} + ))} + + + + + + + + + {(order.user?.name || '?').charAt(0).toUpperCase()} + + + {order.user?.name || 'Unknown User'} + + ${order.totalAmount.toFixed(2)} + + + )) + )} + + + ); +} + +const StatCard = ({ label, value }: { label: string, value: string }) => ( + + {value} + {label} + +); diff --git a/client/app/admin/orders.tsx b/client/app/admin/orders.tsx new file mode 100644 index 0000000..76813b9 --- /dev/null +++ b/client/app/admin/orders.tsx @@ -0,0 +1,173 @@ +import React, { useEffect, useState } from "react"; +import { ScrollView, Text, TouchableOpacity, View, ActivityIndicator, RefreshControl, Alert, Modal, TouchableWithoutFeedback, FlatList } from "react-native"; +import { COLORS, getStatusColor } from "@/constants"; +import { Ionicons } from "@expo/vector-icons"; +import { dummyOrders, dummyUser } from "@/assets/assets"; + +export default function AdminOrders() { + const [loading, setLoading] = useState(true); + const [refreshing, setRefreshing] = useState(false); + const [orders, setOrders] = useState([]); + + // Status Modal State + const [statusModalVisible, setStatusModalVisible] = useState(false); + const [selectedOrder, setSelectedOrder] = useState(null); + const [updating, setUpdating] = useState(false); + + const STATUSES = ["placed", "processing", "shipped", "delivered", "cancelled"]; + + const fetchOrders = async () => { + setOrders(dummyOrders.map((order: any) => ({ + ...order, + user: dummyUser + })) as any); + setLoading(false); + setRefreshing(false); + }; + + useEffect(() => { + fetchOrders(); + }, []); + + const onRefresh = () => { + setRefreshing(true); + fetchOrders(); + }; + + const openStatusModal = (order: any) => { + setSelectedOrder(order); + setStatusModalVisible(true); + }; + + const updateStatus = async (newStatus: string) => { + if (!selectedOrder) return; + setOrders(orders.map((order: any) => order._id === selectedOrder._id ? { ...order, orderStatus: newStatus } : order) as any); + setStatusModalVisible(false); + setUpdating(false); + }; + + if (loading && !refreshing) { + return ( + + + + ); + } + + return ( + + } + > + {orders.length === 0 ? ( + + No orders found + + ) : ( + orders.map((order: any) => ( + + + Order ID : #{order._id} + {new Date(order.createdAt).toLocaleDateString()} + + + + CUSTOMER + {order.user?.name || 'Unknown User'} + {order.user?.email || 'No email'} + {!order.user && ID: {order.user?._id || 'N/A'}} + + + + SHIPPING ADDRESS + + {order.shippingAddress?.street}, {order.shippingAddress?.city} + + + {order.shippingAddress?.state}, {order.shippingAddress?.zipCode}, {order.shippingAddress?.country} + + + + + ITEMS + {order.items.map((item: any) => ( + + + {item.quantity}x {item.product?.name || item.name} + {(item.size) && ( + + {" "}({item.size || '-'}) + + )} + + + ${item.price.toFixed(2)} + + + ))} + + + + ${order.totalAmount.toFixed(2)} + + openStatusModal(order)} + className={`flex-row items-center px-4 py-2 rounded-full ${getStatusColor(order.orderStatus)}`} + > + {order.orderStatus} + + + + + )) + )} + + + {/* STATUS MODAL */} + + setStatusModalVisible(false)}> + + + + + Update Order Status + + setStatusModalVisible(false)}> + + + + + {updating ? ( + + + Updating status... + + ) : ( + item} + renderItem={({ item }) => ( + updateStatus(item)} + > + + {item} + + {selectedOrder?.orderStatus === item && ( + + )} + + )} + /> + )} + + + + + + ); +} diff --git a/client/app/admin/products/_layout.tsx b/client/app/admin/products/_layout.tsx new file mode 100644 index 0000000..7c8e126 --- /dev/null +++ b/client/app/admin/products/_layout.tsx @@ -0,0 +1,19 @@ +import { Stack } from "expo-router"; +import { COLORS } from "@/constants"; + +export default function ProductsLayout() { + return ( + + + + + + ); +} diff --git a/client/app/admin/products/add.tsx b/client/app/admin/products/add.tsx new file mode 100644 index 0000000..a3975ac --- /dev/null +++ b/client/app/admin/products/add.tsx @@ -0,0 +1,225 @@ +import React, { useState } from "react"; +import { ScrollView, Text, TextInput, TouchableOpacity, View, Switch, Image, ActivityIndicator, Modal, FlatList, TouchableWithoutFeedback, Platform, } from "react-native"; +import Toast from 'react-native-toast-message'; +import { COLORS } from "@/constants"; +import { Ionicons } from "@expo/vector-icons"; +import * as ImagePicker from "expo-image-picker"; +import { CATEGORIES } from "@/constants"; + +export default function AddProduct() { + + const [submitting, setSubmitting] = useState(false); + const [modalVisible, setModalVisible] = useState(false); + + // Form state + const [name, setName] = useState(""); + const [description, setDescription] = useState(""); + const [price, setPrice] = useState(""); + const [stock, setStock] = useState(""); + const [category, setCategory] = useState("Men"); + const [sizes, setSizes] = useState(""); + const [images, setImages] = useState([]); + const [isFeatured, setIsFeatured] = useState(false); + + // PICK MULTIPLE IMAGES (MAX 5) + const pickImages = async () => { + const result = await ImagePicker.launchImageLibraryAsync({ + mediaTypes: ImagePicker.MediaTypeOptions.Images, + allowsMultipleSelection: true, + selectionLimit: 5, + quality: 0.8, + }); + + if (!result.canceled) { + const uris = result.assets.map((asset) => asset.uri); + setImages(uris.slice(0, 5)); + } + }; + + // Add Product + const handleSubmit = async () => { + if (!name || !price || !category || sizes.length < 1) { + Toast.show({ + type: 'error', + text1: 'Missing Fields', + text2: 'Please fill in all required fields' + }); + return; + } + }; + + return ( + + + {/* NAME */} + + Product Name * + + + + {/* PRICE */} + + Price ($) * + + + + {/* CATEGORY */} + + Category + + setModalVisible(true)} + className="bg-surface p-3 rounded-lg mb-4 flex-row justify-between items-center" + > + {category} + + + + {/* CATEGORY MODAL */} + + setModalVisible(false)}> + + + + Select Category + + + String(item.id)} + renderItem={({ item }) => ( + { + setCategory(item.name); + setModalVisible(false); + }} + > + + + {item.name} + + {category === item.name && ( + + )} + + + )} + /> + + + + + + {/* STOCK */} + + Stock Level + + + + {/* SIZES */} + + Sizes (comma separated) + + + + {/* IMAGE PICKER */} + + Product Images (max 5) + + + + {images.length > 0 ? ( + + {images.map((uri, i) => ( + + ))} + + ) : ( + + + + Tap to upload images + + + )} + + + {/* DESCRIPTION */} + + Description + + + + {/* FEATURED */} + + Featured Product + + + + {/* SUBMIT */} + + {submitting ? ( + + ) : ( + + Create Product + + )} + + + + ); +} diff --git a/client/app/admin/products/edit/[id].tsx b/client/app/admin/products/edit/[id].tsx new file mode 100644 index 0000000..d95b38d --- /dev/null +++ b/client/app/admin/products/edit/[id].tsx @@ -0,0 +1,291 @@ +import { useLocalSearchParams, useRouter } from "expo-router"; +import React, { useEffect, useState } from "react"; +import { ScrollView, Text, TextInput, TouchableOpacity, View, Switch, Image, ActivityIndicator, Platform, Modal, FlatList, TouchableWithoutFeedback } from "react-native"; +import Toast from 'react-native-toast-message'; +import { COLORS, CATEGORIES } from "@/constants"; +import { Ionicons } from "@expo/vector-icons"; +import * as ImagePicker from "expo-image-picker"; +import { dummyProducts } from "@/assets/assets"; + +export default function EditProduct() { + const { id } = useLocalSearchParams(); + const router = useRouter(); + + const [loading, setLoading] = useState(true); + const [submitting, setSubmitting] = useState(false); + const [modalVisible, setModalVisible] = useState(false); + + // Form State + const [name, setName] = useState(""); + const [description, setDescription] = useState(""); + const [price, setPrice] = useState(""); + const [stock, setStock] = useState(""); + const [category, setCategory] = useState(""); + const [sizes, setSizes] = useState(""); + const [isFeatured, setIsFeatured] = useState(false); + + // Image State + const [existingImages, setExistingImages] = useState([]); + const [newImages, setNewImages] = useState([]); + + useEffect(() => { + const fetchProduct = async () => { + try { + const product: any = dummyProducts.find((p) => p._id === id); + setName(product.name); + setDescription(product.description || ""); + setPrice(product.price.toString()); + setStock(product.stock.toString()); + setCategory(typeof product.category === 'object' ? product.category.name : product.category); + setIsFeatured(product.isFeatured); + + if (product.sizes) setSizes(Array.isArray(product.sizes) ? product.sizes.join(", ") : product.sizes); + + if (product.images && Array.isArray(product.images)) { + setExistingImages(product.images); + } else if (product.images) { + setExistingImages([product.images]); + } + } catch (error: any) { + console.error("Failed to fetch product:", error); + Toast.show({ + type: 'error', + text1: 'Failed to Fetch Product', + text2: error.response?.data?.message || "Something went wrong" + }); + router.back(); + } finally { + setLoading(false); + } + }; + + if (id) fetchProduct(); + }, [id]); + + const pickImages = async () => { + const result = await ImagePicker.launchImageLibraryAsync({ + mediaTypes: ImagePicker.MediaTypeOptions.Images, + allowsMultipleSelection: true, + selectionLimit: 5 - (existingImages.length + newImages.length), + quality: 0.8, + }); + + if (!result.canceled) { + const uris = result.assets.map((asset) => asset.uri); + setNewImages([...newImages, ...uris]); + } + }; + + const removeExistingImage = (index: number) => { + const updated = [...existingImages]; + updated.splice(index, 1); + setExistingImages(updated); + }; + + const removeNewImage = (index: number) => { + const updated = [...newImages]; + updated.splice(index, 1); + setNewImages(updated); + }; + + const handleSubmit = async () => { + if (!name || !price || sizes.length < 1) { + Toast.show({ + type: 'error', + text1: 'Missing Fields', + text2: 'Please fill in all required fields' + }); + return; + } + + try { + setSubmitting(true); + const formData = new FormData(); + + formData.append("name", name); + formData.append("description", description); + formData.append("price", price); + formData.append("stock", stock); + formData.append("category", category); + formData.append("isFeatured", String(isFeatured)); + formData.append("sizes", sizes); + + // Append existing images + existingImages.forEach((img) => { + formData.append("existingImages", img); + }); + + // Append new images + for (const [i, uri] of newImages.entries()) { + const filename = `new-image-${i}.jpg`; + if (Platform.OS === "web") { + const blob = await (await fetch(uri)).blob(); + formData.append("images", new File([blob], filename, { type: "image/jpeg" })); + } else { + formData.append("images", { uri, name: filename, type: "image/jpeg" } as any); + } + } + router.back(); + } catch (error: any) { + console.error("Failed to update product:", error); + Toast.show({ + type: 'error', + text1: 'Failed to Update Product', + text2: error.response?.data?.message || "Something went wrong" + }); + } finally { + setSubmitting(false); + } + }; + + if (loading) { + return ( + + + + ); + } + + return ( + + + Product Name * + + + Price ($) * + + + Stock Level + + + Sizes (comma separated) + + + + Category + + setModalVisible(true)} + className="bg-surface p-3 rounded-lg mb-4 flex-row justify-between items-center" + > + {category || "Select Category"} + + + + + setModalVisible(false)}> + + + Select Category + String(item.id)} + renderItem={({ item }) => ( + { + setCategory(item.name); + setModalVisible(false); + }} + > + + {item.name} + {category === item.name && } + + + )} + /> + + + + + + Images + + + {existingImages.map((uri, index) => ( + + + removeExistingImage(index)} + className="absolute top-1 right-1 bg-black/50 rounded-full p-1" + > + + + + ))} + {newImages.map((uri, index) => ( + + + removeNewImage(index)} + className="absolute top-1 right-1 bg-primary rounded-full p-1" + > + + + + ))} + {(existingImages.length + newImages.length) < 5 && ( + + + Add + + )} + + + + Description + + + + Featured Product + + + + + {submitting ? ( + + ) : ( + Update Product + )} + + + + ); +} diff --git a/client/app/admin/products/index.tsx b/client/app/admin/products/index.tsx new file mode 100644 index 0000000..fb69b8c --- /dev/null +++ b/client/app/admin/products/index.tsx @@ -0,0 +1,114 @@ +import { useRouter } from "expo-router"; +import React, { useEffect, useState } from "react"; +import { ScrollView, Text, TouchableOpacity, View, ActivityIndicator, RefreshControl, Image, Alert } from "react-native"; +import { Ionicons } from "@expo/vector-icons"; +import { COLORS } from "@/constants"; +import { dummyProducts } from "@/assets/assets"; + +export default function AdminProducts() { + const router = useRouter(); + const [loading, setLoading] = useState(true); + const [refreshing, setRefreshing] = useState(false); + const [products, setProducts] = useState([]); + + const fetchProducts = async () => { + setProducts(dummyProducts as any); + setLoading(false); + setRefreshing(false); + }; + + useEffect(() => { + fetchProducts(); + }, []); + + const onRefresh = () => { + setRefreshing(true); + fetchProducts(); + }; + + const performDelete = async (id: string) => { + setProducts(products.filter((product: any) => product._id !== id) as any); + }; + + const deleteProduct = async (id: string) => { + Alert.alert( + "Delete Product", + "Are you sure you want to delete this product?", + [ + { text: "Cancel", style: "cancel" as const }, + { + text: "Delete", + style: "destructive" as const, + onPress: () => performDelete(id) + } + ] + ); + }; + + if (loading && !refreshing) { + return ( + + + + ); + } + + return ( + + + Total Products ({products.length}) + router.push("/admin/products/add")} + className="bg-gray-800 px-4 py-2 rounded-full flex-row items-center" + > + + Add Product + + + + } + > + {products.length === 0 ? ( + + No products found + + ) : ( + products.map((product: any) => ( + + 0 ? product.images[0] : 'https://via.placeholder.com/150' }} + className="w-16 h-16 rounded-lg bg-gray-100 mr-3" + resizeMode="cover" + /> + + + {product.name} + Category : {product.category || 'Others'} + Stock : {product.stock} + Sizes : {product.sizes.join(", ")} + ${product.price.toFixed(2)} + + + + router.push(`/admin/products/edit/${product._id}`)} + className="p-2 bg-slate-50 rounded-full mr-2" + > + + + deleteProduct(product._id)} + className="p-2 bg-gray-50 rounded-full" + > + + + + + )) + )} + + + ); +} diff --git a/client/app/checkout.tsx b/client/app/checkout.tsx new file mode 100644 index 0000000..b7cf2eb --- /dev/null +++ b/client/app/checkout.tsx @@ -0,0 +1,152 @@ +import { View, Text, ActivityIndicator, ScrollView, TouchableOpacity } from 'react-native' +import React, { useEffect, useState } from 'react' +import { useCart } from '@/context/CartContext' +import { useRouter } from 'expo-router' +import { Address } from '@/constants/types' +import { dummyAddress } from '@/assets/assets' +import Toast from 'react-native-toast-message' +import { SafeAreaView } from 'react-native-safe-area-context' +import { COLORS } from '@/constants' +import Header from '@/components/Header' +import { Ionicons } from '@expo/vector-icons' + +export default function Checkout() { + + const { cartTotal } = useCart() + const router = useRouter() + + const [loading, setLoading] = useState(false) + const [pageLoading, setPageLoading] = useState(true) + + const [selectedAddress, setSelectedAddress] = useState
(null) + const [payementMethod, setPaymentMethod] = useState<'cash' | 'stripe'>('cash') + + const shipping = 2.0 + const tax = 0 + const total = cartTotal + shipping + tax + + const fetchAddress = async () => { + const addressList = dummyAddress + if (addressList.length > 0) { + const def = addressList.find((a: any) => a.isDefault) || addressList[0] + setSelectedAddress(def as Address) + } + setPageLoading(false) + } + + const handlePlaceOrder = async () => { + if (!selectedAddress) { + Toast.show({ + type: 'error', + text1: 'Error', + text2: 'Please add a shipping address' + }) + return + } + if (payementMethod === 'stripe') { + return Toast.show({ + type: 'error', + text1: 'Info', + text2: 'Stripe not implemented yet' + }) + } + router.replace('/orders') + } + + useEffect(() => { + fetchAddress() + }, []) + + if (pageLoading) { + return ( + + + + ) + } + + return ( + +
+ + + {/* Address Section */} + Shipping Address + {selectedAddress ? ( + + + {selectedAddress.type} + router.push('/addresses')}> + Change + + + + {selectedAddress.street}, {selectedAddress.city} + {'\n'} + {selectedAddress.state}, {selectedAddress.zipCode} + {'\n'} + {selectedAddress.country} + + + ) : ( + router.push('/addresses')} className='bg-white p-6 rounded-xl mb-6 items-center justify-center border-dashed border-2 border-gray-100'> + Add Address + + )} + + {/* Payment Section */} + Payment Method + + {/* Cash on Delivery Option */} + setPaymentMethod('cash')} className={`bg-white p-4 rounded-xl mb-4 shadow-sm flex-row items-center border-2 ${payementMethod === 'cash' ? 'border-primary' : 'border-transparent'}`}> + + + Cash on Delivery + Pay when you recieve the order + + {payementMethod === 'cash' && } + + + {/* Stripe Option */} + setPaymentMethod('stripe')} className={`bg-white p-4 rounded-xl mb-4 shadow-sm flex-row items-center border-2 ${payementMethod === 'stripe' ? 'border-primary' : 'border-transparent'}`}> + + + Pay with Card + Credit or Debit Card + + {payementMethod === 'stripe' && } + + + + {/* Order Summary */} + + Order Summary + + {/* Subtotal */} + + Subtotal + {cartTotal.toFixed(2)} + + {/* Tax */} + + Tax + {tax.toFixed(2)} + + {/* Shipping */} + + Shipping + {shipping.toFixed(2)} + + {/* Total */} + + Total + {total.toFixed(2)} + + {/* Place Order Button */} + + {loading ? : Place Order} + + + + ) +} \ No newline at end of file diff --git a/client/app/orders/[id].tsx b/client/app/orders/[id].tsx new file mode 100644 index 0000000..bde16b9 --- /dev/null +++ b/client/app/orders/[id].tsx @@ -0,0 +1,148 @@ +import { Ionicons } from "@expo/vector-icons"; +import { useLocalSearchParams } from "expo-router"; +import React, { useEffect, useState } from "react"; +import { Image, ScrollView, Text, View, ActivityIndicator } from "react-native"; +import { SafeAreaView } from "react-native-safe-area-context"; +import Header from "@/components/Header"; +import { COLORS } from "@/constants"; +import type { Order, Product } from "@/constants/types"; +import { dummyOrders } from "@/assets/assets"; + +export default function OrderDetails() { + const { id } = useLocalSearchParams(); + const [order, setOrder] = useState(null); + const [loading, setLoading] = useState(true); + + const fetchOrderDetails = async () => { + setOrder(dummyOrders.find((order) => order._id === id) as any); + setLoading(false); + }; + + useEffect(() => { + fetchOrderDetails(); + }, [id]); + + if (loading) { + return ( + + + + ); + } + + if (!order) { + return ( + + Order not found + + ); + } + + const formatDate = (dateString: string) => { + const options: Intl.DateTimeFormatOptions = { year: 'numeric', month: 'short', day: 'numeric' }; + return new Date(dateString).toLocaleDateString(undefined, options); + }; + + const ORDER_STEPS = [ + { title: "Order Placed", date: formatDate(order.createdAt), completed: true }, + { title: "Processing", date: "", completed: ['processing', 'shipped', 'delivered'].includes(order.orderStatus) }, + { title: "Shipped", date: "", completed: ['shipped', 'delivered'].includes(order.orderStatus) }, + { title: "Delivered", date: "", completed: order.orderStatus === 'delivered' }, + ]; + + return ( + +
+ + + {/* Order Status */} + + Order Status + + {ORDER_STEPS.map((step, index) => ( + + + + {index !== ORDER_STEPS.length - 1 && ( + + )} + + + {step.title} + {step.date ? {step.date} : null} + + + ))} + + + {/* Items */} + + Products + {order.items.map((item: any, index: number) => { + + const productData = item.product as Product; + const image = productData?.images?.[0]; + + return ( + + {image && } + + {item.name} + Size: {item.size} + + ${item.price} + Qty: {item.quantity} + + + + ) + })} + + + {/* Shipping Details */} + + Shipping Details + + + + {order.shippingAddress?.street}, {order.shippingAddress?.city}, {order.shippingAddress?.zipCode}, {order.shippingAddress?.country} + + + + + {/* Payment Summary */} + + Payment Summary + + Payment Method + {order.paymentMethod} + + + Payment Status + + {order.paymentStatus} + + + + + Subtotal + ${order.subtotal.toFixed(2)} + + + Shipping + ${order.shippingCost.toFixed(2)} + + + Tax + ${order.tax.toFixed(2)} + + + + Total + ${order.totalAmount.toFixed(2)} + + + + + ); +} diff --git a/client/app/orders/index.tsx b/client/app/orders/index.tsx new file mode 100644 index 0000000..fc24020 --- /dev/null +++ b/client/app/orders/index.tsx @@ -0,0 +1,105 @@ +import { useRouter } from "expo-router"; +import React, { useEffect, useState } from "react"; +import { FlatList, Text, TouchableOpacity, View, ActivityIndicator, ScrollView, Image } from "react-native"; +import { Ionicons } from "@expo/vector-icons"; +import { SafeAreaView } from "react-native-safe-area-context"; +import Header from "@/components/Header"; +import { COLORS, getStatusColor } from "@/constants"; +import type { Order } from "@/constants/types"; +import { dummyOrders, formatDate } from "@/assets/assets"; + +export default function Orders() { + const router = useRouter(); + const [orders, setOrders] = useState([]); + const [loading, setLoading] = useState(true); + + const fetchOrders = async () => { + setOrders(dummyOrders as any[]); + setLoading(false); + }; + + useEffect(() => { + fetchOrders(); + }, []); + + return ( + +
+ + {loading ? ( + + + + ) : orders.length === 0 ? ( + + No orders found + + ) : ( + item._id} + contentContainerStyle={{ padding: 16 }} + renderItem={({ item, index }) => ( + router.push(`/orders/${item._id}`)} + > + + Order #{item.orderNumber} + {formatDate(item.createdAt)} + + + {/* Status Badges */} + + + + {item.orderStatus} + + + + + + {item.paymentStatus} + + + + + + Payment Method: {item.paymentMethod} + + + {/* Product Images */} + + {item.items.map((prod: any, idx) => { + const image = prod.product?.images?.[0]; + return ( + + {image ? ( + + ) : ( + + + + )} + + ); + })} + + + + Items: {item.items.length} + ${item.totalAmount.toFixed(2)} + + + )} + /> + )} + + ); +} diff --git a/client/package-lock.json b/client/package-lock.json index 24c3a3c..346e242 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -18,6 +18,7 @@ "expo-font": "~14.0.11", "expo-haptics": "~15.0.8", "expo-image": "~3.0.11", + "expo-image-picker": "~17.0.11", "expo-linking": "~8.0.12", "expo-router": "~6.0.23", "expo-splash-screen": "~31.0.13", @@ -6453,6 +6454,27 @@ } } }, + "node_modules/expo-image-loader": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/expo-image-loader/-/expo-image-loader-6.0.0.tgz", + "integrity": "sha512-nKs/xnOGw6ACb4g26xceBD57FKLFkSwEUTDXEDF3Gtcu3MqF3ZIYd3YM+sSb1/z9AKV1dYT7rMSGVNgsveXLIQ==", + "license": "MIT", + "peerDependencies": { + "expo": "*" + } + }, + "node_modules/expo-image-picker": { + "version": "17.0.11", + "resolved": "https://registry.npmjs.org/expo-image-picker/-/expo-image-picker-17.0.11.tgz", + "integrity": "sha512-/apkoyukDvsCHHb9fzP+F34A1uQqSzUtYH/2P/xJACNEwq+mwEXjXvVU8bzlJq6ih0Qo1+tpVivIa7B9kYSwOQ==", + "license": "MIT", + "dependencies": { + "expo-image-loader": "~6.0.0" + }, + "peerDependencies": { + "expo": "*" + } + }, "node_modules/expo-linking": { "version": "8.0.12", "resolved": "https://registry.npmjs.org/expo-linking/-/expo-linking-8.0.12.tgz", diff --git a/client/package.json b/client/package.json index 5c0a861..e37dc77 100644 --- a/client/package.json +++ b/client/package.json @@ -21,6 +21,7 @@ "expo-font": "~14.0.11", "expo-haptics": "~15.0.8", "expo-image": "~3.0.11", + "expo-image-picker": "~17.0.11", "expo-linking": "~8.0.12", "expo-router": "~6.0.23", "expo-splash-screen": "~31.0.13",