Skip to content

Commit

Permalink
Unstyled cart screen in client global state with AsyncStorage
Browse files Browse the repository at this point in the history
  • Loading branch information
Diego Romero committed Nov 25, 2023
1 parent 828a7df commit adea84a
Show file tree
Hide file tree
Showing 10 changed files with 185 additions and 25 deletions.
1 change: 1 addition & 0 deletions apps/expo/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
"@acme/tailwind-config": "*",
"@clerk/clerk-expo": "^0.17.7",
"@hookform/resolvers": "^3.3.2",
"@react-native-async-storage/async-storage": "^1.19.8",
"@react-navigation/bottom-tabs": "^6.5.11",
"@react-navigation/native": "^6.1.9",
"@react-navigation/native-stack": "^6.9.17",
Expand Down
12 changes: 3 additions & 9 deletions apps/expo/src/_app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,13 @@ import { tokenCache } from "./utils/cache";
import Constants from "expo-constants";
import HomeScreen from "./screens/home";
import { TRPCProvider } from "./utils/trpc";
import { NavigationContainer, NavigationProp } from "@react-navigation/native";
import { NavigationContainer } from "@react-navigation/native";
import SingleItemScreen from "./screens/single-item";
import { createNativeStackNavigator } from "@react-navigation/native-stack";
import { Provider } from "jotai";
import type { RootStackParamList } from "./navigation/types";

export type MainNavigationStackParams = {
HomeScreen: undefined;
SingleItemScreen: { id: number };
};

export type MainNavigationStack = NavigationProp<MainNavigationStackParams>;

const Stack = createNativeStackNavigator<MainNavigationStackParams>();
const Stack = createNativeStackNavigator<RootStackParamList>();

