feat: implement core architecture, Redux store, product interfaces, and foundational UI components for Home and Login screens

This commit is contained in:
Tamojit Biswas 2026-07-14 18:52:58 +05:30
parent 0183e2694e
commit c7dbdd3b8a
19 changed files with 203 additions and 19 deletions

3
.env Normal file
View File

@ -0,0 +1,3 @@
STRIPE_SECRET_KEY=sk_test_51Ox04DSHlFQYe8R5HXy6nj0eQqtqAP4ynF7ODFg71ork78B38MPsDV3gQEo2EYaFL9OG75L8tG7bKxptsmVeONrS00ji44eUIl
RAZORPAY_KEY_ID=rzp_test_TCxbW8AxcXgMCj
RAZORPAY_KEY_SECRET=rLlM87yFehhsVNSqjCE2qRz5

View File

@ -6,3 +6,4 @@ export * from './cartApi';
export * from './paymentMethodsApi';
export * from './customerDetailsApi';
export * from './orderApi';
export * from './offerApi';

9
app/api/offerApi.ts Normal file
View File

@ -0,0 +1,9 @@
import { apiClient } from '@services';
import { PromotionResponse } from '@interfaces';
export const fetchOffers = async (): Promise<PromotionResponse> => {
const response = await apiClient.get<PromotionResponse>(
'/promotions/customer/offers',
);
return response;
};

View File

