From 6399ddb85daf84322b34299567bdce18f8be63ed Mon Sep 17 00:00:00 2001 From: mrevanzak Date: Sat, 4 Nov 2023 23:25:40 +0700 Subject: [PATCH] feat: product on search screen --- apps/expo/package.json | 3 +- apps/expo/src/app/(app)/search.tsx | 65 +++++++++++++++++-- apps/expo/src/components/Header.tsx | 14 ++-- apps/expo/src/lib/hooks/useDebouncedValues.ts | 36 ++++++++++ apps/expo/src/lib/stores/useSearchStore.ts | 11 ++++ apps/expo/src/utils/rupiahFormatter.ts | 7 ++ packages/api/src/router/product.ts | 17 +++-- packages/db/schema/user.ts | 2 +- pnpm-lock.yaml | 23 +++++++ 9 files changed, 156 insertions(+), 22 deletions(-) create mode 100644 apps/expo/src/lib/hooks/useDebouncedValues.ts create mode 100644 apps/expo/src/lib/stores/useSearchStore.ts create mode 100644 apps/expo/src/utils/rupiahFormatter.ts diff --git a/apps/expo/package.json b/apps/expo/package.json index ab544e2..a562446 100644 --- a/apps/expo/package.json +++ b/apps/expo/package.json @@ -40,7 +40,8 @@ "react-native-safe-area-context": "4.6.3", "react-native-screens": "~3.22.1", "react-native-ui-lib": "^7.9.1", - "superjson": "1.13.1" + "superjson": "1.13.1", + "zustand": "^4.4.6" }, "devDependencies": { "@babel/core": "^7.23.2", diff --git a/apps/expo/src/app/(app)/search.tsx b/apps/expo/src/app/(app)/search.tsx index 077f2be..5595563 100644 --- a/apps/expo/src/app/(app)/search.tsx +++ b/apps/expo/src/app/(app)/search.tsx @@ -1,15 +1,66 @@ -import { api } from "@/utils/api"; import React from "react"; -import { Text, View } from "react-native-ui-lib"; +import { Avatar, BorderRadiuses, Card, Text, View } from "react-native-ui-lib"; +import { api } from "@/utils/api"; +import rupiahFormatter from "@/utils/rupiahFormatter"; +import { FlashList } from "@shopify/flash-list"; +import { useSearchStore } from "@/lib/stores/useSearchStore"; +import { useDebouncedValue } from "@/lib/hooks/useDebouncedValues"; export default function SearchScreen() { - const { data, isLoading, error } = api.product.getProduct.useQuery(); - - console.log(data, isLoading, error); + const search = useSearchStore((state) => state.search); + const [debouncedSearch] = useDebouncedValue(search, 500); + const { data, isFetching, refetch } = api.product.getProduct.useQuery(debouncedSearch); + return ( - - Produk + + + Produk + + refetch()} + refreshing={isFetching} + renderItem={({ item }) => { + return ( + + + + {item.name} + {rupiahFormatter(item.price)} + + + + + {item.user.name} + + + {item.user.email} + + + + + + ); + }} + /> ); diff --git a/apps/expo/src/components/Header.tsx b/apps/expo/src/components/Header.tsx index 5a7a31a..ac4791d 100644 --- a/apps/expo/src/components/Header.tsx +++ b/apps/expo/src/components/Header.tsx @@ -5,6 +5,7 @@ import { Link, usePathname } from "expo-router"; import { MaterialIcons } from "@expo/vector-icons"; import type { BottomTabHeaderProps } from "@react-navigation/bottom-tabs"; import type { NativeStackHeaderProps } from "@react-navigation/native-stack"; +import { useSearchStore } from "@/lib/stores/useSearchStore"; export default function Header( props: BottomTabHeaderProps | NativeStackHeaderProps, @@ -12,10 +13,12 @@ export default function Header( const pathname = usePathname(); const isSearchScreen = pathname === "/search"; + const setSearch = useSearchStore((state) => state.setSearch); + return ( {isSearchScreen && ( - + diff --git a/apps/expo/src/lib/hooks/useDebouncedValues.ts b/apps/expo/src/lib/hooks/useDebouncedValues.ts new file mode 100644 index 0000000..d8048ea --- /dev/null +++ b/apps/expo/src/lib/hooks/useDebouncedValues.ts @@ -0,0 +1,36 @@ +import { useEffect, useRef, useState } from "react"; + +export function useDebouncedValue( + value: T, + wait: number, + options = { leading: false }, +) { + const [_value, setValue] = useState(value); + const mountedRef = useRef(false); + const timeoutRef = useRef(null); + const cooldownRef = useRef(false); + + const cancel = () => window.clearTimeout(timeoutRef.current!); + + useEffect(() => { + if (mountedRef.current) { + if (!cooldownRef.current && options.leading) { + cooldownRef.current = true; + setValue(value); + } else { + cancel(); + timeoutRef.current = window.setTimeout(() => { + cooldownRef.current = false; + setValue(value); + }, wait); + } + } + }, [value, options.leading, wait]); + + useEffect(() => { + mountedRef.current = true; + return cancel; + }, []); + + return [_value, cancel] as const; +} diff --git a/apps/expo/src/lib/stores/useSearchStore.ts b/apps/expo/src/lib/stores/useSearchStore.ts new file mode 100644 index 0000000..e7ac26a --- /dev/null +++ b/apps/expo/src/lib/stores/useSearchStore.ts @@ -0,0 +1,11 @@ +import { create } from "zustand"; + +interface SearchState { + search: string; + setSearch: (search: string) => void; +} + +export const useSearchStore = create()((set) => ({ + search: "", + setSearch: (search) => set({ search }), +})); diff --git a/apps/expo/src/utils/rupiahFormatter.ts b/apps/expo/src/utils/rupiahFormatter.ts new file mode 100644 index 0000000..0edbb0f --- /dev/null +++ b/apps/expo/src/utils/rupiahFormatter.ts @@ -0,0 +1,7 @@ +export default function rupiahFormatter(value: number): string { + return new Intl.NumberFormat("id-ID", { + style: "currency", + currency: "IDR", + minimumFractionDigits: 0, + }).format(value); +} diff --git a/packages/api/src/router/product.ts b/packages/api/src/router/product.ts index 17f9e48..eb1be96 100644 --- a/packages/api/src/router/product.ts +++ b/packages/api/src/router/product.ts @@ -1,7 +1,16 @@ -import { createTRPCRouter, protectedProcedure, publicProcedure } from "../trpc"; +import { z } from "zod"; + +import { createTRPCRouter, protectedProcedure } from "../trpc"; export const productRouter = createTRPCRouter({ - getProduct: protectedProcedure.query(({ ctx }) => { - return ctx.db.query.products.findMany(); - }), + getProduct: protectedProcedure + .input(z.string()) + .query(({ input, ctx }) => { + return ctx.db.query.products.findMany({ + with: { + user: true, + }, + where: (products, { like }) => like(products.name, `%${input.toLowerCase()}%`), + }); + }), }); diff --git a/packages/db/schema/user.ts b/packages/db/schema/user.ts index f2de17f..6743cfa 100644 --- a/packages/db/schema/user.ts +++ b/packages/db/schema/user.ts @@ -16,7 +16,7 @@ export const users = mySqlTable("user", { email: varchar("email", { length: 255 }) .notNull() .unique(), - imageUrl: varchar("image_url", { length: 255 }), + imageUrl: varchar("image_url", { length: 255 }).notNull(), }); export const usersRelations = relations(users, ({ many }) => ({ diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2977adf..bb87cd0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -107,6 +107,9 @@ importers: superjson: specifier: 1.13.1 version: 1.13.1 + zustand: + specifier: ^4.4.6 + version: 4.4.6(@types/react@18.2.33)(react@18.2.0) devDependencies: '@babel/core': specifier: ^7.23.2 @@ -12442,3 +12445,23 @@ packages: /zod@3.22.4: resolution: {integrity: sha512-iC+8Io04lddc+mVqQ9AZ7OQ2MrUKGN+oIQyq1vemgt46jwCwLfhq7/pwnBnNXXXZb8VTVLKwp9EDkx+ryxIWmg==} + + /zustand@4.4.6(@types/react@18.2.33)(react@18.2.0): + resolution: {integrity: sha512-Rb16eW55gqL4W2XZpJh0fnrATxYEG3Apl2gfHTyDSE965x/zxslTikpNch0JgNjJA9zK6gEFW8Fl6d1rTZaqgg==} + engines: {node: '>=12.7.0'} + peerDependencies: + '@types/react': '>=16.8' + immer: '>=9.0' + react: '>=16.8' + peerDependenciesMeta: + '@types/react': + optional: true + immer: + optional: true + react: + optional: true + dependencies: + '@types/react': 18.2.33 + react: 18.2.0 + use-sync-external-store: 1.2.0(react@18.2.0) + dev: false