function App() {
return (
Expand Down
37 changes: 37 additions & 0 deletions apps/expo/src/atoms/cart.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { atom } from "jotai";
import { atomFamily, atomWithStorage, createJSONStorage } from "jotai/utils";
import { singleProductAtomFamily } from "./products";
import AsyncStorage from "@react-native-async-storage/async-storage";

export const cartAtom = atomWithStorage<Record<number, number>>(
"cart",
{},
createJSONStorage(() => AsyncStorage),
);

export const cartItemQuantityAtomFamily = atomFamily((id: number) =>
atom(async (get) => {
return (await get(cartAtom))[id] ?? 0;
}),
);

export const addItemToCartAtom = atomFamily(
({ id, quantity }: { id: number; quantity: number }) =>
atom(null, async (get, set) => {
const currentCart = await get(cartAtom);
const currentQuantity = currentCart[id];
const currentItemStock =
get(singleProductAtomFamily(id))?.stock.toNumber() ?? 0;

const q = currentQuantity ? currentQuantity + quantity : quantity;

if (q <= currentItemStock) {
set(cartAtom, { ...currentCart, [id]: q });
} else if (q < 0) {
// prevents negative quantity
set(cartAtom, { ...currentCart, [id]: 0 });
} else {
set(cartAtom, { ...currentCart, [id]: currentItemStock });
}
}),
);
15 changes: 15 additions & 0 deletions apps/expo/src/components/CartItem.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { useAtomValue } from "jotai";
import { singleProductAtomFamily } from "../atoms/products";
import { Text } from "react-native";

function CartItem({ id, quantity }: { id: number; quantity: number }) {
const item = useAtomValue(singleProductAtomFamily(id));

return (
<Text>
{item?.name} : {quantity}
</Text>
);
}

export default CartItem;
3 changes: 1 addition & 2 deletions apps/expo/src/components/ProductCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,14 @@ import { useNavigation } from "@react-navigation/native";
import type { inferProcedureOutput } from "@trpc/server";
import { memo } from "react";
import { View, Image, Text, TouchableOpacity } from "react-native";
import { MainNavigationStack } from "../_app";

export interface ProductCardProps {
item: inferProcedureOutput<AppRouter["item"]["byId"]>;
}

function ProductCard({ item }: ProductCardProps) {
const hasStock = item.stock.greaterThan(0);
const navigator = useNavigation<MainNavigationStack>();
const navigator = useNavigation();

return (
<TouchableOpacity
Expand Down
33 changes: 33 additions & 0 deletions apps/expo/src/navigation/types.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { BottomTabScreenProps } from "@react-navigation/bottom-tabs";
import {
CompositeScreenProps,
NavigatorScreenParams,
} from "@react-navigation/native";
import { StackScreenProps } from "@react-navigation/stack";

export type RootStackParamList = {
HomeScreen: NavigatorScreenParams<HomeTabParamList>;
SingleItemScreen: { id: number };
};

export type RootStackScreenProps<T extends keyof RootStackParamList> =
StackScreenProps<RootStackParamList, T>;

export type HomeTabParamList = {
Search: undefined;
Cart: undefined;
"My Profile": undefined;
};

export type HomeTabScreenProps<T extends keyof HomeTabParamList> =
CompositeScreenProps<
BottomTabScreenProps<HomeTabParamList, T>,
RootStackScreenProps<keyof RootStackParamList>
>;

declare global {
namespace ReactNavigation {
// eslint-disable-next-line @typescript-eslint/no-empty-interface
interface RootParamList extends RootStackParamList {}
}
}
17 changes: 17 additions & 0 deletions apps/expo/src/screens/CartScreen.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { useAtomValue } from "jotai";
import { cartAtom } from "../atoms/cart";
import CartItem from "../components/CartItem";

function CartScreen() {
const cart = useAtomValue(cartAtom);

return (
<>
{Object.entries(cart).map(([key, value]) => (
<CartItem id={Number(key)} key={key} quantity={value} />
))}
</>
);
}

export default CartScreen;
16 changes: 16 additions & 0 deletions apps/expo/src/screens/home.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@ import { createBottomTabNavigator } from "@react-navigation/bottom-tabs";
import ItemListScreen from "./item-list";
import ProfileScreen from "./profile";
import { SafeAreaView } from "react-native-safe-area-context";
import { useAtomValue } from "jotai";
import { cartAtom } from "../atoms/cart";
import CartItem from "../components/CartItem";

const Tab = createBottomTabNavigator();

Expand All @@ -10,10 +13,23 @@ function HomeScreen() {
<SafeAreaView className="h-full">
<Tab.Navigator screenOptions={{ headerShown: false }}>
<Tab.Screen name="Search" component={ItemListScreen} />
<Tab.Screen name="Cart" component={CartScreen} />
<Tab.Screen name="My Profile" component={ProfileScreen} />
</Tab.Navigator>
</SafeAreaView>
);
}

function CartScreen() {
const cart = useAtomValue(cartAtom);

return (
<>
{Object.entries(cart).map(([key, value]) => (
<CartItem id={Number(key)} key={key} quantity={value} />
))}
</>
);
}

export default HomeScreen;
52 changes: 38 additions & 14 deletions apps/expo/src/screens/single-item.tsx
Original file line number Diff line number Diff line change
@@ -1,22 +1,24 @@
import { StackScreenProps } from "@react-navigation/stack";
import { MainNavigationStack, MainNavigationStackParams } from "../_app";
import { Image, Text, View } from "react-native";
import { useNavigation } from "@react-navigation/native";
import { Image, Pressable, Text, View } from "react-native";
import { useEffect } from "react";
import { useAtomValue } from "jotai";
import { useAtomValue, useSetAtom } from "jotai";
import { singleProductAtomFamily } from "../atoms/products";
import { addItemToCartAtom, cartItemQuantityAtomFamily } from "../atoms/cart";
import type { RootStackScreenProps } from "../navigation/types";

export type SingleItemScreenProps = StackScreenProps<
MainNavigationStackParams,
"SingleItemScreen"
>;

function SingleItemScreen({ route }: SingleItemScreenProps) {
const nav = useNavigation<MainNavigationStack>();
function SingleItemScreen({
route,
navigation,
}: RootStackScreenProps<"SingleItemScreen">) {
const item = useAtomValue(singleProductAtomFamily(route.params.id));
const cartQuantity = useAtomValue(
cartItemQuantityAtomFamily(route.params.id),
);
const addToCart = useSetAtom(
addItemToCartAtom({ id: route.params.id, quantity: 1 }),
);

useEffect(() => {
nav.setOptions({ title: item?.name ?? "Loading Item" });
navigation.setOptions({ title: item?.name ?? "Loading Item" });
}, [item?.name]);

if (!item) {
Expand Down Expand Up @@ -61,10 +63,16 @@ function SingleItemScreen({ route }: SingleItemScreenProps) {
{item.stock.toNumber().toFixed(2)}
</Text>
) : (
<Text className="text-base font-normal">0.00</Text>
<Text className="text-base font-normal"> 0.00</Text>
)}
</Text>
</View>
<View>
<Text className="text-lg font-semibold">
In Cart:
<Text className="font-normal"> {cartQuantity.toFixed(2)}</Text>
</Text>
</View>
<View>
<Text className="text-lg font-semibold">Description:</Text>

Expand All @@ -77,6 +85,22 @@ function SingleItemScreen({ route }: SingleItemScreenProps) {
)}
</View>
</View>
<Pressable
className="mt-2.5 rounded bg-emerald-500 p-2 active:bg-emerald-600"
onPress={() => addToCart()}
>
<Text className="self-center text-base font-medium text-white">
Add to cart
</Text>
</Pressable>
<Pressable
className="mt-2.5 rounded border-2 border-emerald-500 p-2"
onPress={() => navigation.navigate("HomeScreen", { screen: "Cart" })}
>
<Text className="self-center text-base font-medium text-emerald-700">
Go to cart
</Text>
</Pressable>
</View>
);
}
Expand Down
24 changes: 24 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit adea84a

Please sign in to comment.