@ -1,4 +1,9 @@
import { Products, PaginationMeta, Product } from '@interfaces';
import {
Products,
PaginationMeta,
Product,
CategoriesResponse,
} from '@interfaces';
import { apiClient } from '@services';
export interface GetProductsResponse {
@ -15,3 +20,7 @@ export const getProductDetailsApi = async (
): Promise<Product> => {
return await apiClient.get<Product>(`/products/${productId}`);
};
export const getCategoriesApi = async (): Promise<CategoriesResponse> => {
return await apiClient.get<CategoriesResponse>(`/categories/selected`);
};

View File

@ -13,6 +13,7 @@ import {
import {
useNavigation,
CompositeNavigationProp,
useFocusEffect,
} from '@react-navigation/native';
import { BottomTabNavigationProp } from '@react-navigation/bottom-tabs';
import { StackNavigationProp } from '@react-navigation/stack';
@ -22,8 +23,13 @@ import { useAppTheme } from '@theme';
import { Product } from '../../../interfaces';
import { AppStackParamList } from '../../../navigation/appStack';
import { MainTabParamList } from '../../../navigation/mainTabNavigator';
import { fetchCustomerDetails, useAppDispatch, useAppSelector } from '@store';
import { getAllProductsThunk } from './thunk';
import {
fetchCustomerDetails,
getAllOffersThunk,
useAppDispatch,
useAppSelector,
} from '@store';
import { getAllCategoriesThunk, getAllProductsThunk } from './thunk';
type NavProp = CompositeNavigationProp<
BottomTabNavigationProp<MainTabParamList, 'HomeScreen'>,
@ -76,7 +82,9 @@ export const HomeScreen: React.FC = () => {
const navigation = useNavigation<NavProp>();
const dispatch = useAppDispatch();
const { products, isLoading } = useAppSelector(state => state.home);
const { products, isLoading, categories } = useAppSelector(
state => state.home,
);
const [selectedCategory, setSelectedCategory] = useState('all');
const [activeBanner, setActiveBanner] = useState(0);
@ -84,7 +92,13 @@ export const HomeScreen: React.FC = () => {
useEffect(() => {
dispatch(getAllProductsThunk());
dispatch(fetchCustomerDetails());
dispatch(getAllCategoriesThunk());
}, [dispatch]);
useFocusEffect(
useCallback(() => {
dispatch(getAllOffersThunk());
}, [dispatch]),
);
const filteredProducts =
selectedCategory === 'all'

View File

@ -1,17 +1,19 @@
import { createReducer } from '@reduxjs/toolkit';
import { Product, Products } from '@interfaces';
import { getAllProductsThunk } from './thunk';
import { Categories, Product, Products } from '@interfaces';
import { getAllCategoriesThunk, getAllProductsThunk } from './thunk';
export interface HomeState {
products: Products[];
isLoading: boolean;
error: string | null;
categories: Categories[];
}
const initialState: HomeState = {
products: [],
isLoading: false,
error: null,
categories: [],
};
const homeReducer = createReducer(initialState, builder => {
@ -30,6 +32,14 @@ const homeReducer = createReducer(initialState, builder => {
.addCase(getAllProductsThunk.rejected, (state, action) => {
state.isLoading = false;
state.error = action.payload || 'Failed to fetch products';
})
.addCase(getAllCategoriesThunk.fulfilled, (state, action) => {
state.categories = action.payload || [];
state.isLoading = false;
})
.addCase(getAllCategoriesThunk.rejected, (state, action) => {
state.isLoading = false;
state.error = (action.payload as string) || 'Failed to fetch categories';
});
});

View File

@ -1,5 +1,5 @@
import { createAsyncThunk } from '@reduxjs/toolkit';
import { getProductsApi, GetProductsResponse } from '@api';
import { getCategoriesApi, getProductsApi, GetProductsResponse } from '@api';
export const getAllProductsThunk = createAsyncThunk<
GetProductsResponse,
@ -15,3 +15,15 @@ export const getAllProductsThunk = createAsyncThunk<
);
}
});
export const getAllCategoriesThunk = createAsyncThunk(
'categories/getAll',
async (_, { rejectWithValue }) => {
try {
const response = await getCategoriesApi();
return response;
} catch (error) {
return rejectWithValue(error);
}
},
);

View File

@ -3,29 +3,49 @@ import { View, Text, FlatList, TouchableOpacity } from 'react-native';
import { getStyles } from './offersScreen.styles';
import { Header } from '@components';
import { useAppTheme } from '@theme';
import { useAppSelector } from '@store';
const OFFERS = [
{ id: '1', code: 'WELCOME50', description: '50% off on your first order', expiry: '30 Jul 2026' },
{ id: '2', code: 'FLAT20', description: 'Flat ₹20 off on orders above ₹199', expiry: '15 Aug 2026' },
{ id: '3', code: 'FREEDEL', description: 'Free delivery on orders above ₹299', expiry: '31 Jul 2026' },
];
// const OFFERS = [
// {
// id: '1',
// code: 'WELCOME50',
// description: '50% off on your first order',
// expiry: '30 Jul 2026',
// },
// {
// id: '2',
// code: 'FLAT20',
// description: 'Flat ₹20 off on orders above ₹199',
// expiry: '15 Aug 2026',
// },
// {
// id: '3',
// code: 'FREEDEL',
// description: 'Free delivery on orders above ₹299',
// expiry: '31 Jul 2026',
// },
// ];
export const OffersScreen: React.FC = () => {
const { colors } = useAppTheme();
const styles = getStyles(colors);
const { offers } = useAppSelector(state => state.offer);
// console.log(offers);
return (
<View style={styles.container}>
<Header title="Offers" />
<FlatList
data={OFFERS}
keyExtractor={(item) => item.id}
data={offers}
keyExtractor={item => item.id}
renderItem={({ item }) => (
<View style={styles.offerCard}>
<View style={styles.offerInfo}>
<Text style={styles.offerCode}>{item.code}</Text>
<Text style={styles.offerDescription}>{item.description}</Text>
<Text style={styles.offerExpiry}>Expires: {item.expiry}</Text>
<Text style={styles.offerCode}>{item?.title}</Text>
<Text style={styles.offerDescription}>{item?.description}</Text>
{/* <Text style={styles.offerExpiry}>
Expires: {new Date(item?.).toLocaleDateString()}
</Text> */}
</View>
<TouchableOpacity
style={styles.copyButton}

View File

@ -96,3 +96,4 @@ export * from './cart';
export * from './paymentMethods';
export * from './customerProfile';
export * from './order';
export * from './offers';

33
app/interfaces/offers.ts Normal file
View File

@ -0,0 +1,33 @@
export interface PromotionResponse {
promotions: Promotion[];
}
export interface Promotion {
id: string;
title: string;
description: string;
badgeText: string;
promotionType: PromotionType;
discountValue: number;
minimumOrderAmount: number;
maximumDiscount: number | null;
merchantId: string | null;
applicableProducts: ApplicableProduct[];
applicableCategories: ApplicableCategory[];
}
export type PromotionType =
| 'PERCENTAGE_DISCOUNT'
| 'FLAT_DISCOUNT'
| 'BUY_ONE_GET_ONE'
| 'FREE_DELIVERY';
export interface ApplicableProduct {
id: string;
name?: string;
}
export interface ApplicableCategory {
id: string;
name?: string;
}

View File

@ -141,3 +141,16 @@ export enum MediaType {
IMAGE = 'IMAGE',
VIDEO = 'VIDEO',
}
//catagories
export type CategoriesResponse = Categories[];
export interface Categories {
id: string;
parentId: string | null;
name: string;
slug: string;
imageUrl: string | null;
isActive: boolean;
// children: Categories[];
}

View File

@ -15,7 +15,7 @@ const STORAGE_KEYS = {
} as const;
// ─── Config ──────────────────────────────────────────────────────────────────
const BASE_URL = 'https://db1d-202-8-116-13.ngrok-free.app'; // TODO: replace with your actual base URL
const BASE_URL = 'https://157a-202-8-116-13.ngrok-free.app'; // TODO: replace with your actual base URL
// ─── Token Helpers ───────────────────────────────────────────────────────────
export const tokenManager = {

View File

@ -0,0 +1,2 @@
export * from './reducer';
export * from './thunk';

View File

@ -0,0 +1,29 @@
import { Promotion } from '@interfaces';
import { getAllOffersThunk } from './thunk';
import { createReducer } from '@reduxjs/toolkit';
export interface OfferState {
offers: Promotion[];
isLoading: boolean;
error: string | null;
}
const initialState: OfferState = {
offers: [],
isLoading: false,
error: null,
};
export const offerReducer = createReducer(initialState, builder => {
builder
.addCase(getAllOffersThunk.fulfilled, (state, action) => {
state.offers = action.payload?.promotions;
state.isLoading = false;
})
.addCase(getAllOffersThunk.pending, state => {
state.isLoading = true;
})
.addCase(getAllOffersThunk.rejected, (state, action) => {
state.isLoading = false;
state.error = action.payload as string;
});
});

View File

@ -0,0 +1,14 @@
import { fetchOffers } from '@api';
import { createAsyncThunk } from '@reduxjs/toolkit';
export const getAllOffersThunk = createAsyncThunk(
'offers/getAll',
async (_, { rejectWithValue }) => {
try {
const response = await fetchOffers();
return response;
} catch (error) {
return rejectWithValue(error);
}
},
);

View File

@ -10,6 +10,7 @@ import homeReducer from '@features/screens/homeScreen/reducer';
import providerDetailsReducer from '@features/screens/providerDetailsScreen/reducer';
import paymentMethodsReducer from '@features/screens/checkoutPaymentScreen/reducer';
import customerProfileReducer from './commonreducers/customerProfile/reducer';
import { offerReducer } from './commonreducers/offer';
const rootReducer = combineReducers({
auth: authReducer,
@ -22,6 +23,7 @@ const rootReducer = combineReducers({
providerDetails: providerDetailsReducer,
paymentMethods: paymentMethodsReducer,
customerProfile: customerProfileReducer,
offer: offerReducer,
});
export type RootState = ReturnType<typeof rootReducer>;

View File

@ -1,5 +1,5 @@
export const getFullUrl = (url?: string) => {
const BASE_URL = 'https://db1d-202-8-116-13.ngrok-free.app';
const BASE_URL = 'https://157a-202-8-116-13.ngrok-free.app';
if (!url) return '';
return url.startsWith('/') ? `${BASE_URL}${url}` : url;
};

View File

@ -17,12 +17,14 @@
"@react-navigation/native": "^7.3.4",
"@react-navigation/stack": "^7.10.6",
"@reduxjs/toolkit": "^2.12.0",
"@stripe/stripe-react-native": "^0.68.0",
"axios": "^1.18.1",
"react": "19.2.3",
"react-native": "0.86.0",
"react-native-geolocation-service": "^5.3.1",
"react-native-gesture-handler": "^2.32.0",
"react-native-maps": "^1.29.0",
"react-native-razorpay": "^3.0.0",
"react-native-reanimated": "^4.5.0",
"react-native-safe-area-context": "^5.8.0",
"react-native-screens": "^4.25.2",

View File

@ -1847,6 +1847,11 @@
resolved "https://registry.yarnpkg.com/@standard-schema/utils/-/utils-0.3.0.tgz#3d5e608f16c2390c10528e98e59aef6bf73cae7b"
integrity sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==
"@stripe/stripe-react-native@^0.68.0":
version "0.68.0"
resolved "https://registry.yarnpkg.com/@stripe/stripe-react-native/-/stripe-react-native-0.68.0.tgz#1c95930943e7538e9930dc19f75b0a5e435b94cc"
integrity sha512-5Sv0bUX9ABxttRL2qWGfxzaq5mVLNFJ3XTOmyWw0IpgrL1vXqVHzx8d6qWX8iHbosu1985x8BxbquIns+P6g3w==
"@types/babel__core@^7.1.14":
version "7.20.5"
resolved "https://registry.yarnpkg.com/@types/babel__core/-/babel__core-7.20.5.tgz#3df15f27ba85319caa07ba08d0721889bb39c017"
@ -5829,6 +5834,11 @@ react-native-maps@^1.29.0:
dependencies:
"@types/geojson" "^7946.0.13"
react-native-razorpay@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/react-native-razorpay/-/react-native-razorpay-3.0.0.tgz#05fca397b132be97aac3f8d6c54576b3fcf269fc"
integrity sha512-p7bcEJd8P3/FuNuV/kDvJAhF2QCHXaOwkMA7hkZV3XnmU2IJRap3dF8BYuSBAb+OEvzAm5RQbRHlZAnkr/+h+g==
react-native-reanimated@^4.5.0:
version "4.5.1"
resolved "https://registry.yarnpkg.com/react-native-reanimated/-/react-native-reanimated-4.5.1.tgz#be81ed3a96bf70baeeccdc24d3350883099b7dd3"