├── nativewind-env.d.ts
├── .example.env
├── app
├── assets
│ ├── icon.png
│ ├── splash.png
│ ├── favicon.png
│ ├── adaptive-icon.png
│ └── images
│ │ └── banner.png
├── components
│ ├── screens
│ │ ├── search
│ │ │ ├── search.interface.ts
│ │ │ ├── useSearchForm.ts
│ │ │ ├── useSearch.ts
│ │ │ └── Search.tsx
│ │ ├── product
│ │ │ ├── product-page.interface.ts
│ │ │ ├── useProduct.ts
│ │ │ ├── product-info
│ │ │ │ ├── ProductInfo.tsx
│ │ │ │ └── AddToCartButton.tsx
│ │ │ ├── ProductHeader.tsx
│ │ │ ├── ProductButton.tsx
│ │ │ ├── Product.tsx
│ │ │ └── favorite-button
│ │ │ │ └── FavoriteButton.tsx
│ │ ├── auth
│ │ │ ├── email.regex.ts
│ │ │ ├── AuthFields.tsx
│ │ │ ├── useAuthMutations.ts
│ │ │ └── Auth.tsx
│ │ ├── profile
│ │ │ ├── useProfile.ts
│ │ │ └── Profile.tsx
│ │ ├── explorer
│ │ │ ├── useGetAllProducts.ts
│ │ │ └── Explorer.tsx
│ │ ├── home
│ │ │ ├── products
│ │ │ │ ├── useProducts.ts
│ │ │ │ └── Products.tsx
│ │ │ ├── categories
│ │ │ │ ├── useGetAllCategories.ts
│ │ │ │ └── Categories.tsx
│ │ │ ├── Home.tsx
│ │ │ ├── Header.tsx
│ │ │ └── banner
│ │ │ │ └── Banner.tsx
│ │ ├── thanks
│ │ │ └── Thanks.tsx
│ │ ├── favorites
│ │ │ └── Favorites.tsx
│ │ ├── category
│ │ │ ├── Category.tsx
│ │ │ └── useCategory.ts
│ │ └── cart
│ │ │ ├── cart-item
│ │ │ ├── CartActions.tsx
│ │ │ └── CartItem.tsx
│ │ │ ├── Cart.tsx
│ │ │ └── useCheckout.ts
│ ├── ui
│ │ ├── button
│ │ │ ├── button.interface.ts
│ │ │ └── Button.tsx
│ │ ├── catalog
│ │ │ ├── catalog.interface.ts
│ │ │ ├── product-item
│ │ │ │ ├── ProductInfo.tsx
│ │ │ │ └── ProductItem.tsx
│ │ │ └── Catalog.tsx
│ │ ├── Loader.tsx
│ │ ├── field
│ │ │ ├── field.interface.ts
│ │ │ ├── DismissKeyboard.tsx
│ │ │ └── Field.tsx
│ │ ├── Heading.tsx
│ │ └── Toast.tsx
│ └── layout
│ │ ├── bottom-menu
│ │ ├── menu.interface.ts
│ │ ├── menu.data.ts
│ │ ├── MenuItem.tsx
│ │ └── BottomMenu.tsx
│ │ └── Layout.tsx
├── types
│ ├── category.interface.ts
│ ├── cart.interface.ts
│ ├── icon.interface.ts
│ ├── user.interface.ts
│ ├── order.interface.ts
│ ├── product.interface.ts
│ └── auth.interface.ts
├── store
│ ├── root-actions.ts
│ ├── cart
│ │ ├── cart.types.ts
│ │ └── cart.slice.ts
│ └── store.ts
├── utils
│ ├── getMediaSource.ts
│ └── convertPrice.ts
├── hooks
│ ├── useAuth.ts
│ ├── useTypedSelector.ts
│ ├── useTypedNavigation.ts
│ ├── useTypedRoute.ts
│ ├── useCart.ts
│ ├── useActions.ts
│ └── useDebounce.ts
├── services
│ ├── api
│ │ ├── error.api.ts
│ │ ├── request.api.ts
│ │ ├── helper.auth.ts
│ │ └── interceptors.api.ts
│ ├── category.service.ts
│ ├── user.service.ts
│ ├── order.service.ts
│ ├── product.service.ts
│ └── auth
│ │ ├── auth.service.ts
│ │ └── auth.helper.ts
├── providers
│ ├── auth-provder.interface.ts
│ ├── AuthProvider.tsx
│ └── useCheckAuth.ts
├── config
│ └── api.config.ts
└── navigation
│ ├── navigation.types.ts
│ ├── PrivateNavigator.tsx
│ ├── routes.ts
│ └── Navigation.tsx
├── tailwind.config.js
├── tsconfig.json
├── metro.config.js
├── babel.config.js
├── .gitignore
├── .prettierrc
├── app.json
├── package.json
└── App.tsx
/nativewind-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/.example.env:
--------------------------------------------------------------------------------
1 | SERVER_URL=http://192.168.88.20:4200
2 | STRIPE_KEY=
3 |
--------------------------------------------------------------------------------
/app/assets/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/KaplunMaxym/Delivery-app/HEAD/app/assets/icon.png
--------------------------------------------------------------------------------
/app/assets/splash.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/KaplunMaxym/Delivery-app/HEAD/app/assets/splash.png
--------------------------------------------------------------------------------
/app/assets/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/KaplunMaxym/Delivery-app/HEAD/app/assets/favicon.png
--------------------------------------------------------------------------------
/app/assets/adaptive-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/KaplunMaxym/Delivery-app/HEAD/app/assets/adaptive-icon.png
--------------------------------------------------------------------------------
/app/assets/images/banner.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/KaplunMaxym/Delivery-app/HEAD/app/assets/images/banner.png
--------------------------------------------------------------------------------
/app/components/screens/search/search.interface.ts:
--------------------------------------------------------------------------------
1 | export interface ISearchFormData {
2 | searchTerm: string
3 | }
4 |
--------------------------------------------------------------------------------
/app/types/category.interface.ts:
--------------------------------------------------------------------------------
1 | export interface ICategory {
2 | id: string
3 | name: string
4 | slug: string
5 | image: string
6 | }
7 |
--------------------------------------------------------------------------------
/app/store/root-actions.ts:
--------------------------------------------------------------------------------
1 | import { cartSlice } from './cart/cart.slice'
2 |
3 | export const rootActions = {
4 | ...cartSlice.actions
5 | }
6 |
--------------------------------------------------------------------------------
/app/utils/getMediaSource.ts:
--------------------------------------------------------------------------------
1 | import { SERVER_URL } from '@/config/api.config'
2 |
3 | export const getMediaSource = (path: string) => ({
4 | uri: SERVER_URL + path
5 | })
6 |
--------------------------------------------------------------------------------
/app/components/ui/button/button.interface.ts:
--------------------------------------------------------------------------------
1 | import { PressableProps } from 'react-native'
2 |
3 | export interface IButton extends PressableProps {
4 | className?: string
5 | }
6 |
--------------------------------------------------------------------------------
/app/components/screens/product/product-page.interface.ts:
--------------------------------------------------------------------------------
1 | import { IProduct } from '@/types/product.interface'
2 |
3 | export interface IProductComponent {
4 | product: IProduct
5 | }
6 |
--------------------------------------------------------------------------------
/app/hooks/useAuth.ts:
--------------------------------------------------------------------------------
1 | import { useContext } from 'react'
2 |
3 | import { AuthContext } from '@/providers/AuthProvider'
4 |
5 | export const useAuth = () => useContext(AuthContext)
6 |
--------------------------------------------------------------------------------
/app/utils/convertPrice.ts:
--------------------------------------------------------------------------------
1 | export const convertPrice = (price: number) => {
2 | return price.toLocaleString('en-US', {
3 | style: 'currency',
4 | currency: 'USD'
5 | })
6 | }
7 |
--------------------------------------------------------------------------------
/app/components/ui/catalog/catalog.interface.ts:
--------------------------------------------------------------------------------
1 | import { IProduct } from '@/types/product.interface'
2 |
3 | export interface ICatalog {
4 | title?: string
5 | products: IProduct[]
6 | }
7 |
--------------------------------------------------------------------------------
/app/types/cart.interface.ts:
--------------------------------------------------------------------------------
1 | import { IProduct } from './product.interface'
2 |
3 | export interface ICartItem {
4 | id: string
5 | product: IProduct
6 | quantity: number
7 | price: number
8 | }
9 |
--------------------------------------------------------------------------------
/app/components/screens/auth/email.regex.ts:
--------------------------------------------------------------------------------
1 | export const validEmail =
2 | /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/
3 |
--------------------------------------------------------------------------------
/app/hooks/useTypedSelector.ts:
--------------------------------------------------------------------------------
1 | import { TypedUseSelectorHook, useSelector } from 'react-redux'
2 |
3 | import { TypeRootState } from '../store/store'
4 |
5 | export const useTypedSelector: TypedUseSelectorHook = useSelector
6 |
--------------------------------------------------------------------------------
/app/types/icon.interface.ts:
--------------------------------------------------------------------------------
1 | import { Feather, MaterialIcons } from '@expo/vector-icons'
2 |
3 | export type TypeFeatherIconNames = keyof typeof Feather.glyphMap
4 | export type TypeMaterialIconNames = keyof typeof MaterialIcons.glyphMap
5 |
--------------------------------------------------------------------------------
/tailwind.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('tailwindcss').Config} */
2 | module.exports = {
3 | content: ["./App.{js,jsx,ts,tsx}", "./app/**/*.{js,jsx,ts,tsx}"],
4 | theme: {
5 | extend: {},
6 | },
7 | plugins: [],
8 | }
9 |
10 |
--------------------------------------------------------------------------------
/app/components/ui/Loader.tsx:
--------------------------------------------------------------------------------
1 | import { FC } from 'react'
2 | import { ActivityIndicator } from 'react-native'
3 |
4 | const Loader: FC = () => {
5 | return
6 | }
7 |
8 | export default Loader
9 |
--------------------------------------------------------------------------------
/app/types/user.interface.ts:
--------------------------------------------------------------------------------
1 | import { IProduct } from './product.interface'
2 |
3 | export interface IUser {
4 | id: string
5 | email: string
6 | password: string
7 | name: string
8 | avatarPath: string
9 | favorites: IProduct[]
10 | }
11 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "expo/tsconfig.base",
3 | "compilerOptions": {
4 | "allowSyntheticDefaultImports": true,
5 | "strict": true,
6 | "baseUrl": ".",
7 | "paths": {
8 | "@/*": ["./app/*"]
9 | }
10 | },
11 | }
12 |
--------------------------------------------------------------------------------
/app/types/order.interface.ts:
--------------------------------------------------------------------------------
1 | import { ICartItem } from './cart.interface'
2 | import { IUser } from './user.interface'
3 |
4 | export interface IOrder {
5 | id: string
6 | createdAt: string
7 | items: ICartItem[]
8 | user: IUser
9 | total: number
10 | }
11 |
--------------------------------------------------------------------------------
/app/services/api/error.api.ts:
--------------------------------------------------------------------------------
1 | export const errorCatch = (error: any): string => {
2 | const message = error?.response?.data?.message
3 |
4 | return message
5 | ? typeof error.response.data.message === 'object'
6 | ? message[0]
7 | : message
8 | : error.message
9 | }
10 |
--------------------------------------------------------------------------------
/app/hooks/useTypedNavigation.ts:
--------------------------------------------------------------------------------
1 | import { NavigationProp, useNavigation } from '@react-navigation/native'
2 |
3 | import { TypeRootStackParamList } from '@/navigation/navigation.types'
4 |
5 | export const useTypedNavigation = () =>
6 | useNavigation>()
7 |
--------------------------------------------------------------------------------
/app/hooks/useTypedRoute.ts:
--------------------------------------------------------------------------------
1 | import { RouteProp, useRoute } from '@react-navigation/native'
2 |
3 | import { TypeRootStackParamList } from '@/navigation/navigation.types'
4 |
5 | export const useTypedRoute = () =>
6 | useRoute>()
7 |
--------------------------------------------------------------------------------
/app/types/product.interface.ts:
--------------------------------------------------------------------------------
1 | import { ICategory } from './category.interface'
2 |
3 | export interface IProduct {
4 | id: string
5 | name: string
6 | slug: string
7 | description: string
8 | price: number
9 | image: string
10 | createdAt: string
11 | category: ICategory
12 | }
13 |
--------------------------------------------------------------------------------
/app/providers/auth-provder.interface.ts:
--------------------------------------------------------------------------------
1 | import { Dispatch, SetStateAction } from 'react'
2 |
3 | import { IUser } from '@/types/user.interface'
4 |
5 | export type TypeUserState = IUser | null
6 |
7 | export interface IContext {
8 | user: TypeUserState
9 | setUser: Dispatch>
10 | }
11 |
--------------------------------------------------------------------------------
/app/hooks/useCart.ts:
--------------------------------------------------------------------------------
1 | import { useTypedSelector } from './useTypedSelector'
2 |
3 | export const useCart = () => {
4 | const items = useTypedSelector(state => state.cart.items)
5 |
6 | const total = items.reduce(
7 | (acc, item) => acc + item.price * item.quantity,
8 | 0
9 | )
10 |
11 | return { items, total }
12 | }
13 |
--------------------------------------------------------------------------------
/app/store/cart/cart.types.ts:
--------------------------------------------------------------------------------
1 | import { ICartItem } from '@/types/cart.interface'
2 |
3 | export interface ICartInitialState {
4 | items: ICartItem[]
5 | }
6 |
7 | export interface IAddToCartPayload extends Omit {}
8 |
9 | export interface IChangeQuantityPayload extends Pick {
10 | type: 'minus' | 'plus'
11 | }
12 |
--------------------------------------------------------------------------------
/app/components/screens/profile/useProfile.ts:
--------------------------------------------------------------------------------
1 | import { useQuery } from '@tanstack/react-query'
2 |
3 | import { UserService } from '@/services/user.service'
4 |
5 | export const useProfile = () => {
6 | const { data: profile } = useQuery({
7 | queryKey: ['get profile'],
8 | queryFn: () => UserService.getProfile()
9 | })
10 |
11 | return { profile }
12 | }
13 |
--------------------------------------------------------------------------------
/app/hooks/useActions.ts:
--------------------------------------------------------------------------------
1 | import { bindActionCreators } from '@reduxjs/toolkit'
2 | import { useMemo } from 'react'
3 | import { useDispatch } from 'react-redux'
4 |
5 | import { rootActions } from '@/store/root-actions'
6 |
7 | export const useActions = () => {
8 | const dispatch = useDispatch()
9 |
10 | return useMemo(() => bindActionCreators(rootActions, dispatch), [dispatch])
11 | }
12 |
--------------------------------------------------------------------------------
/app/components/layout/bottom-menu/menu.interface.ts:
--------------------------------------------------------------------------------
1 | import { TypeFeatherIconNames } from '@/types/icon.interface'
2 |
3 | import { TypeRootStackParamList } from '@/navigation/navigation.types'
4 |
5 | export interface IMenuItem {
6 | iconName: TypeFeatherIconNames
7 | path: keyof TypeRootStackParamList
8 | }
9 |
10 | export type TypeNavigate = (screenName: keyof TypeRootStackParamList) => void
11 |
--------------------------------------------------------------------------------
/metro.config.js:
--------------------------------------------------------------------------------
1 | const { getDefaultConfig } = require('expo/metro-config');
2 |
3 | module.exports = (async () => {
4 | const defaultConfig = await getDefaultConfig(__dirname);
5 |
6 | return {
7 | ...defaultConfig,
8 | resolver: {
9 | ...defaultConfig.resolver,
10 | extraNodeModules: {
11 | ...defaultConfig.resolver.extraNodeModules,
12 | '@/': './app/',
13 | },
14 | },
15 | };
16 | })();
17 |
--------------------------------------------------------------------------------
/app/components/screens/explorer/useGetAllProducts.ts:
--------------------------------------------------------------------------------
1 | import { useQuery } from '@tanstack/react-query'
2 |
3 | import { ProductService } from '@/services/product.service'
4 |
5 | export const useGetAllProducts = () => {
6 | const { data: products, isLoading } = useQuery({
7 | queryKey: ['get all products'],
8 | queryFn: () => ProductService.getAll(),
9 | select: data => data
10 | })
11 |
12 | return { products, isLoading }
13 | }
14 |
--------------------------------------------------------------------------------
/app/components/screens/home/products/useProducts.ts:
--------------------------------------------------------------------------------
1 | import { useQuery } from '@tanstack/react-query'
2 |
3 | import { ProductService } from '@/services/product.service'
4 |
5 | export const useProducts = () => {
6 | const { data: products, isLoading } = useQuery({
7 | queryKey: ['get products'],
8 | queryFn: () => ProductService.getAll(),
9 | select: data => data.slice(0, 4)
10 | })
11 |
12 | return { products, isLoading }
13 | }
14 |
--------------------------------------------------------------------------------
/app/components/screens/home/categories/useGetAllCategories.ts:
--------------------------------------------------------------------------------
1 | import { useQuery } from '@tanstack/react-query'
2 |
3 | import { CategoryService } from '@/services/category.service'
4 |
5 | export const useGetAllCategories = () => {
6 | const { data: categories, isLoading } = useQuery({
7 | queryKey: ['get categories'],
8 | queryFn: () => CategoryService.getAll(),
9 | select: data => data
10 | })
11 |
12 | return { categories, isLoading }
13 | }
14 |
--------------------------------------------------------------------------------
/app/hooks/useDebounce.ts:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from 'react'
2 |
3 | export const useDebounce = (value: T, delay: number): T => {
4 | const [debouncedValue, setDebouncedValue] = useState(value)
5 |
6 | useEffect(() => {
7 | const handler = setTimeout(() => {
8 | setDebouncedValue(value)
9 | }, delay)
10 |
11 | return () => {
12 | clearTimeout(handler)
13 | }
14 | }, [value, delay])
15 |
16 | return debouncedValue
17 | }
18 |
--------------------------------------------------------------------------------
/app/config/api.config.ts:
--------------------------------------------------------------------------------
1 | export const SERVER_URL = process.env.SERVER_URL
2 | export const API_URL = `${SERVER_URL}/api`
3 |
4 | export const getAuthUrl = (string: string) => `/auth${string}`
5 | export const getUsersUrl = (string: string) => `/users${string}`
6 | export const getProductsUrl = (string: string) => `/products${string}`
7 | export const getCategoriesUrl = (string: string) => `/categories${string}`
8 | export const getOrdersUrl = (string: string) => `/orders${string}`
9 |
--------------------------------------------------------------------------------
/app/components/screens/thanks/Thanks.tsx:
--------------------------------------------------------------------------------
1 | import { FontAwesome } from '@expo/vector-icons'
2 | import { FC } from 'react'
3 | import { Text, View } from 'react-native'
4 |
5 | const Thanks: FC = () => {
6 | return (
7 |
8 |
9 | Thank you!
10 |
11 | )
12 | }
13 |
14 | export default Thanks
15 |
--------------------------------------------------------------------------------
/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = function (api) {
2 | api.cache(true);
3 | return {
4 | presets: ['babel-preset-expo'],
5 | plugins: [
6 | [
7 | 'babel-plugin-root-import',
8 | {
9 | rootPathSuffix: 'app/',
10 | rootPathPrefix: '@/'
11 | }
12 | ],
13 | ['nativewind/babel'],
14 | ['inline-dotenv'],
15 | ]
16 | };
17 | };
18 |
--------------------------------------------------------------------------------
/app/components/layout/bottom-menu/menu.data.ts:
--------------------------------------------------------------------------------
1 | import { IMenuItem } from './menu.interface'
2 |
3 | export const menuItems: IMenuItem[] = [
4 | {
5 | iconName: 'home',
6 | path: 'Home'
7 | },
8 | {
9 | iconName: 'heart',
10 | path: 'Favorites'
11 | },
12 | {
13 | iconName: 'search',
14 | path: 'Search'
15 | },
16 | {
17 | iconName: 'shopping-bag',
18 | path: 'Explorer'
19 | },
20 | {
21 | iconName: 'user',
22 | path: 'Profile'
23 | }
24 | ]
25 |
--------------------------------------------------------------------------------
/app/components/screens/favorites/Favorites.tsx:
--------------------------------------------------------------------------------
1 | import { FC } from 'react'
2 |
3 | import Layout from '@/components/layout/Layout'
4 | import Catalog from '@/components/ui/catalog/Catalog'
5 |
6 | import { useProfile } from '../profile/useProfile'
7 |
8 | const Favorites: FC = () => {
9 | const { profile } = useProfile()
10 |
11 | return (
12 |
13 |
14 |
15 | )
16 | }
17 |
18 | export default Favorites
19 |
--------------------------------------------------------------------------------
/app/components/screens/home/products/Products.tsx:
--------------------------------------------------------------------------------
1 | import { FC } from 'react'
2 |
3 | import Loader from '@/components/ui/Loader'
4 | import Catalog from '@/components/ui/catalog/Catalog'
5 |
6 | import { useProducts } from './useProducts'
7 |
8 | const Products: FC = () => {
9 | const { isLoading, products } = useProducts()
10 |
11 | return isLoading ? (
12 |
13 | ) : (
14 |
15 | )
16 | }
17 |
18 | export default Products
19 |
--------------------------------------------------------------------------------
/app/components/screens/home/Home.tsx:
--------------------------------------------------------------------------------
1 | import { FC } from 'react'
2 |
3 | import Layout from '@/components/layout/Layout'
4 |
5 | import Header from './Header'
6 | import Banner from './banner/Banner'
7 | import Categories from './categories/Categories'
8 | import Products from './products/Products'
9 |
10 | const Home: FC = () => {
11 | return (
12 |
13 |
14 |
15 |
16 |
17 |
18 | )
19 | }
20 |
21 | export default Home
22 |
--------------------------------------------------------------------------------
/app/components/ui/field/field.interface.ts:
--------------------------------------------------------------------------------
1 | import {
2 | Control,
3 | FieldPath,
4 | FieldValues,
5 | RegisterOptions
6 | } from 'react-hook-form'
7 | import { TextInputProps } from 'react-native'
8 |
9 | export interface IField
10 | extends Omit {
11 | control: Control
12 | name: FieldPath
13 | rules?: Omit<
14 | RegisterOptions>,
15 | 'valueAsNumber' | 'valueAsDate' | 'setValueAs' | 'disabled'
16 | >
17 | }
18 |
--------------------------------------------------------------------------------
/app/navigation/navigation.types.ts:
--------------------------------------------------------------------------------
1 | import { ComponentType } from 'react'
2 |
3 | export type TypeRootStackParamList = {
4 | Auth: undefined
5 | Home: undefined
6 | Favorites: undefined
7 | Search: undefined
8 | Explorer: undefined
9 | Profile: undefined
10 | Cart: undefined
11 | Thanks: undefined
12 | Category: {
13 | slug: string
14 | }
15 | Product: {
16 | slug: string
17 | }
18 | }
19 |
20 | export interface IRoute {
21 | name: keyof TypeRootStackParamList
22 | component: ComponentType
23 | }
24 |
--------------------------------------------------------------------------------
/app/types/auth.interface.ts:
--------------------------------------------------------------------------------
1 | import { IUser } from '@/types/user.interface'
2 |
3 | export interface IAuthFormData extends Pick {}
4 |
5 | export enum EnumSecureStore {
6 | ACCESS_TOKEN = 'accessToken',
7 | REFRESH_TOKEN = 'refreshToken'
8 | }
9 |
10 | export enum EnumAsyncStorage {
11 | USER = 'user'
12 | }
13 |
14 | export interface ITokens {
15 | accessToken: string
16 | refreshToken: string
17 | }
18 |
19 | export interface IAuthResponse extends ITokens {
20 | user: IUser
21 | }
22 |
--------------------------------------------------------------------------------
/app/components/screens/product/useProduct.ts:
--------------------------------------------------------------------------------
1 | import { useQuery } from '@tanstack/react-query'
2 |
3 | import { useTypedRoute } from '@/hooks/useTypedRoute'
4 |
5 | import { ProductService } from '@/services/product.service'
6 |
7 | export const useProduct = () => {
8 | const { params } = useTypedRoute<'Product'>()
9 |
10 | const { isLoading, data: product } = useQuery({
11 | queryKey: ['get product by slug', params.slug],
12 | queryFn: () => ProductService.getBySlug(params.slug)
13 | })
14 |
15 | return { isLoading, product }
16 | }
17 |
--------------------------------------------------------------------------------
/app/components/layout/Layout.tsx:
--------------------------------------------------------------------------------
1 | import cn from 'clsx'
2 | import { FC, PropsWithChildren } from 'react'
3 | import { ScrollView, View } from 'react-native'
4 |
5 | interface ILayout {
6 | className?: string
7 | }
8 |
9 | const Layout: FC> = ({ children, className }) => {
10 | return (
11 |
12 |
13 | {children}
14 |
15 |
16 | )
17 | }
18 |
19 | export default Layout
20 |
--------------------------------------------------------------------------------
/app/services/category.service.ts:
--------------------------------------------------------------------------------
1 | import { ICategory } from '@/types/category.interface'
2 |
3 | import { getCategoriesUrl } from '@/config/api.config'
4 |
5 | import { request } from './api/request.api'
6 |
7 | export const CategoryService = {
8 | async getAll() {
9 | return request({
10 | url: getCategoriesUrl(''),
11 | method: 'GET'
12 | })
13 | },
14 |
15 | async getBySlug(slug: string) {
16 | return request({
17 | url: getCategoriesUrl(`/by-slug/${slug}`),
18 | method: 'GET'
19 | })
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/app/services/user.service.ts:
--------------------------------------------------------------------------------
1 | import { IUser } from '@/types/user.interface'
2 |
3 | import { getUsersUrl } from '@/config/api.config'
4 |
5 | import { request } from './api/request.api'
6 |
7 | export const UserService = {
8 | async getProfile() {
9 | return request({
10 | url: getUsersUrl('/profile'),
11 | method: 'GET'
12 | })
13 | },
14 |
15 | async toggleFavorite(productId: string) {
16 | return request({
17 | url: getUsersUrl(`/profile/favorites/${productId}`),
18 | method: 'PATCH'
19 | })
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Learn more https://docs.github.com/en/get-started/getting-started-with-git/ignoring-files
2 |
3 | # dependencies
4 | node_modules/
5 |
6 | # Expo
7 | .expo/
8 | dist/
9 | web-build/
10 |
11 | # Native
12 | *.orig.*
13 | *.jks
14 | *.p8
15 | *.p12
16 | *.key
17 | *.mobileprovision
18 |
19 | # Metro
20 | .metro-health-check*
21 |
22 | # debug
23 | npm-debug.*
24 | yarn-debug.*
25 | yarn-error.*
26 |
27 | # macOS
28 | .DS_Store
29 | *.pem
30 |
31 | # local env files
32 | .env*.local
33 |
34 | # typescript
35 | *.tsbuildinfo
36 | .env
37 | /.idea
38 | /ios
39 | /android
40 | /.expo
41 |
--------------------------------------------------------------------------------
/app/components/screens/search/useSearchForm.ts:
--------------------------------------------------------------------------------
1 | import { useMemo } from 'react'
2 | import { useForm } from 'react-hook-form'
3 |
4 | import { useDebounce } from '@/hooks/useDebounce'
5 |
6 | import { ISearchFormData } from './search.interface'
7 |
8 | export const useSearchForm = () => {
9 | const { control, watch } = useForm({
10 | mode: 'onChange'
11 | })
12 |
13 | const searchTerm = watch('searchTerm')
14 | const debouncedSearch = useDebounce(searchTerm, 500)
15 |
16 | return useMemo(
17 | () => ({ debouncedSearch, searchTerm, control }),
18 | [searchTerm]
19 | )
20 | }
21 |
--------------------------------------------------------------------------------
/app/components/screens/search/useSearch.ts:
--------------------------------------------------------------------------------
1 | import { useQuery } from '@tanstack/react-query'
2 |
3 | import { ProductService } from '@/services/product.service'
4 |
5 | import { useSearchForm } from './useSearchForm'
6 |
7 | export const useSearch = () => {
8 | const { searchTerm, debouncedSearch, control } = useSearchForm()
9 |
10 | const { data: products, isLoading } = useQuery({
11 | queryKey: ['search products', debouncedSearch],
12 | queryFn: () => ProductService.getAll(debouncedSearch),
13 | enabled: !!debouncedSearch
14 | })
15 |
16 | return { products, isLoading, control, searchTerm }
17 | }
18 |
--------------------------------------------------------------------------------
/app/components/ui/Heading.tsx:
--------------------------------------------------------------------------------
1 | import cn from 'clsx'
2 | import { FC, PropsWithChildren } from 'react'
3 | import { Text } from 'react-native'
4 |
5 | interface IHeading {
6 | isCenter?: boolean
7 | className?: string
8 | }
9 |
10 | const Heading: FC> = ({
11 | children,
12 | isCenter = false,
13 | className
14 | }) => {
15 | return (
16 |
23 | {children}
24 |
25 | )
26 | }
27 |
28 | export default Heading
29 |
--------------------------------------------------------------------------------
/app/components/screens/explorer/Explorer.tsx:
--------------------------------------------------------------------------------
1 | import { FC } from 'react'
2 |
3 | import Layout from '@/components/layout/Layout'
4 | import Loader from '@/components/ui/Loader'
5 | import Catalog from '@/components/ui/catalog/Catalog'
6 |
7 | import { useGetAllProducts } from './useGetAllProducts'
8 |
9 | const Explorer: FC = () => {
10 | const { products, isLoading } = useGetAllProducts()
11 |
12 | return (
13 |
14 | {isLoading ? (
15 |
16 | ) : (
17 |
18 | )}
19 |
20 | )
21 | }
22 |
23 | export default Explorer
24 |
--------------------------------------------------------------------------------
/app/components/ui/field/DismissKeyboard.tsx:
--------------------------------------------------------------------------------
1 | import { FC, PropsWithChildren } from 'react'
2 | import {
3 | Keyboard,
4 | TouchableWithoutFeedback,
5 | View,
6 | ViewProps
7 | } from 'react-native'
8 |
9 | const DismissKeyboard: FC> = ({
10 | children,
11 | ...rest
12 | }) => {
13 | return (
14 |
15 |
21 | {children}
22 |
23 |
24 | )
25 | }
26 |
27 | export default DismissKeyboard
28 |
--------------------------------------------------------------------------------
/app/services/api/request.api.ts:
--------------------------------------------------------------------------------
1 | import { AxiosError, AxiosRequestConfig, AxiosResponse } from 'axios'
2 | import Toast from 'react-native-toast-message'
3 |
4 | import { errorCatch } from './error.api'
5 | import instance from './interceptors.api'
6 |
7 | export const request = async (config: AxiosRequestConfig) => {
8 | const onSuccess = (response: AxiosResponse) => response.data
9 |
10 | const onError = (error: AxiosError) => {
11 | Toast.show({
12 | type: 'error',
13 | text1: 'Request error',
14 | text2: errorCatch(error)
15 | })
16 |
17 | return Promise.reject(error)
18 | }
19 |
20 | return instance(config).then(onSuccess).catch(onError)
21 | }
22 |
--------------------------------------------------------------------------------
/app/components/ui/button/Button.tsx:
--------------------------------------------------------------------------------
1 | import cn from 'clsx'
2 | import { FC, PropsWithChildren } from 'react'
3 | import { Pressable, Text } from 'react-native'
4 |
5 | import { IButton } from './button.interface'
6 |
7 | const Button: FC> = ({
8 | className,
9 | children,
10 | ...rest
11 | }) => {
12 | return (
13 |
20 |
21 | {children}
22 |
23 |
24 | )
25 | }
26 |
27 | export default Button
28 |
--------------------------------------------------------------------------------
/app/components/screens/category/Category.tsx:
--------------------------------------------------------------------------------
1 | import { FC } from 'react'
2 | import { Text } from 'react-native'
3 |
4 | import Layout from '@/components/layout/Layout'
5 | import Loader from '@/components/ui/Loader'
6 | import Catalog from '@/components/ui/catalog/Catalog'
7 |
8 | import { useCategory } from './useCategory'
9 |
10 | const Category: FC = () => {
11 | const { category, products, isLoading } = useCategory()
12 |
13 | if (isLoading) return
14 |
15 | return (
16 |
17 | {category ? (
18 |
19 | ) : (
20 | Category not found
21 | )}
22 |
23 | )
24 | }
25 |
26 | export default Category
27 |
--------------------------------------------------------------------------------
/app/components/screens/product/product-info/ProductInfo.tsx:
--------------------------------------------------------------------------------
1 | import { FC } from 'react'
2 | import { Text, View } from 'react-native'
3 |
4 | import { convertPrice } from '@/utils/convertPrice'
5 |
6 | import { IProductComponent } from '../product-page.interface'
7 |
8 | const ProductInfo: FC = ({ product }) => {
9 | return (
10 |
11 | {product.name}
12 |
13 | {product.description}
14 |
15 |
16 | {convertPrice(product.price)}
17 |
18 |
19 | )
20 | }
21 |
22 | export default ProductInfo
23 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "trailingComma": "none",
3 | "useTabs": true,
4 | "semi": false,
5 | "tabWidth": 4,
6 | "jsxSingleQuote": true,
7 | "singleQuote": true,
8 | "arrowParens": "avoid",
9 | "importOrder": [
10 | "",
11 | "^@/components/(.*)$",
12 | "^@/hooks/(.*)$",
13 | "^@/types/(.*)$",
14 | "^@/services/(.*)$",
15 | "^@/assets/(.*)$",
16 | "^@/utils/(.*)$",
17 | "^@/config/(.*)$",
18 | "^@/providers/(.*)$",
19 | "^@/navigation/(.*)$",
20 | "^@/api/(.*)$",
21 | "^@/store/(.*)$",
22 | "^../(.*)$",
23 | "^./(.*)$"
24 | ],
25 | "importOrderSeparation": true,
26 | "importOrderSortSpecifiers": true,
27 | "plugins": ["@trivago/prettier-plugin-sort-imports"]
28 | }
29 |
--------------------------------------------------------------------------------
/app.json:
--------------------------------------------------------------------------------
1 | {
2 | "expo": {
3 | "name": "Delivery",
4 | "slug": "client",
5 | "version": "1.0.0",
6 | "orientation": "portrait",
7 | "icon": "./app/assets/icon.png",
8 | "userInterfaceStyle": "light",
9 | "splash": {
10 | "image": "./app/assets/splash.png",
11 | "resizeMode": "contain",
12 | "backgroundColor": "#4fae5a"
13 | },
14 | "ios": {
15 | "supportsTablet": true,
16 | "bundleIdentifier": "com.kaplunmaxym.client"
17 | },
18 | "android": {
19 | "adaptiveIcon": {
20 | "foregroundImage": "./app/assets/adaptive-icon.png",
21 | "backgroundColor": "#4fae5a"
22 | },
23 | "package": "com.kaplunmaxym.client"
24 | },
25 | "web": {
26 | "favicon": "./app/assets/favicon.png"
27 | }
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/app/components/ui/Toast.tsx:
--------------------------------------------------------------------------------
1 | import { FC } from 'react'
2 | import RnToast, { BaseToast } from 'react-native-toast-message'
3 |
4 | const options = (primaryColor: string) => ({
5 | style: { backgroundColor: '#080808', borderLeftColor: primaryColor },
6 | text1Style: {
7 | color: '#fff',
8 | fontSize: 16
9 | },
10 | text2Style: {
11 | fontSize: 14
12 | }
13 | })
14 |
15 | const Toast: FC = () => {
16 | return (
17 | (
21 |
22 | ),
23 | info: props => ,
24 | error: props =>
25 | }}
26 | />
27 | )
28 | }
29 |
30 | export default Toast
31 |
--------------------------------------------------------------------------------
/app/components/ui/catalog/product-item/ProductInfo.tsx:
--------------------------------------------------------------------------------
1 | import { FC } from 'react'
2 | import { Text, View } from 'react-native'
3 |
4 | import { IProduct } from '@/types/product.interface'
5 |
6 | import { convertPrice } from '@/utils/convertPrice'
7 |
8 | interface IProductInfo {
9 | product: IProduct
10 | }
11 |
12 | const ProductInfo: FC = ({ product }) => {
13 | return (
14 |
15 | {product.name}
16 | {product.category.name}
17 |
18 | {convertPrice(product.price)}
19 |
20 |
21 | )
22 | }
23 |
24 | export default ProductInfo
25 |
--------------------------------------------------------------------------------
/app/components/ui/catalog/Catalog.tsx:
--------------------------------------------------------------------------------
1 | import { FC } from 'react'
2 | import { Text, View } from 'react-native'
3 |
4 | import Heading from '../Heading'
5 |
6 | import { ICatalog } from './catalog.interface'
7 | import ProductItem from './product-item/ProductItem'
8 |
9 | const Catalog: FC = ({ title, products }) => {
10 | return (
11 |
12 | {title && {title}}
13 |
14 | {products?.length ? (
15 |
16 | {products.map(product => (
17 |
18 | ))}
19 |
20 | ) : (
21 | Products not found
22 | )}
23 |
24 | )
25 | }
26 |
27 | export default Catalog
28 |
--------------------------------------------------------------------------------
/app/components/layout/bottom-menu/MenuItem.tsx:
--------------------------------------------------------------------------------
1 | import { Feather } from '@expo/vector-icons'
2 | import { FC } from 'react'
3 | import { Pressable } from 'react-native'
4 |
5 | import { IMenuItem, TypeNavigate } from './menu.interface'
6 |
7 | interface IMenuItemProps {
8 | item: IMenuItem
9 | nav: TypeNavigate
10 | currentRoute?: string
11 | }
12 |
13 | const MenuItem: FC = ({ currentRoute, item, nav }) => {
14 | const isActive = currentRoute === item.path
15 |
16 | return (
17 | nav(item.path)}
20 | >
21 |
26 |
27 | )
28 | }
29 |
30 | export default MenuItem
31 |
--------------------------------------------------------------------------------
/app/components/screens/home/Header.tsx:
--------------------------------------------------------------------------------
1 | import { Ionicons } from '@expo/vector-icons'
2 | import { FC } from 'react'
3 | import { Pressable, Text, View } from 'react-native'
4 |
5 | import { useTypedNavigation } from '@/hooks/useTypedNavigation'
6 |
7 | import { useProfile } from '../profile/useProfile'
8 |
9 | const Header: FC = () => {
10 | const { navigate } = useTypedNavigation()
11 |
12 | const { profile } = useProfile()
13 |
14 | return (
15 |
16 |
17 | Hello, {profile?.name}!
18 |
19 |
20 | navigate('Cart')}>
21 |
22 |
23 |
24 | )
25 | }
26 |
27 | export default Header
28 |
--------------------------------------------------------------------------------
/app/services/order.service.ts:
--------------------------------------------------------------------------------
1 | import { IOrder } from '@/types/order.interface'
2 |
3 | import { getOrdersUrl } from '@/config/api.config'
4 |
5 | import { request } from './api/request.api'
6 |
7 | type TypeData = {
8 | items: {
9 | quantity: number
10 | price: number
11 | productId: string
12 | }[]
13 | }
14 |
15 | export const OrderService = {
16 | async getAll() {
17 | return request({
18 | url: getOrdersUrl(''),
19 | method: 'GET'
20 | })
21 | },
22 |
23 | async getByUserId() {
24 | return request({
25 | url: getOrdersUrl('/by-user'),
26 | method: 'GET'
27 | })
28 | },
29 |
30 | async place(data: TypeData) {
31 | return request<{ clientSecret: string }>({
32 | url: getOrdersUrl(''),
33 | method: 'POST',
34 | data
35 | })
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/app/components/screens/product/ProductHeader.tsx:
--------------------------------------------------------------------------------
1 | import { FC } from 'react'
2 | import { View } from 'react-native'
3 |
4 | import { useTypedNavigation } from '@/hooks/useTypedNavigation'
5 |
6 | import ProductButton from './ProductButton'
7 | import FavoriteButton from './favorite-button/FavoriteButton'
8 | import { IProductComponent } from './product-page.interface'
9 |
10 | const ProductHeader: FC = ({ product }) => {
11 | const { goBack } = useTypedNavigation()
12 |
13 | return (
14 |
15 |
16 |
22 |
23 |
24 |
25 | )
26 | }
27 |
28 | export default ProductHeader
29 |
--------------------------------------------------------------------------------
/app/services/api/helper.auth.ts:
--------------------------------------------------------------------------------
1 | import axios from 'axios'
2 | import { getItemAsync } from 'expo-secure-store'
3 |
4 | import { EnumSecureStore, IAuthResponse } from '@/types/auth.interface'
5 |
6 | import { saveToStorage } from '@/services/auth/auth.helper'
7 |
8 | import { API_URL, getAuthUrl } from '@/config/api.config'
9 |
10 | export const getNewTokens = async () => {
11 | try {
12 | const refreshToken = await getItemAsync(EnumSecureStore.REFRESH_TOKEN)
13 | const response = await axios.post(
14 | API_URL + getAuthUrl('/login/access-token'),
15 | { refreshToken },
16 | {
17 | headers: {
18 | 'Content-Type': 'application/json'
19 | }
20 | }
21 | )
22 |
23 | if (response.data.accessToken) await saveToStorage(response.data)
24 |
25 | return response
26 | } catch (e) {}
27 | }
28 |
--------------------------------------------------------------------------------
/app/services/product.service.ts:
--------------------------------------------------------------------------------
1 | import { IProduct } from '@/types/product.interface'
2 |
3 | import { getProductsUrl } from '@/config/api.config'
4 |
5 | import { request } from './api/request.api'
6 |
7 | export const ProductService = {
8 | async getAll(searchTerm?: string) {
9 | return request({
10 | url: getProductsUrl(''),
11 | method: 'GET',
12 | params: searchTerm
13 | ? {
14 | searchTerm
15 | }
16 | : {}
17 | })
18 | },
19 |
20 | async getBySlug(slug: string) {
21 | return request({
22 | url: getProductsUrl(`/by-slug/${slug}`),
23 | method: 'GET'
24 | })
25 | },
26 |
27 | async getByCategory(categorySlug: string) {
28 | return request({
29 | url: getProductsUrl(`/by-category/${categorySlug}`),
30 | method: 'GET',
31 | // data: { categorySlug }r
32 | })
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/app/components/screens/home/banner/Banner.tsx:
--------------------------------------------------------------------------------
1 | import { FC } from 'react'
2 | import { Image, Pressable, Text, View } from 'react-native'
3 |
4 | import { useTypedNavigation } from '@/hooks/useTypedNavigation'
5 |
6 | const Banner: FC = () => {
7 | const { navigate } = useTypedNavigation()
8 |
9 | return (
10 |
11 |
12 |
13 | The best choice for you - and your family
14 |
15 |
16 | navigate('Explorer')}
18 | className='bg-black py-2 rounded-full w-28 mt-4'
19 | >
20 |
21 | Order now
22 |
23 |
24 |
25 |
26 | )
27 | }
28 |
29 | export default Banner
30 |
--------------------------------------------------------------------------------
/app/components/layout/bottom-menu/BottomMenu.tsx:
--------------------------------------------------------------------------------
1 | import { FC } from 'react'
2 | import { View } from 'react-native'
3 | import { useSafeAreaInsets } from 'react-native-safe-area-context'
4 |
5 | import MenuItem from './MenuItem'
6 | import { menuItems } from './menu.data'
7 | import { TypeNavigate } from './menu.interface'
8 |
9 | interface IBottomMenu {
10 | nav: TypeNavigate
11 | currentRoute?: string
12 | }
13 |
14 | const BottomMenu: FC = props => {
15 | const { bottom } = useSafeAreaInsets()
16 |
17 | return (
18 |
24 | {menuItems.map(item => (
25 |
26 | ))}
27 |
28 | )
29 | }
30 |
31 | export default BottomMenu
32 |
--------------------------------------------------------------------------------
/app/services/auth/auth.service.ts:
--------------------------------------------------------------------------------
1 | import AsyncStorage from '@react-native-async-storage/async-storage'
2 |
3 | import { EnumAsyncStorage, IAuthResponse } from '@/types/auth.interface'
4 |
5 | import { deleteTokensStorage, saveToStorage } from '@/services/auth/auth.helper'
6 |
7 | import { getAuthUrl } from '@/config/api.config'
8 |
9 | import { request } from '../api/request.api'
10 |
11 | export const AuthService = {
12 | async main(variant: 'reg' | 'login', email: string, password: string) {
13 | const response = await request({
14 | url: getAuthUrl(`/${variant === 'reg' ? 'register' : 'login'}`),
15 | method: 'POST',
16 | data: { email, password }
17 | })
18 |
19 | if (response.accessToken) await saveToStorage(response)
20 |
21 | return response
22 | },
23 |
24 | async logout() {
25 | await deleteTokensStorage()
26 | await AsyncStorage.removeItem(EnumAsyncStorage.USER)
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/app/navigation/PrivateNavigator.tsx:
--------------------------------------------------------------------------------
1 | import { createNativeStackNavigator } from '@react-navigation/native-stack'
2 | import { FC } from 'react'
3 |
4 | import Auth from '@/components/screens/auth/Auth'
5 |
6 | import { useAuth } from '@/hooks/useAuth'
7 |
8 | import { TypeRootStackParamList } from './navigation.types'
9 | import { routes } from './routes'
10 |
11 | const Stack = createNativeStackNavigator()
12 |
13 | const PrivateNavigator: FC = () => {
14 | const { user } = useAuth()
15 |
16 | return (
17 |
25 | {user ? (
26 | routes.map(route => (
27 |
28 | ))
29 | ) : (
30 |
31 | )}
32 |
33 | )
34 | }
35 |
36 | export default PrivateNavigator
37 |
--------------------------------------------------------------------------------
/app/components/screens/category/useCategory.ts:
--------------------------------------------------------------------------------
1 | import { useQuery } from '@tanstack/react-query'
2 |
3 | import { useTypedRoute } from '@/hooks/useTypedRoute'
4 |
5 | import { CategoryService } from '@/services/category.service'
6 | import { ProductService } from '@/services/product.service'
7 |
8 | export const useCategory = () => {
9 | const { params } = useTypedRoute<'Category'>()
10 |
11 | const { isLoading: isCategoryLoading, data: category } = useQuery({
12 | queryKey: ['get category by slug', params.slug],
13 | queryFn: () => CategoryService.getBySlug(params.slug)
14 | })
15 |
16 | const categoryId = category?.id || ''
17 |
18 | const { isLoading: isProductLoading, data: products } = useQuery({
19 | queryKey: ['get products by category', params.slug],
20 | queryFn: () => ProductService.getByCategory(params.slug),
21 | enabled: !!categoryId
22 | })
23 |
24 | return {
25 | category,
26 | products,
27 | isLoading: isCategoryLoading || isProductLoading
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/app/components/screens/product/ProductButton.tsx:
--------------------------------------------------------------------------------
1 | import { Feather } from '@expo/vector-icons'
2 | import cn from 'clsx'
3 | import { FC, PropsWithChildren } from 'react'
4 | import { Pressable, PressableProps, View } from 'react-native'
5 |
6 | import { TypeFeatherIconNames } from '@/types/icon.interface'
7 |
8 | interface IProductButton extends PressableProps {
9 | icon?: TypeFeatherIconNames
10 | iconSize?: number
11 | color?: string
12 | className?: string
13 | }
14 |
15 | const ProductButton: FC> = ({
16 | children,
17 | icon,
18 | iconSize,
19 | color,
20 | className,
21 | ...rest
22 | }) => {
23 | return (
24 |
25 |
31 | {children ? (
32 | children
33 | ) : (
34 |
35 | )}
36 |
37 |
38 | )
39 | }
40 |
41 | export default ProductButton
42 |
--------------------------------------------------------------------------------
/app/components/screens/profile/Profile.tsx:
--------------------------------------------------------------------------------
1 | import { FC } from 'react'
2 | import { Image, View } from 'react-native'
3 |
4 | import Layout from '@/components/layout/Layout'
5 | import Heading from '@/components/ui/Heading'
6 | import Button from '@/components/ui/button/Button'
7 |
8 | import { useAuth } from '@/hooks/useAuth'
9 |
10 | import { AuthService } from '@/services/auth/auth.service'
11 |
12 | import { useProfile } from './useProfile'
13 |
14 | const Profile: FC = () => {
15 | const { setUser } = useAuth()
16 |
17 | const { profile } = useProfile()
18 |
19 | return (
20 |
21 | Profile
22 |
23 |
24 |
28 |
29 |
30 |
36 |
37 | )
38 | }
39 |
40 | export default Profile
41 |
--------------------------------------------------------------------------------
/app/components/screens/product/product-info/AddToCartButton.tsx:
--------------------------------------------------------------------------------
1 | import { FC } from 'react'
2 |
3 | import Button from '@/components/ui/button/Button'
4 |
5 | import { useActions } from '@/hooks/useActions'
6 | import { useCart } from '@/hooks/useCart'
7 |
8 | import { IProduct } from '@/types/product.interface'
9 |
10 | interface IAddToCartButton {
11 | product: IProduct
12 | }
13 |
14 | const AddToCartButton: FC = ({ product }) => {
15 | const { addToCart, removeFromCart } = useActions()
16 | const { items } = useCart()
17 |
18 | const currentElement = items.find(
19 | cartItem => cartItem.product.id === product.id
20 | )
21 |
22 | return (
23 |
37 | )
38 | }
39 |
40 | export default AddToCartButton
41 |
--------------------------------------------------------------------------------
/app/components/ui/catalog/product-item/ProductItem.tsx:
--------------------------------------------------------------------------------
1 | import { FC } from 'react'
2 | import { Image, Pressable, View } from 'react-native'
3 |
4 | import { useTypedNavigation } from '@/hooks/useTypedNavigation'
5 |
6 | import { IProduct } from '@/types/product.interface'
7 |
8 | import { getMediaSource } from '@/utils/getMediaSource'
9 |
10 | import ProductInfo from './ProductInfo'
11 |
12 | interface IProductItem {
13 | product: IProduct
14 | }
15 |
16 | const ProductItem: FC = ({ product }) => {
17 | const { navigate } = useTypedNavigation()
18 |
19 | return (
20 |
21 | navigate('Product', { slug: product.slug })}
23 | className='bg-gray-100 rounded-xl relative overflow-hidden p-5 flex items-center justify-center'
24 | >
25 |
30 |
31 |
32 |
33 | )
34 | }
35 |
36 | export default ProductItem
37 |
--------------------------------------------------------------------------------
/app/components/screens/product/Product.tsx:
--------------------------------------------------------------------------------
1 | import { FC } from 'react'
2 | import { Image, View } from 'react-native'
3 |
4 | import Layout from '@/components/layout/Layout'
5 | import Loader from '@/components/ui/Loader'
6 |
7 | import { getMediaSource } from '@/utils/getMediaSource'
8 |
9 | import ProductHeader from './ProductHeader'
10 | import AddToCartButton from './product-info/AddToCartButton'
11 | import ProductInfo from './product-info/ProductInfo'
12 | import { useProduct } from './useProduct'
13 |
14 | const Product: FC = () => {
15 | const { isLoading, product } = useProduct()
16 |
17 | if (isLoading) return
18 | if (!product) return null
19 |
20 | return (
21 |
22 |
23 |
24 |
29 |
30 |
31 |
32 |
33 | )
34 | }
35 |
36 | export default Product
37 |
--------------------------------------------------------------------------------
/app/components/screens/search/Search.tsx:
--------------------------------------------------------------------------------
1 | import { FC } from 'react'
2 | import { View } from 'react-native'
3 |
4 | import Layout from '@/components/layout/Layout'
5 | import Heading from '@/components/ui/Heading'
6 | import Loader from '@/components/ui/Loader'
7 | import Catalog from '@/components/ui/catalog/Catalog'
8 | import Field from '@/components/ui/field/Field'
9 |
10 | import { ISearchFormData } from './search.interface'
11 | import { useSearch } from './useSearch'
12 |
13 | const Search: FC = () => {
14 | const { searchTerm, isLoading, control, products } = useSearch()
15 |
16 | return (
17 |
18 | Search
19 |
20 |
21 |
22 | placeholder='Type something...'
23 | control={control}
24 | name='searchTerm'
25 | keyboardType='web-search'
26 | />
27 |
28 | {!!searchTerm ? (
29 |
30 | {isLoading ? (
31 |
32 | ) : (
33 |
34 | )}
35 |
36 | ) : null}
37 |
38 | )
39 | }
40 |
41 | export default Search
42 |
--------------------------------------------------------------------------------
/app/components/screens/cart/cart-item/CartActions.tsx:
--------------------------------------------------------------------------------
1 | import { AntDesign } from '@expo/vector-icons'
2 | import { FC } from 'react'
3 | import { Pressable, Text, View } from 'react-native'
4 |
5 | import { useActions } from '@/hooks/useActions'
6 | import { useCart } from '@/hooks/useCart'
7 |
8 | import { ICartItem } from '@/types/cart.interface'
9 |
10 | interface ICartActions {
11 | item: ICartItem
12 | }
13 |
14 | const CartActions: FC = ({ item }) => {
15 | const { changeQuantity } = useActions()
16 |
17 | const { items } = useCart()
18 |
19 | const quantity = items.find(cartItem => cartItem.id === item.id)?.quantity
20 |
21 | return (
22 |
23 | changeQuantity({ id: item.id, type: 'minus' })}
25 | disabled={quantity === 1}
26 | >
27 |
28 |
29 | {quantity}
30 | changeQuantity({ id: item.id, type: 'plus' })}
32 | >
33 |
34 |
35 |
36 | )
37 | }
38 |
39 | export default CartActions
40 |
--------------------------------------------------------------------------------
/app/store/store.ts:
--------------------------------------------------------------------------------
1 | import AsyncStorage from '@react-native-async-storage/async-storage'
2 | import { combineReducers, configureStore } from '@reduxjs/toolkit'
3 | import {
4 | FLUSH,
5 | PAUSE,
6 | PERSIST,
7 | PURGE,
8 | PersistConfig,
9 | REGISTER,
10 | REHYDRATE,
11 | persistStore
12 | } from 'redux-persist'
13 | import persistReducer from 'redux-persist/es/persistReducer'
14 |
15 | import { cartSlice } from './cart/cart.slice'
16 |
17 | const persistConfig: PersistConfig = {
18 | key: 'root',
19 | storage: AsyncStorage,
20 | whitelist: ['cart']
21 | }
22 |
23 | const rootReducer = combineReducers({
24 | cart: cartSlice.reducer
25 | })
26 |
27 | const persistedReducer = persistReducer(
28 | persistConfig,
29 | rootReducer
30 | )
31 |
32 | export const store = configureStore({
33 | reducer: persistedReducer,
34 | middleware: getDefaultMiddleware =>
35 | getDefaultMiddleware({
36 | serializableCheck: {
37 | ignoredActions: [
38 | FLUSH,
39 | REHYDRATE,
40 | PAUSE,
41 | PERSIST,
42 | PURGE,
43 | REGISTER
44 | ]
45 | }
46 | })
47 | })
48 |
49 | export const persistor = persistStore(store)
50 |
51 | export type TypeRootState = ReturnType
52 |
--------------------------------------------------------------------------------
/app/components/screens/auth/AuthFields.tsx:
--------------------------------------------------------------------------------
1 | import { FC } from 'react'
2 | import { Control } from 'react-hook-form'
3 |
4 | import Field from '@/components/ui/field/Field'
5 |
6 | import { IAuthFormData } from '@/types/auth.interface'
7 |
8 | import { validEmail } from './email.regex'
9 |
10 | interface IAuthFields {
11 | control: Control
12 | isPassRequired?: boolean
13 | }
14 |
15 | const AuthFields: FC = ({ control, isPassRequired }) => {
16 | return (
17 | <>
18 |
19 | placeholder='Enter email'
20 | control={control}
21 | name='email'
22 | rules={{
23 | required: 'Email is required!',
24 | pattern: {
25 | value: validEmail,
26 | message: 'Please enter a valid email address'
27 | }
28 | }}
29 | keyboardType='email-address'
30 | />
31 |
32 | placeholder='Enter password'
33 | control={control}
34 | name='password'
35 | secureTextEntry
36 | rules={{
37 | required: 'Password is required!',
38 | minLength: {
39 | value: 6,
40 | message: 'Password should be minimum 6 characters long'
41 | }
42 | }}
43 | />
44 | >
45 | )
46 | }
47 |
48 | export default AuthFields
49 |
--------------------------------------------------------------------------------
/app/navigation/routes.ts:
--------------------------------------------------------------------------------
1 | import Cart from '@/components/screens/cart/Cart'
2 | import Category from '@/components/screens/category/Category'
3 | import Explorer from '@/components/screens/explorer/Explorer'
4 | import Favorites from '@/components/screens/favorites/Favorites'
5 | import Home from '@/components/screens/home/Home'
6 | import Product from '@/components/screens/product/Product'
7 | import Profile from '@/components/screens/profile/Profile'
8 | import Search from '@/components/screens/search/Search'
9 | import Thanks from '@/components/screens/thanks/Thanks'
10 |
11 | import { IRoute } from './navigation.types'
12 |
13 | export const routes: IRoute[] = [
14 | {
15 | name: 'Home',
16 | component: Home
17 | },
18 | {
19 | name: 'Favorites',
20 | component: Favorites
21 | },
22 | {
23 | name: 'Search',
24 | component: Search
25 | },
26 | {
27 | name: 'Explorer',
28 | component: Explorer
29 | },
30 | {
31 | name: 'Profile',
32 | component: Profile
33 | },
34 | {
35 | name: 'Cart',
36 | component: Cart
37 | },
38 | {
39 | name: 'Category',
40 | component: Category
41 | },
42 | {
43 | name: 'Product',
44 | component: Product
45 | },
46 | {
47 | name: 'Thanks',
48 | component: Thanks
49 | }
50 | ]
51 |
--------------------------------------------------------------------------------
/app/store/cart/cart.slice.ts:
--------------------------------------------------------------------------------
1 | import { PayloadAction, createSlice } from '@reduxjs/toolkit'
2 |
3 | import {
4 | IAddToCartPayload,
5 | ICartInitialState,
6 | IChangeQuantityPayload
7 | } from './cart.types'
8 |
9 | const initialState: ICartInitialState = {
10 | items: []
11 | }
12 |
13 | export const cartSlice = createSlice({
14 | name: 'cart',
15 | initialState,
16 | reducers: {
17 | addToCart: (state, action: PayloadAction) => {
18 | const isExist = state.items.some(
19 | item => item.product.id === action.payload.product.id
20 | )
21 |
22 | if (!isExist)
23 | state.items.push({
24 | ...action.payload,
25 | id: state.items.length.toString()
26 | })
27 | },
28 | removeFromCart: (state, action: PayloadAction<{ id: string }>) => {
29 | state.items = state.items.filter(
30 | item => item.id !== action.payload.id
31 | )
32 | },
33 | changeQuantity: (
34 | state,
35 | action: PayloadAction
36 | ) => {
37 | const { id, type } = action.payload
38 | const item = state.items.find(item => item.id === id)
39 | if (item) type === 'plus' ? item.quantity++ : item.quantity--
40 | },
41 | reset: state => {
42 | state.items = []
43 | }
44 | }
45 | })
46 |
--------------------------------------------------------------------------------
/app/components/ui/field/Field.tsx:
--------------------------------------------------------------------------------
1 | import cn from 'clsx'
2 | import { Controller } from 'react-hook-form'
3 | import { Text, TextInput, View } from 'react-native'
4 |
5 | import { IField } from './field.interface'
6 |
7 | const Field = >({
8 | control,
9 | rules,
10 | name,
11 | className,
12 | ...rest
13 | }: IField): JSX.Element => {
14 | return (
15 | (
23 | <>
24 |
30 |
39 |
40 | {error && (
41 | {error.message}
42 | )}
43 | >
44 | )}
45 | />
46 | )
47 | }
48 |
49 | export default Field
50 |
--------------------------------------------------------------------------------
/app/components/screens/cart/Cart.tsx:
--------------------------------------------------------------------------------
1 | import { FC } from 'react'
2 | import { Text, View } from 'react-native'
3 |
4 | import Layout from '@/components/layout/Layout'
5 | import Heading from '@/components/ui/Heading'
6 | import Button from '@/components/ui/button/Button'
7 |
8 | import { useCart } from '@/hooks/useCart'
9 |
10 | import { convertPrice } from '@/utils/convertPrice'
11 |
12 | import CartItem from './cart-item/CartItem'
13 | import { useCheckout } from './useCheckout'
14 |
15 | const Cart: FC = () => {
16 | const { items, total } = useCart()
17 | const { onCheckout } = useCheckout()
18 |
19 | return (
20 | <>
21 |
22 | Cart
23 |
24 | {items.length ? (
25 | items.map(item => {
26 | console.log(item.id);
27 | return ()
28 | })
29 | ) : (
30 | Product not found
31 | )}
32 |
33 |
34 | {items.length ? (
35 |
36 |
37 | Total: {convertPrice(total)}
38 |
39 |
40 |
41 | ) : null}
42 | >
43 | )
44 | }
45 |
46 | export default Cart
47 |
--------------------------------------------------------------------------------
/app/components/screens/auth/useAuthMutations.ts:
--------------------------------------------------------------------------------
1 | import { useMutation } from '@tanstack/react-query'
2 | import { useMemo } from 'react'
3 | import { UseFormReset } from 'react-hook-form'
4 |
5 | import { useAuth } from '@/hooks/useAuth'
6 |
7 | import { IAuthFormData } from '@/types/auth.interface'
8 |
9 | import { AuthService } from '@/services/auth/auth.service'
10 |
11 | export const useAuthMutations = (reset: UseFormReset) => {
12 | const { setUser } = useAuth()
13 |
14 | const { mutate: loginSync, isPending: isLoginLoading } = useMutation({
15 | mutationKey: ['login'],
16 | mutationFn: ({ email, password }: IAuthFormData) =>
17 | AuthService.main('login', email, password),
18 | onSuccess(data) {
19 | reset()
20 | setUser(data.user)
21 | }
22 | })
23 |
24 | const { mutate: registerSync, isPending: isRegisterLoading } = useMutation({
25 | mutationKey: ['register'],
26 | mutationFn: ({ email, password }: IAuthFormData) =>
27 | AuthService.main('reg', email, password),
28 |
29 | onSuccess(data) {
30 | reset()
31 | setUser(data.user)
32 | }
33 | })
34 |
35 | return useMemo(
36 | () => ({
37 | loginSync,
38 | registerSync,
39 | isLoading: isLoginLoading || isRegisterLoading
40 | }),
41 | [isLoginLoading, isRegisterLoading]
42 | )
43 | }
44 |
--------------------------------------------------------------------------------
/app/components/screens/cart/cart-item/CartItem.tsx:
--------------------------------------------------------------------------------
1 | import { FC } from 'react'
2 | import { Image, Pressable, Text, View } from 'react-native'
3 |
4 | import { useTypedNavigation } from '@/hooks/useTypedNavigation'
5 |
6 | import { ICartItem } from '@/types/cart.interface'
7 |
8 | import { convertPrice } from '@/utils/convertPrice'
9 | import { getMediaSource } from '@/utils/getMediaSource'
10 |
11 | import CartIActions from './CartActions'
12 |
13 | interface ICartItemProps {
14 | item: ICartItem
15 | }
16 |
17 | const CartItem: FC = ({ item }) => {
18 | const { navigate } = useTypedNavigation()
19 |
20 | return (
21 |
22 | navigate('Product', { slug: item.product.slug })}
24 | className='bg-gray-100 rounded-xl overflow-hidden py-3 px-3 items-center w-28'
25 | >
26 |
31 |
32 |
33 |
34 |
35 | {item.product.name}
36 |
37 | {convertPrice(item.price)}
38 |
39 |
40 |
41 | )
42 | }
43 |
44 | export default CartItem
45 |
--------------------------------------------------------------------------------
/app/navigation/Navigation.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | NavigationContainer,
3 | useNavigationContainerRef
4 | } from '@react-navigation/native'
5 | import { FC, useEffect, useState } from 'react'
6 |
7 | import BottomMenu from '@/components/layout/bottom-menu/BottomMenu'
8 |
9 | import { useAuth } from '@/hooks/useAuth'
10 |
11 | import { useCheckAuth } from '@/providers/useCheckAuth'
12 |
13 | import PrivateNavigator from './PrivateNavigator'
14 |
15 | const Navigation: FC = () => {
16 | const { user } = useAuth()
17 |
18 | const [currentRoute, setCurrentRoute] = useState(
19 | undefined
20 | )
21 |
22 | const navRef = useNavigationContainerRef()
23 |
24 | useEffect(() => {
25 | setCurrentRoute(navRef.getCurrentRoute()?.name)
26 |
27 | const listener = navRef.addListener('state', () =>
28 | setCurrentRoute(navRef.getCurrentRoute()?.name)
29 | )
30 |
31 | return () => {
32 | navRef.removeListener('state', listener)
33 | }
34 | }, [])
35 |
36 | useCheckAuth(currentRoute)
37 |
38 | return (
39 | <>
40 |
41 |
42 |
43 | {user && currentRoute && (
44 |
45 | )}
46 | >
47 | )
48 | }
49 |
50 | export default Navigation
51 |
--------------------------------------------------------------------------------
/app/providers/AuthProvider.tsx:
--------------------------------------------------------------------------------
1 | import * as SplashScreen from 'expo-splash-screen'
2 | import {
3 | FC,
4 | PropsWithChildren,
5 | createContext,
6 | useEffect,
7 | useState
8 | } from 'react'
9 |
10 | import { getAccessToken, getUserFromStorage } from '@/services/auth/auth.helper'
11 |
12 | import { IContext, TypeUserState } from './auth-provder.interface'
13 |
14 | export const AuthContext = createContext({} as IContext)
15 |
16 | let ignore = SplashScreen.preventAutoHideAsync()
17 |
18 | const AuthProvider: FC> = ({ children }) => {
19 | const [user, setUser] = useState(null)
20 |
21 | useEffect(() => {
22 | let isMounted = true
23 |
24 | const checkAccessToken = async () => {
25 | try {
26 | const accessToken = await getAccessToken()
27 |
28 | if (accessToken) {
29 | const user = await getUserFromStorage()
30 | if (isMounted) setUser(user)
31 | }
32 | } catch {
33 | } finally {
34 | await SplashScreen.hideAsync()
35 | }
36 | }
37 |
38 | let ignore = checkAccessToken()
39 |
40 | return () => {
41 | isMounted = false
42 | }
43 | }, [])
44 |
45 | return (
46 |
47 | {children}
48 |
49 | )
50 | }
51 |
52 | export default AuthProvider
53 |
--------------------------------------------------------------------------------
/app/providers/useCheckAuth.ts:
--------------------------------------------------------------------------------
1 | import { getItemAsync } from 'expo-secure-store'
2 | import { useEffect } from 'react'
3 | import { useAuth } from '@/hooks/useAuth'
4 | import { EnumSecureStore } from '@/types/auth.interface'
5 | import { errorCatch } from '@/services/api/error.api'
6 | import { getNewTokens } from '@/services/api/helper.auth'
7 | import { getAccessToken } from '@/services/auth/auth.helper'
8 | import { AuthService } from '@/services/auth/auth.service'
9 |
10 |
11 | export const useCheckAuth = (routeName?: string) => {
12 | const { user, setUser } = useAuth()
13 |
14 | useEffect(() => {
15 | const checkAccessToken = async () => {
16 | const accessToken = await getAccessToken()
17 | if (accessToken) {
18 | try {
19 | await getNewTokens()
20 | } catch (e) {
21 | if (errorCatch(e) === 'jwt expired') {
22 | await AuthService.logout()
23 | setUser(null)
24 | }
25 | }
26 | }
27 | }
28 |
29 | let ignore = checkAccessToken()
30 | }, [])
31 |
32 | useEffect(() => {
33 | const checkRefreshToken = async () => {
34 | const refreshToken = await getItemAsync(
35 | EnumSecureStore.REFRESH_TOKEN
36 | )
37 | if (!refreshToken && user) {
38 | await AuthService.logout()
39 | setUser(null)
40 | }
41 | }
42 |
43 | let ignore = checkRefreshToken()
44 | }, [routeName])
45 | }
46 |
--------------------------------------------------------------------------------
/app/services/auth/auth.helper.ts:
--------------------------------------------------------------------------------
1 | import AsyncStorage from '@react-native-async-storage/async-storage'
2 | import { deleteItemAsync, getItemAsync, setItemAsync } from 'expo-secure-store'
3 |
4 | import {
5 | EnumAsyncStorage,
6 | EnumSecureStore,
7 | IAuthResponse,
8 | ITokens
9 | } from '@/types/auth.interface'
10 |
11 | export const getAccessToken = async () => {
12 | const accessToken = await getItemAsync(EnumSecureStore.ACCESS_TOKEN)
13 | return accessToken || null
14 | }
15 |
16 | export const saveTokensStorage = async (data: ITokens) => {
17 | await setItemAsync(EnumSecureStore.ACCESS_TOKEN, data.accessToken)
18 | await setItemAsync(EnumSecureStore.REFRESH_TOKEN, data.refreshToken)
19 | }
20 |
21 | export const deleteTokensStorage = async () => {
22 | await deleteItemAsync(EnumSecureStore.ACCESS_TOKEN)
23 | await deleteItemAsync(EnumSecureStore.REFRESH_TOKEN)
24 | }
25 |
26 | export const getUserFromStorage = async () => {
27 | try {
28 | return JSON.parse(
29 | (await AsyncStorage.getItem(EnumAsyncStorage.USER)) || '{}'
30 | )
31 | } catch (e) {
32 | return null
33 | }
34 | }
35 |
36 | export const saveToStorage = async (data: IAuthResponse) => {
37 | await saveTokensStorage(data)
38 | try {
39 | await AsyncStorage.setItem(
40 | EnumAsyncStorage.USER,
41 | JSON.stringify(data.user)
42 | )
43 | } catch (e) {}
44 | }
45 |
--------------------------------------------------------------------------------
/app/services/api/interceptors.api.ts:
--------------------------------------------------------------------------------
1 | import axios from 'axios'
2 |
3 | import {
4 | deleteTokensStorage,
5 | getAccessToken
6 | } from '@/services/auth/auth.helper'
7 |
8 | import { API_URL } from '@/config/api.config'
9 |
10 | import { errorCatch } from './error.api'
11 | import { getNewTokens } from './helper.auth'
12 |
13 | const instance = axios.create({
14 | baseURL: API_URL,
15 | headers: {
16 | 'Content-Type': 'application/json'
17 | }
18 | })
19 |
20 | instance.interceptors.request.use(async config => {
21 | const accessToken = await getAccessToken()
22 |
23 | if (config.headers && accessToken)
24 | config.headers.Authorization = `Bearer ${accessToken}`
25 |
26 | return config
27 | })
28 |
29 | instance.interceptors.response.use(
30 | config => config,
31 | async error => {
32 | const originalRequest = error.config
33 |
34 | if (
35 | (error.response.status === 401 ||
36 | errorCatch(error) === 'jwt expired' ||
37 | errorCatch(error) === 'jwt must be provided') &&
38 | error.config &&
39 | !error.config._isRetry
40 | ) {
41 | originalRequest._isRetry = true
42 | try {
43 | await getNewTokens()
44 | return instance.request(originalRequest)
45 | } catch (error) {
46 | if (errorCatch(error) === 'jwt expired')
47 | await deleteTokensStorage()
48 | }
49 | }
50 |
51 | throw error
52 | }
53 | )
54 | export default instance
55 |
--------------------------------------------------------------------------------
/app/components/screens/product/favorite-button/FavoriteButton.tsx:
--------------------------------------------------------------------------------
1 | import { MaterialCommunityIcons } from '@expo/vector-icons'
2 | import { useMutation, useQueryClient } from '@tanstack/react-query'
3 | import { FC } from 'react'
4 |
5 | import { UserService } from '@/services/user.service'
6 |
7 | import { useProfile } from '../../profile/useProfile'
8 | import ProductButton from '../ProductButton'
9 |
10 | interface IFavoriteButton {
11 | productId: string
12 | }
13 |
14 | const FavoriteButton: FC = ({ productId }) => {
15 | const { profile } = useProfile()
16 |
17 | const queryClient = useQueryClient()
18 |
19 | const { mutate } = useMutation({
20 | mutationKey: ['toggle favorite'],
21 | mutationFn: () => UserService.toggleFavorite(productId),
22 |
23 | onSuccess() {
24 | queryClient.invalidateQueries({ queryKey: ['get profile'] })
25 | }
26 | })
27 |
28 | if (!profile) return null
29 |
30 | const isExists = profile.favorites.some(
31 | favorite => favorite.id === productId
32 | )
33 |
34 | return (
35 | mutate()}>
36 | {isExists ? (
37 |
42 | ) : (
43 |
48 | )}
49 |
50 | )
51 | }
52 |
53 | export default FavoriteButton
54 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "client",
3 | "version": "1.0.0",
4 | "scripts": {
5 | "start": "expo start --dev-client",
6 | "android": "expo run:android",
7 | "ios": "expo run:ios",
8 | "web": "expo start --web"
9 | },
10 | "dependencies": {
11 | "@expo/vector-icons": "^14.0.2",
12 | "@react-native-async-storage/async-storage": "^1.23.1",
13 | "@react-native/metro-config": "^0.74.85",
14 | "@react-navigation/native": "^6.1.17",
15 | "@react-navigation/native-stack": "^6.10.0",
16 | "@reduxjs/toolkit": "^2.2.6",
17 | "@stripe/stripe-react-native": "^0.38.1",
18 | "@tanstack/react-query": "^5.50.1",
19 | "@trivago/prettier-plugin-sort-imports": "^4.3.0",
20 | "axios": "^1.7.2",
21 | "babel-plugin-inline-dotenv": "^1.7.0",
22 | "babel-plugin-root-import": "^6.6.0",
23 | "clsx": "^2.1.1",
24 | "expo": "~51.0.18",
25 | "expo-secure-store": "^13.0.2",
26 | "expo-splash-screen": "^0.27.5",
27 | "expo-status-bar": "~1.12.1",
28 | "nativewind": "^2.0.11",
29 | "prettier": "^3.3.2",
30 | "react": "18.2.0",
31 | "react-hook-form": "^7.52.1",
32 | "react-native": "0.74.3",
33 | "react-native-safe-area-context": "4.10.1",
34 | "react-native-screens": "3.31.1",
35 | "react-native-toast-message": "^2.2.0",
36 | "react-redux": "^9.1.2",
37 | "redux-persist": "^6.0.0"
38 | },
39 | "devDependencies": {
40 | "@babel/core": "^7.20.0",
41 | "@types/node": "^20.14.9",
42 | "@types/react": "~18.2.45",
43 | "tailwindcss": "3.3.2",
44 | "typescript": "^5.1.3"
45 | },
46 | "private": true
47 | }
48 |
--------------------------------------------------------------------------------
/app/components/screens/home/categories/Categories.tsx:
--------------------------------------------------------------------------------
1 | import { FC } from 'react'
2 | import { Image, Pressable, ScrollView, Text, View } from 'react-native'
3 |
4 | import Heading from '@/components/ui/Heading'
5 | import Loader from '@/components/ui/Loader'
6 |
7 | import { useTypedNavigation } from '@/hooks/useTypedNavigation'
8 |
9 | import { getMediaSource } from '@/utils/getMediaSource'
10 |
11 | import { useGetAllCategories } from './useGetAllCategories'
12 |
13 | const Categories: FC = () => {
14 | const { categories, isLoading } = useGetAllCategories()
15 |
16 | const { navigate } = useTypedNavigation()
17 |
18 | return isLoading ? (
19 |
20 | ) : (
21 |
22 |
23 | Categories
24 |
25 |
30 | {categories?.map(category => (
31 |
33 | navigate('Category', { slug: category.slug })
34 | }
35 | key={category.id}
36 | className='rounded-xl bg-gray-100 p-2 w-[100]'
37 | >
38 |
45 |
46 | {category.name}
47 |
48 |
49 | ))}
50 |
51 |
52 | )
53 | }
54 |
55 | export default Categories
56 |
--------------------------------------------------------------------------------
/app/components/screens/cart/useCheckout.ts:
--------------------------------------------------------------------------------
1 | import { useStripe } from '@stripe/stripe-react-native'
2 | import { useMutation } from '@tanstack/react-query'
3 |
4 | import { useActions } from '@/hooks/useActions'
5 | import { useAuth } from '@/hooks/useAuth'
6 | import { useCart } from '@/hooks/useCart'
7 | import { useTypedNavigation } from '@/hooks/useTypedNavigation'
8 |
9 | import { OrderService } from '@/services/order.service'
10 |
11 | export const useCheckout = () => {
12 | const { items, total } = useCart()
13 | const { user } = useAuth()
14 | const { reset } = useActions()
15 | const { navigate } = useTypedNavigation()
16 |
17 | const { initPaymentSheet, presentPaymentSheet } = useStripe()
18 |
19 | const { mutateAsync: placeOrder } = useMutation({
20 | mutationKey: ['place order'],
21 | mutationFn: () =>
22 | OrderService.place({
23 | items: items.map(item => ({
24 | price: item.price,
25 | quantity: item.quantity,
26 | productId: item.product.id
27 | }))
28 | })
29 | })
30 |
31 | const onCheckout = async () => {
32 | try {
33 | const { clientSecret } = await placeOrder()
34 |
35 | const { error } = await initPaymentSheet({
36 | merchantDisplayName: 'Your Merchant Name',
37 | paymentIntentClientSecret: clientSecret
38 | })
39 |
40 | if (error) {
41 | console.error('Error initializing payment sheet:', error)
42 | return
43 | }
44 |
45 | const { error: paymentError } = await presentPaymentSheet()
46 | if (paymentError) {
47 | console.error('Error presenting payment sheet:', paymentError)
48 | return
49 | }
50 |
51 | reset()
52 | navigate('Thanks')
53 | } catch (error) {
54 | console.error('Checkout error:', error)
55 | }
56 | }
57 |
58 | return { onCheckout }
59 | }
60 |
--------------------------------------------------------------------------------
/App.tsx:
--------------------------------------------------------------------------------
1 | import { StripeProvider } from '@stripe/stripe-react-native'
2 | import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
3 | import { StatusBar } from 'expo-status-bar'
4 | import { SafeAreaProvider } from 'react-native-safe-area-context'
5 | import { Provider } from 'react-redux'
6 | import { PersistGate } from 'redux-persist/integration/react'
7 |
8 | import Toast from '@/components/ui/Toast'
9 |
10 | import AuthProvider from '@/providers/AuthProvider'
11 |
12 | import Navigation from '@/navigation/Navigation'
13 |
14 | import { persistor, store } from '@/store/store'
15 | import AsyncStorage from '@react-native-async-storage/async-storage'
16 | import {log} from "expo/build/devtools/logger";
17 |
18 | const queryClient = new QueryClient({
19 | defaultOptions: {
20 | queries: {
21 | refetchOnWindowFocus: false
22 | }
23 | }
24 | })
25 |
26 | // queryClient.clear();
27 | // const clearStorage = async () => {
28 | // try {
29 | // await AsyncStorage.clear();
30 | // console.log('Storage successfully cleared!');
31 | // } catch (e) {
32 | // console.log('Failed to clear the async storage.');
33 | // }
34 | // };
35 | //
36 | // // Викликайте цю функцію в потрібному місці вашого додатку
37 | // clearStorage();
38 |
39 | export default function App() {
40 | return (
41 |
42 |
43 |
44 |
45 |
46 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 | )
61 | }
62 |
--------------------------------------------------------------------------------
/app/components/screens/auth/Auth.tsx:
--------------------------------------------------------------------------------
1 | import { FC, useState } from 'react'
2 | import { SubmitHandler, useForm } from 'react-hook-form'
3 | import { Pressable, Text, View } from 'react-native'
4 |
5 | import Loader from '@/components/ui/Loader'
6 | import Button from '@/components/ui/button/Button'
7 |
8 | import { IAuthFormData } from '@/types/auth.interface'
9 |
10 | import AuthFields from './AuthFields'
11 | import { useAuthMutations } from './useAuthMutations'
12 |
13 | const Auth: FC = () => {
14 | const [isReg, setIsReg] = useState(false)
15 |
16 | const { handleSubmit, reset, control } = useForm({
17 | mode: 'onChange'
18 | })
19 |
20 | const { isLoading, registerSync, loginSync } = useAuthMutations(reset)
21 |
22 | const onSubmit: SubmitHandler = data => {
23 | if (isReg) registerSync(data)
24 | else loginSync(data)
25 | }
26 |
27 | return (
28 |
29 |
30 |
31 | {isReg ? 'Sign Up' : 'Login'}
32 |
33 | {isLoading ? (
34 |
35 | ) : (
36 | <>
37 |
38 |
39 |
42 |
43 | setIsReg(!isReg)}>
44 |
45 | {isReg
46 | ? 'Already have an account? '
47 | : "Don't have an account? "}
48 |
49 | {isReg ? 'Login' : 'Sign up'}
50 |
51 |
52 |
53 | >
54 | )}
55 |
56 |
57 | )
58 | }
59 |
60 | export default Auth
61 |
--------------------------------------------------------------------------------