Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@
"@expo/metro-runtime": "^3.2.3",
"@gorhom/bottom-sheet": "4.6.3",
"@hookform/resolvers": "^3.9.0",
"@lukemorales/query-key-factory": "^1.3.4",
"@shopify/flash-list": "1.6.4",
"@tanstack/react-query": "^5.52.1",
"app-icon-badge": "^0.0.15",
Expand Down
15 changes: 15 additions & 0 deletions pnpm-lock.yaml

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

37 changes: 37 additions & 0 deletions src/api/auth/use-login.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { createMutation } from 'react-query-kit';

import { client } from '../common';

type Variables = {
email?: string; // optional because API doesn't require email
username: string;
password: string;
expiresInMins?: number;
};

type Response = {
id: number;
username: string;
email: string;
accessToken: string;
refreshToken: string;
};

const login = async (variables: Variables) => {
const { data } = await client({
url: 'auth/login',
method: 'POST',
data: {
username: variables.username,
password: variables.password,
},
headers: {
'Content-Type': 'application/json',
},
});
return data;
};

export const useLogin = createMutation<Response, Variables>({
mutationFn: (variables) => login(variables),
});
6 changes: 6 additions & 0 deletions src/api/carts/query-keys.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { createQueryKeys } from '@lukemorales/query-key-factory';

export const cartKeys = createQueryKeys('carts', {
list: (filters) => [filters],
detail: (id) => [id],
});
19 changes: 19 additions & 0 deletions src/api/carts/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
export type Product = {
id: number;
title: string;
price: number;
quantity: number;
total: number;
discountPercentage: number;
discountedTotal: number;
thumbnail: string;
};
export type Cart = {
id: number;
products: Product[];
total: number;
discountedTotal: number;
userId: number;
totalProducts: number;
totalQuantity: number;
};
36 changes: 36 additions & 0 deletions src/api/carts/use-carts.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { useInfiniteQuery } from '@tanstack/react-query';

import { client } from '../common';
import { cartKeys } from './query-keys';

type Variables = {
limit?: number | string;
offset?: number | string;
};

const DEFAULT_LIMIT = 10;
const DEFAULT_OFFSET = 0;

const getCarts = async ({
limit = DEFAULT_LIMIT,
offset = DEFAULT_OFFSET,
}: Variables) => {
const { data } = await client.get('carts', {
params: {
limit,
offset,
},
});
return { data, offset: Number(offset), limit: Number(limit) };
};

export const useCarts = (variables: Variables) =>
useInfiniteQuery({
...cartKeys.list(variables),
queryFn: () => getCarts(variables),
initialPageParam: { offset: DEFAULT_OFFSET, limit: DEFAULT_LIMIT },
getNextPageParam: (lastPage) => {
const { offset, limit } = lastPage;
return { offset: offset + limit, limit };
},
});
8 changes: 7 additions & 1 deletion src/api/common/interceptors.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,19 @@
import type { AxiosError, InternalAxiosRequestConfig } from 'axios';

import { useAuth } from '@/core';

import { client } from './client';
import { toCamelCase, toSnakeCase } from './utils';

export default function interceptors() {
const token = useAuth.getState().token;
client.interceptors.request.use((config: InternalAxiosRequestConfig) => {
if (config.data) {
config.data = toSnakeCase(config.data);
}
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
});

Expand All @@ -16,6 +22,6 @@ export default function interceptors() {
response.data = toCamelCase(response.data);
return response;
},
(error: AxiosError) => Promise.reject(error)
(error: AxiosError) => Promise.reject(error),
);
}
1 change: 1 addition & 0 deletions src/api/posts/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export * from './types';
export * from './use-add-post';
export * from './use-post';
export * from './use-post-comments';
export * from './use-posts';
12 changes: 12 additions & 0 deletions src/api/posts/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,15 @@ export type Post = {
title: string;
body: string;
};

export type Comment = {
id: number;
body: string;
postId: number;
likes: number;
user: {
id: number;
username: string;
fullName: string;
};
};
3 changes: 3 additions & 0 deletions src/api/posts/use-add-post.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,8 @@ export const useAddPost = createMutation<Response, Variables, AxiosError>({
url: 'posts/add',
method: 'POST',
data: variables,
headers: {
'Content-Type': 'application/json',
},
}).then((response) => response.data),
});
24 changes: 24 additions & 0 deletions src/api/posts/use-post-comments.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { createQuery } from 'react-query-kit';

import { queryFactory } from '@/api/query-factory';

import { client } from '../common';
import { type Comment } from './types';

type Variables = {
id?: number;
};

type Response = {
comments: Comment[];
};

const getPostComments = async (id?: number) => {
const { data } = await client.get(`posts/${id}/comments`);
return data;
};

