Skip to content
Merged
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
196 changes: 196 additions & 0 deletions client/app/addresses/index.tsx
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);
Comment on lines +33 to +35

Copy link
Copy Markdown

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 any and 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
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@client/app/addresses/index.tsx` around lines 33 - 35, The address mock data
in fetchAddresses is bypassing the shared type contract with an any cast, which
lets invalid values like "Office" slip into UI state. Remove the as any cast and
either normalize the dummyAddress source values to the existing Address type or
update the shared union to include the correct label, so the data passed into
setAddresses matches the contract used by the label-selection flow.

};

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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The 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
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@client/app/addresses/index.tsx` around lines 51 - 55, The handleSaveAddress
flow only closes the modal, resets the form, and refetches addresses, so it
never persists add/edit changes. Update handleSaveAddress in the addresses
screen to call the actual create/update logic using the current form values
before closing, and only reset/refetch after the save succeeds. Use the existing
handleSaveAddress, resetForm, and fetchAddresses flow to wire in the missing
persistence path.


const handleDeleteAddress = async (id: string) => {

};
Comment on lines +57 to +59

Copy link
Copy Markdown

Choose a reason for hiding this comment

The 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
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@client/app/addresses/index.tsx` around lines 57 - 59, The delete flow is
currently a no-op because handleDeleteAddress is empty even though the UI calls
it from the delete button. Update handleDeleteAddress in the addresses index
component to perform the actual delete action (and any needed state refresh or
API call), and make sure the button’s onClick remains wired to that real handler
so deleting an address works end to end.


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>
);
}
82 changes: 82 additions & 0 deletions client/app/admin/_layout.tsx
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>
);
}
107 changes: 107 additions & 0 deletions client/app/admin/index.tsx
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>
);
Loading