feat: implement core architecture, Redux store, product interfaces, and foundational UI components for Home and Login screens
This commit is contained in:
parent
0183e2694e
commit
c7dbdd3b8a
3
.env
Normal file
3
.env
Normal file
@ -0,0 +1,3 @@
|
||||
STRIPE_SECRET_KEY=sk_test_51Ox04DSHlFQYe8R5HXy6nj0eQqtqAP4ynF7ODFg71ork78B38MPsDV3gQEo2EYaFL9OG75L8tG7bKxptsmVeONrS00ji44eUIl
|
||||
RAZORPAY_KEY_ID=rzp_test_TCxbW8AxcXgMCj
|
||||
RAZORPAY_KEY_SECRET=rLlM87yFehhsVNSqjCE2qRz5
|
||||
@ -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
9
app/api/offerApi.ts
Normal 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;
|
||||
};
|
||||
@ -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`);
|
||||
};
|
||||
|
||||
@ -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'
|
||||
|
||||
@ -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';
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@ -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);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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
33
app/interfaces/offers.ts
Normal 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;
|
||||
}
|
||||
@ -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[];
|
||||
}
|
||||
|
||||
@ -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 = {
|
||||
|
||||
2
app/store/commonreducers/offer/index.ts
Normal file
2
app/store/commonreducers/offer/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export * from './reducer';
|
||||
export * from './thunk';
|
||||
29
app/store/commonreducers/offer/reducer.ts
Normal file
29
app/store/commonreducers/offer/reducer.ts
Normal 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;
|
||||
});
|
||||
});
|
||||
14
app/store/commonreducers/offer/thunk.ts
Normal file
14
app/store/commonreducers/offer/thunk.ts
Normal 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);
|
||||
}
|
||||
},
|
||||
);
|
||||
@ -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>;
|
||||
|
||||
@ -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;
|
||||
};
|
||||
|
||||
@ -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",
|
||||
|
||||
10
yarn.lock
10
yarn.lock
@ -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"
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user