export const usePostComments = createQuery<Response, Variables>({
...queryFactory.posts.detail(1)._ctx.comments,
fetcher: (variables) => getPostComments(variables?.id),
});
13 changes: 9 additions & 4 deletions src/api/posts/use-post.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,20 @@
import type { AxiosError } from 'axios';
import { createQuery } from 'react-query-kit';

import { queryFactory } from '@/api/query-factory';

import { client } from '../common';
import type { Post } from './types';

type Variables = { id: string };
type Response = Post;

const getPosts = async (variables: Variables) => {
const { data } = await client.get(`posts/${variables.id}`);
return data;
};

export const usePost = createQuery<Response, Variables, AxiosError>({
queryKey: ['posts'],
fetcher: (variables) => client
.get(`posts/${variables.id}`)
.then((response) => response.data),
...queryFactory.posts.detail(1),
fetcher: getPosts,
});
5 changes: 4 additions & 1 deletion src/api/posts/use-posts.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
import type { AxiosError } from 'axios';
import { createQuery } from 'react-query-kit';

import { queryFactory } from '@/api/query-factory';

import { client } from '../common';
import type { Post } from './types';

type Response = Post[];
type Variables = void; // as react-query-kit is strongly typed, we need to specify the type of the variables as void in case we don't need them

export const usePosts = createQuery<Response, Variables, AxiosError>({
queryKey: ['posts'],
// old queryKey: ['posts'],
...queryFactory.posts.list({}), // this translates to ['posts', filters]
fetcher: () => client.get(`posts`).then((response) => response.data.posts),
});
28 changes: 28 additions & 0 deletions src/api/query-factory.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import {
createQueryKeys,
mergeQueryKeys,
} from '@lukemorales/query-key-factory';

import { cartKeys } from './carts/query-keys';
type Filters = {
limit?: number;
offset?: number;
};
export const postKeys = createQueryKeys('posts', {
list: (filters: Filters) => [filters],
detail: (id) => ({
queryKey: [id],
contextQueries: {
comments: {
queryKey: null,
},
},
}),
});

const productsKeys = createQueryKeys('products', {
list: (filters) => [filters],
detail: (id) => [id],
});

export const queryFactory = mergeQueryKeys(postKeys, cartKeys, productsKeys);
25 changes: 17 additions & 8 deletions src/app/(app)/_layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
Feed as FeedIcon,
Settings as SettingsIcon,
Style as StyleIcon,
Support as SupportIcon,
} from '@/ui/icons';

export default function TabLayout() {
Expand All @@ -17,7 +18,7 @@ export default function TabLayout() {
await SplashScreen.hideAsync();
}, []);
useEffect(() => {
const TIMEOUT = 1000
const TIMEOUT = 1000;
if (status !== 'idle') {
setTimeout(() => {
hideSplash();
Expand All @@ -42,7 +43,15 @@ export default function TabLayout() {
tabBarTestID: 'feed-tab',
}}
/>

<Tabs.Screen
name="carts"
options={{
title: 'Carts',
headerShown: true,
tabBarIcon: ({ color }) => <SupportIcon color={color} />,
tabBarTestID: 'carts-tab',
}}
/>
<Tabs.Screen
name="style"
options={{
Expand All @@ -66,9 +75,9 @@ export default function TabLayout() {
}

const CreateNewPostLink = () => (
<Link href="/feed/add-post" asChild>
<Pressable>
<Text className="px-3 text-primary-300">Create</Text>
</Pressable>
</Link>
);
<Link href="/feed/add-post" asChild>
<Pressable>
<Text className="px-3 text-primary-300">Create</Text>
</Pressable>
</Link>
);
28 changes: 28 additions & 0 deletions src/app/(app)/carts.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { FlashList } from '@shopify/flash-list';
import React from 'react';

import { type Cart } from '@/api/carts/types';
import { useCarts } from '@/api/carts/use-carts';
import CartItem from '@/components/cart-item/cart-item';
import { ActivityIndicator, FocusAwareStatusBar, Text, View } from '@/ui';

export default function Carts() {
const { data, isLoading } = useCarts({ limit: 10, offset: 0 });

return (
<View className="flex-1">
<FocusAwareStatusBar />
{isLoading ? (
<ActivityIndicator />
) : (
<FlashList<Cart>
data={data?.pages.flatMap((page) => page.data.carts)}
renderItem={({ item }) => <CartItem cart={item} />}
ListEmptyComponent={<Text>No carts</Text>}
estimatedItemSize={30}
keyExtractor={(_, index) => `item-${index}`}
/>
)}
</View>
);
}
3 changes: 2 additions & 1 deletion src/app/(app)/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,10 @@ import { EmptyList, FocusAwareStatusBar, Text, View } from '@/ui';

export default function Feed() {
const { data, isPending, isError } = usePosts();

const renderItem = useCallback(
({ item }: { item: Post }) => <Card {...item} />,
[]
[],
);

if (isError) {
Expand Down
Loading