From ca9eb8a7d2dd745833a646b4cfef94f1576b36d1 Mon Sep 17 00:00:00 2001 From: shafi54 <108669266+shafi-aviz@users.noreply.github.com> Date: Wed, 11 Mar 2026 16:31:23 +0530 Subject: [PATCH] enh --- APIS_TO_REMOVE.md | 4 + apps/backend/.env | 2 +- apps/backend/assets/signed-url-cache.json | 2 +- apps/backend/src/lib/cloud_cache.ts | 58 +- apps/backend/src/lib/init.ts | 4 +- .../src/trpc/apis/user-apis/apis/banners.ts | 48 +- .../src/trpc/apis/user-apis/apis/slots.ts | 19 + .../src/trpc/apis/user-apis/apis/stores.ts | 144 ++-- apps/backend/src/trpc/router.ts | 5 +- apps/backend/tsconfig.json | 4 +- .../(tabs)/me/delivery-slots/_layout.tsx | 9 - .../(tabs)/me/delivery-slots/index.tsx | 230 ------ .../(tabs)/stores/store-detail/[id].tsx | 50 +- apps/user-ui/app/_layout.tsx | 5 +- apps/user-ui/components/BannerCarousel.tsx | 8 +- .../components/PaymentAndOrderComponent.tsx | 17 +- apps/user-ui/components/ProductCard.tsx | 23 +- apps/user-ui/components/ProductDetail.tsx | 26 +- .../QuickDeliveryAddressSelector.tsx | 7 +- apps/user-ui/components/cart-page.tsx | 86 +-- apps/user-ui/components/checkout-page.tsx | 26 +- apps/user-ui/components/floating-cart-bar.tsx | 65 +- apps/user-ui/hooks/cart-query-hooks.tsx | 658 ++++++++---------- .../user-ui/hooks/useProductSlotIdentifier.ts | 42 +- apps/user-ui/metro.config.js | 13 + .../src/components/AddToCartDialog.tsx | 5 +- .../components/CentralStoreInitializer.tsx | 14 + apps/user-ui/src/hooks/prominent-api-hooks.ts | 58 +- apps/user-ui/src/store/centralProductStore.ts | 41 ++ apps/user-ui/src/store/centralSlotStore.ts | 60 ++ apps/user-ui/tsconfig.json | 7 + package-lock.json | 8 + packages/shared/index.ts | 9 + packages/shared/package.json | 7 + 34 files changed, 863 insertions(+), 901 deletions(-) create mode 100644 APIS_TO_REMOVE.md delete mode 100644 apps/user-ui/app/(drawer)/(tabs)/me/delivery-slots/_layout.tsx delete mode 100644 apps/user-ui/app/(drawer)/(tabs)/me/delivery-slots/index.tsx create mode 100644 apps/user-ui/src/components/CentralStoreInitializer.tsx create mode 100644 apps/user-ui/src/store/centralProductStore.ts create mode 100644 apps/user-ui/src/store/centralSlotStore.ts create mode 100644 packages/shared/index.ts create mode 100644 packages/shared/package.json diff --git a/APIS_TO_REMOVE.md b/APIS_TO_REMOVE.md new file mode 100644 index 0000000..b0a1f25 --- /dev/null +++ b/APIS_TO_REMOVE.md @@ -0,0 +1,4 @@ +- trpc.user.tags.getTagsByStore — apps/backend/src/trpc/apis/user-apis/apis/tags.ts +- trpc.common.product.getAllProductsSummary — apps/backend/src/trpc/apis/common-apis/common.ts +- remove slots from products cache +- remove redundant product details like name, description etc from the slots api diff --git a/apps/backend/.env b/apps/backend/.env index 92b5602..a935514 100755 --- a/apps/backend/.env +++ b/apps/backend/.env @@ -21,7 +21,7 @@ S3_BUCKET_NAME=meatfarmer EXPO_ACCESS_TOKEN=Asvpy8cByRh6T4ksnWScO6PLcio2n35-BwES5zK- JWT_SECRET=my_meatfarmer_jwt_secret_key ASSETS_DOMAIN=https://assets.freshyo.in/ -API_CACHE_KEY='api-cache' +API_CACHE_KEY=api-cache-dev # REDIS_URL=redis://default:redis_shafi_password@5.223.55.14:6379 REDIS_URL=redis://default:redis_shafi_password@57.128.212.174:6379 APP_URL=http://localhost:4000 diff --git a/apps/backend/assets/signed-url-cache.json b/apps/backend/assets/signed-url-cache.json index 7264388..8aff057 100644 --- a/apps/backend/assets/signed-url-cache.json +++ b/apps/backend/assets/signed-url-cache.json @@ -1 +1 @@ -{"originalToSigned":{"tags/1763835253683-c9c3e293-0bef-4c58-a976-dd49c050cd36.jpeg":{"value":"https://da9b1aa7c1951c23e2c0c3246ba68a58.r2.cloudflarestorage.com/meatfarmer/tags/1763835253683-c9c3e293-0bef-4c58-a976-dd49c050cd36.jpeg?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Content-Sha256=UNSIGNED-PAYLOAD&X-Amz-Credential=8fab47503efb9547b50e4fb317e35cc7%2F20260307%2Fapac%2Fs3%2Faws4_request&X-Amz-Date=20260307T105258Z&X-Amz-Expires=259200&X-Amz-Signature=a4e4226aa30b5f0fde27efca0f139c00df5d3e13d08b104c4aee12afda1f7498&X-Amz-SignedHeaders=host&x-amz-checksum-mode=ENABLED&x-id=GetObject","expiresAt":1773139918864},"tags/1763835293899-43b3fbe1-9b5b-441c-b4d4-d1691c3f02f3.webp":{"value":"https://da9b1aa7c1951c23e2c0c3246ba68a58.r2.cloudflarestorage.com/meatfarmer/tags/1763835293899-43b3fbe1-9b5b-441c-b4d4-d1691c3f02f3.webp?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Content-Sha256=UNSIGNED-PAYLOAD&X-Amz-Credential=8fab47503efb9547b50e4fb317e35cc7%2F20260307%2Fapac%2Fs3%2Faws4_request&X-Amz-Date=20260307T105259Z&X-Amz-Expires=259200&X-Amz-Signature=bf52ada07a10aef2f03249480efc7c918389e8711c31dec38c7bae4b922ddfd6&X-Amz-SignedHeaders=host&x-amz-checksum-mode=ENABLED&x-id=GetObject","expiresAt":1773139919079},"tags/1768709725124-ebf421c5-ad52-49a9-b65c-1de008110b8a.png":{"value":"https://da9b1aa7c1951c23e2c0c3246ba68a58.r2.cloudflarestorage.com/meatfarmer/tags/1768709725124-ebf421c5-ad52-49a9-b65c-1de008110b8a.png?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Content-Sha256=UNSIGNED-PAYLOAD&X-Amz-Credential=8fab47503efb9547b50e4fb317e35cc7%2F20260307%2Fapac%2Fs3%2Faws4_request&X-Amz-Date=20260307T105259Z&X-Amz-Expires=259200&X-Amz-Signature=3e175d60bbaba792d9dd5d60d4ebad6ad395e9f5f7a00f469e6d74dccec68d59&X-Amz-SignedHeaders=host&x-amz-checksum-mode=ENABLED&x-id=GetObject","expiresAt":1773139919523},"tags/1770321659633-1763869265110-e22b6d94-dac9-499f-babb-1e944d90b01a.jpeg%3FX-Amz-Algorithm%3DAWS4-HMAC-SHA256%26X-Amz-Content-Sha256%3DUNSIGNED-PAYLOAD%26X-Amz-Credential%3D8fab47503efb9547b50e4fb317e35cc7%252F20260205%252Fapac%252Fs3%252Faws4_request%26X-Amz-Date%3D20260205T195535Z%26X-Amz-Expires%3D259200%26X-Amz-Signature%3D917db15bcc60cab7ac5cd5e49d85d13a960fe77b4a5e327dd449048870494cf9%26X-Amz-SignedHeaders%3Dhost%26x-amz-checksum-mode%3DENABLED%26x-id%3DGetObject":{"value":"https://da9b1aa7c1951c23e2c0c3246ba68a58.r2.cloudflarestorage.com/meatfarmer/tags/1770321659633-1763869265110-e22b6d94-dac9-499f-babb-1e944d90b01a.jpeg%253FX-Amz-Algorithm%253DAWS4-HMAC-SHA256%2526X-Amz-Content-Sha256%253DUNSIGNED-PAYLOAD%2526X-Amz-Credential%253D8fab47503efb9547b50e4fb317e35cc7%25252F20260205%25252Fapac%25252Fs3%25252Faws4_request%2526X-Amz-Date%253D20260205T195535Z%2526X-Amz-Expires%253D259200%2526X-Amz-Signature%253D917db15bcc60cab7ac5cd5e49d85d13a960fe77b4a5e327dd449048870494cf9%2526X-Amz-SignedHeaders%253Dhost%2526x-amz-checksum-mode%253DENABLED%2526x-id%253DGetObject?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Content-Sha256=UNSIGNED-PAYLOAD&X-Amz-Credential=8fab47503efb9547b50e4fb317e35cc7%2F20260307%2Fapac%2Fs3%2Faws4_request&X-Amz-Date=20260307T105259Z&X-Amz-Expires=259200&X-Amz-Signature=5cbc2e1c88c8df749d97c59dfec911cbacc58502fd38e97723ee3627e8934d6e&X-Amz-SignedHeaders=host&x-amz-checksum-mode=ENABLED&x-id=GetObject","expiresAt":1773139919790},"tags/1770323410499-1763869436182-bf82f7b4-a1f3-4113-985b-96311b7a910e.jpeg%3FX-Amz-Algorithm%3DAWS4-HMAC-SHA256%26X-Amz-Content-Sha256%3DUNSIGNED-PAYLOAD%26X-Amz-Credential%3D8fab47503efb9547b50e4fb317e35cc7%252F20260205%252Fapac%252Fs3%252Faws4_request%26X-Amz-Date%3D20260205T202804Z%26X-Amz-Expires%3D259200%26X-Amz-Signature%3Dea436390b277935d843cae6b5cfa62aeed5799cb4a962ab31a0be4b132ca4b30%26X-Amz-SignedHeaders%3Dhost%26x-amz-checksum-mode%3DENABLED%26x-id%3DGetObject":{"value":"https://da9b1aa7c1951c23e2c0c3246ba68a58.r2.cloudflarestorage.com/meatfarmer/tags/1770323410499-1763869436182-bf82f7b4-a1f3-4113-985b-96311b7a910e.jpeg%253FX-Amz-Algorithm%253DAWS4-HMAC-SHA256%2526X-Amz-Content-Sha256%253DUNSIGNED-PAYLOAD%2526X-Amz-Credential%253D8fab47503efb9547b50e4fb317e35cc7%25252F20260205%25252Fapac%25252Fs3%25252Faws4_request%2526X-Amz-Date%253D20260205T202804Z%2526X-Amz-Expires%253D259200%2526X-Amz-Signature%253Dea436390b277935d843cae6b5cfa62aeed5799cb4a962ab31a0be4b132ca4b30%2526X-Amz-SignedHeaders%253Dhost%2526x-amz-checksum-mode%253DENABLED%2526x-id%253DGetObject?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Content-Sha256=UNSIGNED-PAYLOAD&X-Amz-Credential=8fab47503efb9547b50e4fb317e35cc7%2F20260307%2Fapac%2Fs3%2Faws4_request&X-Amz-Date=20260307T105300Z&X-Amz-Expires=259200&X-Amz-Signature=aa8adbc434acc3f5c2b8f7a43d300a56df641e21ebefe30510b7e5edbac3e36f&X-Amz-SignedHeaders=host&x-amz-checksum-mode=ENABLED&x-id=GetObject","expiresAt":1773139920026},"tags/1770323560823-fd0ec463-bed0-474e-aa14-dc6480ce36af.jpeg":{"value":"https://da9b1aa7c1951c23e2c0c3246ba68a58.r2.cloudflarestorage.com/meatfarmer/tags/1770323560823-fd0ec463-bed0-474e-aa14-dc6480ce36af.jpeg?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Content-Sha256=UNSIGNED-PAYLOAD&X-Amz-Credential=8fab47503efb9547b50e4fb317e35cc7%2F20260307%2Fapac%2Fs3%2Faws4_request&X-Amz-Date=20260307T105300Z&X-Amz-Expires=259200&X-Amz-Signature=5064525b43e9119a4ec0c19ca134b84697a814c54121f4bca6d2b28582c2fe01&X-Amz-SignedHeaders=host&x-amz-checksum-mode=ENABLED&x-id=GetObject","expiresAt":1773139920248},"store-images/1770281046297.jpg":{"value":"https://da9b1aa7c1951c23e2c0c3246ba68a58.r2.cloudflarestorage.com/meatfarmer/store-images/1770281046297.jpg?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Content-Sha256=UNSIGNED-PAYLOAD&X-Amz-Credential=8fab47503efb9547b50e4fb317e35cc7%2F20260309%2Fapac%2Fs3%2Faws4_request&X-Amz-Date=20260309T193417Z&X-Amz-Expires=259200&X-Amz-Signature=271c0272ba9048bfd6785da9284c96ed15cd8f139aac094acc8883513c9adcb1&X-Amz-SignedHeaders=host&x-amz-checksum-mode=ENABLED&x-id=GetObject","expiresAt":1773343997623},"store-images/1770429593455.jpg":{"value":"https://da9b1aa7c1951c23e2c0c3246ba68a58.r2.cloudflarestorage.com/meatfarmer/store-images/1770429593455.jpg?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Content-Sha256=UNSIGNED-PAYLOAD&X-Amz-Credential=8fab47503efb9547b50e4fb317e35cc7%2F20260309%2Fapac%2Fs3%2Faws4_request&X-Amz-Date=20260309T193417Z&X-Amz-Expires=259200&X-Amz-Signature=e28f8c17df312e020caad9b63a4af47d1c299be263bf6a2289e1755ce37a5f46&X-Amz-SignedHeaders=host&x-amz-checksum-mode=ENABLED&x-id=GetObject","expiresAt":1773343997623}},"signedToOriginal":{"https://da9b1aa7c1951c23e2c0c3246ba68a58.r2.cloudflarestorage.com/meatfarmer/tags/1763835253683-c9c3e293-0bef-4c58-a976-dd49c050cd36.jpeg?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Content-Sha256=UNSIGNED-PAYLOAD&X-Amz-Credential=8fab47503efb9547b50e4fb317e35cc7%2F20260307%2Fapac%2Fs3%2Faws4_request&X-Amz-Date=20260307T105258Z&X-Amz-Expires=259200&X-Amz-Signature=a4e4226aa30b5f0fde27efca0f139c00df5d3e13d08b104c4aee12afda1f7498&X-Amz-SignedHeaders=host&x-amz-checksum-mode=ENABLED&x-id=GetObject":{"value":"tags/1763835253683-c9c3e293-0bef-4c58-a976-dd49c050cd36.jpeg","expiresAt":1773139918864},"https://da9b1aa7c1951c23e2c0c3246ba68a58.r2.cloudflarestorage.com/meatfarmer/tags/1763835293899-43b3fbe1-9b5b-441c-b4d4-d1691c3f02f3.webp?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Content-Sha256=UNSIGNED-PAYLOAD&X-Amz-Credential=8fab47503efb9547b50e4fb317e35cc7%2F20260307%2Fapac%2Fs3%2Faws4_request&X-Amz-Date=20260307T105259Z&X-Amz-Expires=259200&X-Amz-Signature=bf52ada07a10aef2f03249480efc7c918389e8711c31dec38c7bae4b922ddfd6&X-Amz-SignedHeaders=host&x-amz-checksum-mode=ENABLED&x-id=GetObject":{"value":"tags/1763835293899-43b3fbe1-9b5b-441c-b4d4-d1691c3f02f3.webp","expiresAt":1773139919079},"https://da9b1aa7c1951c23e2c0c3246ba68a58.r2.cloudflarestorage.com/meatfarmer/tags/1768709725124-ebf421c5-ad52-49a9-b65c-1de008110b8a.png?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Content-Sha256=UNSIGNED-PAYLOAD&X-Amz-Credential=8fab47503efb9547b50e4fb317e35cc7%2F20260307%2Fapac%2Fs3%2Faws4_request&X-Amz-Date=20260307T105259Z&X-Amz-Expires=259200&X-Amz-Signature=3e175d60bbaba792d9dd5d60d4ebad6ad395e9f5f7a00f469e6d74dccec68d59&X-Amz-SignedHeaders=host&x-amz-checksum-mode=ENABLED&x-id=GetObject":{"value":"tags/1768709725124-ebf421c5-ad52-49a9-b65c-1de008110b8a.png","expiresAt":1773139919523},"https://da9b1aa7c1951c23e2c0c3246ba68a58.r2.cloudflarestorage.com/meatfarmer/tags/1770321659633-1763869265110-e22b6d94-dac9-499f-babb-1e944d90b01a.jpeg%253FX-Amz-Algorithm%253DAWS4-HMAC-SHA256%2526X-Amz-Content-Sha256%253DUNSIGNED-PAYLOAD%2526X-Amz-Credential%253D8fab47503efb9547b50e4fb317e35cc7%25252F20260205%25252Fapac%25252Fs3%25252Faws4_request%2526X-Amz-Date%253D20260205T195535Z%2526X-Amz-Expires%253D259200%2526X-Amz-Signature%253D917db15bcc60cab7ac5cd5e49d85d13a960fe77b4a5e327dd449048870494cf9%2526X-Amz-SignedHeaders%253Dhost%2526x-amz-checksum-mode%253DENABLED%2526x-id%253DGetObject?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Content-Sha256=UNSIGNED-PAYLOAD&X-Amz-Credential=8fab47503efb9547b50e4fb317e35cc7%2F20260307%2Fapac%2Fs3%2Faws4_request&X-Amz-Date=20260307T105259Z&X-Amz-Expires=259200&X-Amz-Signature=5cbc2e1c88c8df749d97c59dfec911cbacc58502fd38e97723ee3627e8934d6e&X-Amz-SignedHeaders=host&x-amz-checksum-mode=ENABLED&x-id=GetObject":{"value":"tags/1770321659633-1763869265110-e22b6d94-dac9-499f-babb-1e944d90b01a.jpeg%3FX-Amz-Algorithm%3DAWS4-HMAC-SHA256%26X-Amz-Content-Sha256%3DUNSIGNED-PAYLOAD%26X-Amz-Credential%3D8fab47503efb9547b50e4fb317e35cc7%252F20260205%252Fapac%252Fs3%252Faws4_request%26X-Amz-Date%3D20260205T195535Z%26X-Amz-Expires%3D259200%26X-Amz-Signature%3D917db15bcc60cab7ac5cd5e49d85d13a960fe77b4a5e327dd449048870494cf9%26X-Amz-SignedHeaders%3Dhost%26x-amz-checksum-mode%3DENABLED%26x-id%3DGetObject","expiresAt":1773139919790},"https://da9b1aa7c1951c23e2c0c3246ba68a58.r2.cloudflarestorage.com/meatfarmer/tags/1770323410499-1763869436182-bf82f7b4-a1f3-4113-985b-96311b7a910e.jpeg%253FX-Amz-Algorithm%253DAWS4-HMAC-SHA256%2526X-Amz-Content-Sha256%253DUNSIGNED-PAYLOAD%2526X-Amz-Credential%253D8fab47503efb9547b50e4fb317e35cc7%25252F20260205%25252Fapac%25252Fs3%25252Faws4_request%2526X-Amz-Date%253D20260205T202804Z%2526X-Amz-Expires%253D259200%2526X-Amz-Signature%253Dea436390b277935d843cae6b5cfa62aeed5799cb4a962ab31a0be4b132ca4b30%2526X-Amz-SignedHeaders%253Dhost%2526x-amz-checksum-mode%253DENABLED%2526x-id%253DGetObject?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Content-Sha256=UNSIGNED-PAYLOAD&X-Amz-Credential=8fab47503efb9547b50e4fb317e35cc7%2F20260307%2Fapac%2Fs3%2Faws4_request&X-Amz-Date=20260307T105300Z&X-Amz-Expires=259200&X-Amz-Signature=aa8adbc434acc3f5c2b8f7a43d300a56df641e21ebefe30510b7e5edbac3e36f&X-Amz-SignedHeaders=host&x-amz-checksum-mode=ENABLED&x-id=GetObject":{"value":"tags/1770323410499-1763869436182-bf82f7b4-a1f3-4113-985b-96311b7a910e.jpeg%3FX-Amz-Algorithm%3DAWS4-HMAC-SHA256%26X-Amz-Content-Sha256%3DUNSIGNED-PAYLOAD%26X-Amz-Credential%3D8fab47503efb9547b50e4fb317e35cc7%252F20260205%252Fapac%252Fs3%252Faws4_request%26X-Amz-Date%3D20260205T202804Z%26X-Amz-Expires%3D259200%26X-Amz-Signature%3Dea436390b277935d843cae6b5cfa62aeed5799cb4a962ab31a0be4b132ca4b30%26X-Amz-SignedHeaders%3Dhost%26x-amz-checksum-mode%3DENABLED%26x-id%3DGetObject","expiresAt":1773139920026},"https://da9b1aa7c1951c23e2c0c3246ba68a58.r2.cloudflarestorage.com/meatfarmer/tags/1770323560823-fd0ec463-bed0-474e-aa14-dc6480ce36af.jpeg?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Content-Sha256=UNSIGNED-PAYLOAD&X-Amz-Credential=8fab47503efb9547b50e4fb317e35cc7%2F20260307%2Fapac%2Fs3%2Faws4_request&X-Amz-Date=20260307T105300Z&X-Amz-Expires=259200&X-Amz-Signature=5064525b43e9119a4ec0c19ca134b84697a814c54121f4bca6d2b28582c2fe01&X-Amz-SignedHeaders=host&x-amz-checksum-mode=ENABLED&x-id=GetObject":{"value":"tags/1770323560823-fd0ec463-bed0-474e-aa14-dc6480ce36af.jpeg","expiresAt":1773139920248},"https://da9b1aa7c1951c23e2c0c3246ba68a58.r2.cloudflarestorage.com/meatfarmer/store-images/1770281046297.jpg?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Content-Sha256=UNSIGNED-PAYLOAD&X-Amz-Credential=8fab47503efb9547b50e4fb317e35cc7%2F20260309%2Fapac%2Fs3%2Faws4_request&X-Amz-Date=20260309T193417Z&X-Amz-Expires=259200&X-Amz-Signature=271c0272ba9048bfd6785da9284c96ed15cd8f139aac094acc8883513c9adcb1&X-Amz-SignedHeaders=host&x-amz-checksum-mode=ENABLED&x-id=GetObject":{"value":"store-images/1770281046297.jpg","expiresAt":1773343997623},"https://da9b1aa7c1951c23e2c0c3246ba68a58.r2.cloudflarestorage.com/meatfarmer/store-images/1770429593455.jpg?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Content-Sha256=UNSIGNED-PAYLOAD&X-Amz-Credential=8fab47503efb9547b50e4fb317e35cc7%2F20260309%2Fapac%2Fs3%2Faws4_request&X-Amz-Date=20260309T193417Z&X-Amz-Expires=259200&X-Amz-Signature=e28f8c17df312e020caad9b63a4af47d1c299be263bf6a2289e1755ce37a5f46&X-Amz-SignedHeaders=host&x-amz-checksum-mode=ENABLED&x-id=GetObject":{"value":"store-images/1770429593455.jpg","expiresAt":1773343997623}}} \ No newline at end of file +{"originalToSigned":{"store-images/1770281046297.jpg":{"value":"https://da9b1aa7c1951c23e2c0c3246ba68a58.r2.cloudflarestorage.com/meatfarmer/store-images/1770281046297.jpg?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Content-Sha256=UNSIGNED-PAYLOAD&X-Amz-Credential=8fab47503efb9547b50e4fb317e35cc7%2F20260309%2Fapac%2Fs3%2Faws4_request&X-Amz-Date=20260309T193417Z&X-Amz-Expires=259200&X-Amz-Signature=271c0272ba9048bfd6785da9284c96ed15cd8f139aac094acc8883513c9adcb1&X-Amz-SignedHeaders=host&x-amz-checksum-mode=ENABLED&x-id=GetObject","expiresAt":1773343997623},"store-images/1770429593455.jpg":{"value":"https://da9b1aa7c1951c23e2c0c3246ba68a58.r2.cloudflarestorage.com/meatfarmer/store-images/1770429593455.jpg?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Content-Sha256=UNSIGNED-PAYLOAD&X-Amz-Credential=8fab47503efb9547b50e4fb317e35cc7%2F20260309%2Fapac%2Fs3%2Faws4_request&X-Amz-Date=20260309T193417Z&X-Amz-Expires=259200&X-Amz-Signature=e28f8c17df312e020caad9b63a4af47d1c299be263bf6a2289e1755ce37a5f46&X-Amz-SignedHeaders=host&x-amz-checksum-mode=ENABLED&x-id=GetObject","expiresAt":1773343997623},"tags/1773132996598-1773119837289-8c93f343-2885-415e-b545-dcaa1dc88857.jpeg%3FX-Amz-Algorithm%3DAWS4-HMAC-SHA256%26X-Amz-Content-Sha256%3DUNSIGNED-PAYLOAD%26X-Amz-Credential%3D8fab47503efb9547b50e4fb317e35cc7%252F20260310%252Fapac%252Fs3%252Faws4_request%26X-Amz-Date%3D20260310T051718Z%26X-Amz-Expires%3D259200%26X-Amz-Signature%3D22358087a6f102caf7eb7a4b3cfd455df9aca13685fff8bb751d3c3d813b9d72%26X-Amz-SignedHeaders%3Dhost%26x-amz-checksum-mode%3DENABLED%26x-id%3DGetObject":{"value":"https://da9b1aa7c1951c23e2c0c3246ba68a58.r2.cloudflarestorage.com/meatfarmer/tags/1773132996598-1773119837289-8c93f343-2885-415e-b545-dcaa1dc88857.jpeg%253FX-Amz-Algorithm%253DAWS4-HMAC-SHA256%2526X-Amz-Content-Sha256%253DUNSIGNED-PAYLOAD%2526X-Amz-Credential%253D8fab47503efb9547b50e4fb317e35cc7%25252F20260310%25252Fapac%25252Fs3%25252Faws4_request%2526X-Amz-Date%253D20260310T051718Z%2526X-Amz-Expires%253D259200%2526X-Amz-Signature%253D22358087a6f102caf7eb7a4b3cfd455df9aca13685fff8bb751d3c3d813b9d72%2526X-Amz-SignedHeaders%253Dhost%2526x-amz-checksum-mode%253DENABLED%2526x-id%253DGetObject?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Content-Sha256=UNSIGNED-PAYLOAD&X-Amz-Credential=8fab47503efb9547b50e4fb317e35cc7%2F20260310%2Fapac%2Fs3%2Faws4_request&X-Amz-Date=20260310T091443Z&X-Amz-Expires=259200&X-Amz-Signature=b107b388b2c1507b70d611c1c91dcdda5450083e7f8888ad99572b770b7efcf1&X-Amz-SignedHeaders=host&x-amz-checksum-mode=ENABLED&x-id=GetObject","expiresAt":1773393223027},"profile-images/1766160314135-1000000018.jpg":{"value":"https://da9b1aa7c1951c23e2c0c3246ba68a58.r2.cloudflarestorage.com/meatfarmer/profile-images/1766160314135-1000000018.jpg?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Content-Sha256=UNSIGNED-PAYLOAD&X-Amz-Credential=8fab47503efb9547b50e4fb317e35cc7%2F20260310%2Fapac%2Fs3%2Faws4_request&X-Amz-Date=20260310T091448Z&X-Amz-Expires=259200&X-Amz-Signature=e2595e57f41d7b66b08c23ab6a5a89631eb9c69c323dd558743938e7673cceda&X-Amz-SignedHeaders=host&x-amz-checksum-mode=ENABLED&x-id=GetObject","expiresAt":1773393228306}},"signedToOriginal":{"https://da9b1aa7c1951c23e2c0c3246ba68a58.r2.cloudflarestorage.com/meatfarmer/store-images/1770281046297.jpg?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Content-Sha256=UNSIGNED-PAYLOAD&X-Amz-Credential=8fab47503efb9547b50e4fb317e35cc7%2F20260309%2Fapac%2Fs3%2Faws4_request&X-Amz-Date=20260309T193417Z&X-Amz-Expires=259200&X-Amz-Signature=271c0272ba9048bfd6785da9284c96ed15cd8f139aac094acc8883513c9adcb1&X-Amz-SignedHeaders=host&x-amz-checksum-mode=ENABLED&x-id=GetObject":{"value":"store-images/1770281046297.jpg","expiresAt":1773343997623},"https://da9b1aa7c1951c23e2c0c3246ba68a58.r2.cloudflarestorage.com/meatfarmer/store-images/1770429593455.jpg?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Content-Sha256=UNSIGNED-PAYLOAD&X-Amz-Credential=8fab47503efb9547b50e4fb317e35cc7%2F20260309%2Fapac%2Fs3%2Faws4_request&X-Amz-Date=20260309T193417Z&X-Amz-Expires=259200&X-Amz-Signature=e28f8c17df312e020caad9b63a4af47d1c299be263bf6a2289e1755ce37a5f46&X-Amz-SignedHeaders=host&x-amz-checksum-mode=ENABLED&x-id=GetObject":{"value":"store-images/1770429593455.jpg","expiresAt":1773343997623},"https://da9b1aa7c1951c23e2c0c3246ba68a58.r2.cloudflarestorage.com/meatfarmer/tags/1773132996598-1773119837289-8c93f343-2885-415e-b545-dcaa1dc88857.jpeg%253FX-Amz-Algorithm%253DAWS4-HMAC-SHA256%2526X-Amz-Content-Sha256%253DUNSIGNED-PAYLOAD%2526X-Amz-Credential%253D8fab47503efb9547b50e4fb317e35cc7%25252F20260310%25252Fapac%25252Fs3%25252Faws4_request%2526X-Amz-Date%253D20260310T051718Z%2526X-Amz-Expires%253D259200%2526X-Amz-Signature%253D22358087a6f102caf7eb7a4b3cfd455df9aca13685fff8bb751d3c3d813b9d72%2526X-Amz-SignedHeaders%253Dhost%2526x-amz-checksum-mode%253DENABLED%2526x-id%253DGetObject?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Content-Sha256=UNSIGNED-PAYLOAD&X-Amz-Credential=8fab47503efb9547b50e4fb317e35cc7%2F20260310%2Fapac%2Fs3%2Faws4_request&X-Amz-Date=20260310T091443Z&X-Amz-Expires=259200&X-Amz-Signature=b107b388b2c1507b70d611c1c91dcdda5450083e7f8888ad99572b770b7efcf1&X-Amz-SignedHeaders=host&x-amz-checksum-mode=ENABLED&x-id=GetObject":{"value":"tags/1773132996598-1773119837289-8c93f343-2885-415e-b545-dcaa1dc88857.jpeg%3FX-Amz-Algorithm%3DAWS4-HMAC-SHA256%26X-Amz-Content-Sha256%3DUNSIGNED-PAYLOAD%26X-Amz-Credential%3D8fab47503efb9547b50e4fb317e35cc7%252F20260310%252Fapac%252Fs3%252Faws4_request%26X-Amz-Date%3D20260310T051718Z%26X-Amz-Expires%3D259200%26X-Amz-Signature%3D22358087a6f102caf7eb7a4b3cfd455df9aca13685fff8bb751d3c3d813b9d72%26X-Amz-SignedHeaders%3Dhost%26x-amz-checksum-mode%3DENABLED%26x-id%3DGetObject","expiresAt":1773393223027},"https://da9b1aa7c1951c23e2c0c3246ba68a58.r2.cloudflarestorage.com/meatfarmer/profile-images/1766160314135-1000000018.jpg?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Content-Sha256=UNSIGNED-PAYLOAD&X-Amz-Credential=8fab47503efb9547b50e4fb317e35cc7%2F20260310%2Fapac%2Fs3%2Faws4_request&X-Amz-Date=20260310T091448Z&X-Amz-Expires=259200&X-Amz-Signature=e2595e57f41d7b66b08c23ab6a5a89631eb9c69c323dd558743938e7673cceda&X-Amz-SignedHeaders=host&x-amz-checksum-mode=ENABLED&x-id=GetObject":{"value":"profile-images/1766160314135-1000000018.jpg","expiresAt":1773393228306}}} \ No newline at end of file diff --git a/apps/backend/src/lib/cloud_cache.ts b/apps/backend/src/lib/cloud_cache.ts index 5dc6287..6ea9a24 100644 --- a/apps/backend/src/lib/cloud_cache.ts +++ b/apps/backend/src/lib/cloud_cache.ts @@ -2,8 +2,13 @@ import { scaffoldProducts } from '@/src/trpc/apis/common-apis/common' import { scaffoldEssentialConsts } from '@/src/trpc/apis/common-apis/common-trpc-index' import { scaffoldStores } from '@/src/trpc/apis/user-apis/apis/stores' import { scaffoldSlotsWithProducts } from '@/src/trpc/apis/user-apis/apis/slots' +import { scaffoldBanners } from '@/src/trpc/apis/user-apis/apis/banners' +import { scaffoldStoreWithProducts } from '@/src/trpc/apis/user-apis/apis/stores' +import { storeInfo } from '@/src/db/schema' +import { db } from '@/src/db/db_index' import { imageUploadS3 } from '@/src/lib/s3-client' import { apiCacheKey } from '@/src/lib/env-exporter' +import { CACHE_FILENAMES } from '@packages/shared' export async function createProductsFile(): Promise { // Get products data from the API method @@ -16,7 +21,7 @@ export async function createProductsFile(): Promise { const buffer = Buffer.from(jsonContent, 'utf-8') // Upload to S3 at the specified path using apiCacheKey - const s3Key = await imageUploadS3(buffer, 'application/json', `${apiCacheKey}/products.json`) + const s3Key = await imageUploadS3(buffer, 'application/json', `${apiCacheKey}/${CACHE_FILENAMES.products}`) return s3Key } @@ -32,7 +37,7 @@ export async function createEssentialConstsFile(): Promise { const buffer = Buffer.from(jsonContent, 'utf-8') // Upload to S3 at the specified path using apiCacheKey - const s3Key = await imageUploadS3(buffer, 'application/json', `${apiCacheKey}/essential-consts.json`) + const s3Key = await imageUploadS3(buffer, 'application/json', `${apiCacheKey}/${CACHE_FILENAMES.essentialConsts}`) return s3Key } @@ -48,7 +53,7 @@ export async function createStoresFile(): Promise { const buffer = Buffer.from(jsonContent, 'utf-8') // Upload to S3 at the specified path using apiCacheKey - const s3Key = await imageUploadS3(buffer, 'application/json', `${apiCacheKey}/stores.json`) + const s3Key = await imageUploadS3(buffer, 'application/json', `${apiCacheKey}/${CACHE_FILENAMES.stores}`) return s3Key } @@ -64,7 +69,52 @@ export async function createSlotsFile(): Promise { const buffer = Buffer.from(jsonContent, 'utf-8') // Upload to S3 at the specified path using apiCacheKey - const s3Key = await imageUploadS3(buffer, 'application/json', `${apiCacheKey}/slots.json`) + const s3Key = await imageUploadS3(buffer, 'application/json', `${apiCacheKey}/${CACHE_FILENAMES.slots}`) return s3Key } + +export async function createBannersFile(): Promise { + // Get banners data from the API method + const bannersData = await scaffoldBanners() + + // Convert to JSON string with pretty formatting + const jsonContent = JSON.stringify(bannersData, null, 2) + + // Convert to Buffer for S3 upload + const buffer = Buffer.from(jsonContent, 'utf-8') + + // Upload to S3 at the specified path using apiCacheKey + const s3Key = await imageUploadS3(buffer, 'application/json', `${apiCacheKey}/${CACHE_FILENAMES.banners}`) + + return s3Key +} + +export async function createStoreFile(storeId: number): Promise { + // Get store data from the API method + const storeData = await scaffoldStoreWithProducts(storeId) + + // Convert to JSON string with pretty formatting + const jsonContent = JSON.stringify(storeData, null, 2) + + // Convert to Buffer for S3 upload + const buffer = Buffer.from(jsonContent, 'utf-8') + + // Upload to S3 at the specified path using apiCacheKey + const s3Key = await imageUploadS3(buffer, 'application/json', `${apiCacheKey}/stores/${storeId}.json`) + + return s3Key +} + +export async function createAllStoresFiles(): Promise { + // Fetch all store IDs from database + const stores = await db.select({ id: storeInfo.id }).from(storeInfo) + + // Create cache files for all stores in parallel + const results = await Promise.all( + stores.map(store => createStoreFile(store.id)) + ) + + console.log(`Created ${results.length} store cache files`) + return results +} diff --git a/apps/backend/src/lib/init.ts b/apps/backend/src/lib/init.ts index 4635f23..f8f4688 100755 --- a/apps/backend/src/lib/init.ts +++ b/apps/backend/src/lib/init.ts @@ -3,7 +3,7 @@ import { initializeAllStores } from '@/src/stores/store-initializer' import { initializeUserNegativityStore } from '@/src/stores/user-negativity-store' import { startOrderHandler, startCancellationHandler, publishOrder } from '@/src/lib/post-order-handler' import { deleteOrders } from '@/src/lib/delete-orders' -import { createProductsFile, createEssentialConstsFile, createStoresFile, createSlotsFile } from '@/src/lib/cloud_cache' +import { createProductsFile, createEssentialConstsFile, createStoresFile, createSlotsFile, createBannersFile, createAllStoresFiles } from '@/src/lib/cloud_cache' /** * Initialize all application services @@ -32,6 +32,8 @@ export const initFunc = async (): Promise => { createEssentialConstsFile(), createStoresFile(), createSlotsFile(), + createBannersFile(), + createAllStoresFiles(), ]); console.log('Cache files created successfully'); diff --git a/apps/backend/src/trpc/apis/user-apis/apis/banners.ts b/apps/backend/src/trpc/apis/user-apis/apis/banners.ts index 6f4a53b..8e6a001 100644 --- a/apps/backend/src/trpc/apis/user-apis/apis/banners.ts +++ b/apps/backend/src/trpc/apis/user-apis/apis/banners.ts @@ -1,38 +1,30 @@ import { db } from '@/src/db/db_index'; import { homeBanners } from '@/src/db/schema'; import { publicProcedure, router } from '@/src/trpc/trpc-index'; -import { generateSignedUrlFromS3Url } from '@/src/lib/s3-client'; +import { scaffoldAssetUrl } from '@/src/lib/s3-client'; import { isNotNull, asc } from 'drizzle-orm'; +export async function scaffoldBanners() { + const banners = await db.query.homeBanners.findMany({ + where: isNotNull(homeBanners.serialNum), // Only show assigned banners + orderBy: asc(homeBanners.serialNum), // Order by slot number 1-4 + }); + + // Convert S3 keys to signed URLs for client + const bannersWithSignedUrls = banners.map((banner) => ({ + ...banner, + imageUrl: banner.imageUrl ? scaffoldAssetUrl(banner.imageUrl) : banner.imageUrl, + })); + + return { + banners: bannersWithSignedUrls, + }; +} + export const bannerRouter = router({ getBanners: publicProcedure .query(async () => { - const banners = await db.query.homeBanners.findMany({ - where: isNotNull(homeBanners.serialNum), // Only show assigned banners - orderBy: asc(homeBanners.serialNum), // Order by slot number 1-4 - }); - - // Convert S3 keys to signed URLs for client - const bannersWithSignedUrls = await Promise.all( - banners.map(async (banner) => { - try { - return { - ...banner, - imageUrl: banner.imageUrl ? await generateSignedUrlFromS3Url(banner.imageUrl) : banner.imageUrl, - }; - } catch (error) { - console.error(`Failed to generate signed URL for banner ${banner.id}:`, error); - return { - ...banner, - imageUrl: banner.imageUrl, // Keep original on error - }; - } - }) - ); - - - return { - banners: bannersWithSignedUrls, - }; + const response = await scaffoldBanners(); + return response; }), }); diff --git a/apps/backend/src/trpc/apis/user-apis/apis/slots.ts b/apps/backend/src/trpc/apis/user-apis/apis/slots.ts index 61f7c6c..858286f 100644 --- a/apps/backend/src/trpc/apis/user-apis/apis/slots.ts +++ b/apps/backend/src/trpc/apis/user-apis/apis/slots.ts @@ -43,8 +43,27 @@ export async function scaffoldSlotsWithProducts() { }) .sort((a, b) => dayjs(a.deliveryTime).valueOf() - dayjs(b.deliveryTime).valueOf()); + // Fetch all products for availability info + const allProducts = await db + .select({ + id: productInfo.id, + name: productInfo.name, + isOutOfStock: productInfo.isOutOfStock, + isFlashAvailable: productInfo.isFlashAvailable, + }) + .from(productInfo) + .where(eq(productInfo.isSuspended, false)); + + const productAvailability = allProducts.map(product => ({ + id: product.id, + name: product.name, + isOutOfStock: product.isOutOfStock, + isFlashAvailable: product.isFlashAvailable, + })); + return { slots: validSlots, + productAvailability, count: validSlots.length, }; } diff --git a/apps/backend/src/trpc/apis/user-apis/apis/stores.ts b/apps/backend/src/trpc/apis/user-apis/apis/stores.ts index 1f82421..9522b7a 100644 --- a/apps/backend/src/trpc/apis/user-apis/apis/stores.ts +++ b/apps/backend/src/trpc/apis/user-apis/apis/stores.ts @@ -5,6 +5,7 @@ import { storeInfo, productInfo, units } from '@/src/db/schema'; import { eq, and, sql } from 'drizzle-orm'; import { scaffoldAssetUrl } from '@/src/lib/s3-client'; import { ApiError } from '@/src/lib/api-error'; +import { getTagsByStoreId } from '@/src/stores/product-tag-store'; export async function scaffoldStores() { const storesData = await db @@ -66,6 +67,82 @@ export async function scaffoldStores() { }; } +export async function scaffoldStoreWithProducts(storeId: number) { + // Fetch store info + const storeData = await db.query.storeInfo.findFirst({ + where: eq(storeInfo.id, storeId), + columns: { + id: true, + name: true, + description: true, + imageUrl: true, + }, + }); + + if (!storeData) { + throw new ApiError('Store not found', 404); + } + + // Generate signed URL for store image + const signedImageUrl = storeData.imageUrl ? scaffoldAssetUrl(storeData.imageUrl) : null; + + // Fetch products for this store + const productsData = await db + .select({ + id: productInfo.id, + name: productInfo.name, + shortDescription: productInfo.shortDescription, + price: productInfo.price, + marketPrice: productInfo.marketPrice, + images: productInfo.images, + isOutOfStock: productInfo.isOutOfStock, + incrementStep: productInfo.incrementStep, + unitShortNotation: units.shortNotation, + unitNotation: units.shortNotation, + productQuantity: productInfo.productQuantity, + }) + .from(productInfo) + .innerJoin(units, eq(productInfo.unitId, units.id)) + .where(and(eq(productInfo.storeId, storeId), eq(productInfo.isSuspended, false))); + + + // Generate signed URLs for product images + const productsWithSignedUrls = await Promise.all( + productsData.map(async (product) => ({ + id: product.id, + name: product.name, + shortDescription: product.shortDescription, + price: product.price, + marketPrice: product.marketPrice, + incrementStep: product.incrementStep, + unit: product.unitShortNotation, + unitNotation: product.unitNotation, + images: scaffoldAssetUrl((product.images as string[]) || []), + isOutOfStock: product.isOutOfStock, + productQuantity: product.productQuantity + })) + ); + + const tags = await getTagsByStoreId(storeId); + + return { + store: { + id: storeData.id, + name: storeData.name, + description: storeData.description, + signedImageUrl, + }, + products: productsWithSignedUrls, + tags: tags.map(tag => ({ + id: tag.id, + tagName: tag.tagName, + tagDescription: tag.tagDescription, + imageUrl: tag.imageUrl, + productIds: tag.productIds, + })), + }; +} + export const storesRouter = router({ getStores: publicProcedure .query(async () => { @@ -79,70 +156,7 @@ export const storesRouter = router({ })) .query(async ({ input }) => { const { storeId } = input; - - // Fetch store info - const storeData = await db.query.storeInfo.findFirst({ - where: eq(storeInfo.id, storeId), - columns: { - id: true, - name: true, - description: true, - imageUrl: true, - }, - }); - - if (!storeData) { - throw new ApiError('Store not found', 404); - } - - // Generate signed URL for store image - const signedImageUrl = storeData.imageUrl ? scaffoldAssetUrl(storeData.imageUrl) : null; - - // Fetch products for this store - const productsData = await db - .select({ - id: productInfo.id, - name: productInfo.name, - shortDescription: productInfo.shortDescription, - price: productInfo.price, - marketPrice: productInfo.marketPrice, - images: productInfo.images, - isOutOfStock: productInfo.isOutOfStock, - incrementStep: productInfo.incrementStep, - unitShortNotation: units.shortNotation, - unitNotation: units.shortNotation, - productQuantity: productInfo.productQuantity, - }) - .from(productInfo) - .innerJoin(units, eq(productInfo.unitId, units.id)) - .where(and(eq(productInfo.storeId, storeId), eq(productInfo.isSuspended, false))); - - - // Generate signed URLs for product images - const productsWithSignedUrls = await Promise.all( - productsData.map(async (product) => ({ - id: product.id, - name: product.name, - shortDescription: product.shortDescription, - price: product.price, - marketPrice: product.marketPrice, - incrementStep: product.incrementStep, - unit: product.unitShortNotation, - unitNotation: product.unitNotation, - images: scaffoldAssetUrl((product.images as string[]) || []), - isOutOfStock: product.isOutOfStock, - productQuantity: product.productQuantity - })) - ); - - return { - store: { - id: storeData.id, - name: storeData.name, - description: storeData.description, - signedImageUrl, - }, - products: productsWithSignedUrls, - }; + const response = await scaffoldStoreWithProducts(storeId); + return response; }), }); diff --git a/apps/backend/src/trpc/router.ts b/apps/backend/src/trpc/router.ts index 2f6e9a7..f7ccecc 100644 --- a/apps/backend/src/trpc/router.ts +++ b/apps/backend/src/trpc/router.ts @@ -4,9 +4,10 @@ import { adminRouter } from '@/src/trpc/apis/admin-apis/apis/admin-trpc-index' import { userRouter } from '@/src/trpc/apis/user-apis/apis/user-trpc-index' import { commonApiRouter } from '@/src/trpc/apis/common-apis/common-trpc-index' import { scaffoldProducts } from './apis/common-apis/common'; -import { scaffoldStores } from './apis/user-apis/apis/stores'; +import { scaffoldStores, scaffoldStoreWithProducts } from './apis/user-apis/apis/stores'; import { scaffoldSlotsWithProducts } from './apis/user-apis/apis/slots'; import { scaffoldEssentialConsts } from './apis/common-apis/common-trpc-index'; +import { scaffoldBanners } from './apis/user-apis/apis/banners'; // Create the main app router export const appRouter = router({ @@ -28,3 +29,5 @@ export type AllProductsApiType = Awaited>; export type StoresApiType = Awaited>; export type SlotsApiType = Awaited>; export type EssentialConstsApiType = Awaited>; +export type BannersApiType = Awaited>; +export type StoreWithProductsApiType = Awaited>; diff --git a/apps/backend/tsconfig.json b/apps/backend/tsconfig.json index d522745..4da0200 100755 --- a/apps/backend/tsconfig.json +++ b/apps/backend/tsconfig.json @@ -33,6 +33,8 @@ "shared-types": ["../shared-types"], "@commonTypes": ["../../packages/ui/shared-types"], "@commonTypes/*": ["../../packages/ui/shared-types/*"], + "@packages/shared": ["../../packages/shared"], + "@packages/shared/*": ["../../packages/shared/*"] }, // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ // "typeRoots": [""], /* Specify multiple folders that act like './node_modules/@types'. */ @@ -116,6 +118,6 @@ // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ "skipLibCheck": true /* Skip type checking all .d.ts files. */ }, - "include": ["src", "types", "index.ts", "../shared-types"] + "include": ["src", "types", "index.ts", "../shared-types", "../../packages/shared"] } diff --git a/apps/user-ui/app/(drawer)/(tabs)/me/delivery-slots/_layout.tsx b/apps/user-ui/app/(drawer)/(tabs)/me/delivery-slots/_layout.tsx deleted file mode 100644 index 8a04d45..0000000 --- a/apps/user-ui/app/(drawer)/(tabs)/me/delivery-slots/_layout.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import { Stack } from 'expo-router' - -function DeliverySlotsLayout() { - return ( - - ) -} - -export default DeliverySlotsLayout \ No newline at end of file diff --git a/apps/user-ui/app/(drawer)/(tabs)/me/delivery-slots/index.tsx b/apps/user-ui/app/(drawer)/(tabs)/me/delivery-slots/index.tsx deleted file mode 100644 index e239577..0000000 --- a/apps/user-ui/app/(drawer)/(tabs)/me/delivery-slots/index.tsx +++ /dev/null @@ -1,230 +0,0 @@ -import React, { useState } from 'react'; -import { View, ScrollView } from 'react-native'; -import { Image } from 'expo-image'; -import { useRouter } from 'expo-router'; -import { MyFlatList, MyText, tw, useMarkDataFetchers, BottomDialog, theme, MyTouchableOpacity } from 'common-ui'; -import { trpc } from '@/src/trpc-client'; -import MaterialIcons from '@expo/vector-icons/MaterialIcons'; -import dayjs from 'dayjs'; - -export default function DeliverySlots() { - const router = useRouter(); - const { data, isLoading, error, refetch } = trpc.user.slots.getSlotsWithProducts.useQuery(); - const [selectedSlotForDialog, setSelectedSlotForDialog] = useState(null); - - useMarkDataFetchers(() => { - refetch(); - }); - - if (isLoading) { - return ( - null} - ListHeaderComponent={() => ( - - Loading delivery slots... - - )} - /> - ); - } - - if (error) { - return ( - null} - ListHeaderComponent={() => ( - - Error loading delivery slots - refetch()} - style={tw`mt-4 bg-blue-500 px-4 py-2 rounded-lg`} - > - Retry - - - )} - /> - ); - } - - const slots = data?.slots || []; - - if (slots.length === 0) { - return ( - null} - ListHeaderComponent={() => ( - - - - No upcoming delivery slots available - - - Check back later for new delivery schedules - - - )} - /> - ); - } - - return ( - <> - item.id.toString()} - // ListHeaderComponent={() => ( - // - // Delivery Slots - // - // Choose your preferred delivery time - // - // - // )} - renderItem={({ item: slot }) => ( - - {/* Slot Header */} - - - - - {dayjs(slot.deliveryTime).format('ddd DD MMM, h:mm a')} - - - Orders close by: {dayjs(slot.freezeTime).format('h:mm a')} - - - - - - {slot.products.length} items - - - router.push(`/(drawer)/(tabs)/home/cart?slot=${slot.id}`)} - style={tw`bg-pink-500 p-2 rounded-full`} - > - - - - - - - {/* Products List */} - - - Available Products - - - {slot.products.slice(0, 2).map((product) => ( - router.push(`/(drawer)/(tabs)/home/product-detail/${product.id}`)} - style={tw`bg-gray-50 rounded-lg p-3 flex-row items-center`} - > - {product.images && product.images.length > 0 ? ( - - ) : ( - - - - )} - - - {product.name} - - - ₹{product.price} {product.unit && `per ${product.unit}`} - - - {product.isOutOfStock && ( - Out of stock - )} - - ))} - - {slot.products.length > 2 && ( - setSelectedSlotForDialog(slot)} - style={tw`bg-pink-50 rounded-lg p-3 flex-row items-center justify-center border border-pink-200`} - > - - +{slot.products.length - 2} more products - - - - )} - - - - )} - ListFooterComponent={() => } - showsVerticalScrollIndicator={false} - contentContainerStyle={tw`pt-2`} - /> - - {/* Products Dialog */} - setSelectedSlotForDialog(null)} - > - - - All Products - {dayjs(selectedSlotForDialog?.deliveryTime).format('ddd DD MMM, h:mm a')} - - - - - {selectedSlotForDialog?.products.map((product: any) => ( - { - setSelectedSlotForDialog(null); - router.push(`/(drawer)/(tabs)/home/product-detail/${product.id}`); - }} - style={tw`bg-gray-50 rounded-lg p-4 flex-row items-center`} - > - {product.images && product.images.length > 0 ? ( - - ) : ( - - - - )} - - - {product.name} - - - ₹{product.price} {product.unit && `per ${product.unit}`} - - {product.marketPrice && ( - - ₹{product.marketPrice} - - )} - - {product.isOutOfStock && ( - Out of stock - )} - - ))} - - - - - - ); -} \ No newline at end of file diff --git a/apps/user-ui/app/(drawer)/(tabs)/stores/store-detail/[id].tsx b/apps/user-ui/app/(drawer)/(tabs)/stores/store-detail/[id].tsx index af62179..e8538a8 100644 --- a/apps/user-ui/app/(drawer)/(tabs)/stores/store-detail/[id].tsx +++ b/apps/user-ui/app/(drawer)/(tabs)/stores/store-detail/[id].tsx @@ -1,4 +1,4 @@ -import React, { useState } from "react"; +import React, { useMemo, useState } from "react"; import { View, Dimensions, ScrollView, TouchableOpacity } from "react-native"; import { useRouter, useLocalSearchParams } from "expo-router"; import { Image } from 'expo-image'; @@ -13,10 +13,10 @@ import { } from "common-ui"; import MaterialIcons from "@expo/vector-icons/MaterialIcons"; import FontAwesome5 from "@expo/vector-icons/FontAwesome5"; -import { trpc } from "@/src/trpc-client"; import ProductCard from "@/components/ProductCard"; import FloatingCartBar from "@/components/floating-cart-bar"; import { useStoreHeaderStore } from "@/src/store/storeHeaderStore"; +import { useAllProducts, useStoreWithProducts } from "@/src/hooks/prominent-api-hooks"; const { width: screenWidth } = Dimensions.get("window"); const itemWidth = (screenWidth - 48) / 2; @@ -63,24 +63,32 @@ export default function StoreDetail() { const [selectedTagId, setSelectedTagId] = useState(null); const { data: storeData, isLoading, refetch, error } = - trpc.user.stores.getStoreWithProducts.useQuery( - { storeId: storeIdNum }, - { enabled: !!storeIdNum } - ); + useStoreWithProducts(storeIdNum); - const { data: tagsData, isLoading: isLoadingTags } = - trpc.user.tags.getTagsByStore.useQuery( - { storeId: storeIdNum }, - { enabled: !!storeIdNum } - ); + const { data: productsData, isLoading: isProductsLoading } = useAllProducts(); + + const productById = useMemo(() => { + const map = new Map(); + productsData?.products?.forEach((product) => { + map.set(product.id, product); + }); + return map; + }, [productsData]); + + const storeProducts = useMemo(() => { + if (!storeData?.products) return []; + return storeData.products + .map((product) => productById.get(product.id)) + .filter(Boolean); + }, [storeData, productById]); // Filter products based on selected tag const filteredProducts = selectedTagId - ? storeData?.products.filter(product => { - const selectedTag = tagsData?.tags.find(t => t.id === selectedTagId); + ? storeProducts.filter(product => { + const selectedTag = storeData?.tags.find(t => t.id === selectedTagId); return selectedTag?.productIds?.includes(product.id) ?? false; - }) || [] - : storeData?.products || []; + }) + : storeProducts; // Set the store header title const setStoreHeaderTitle = useStoreHeaderStore((state) => state.setTitle); @@ -98,10 +106,12 @@ export default function StoreDetail() { useDrawerTitle(storeData?.store?.name || "Store", [storeData?.store?.name]); - if (isLoading) { + if (isLoading || isProductsLoading) { return ( - Loading store... + + {isLoading ? 'Loading store...' : 'Loading products...'} + ); } @@ -184,13 +194,13 @@ export default function StoreDetail() { )} {/* Tags Section */} - {tagsData && tagsData.tags.length > 0 && ( + {storeData?.tags && storeData.tags.length > 0 && ( - {tagsData.tags.map((tag) => ( + {storeData.tags.map((tag) => ( {selectedTagId - ? `${tagsData?.tags.find(t => t.id === selectedTagId)?.tagName} items` + ? `${storeData?.tags.find(t => t.id === selectedTagId)?.tagName} items` : `${filteredProducts.length} products`} diff --git a/apps/user-ui/app/_layout.tsx b/apps/user-ui/app/_layout.tsx index 7c1c49b..2c02370 100755 --- a/apps/user-ui/app/_layout.tsx +++ b/apps/user-ui/app/_layout.tsx @@ -22,6 +22,7 @@ import LocationTestWrapper from "@/components/LocationTestWrapper"; import HealthTestWrapper from "@/components/HealthTestWrapper"; import FirstUserWrapper from "@/components/FirstUserWrapper"; import UpdateChecker from "@/components/UpdateChecker"; +import CentralStoreInitializer from "@/src/components/CentralStoreInitializer"; import { RefreshProvider } from "../../../packages/ui/src/lib/refresh-context"; import WebViewWrapper from "@/components/WebViewWrapper"; import BackHandlerWrapper from "@/components/BackHandler"; @@ -68,10 +69,12 @@ export default function RootLayout() { + - + + diff --git a/apps/user-ui/components/BannerCarousel.tsx b/apps/user-ui/components/BannerCarousel.tsx index 7844d3f..893f8b0 100644 --- a/apps/user-ui/components/BannerCarousel.tsx +++ b/apps/user-ui/components/BannerCarousel.tsx @@ -2,7 +2,7 @@ import React, { useState, useRef, useEffect } from 'react'; import { View, Dimensions, Image, ScrollView, NativeSyntheticEvent, NativeScrollEvent } from 'react-native'; import { MyTouchableOpacity, MyText, tw } from 'common-ui'; import { useRouter } from 'expo-router'; -import { trpc } from '@/src/trpc-client'; +import { useBanners } from '@/src/hooks/prominent-api-hooks'; import MaterialIcons from '@expo/vector-icons/MaterialIcons'; const { width: screenWidth } = Dimensions.get('window'); @@ -25,7 +25,7 @@ export default function BannerCarousel() { const [isAutoPlaying, setIsAutoPlaying] = useState(true); // Fetch banners data - const { data: bannersData, isLoading, error } = trpc.user.banner.getBanners.useQuery(); + const { data: bannersData, isLoading, error } = useBanners(); const banners = bannersData?.banners || []; @@ -123,7 +123,7 @@ export default function BannerCarousel() { {/* Pagination Dots */} {banners.length > 1 && ( - {banners.map((_, index: number) => ( + {banners.map((_: Banner, index: number) => ( goToSlide(index)} @@ -140,4 +140,4 @@ export default function BannerCarousel() { )} ); -} \ No newline at end of file +} diff --git a/apps/user-ui/components/PaymentAndOrderComponent.tsx b/apps/user-ui/components/PaymentAndOrderComponent.tsx index 7e9f998..864bcec 100644 --- a/apps/user-ui/components/PaymentAndOrderComponent.tsx +++ b/apps/user-ui/components/PaymentAndOrderComponent.tsx @@ -6,7 +6,7 @@ import MaterialIcons from '@expo/vector-icons/MaterialIcons'; // import RazorpayCheckout from 'react-native-razorpay'; import { trpc } from '@/src/trpc-client'; -import { useAllProducts } from '@/src/hooks/prominent-api-hooks'; +import { useCentralProductStore } from '@/src/store/centralProductStore'; import { clearLocalCart } from '@/hooks/cart-query-hooks'; import { useQueryClient } from '@tanstack/react-query'; import { FontAwesome5, FontAwesome6 } from '@expo/vector-icons'; @@ -55,17 +55,18 @@ const PaymentAndOrderComponent: React.FC = ({ queryClient.invalidateQueries({ queryKey: [`local-cart-${cartType}`] }); }; - const { data: productsData } = useAllProducts(); + const products = useCentralProductStore((state) => state.products); + const productsById = useCentralProductStore((state) => state.productsById); // Memoized flash-eligible product IDs const flashEligibleProductIds = useMemo(() => { - if (!productsData?.products) return new Set(); + if (!products.length) return new Set(); return new Set( - productsData.products - .filter((product: any) => product.isFlashAvailable) - .map((product: any) => product.id) + products + .filter((product) => product.isFlashAvailable) + .map((product) => product.id) ); - }, [productsData]); + }, [products]); const placeOrderMutation = trpc.user.order.placeOrder.useMutation({ onSuccess: (data) => { @@ -127,7 +128,7 @@ const PaymentAndOrderComponent: React.FC = ({ const availableItems = cartItems .filter(item => { - if (item.product?.isOutOfStock) return false; + if (productsById[item.productId]?.isOutOfStock) return false; // For flash delivery, check if product supports flash delivery if (isFlashDelivery) { return flashEligibleProductIds.has(item.productId); diff --git a/apps/user-ui/components/ProductCard.tsx b/apps/user-ui/components/ProductCard.tsx index 179b4e2..055b8ee 100644 --- a/apps/user-ui/components/ProductCard.tsx +++ b/apps/user-ui/components/ProductCard.tsx @@ -1,4 +1,4 @@ -import React, { useMemo } from 'react'; +import React from 'react'; import { View, Alert, TouchableOpacity, Text } from 'react-native'; import { Image } from 'expo-image'; import { tw, theme, MyText, MyTouchableOpacity, Quantifier, MiniQuantifier } from 'common-ui'; @@ -14,7 +14,7 @@ import { } from '@/hooks/cart-query-hooks'; import { useProductSlotIdentifier } from '@/hooks/useProductSlotIdentifier'; import { useCartStore } from '@/src/store/cartStore'; -import { trpc } from '@/src/trpc-client'; +import { useCentralSlotStore } from '@/src/store/centralSlotStore'; interface ProductCardProps { @@ -68,17 +68,18 @@ const ProductCard: React.FC = ({ const cartItem = cartData?.items?.find((cartItem: any) => cartItem.productId === item.id); const quantity = cartItem?.quantity || 0; - // Query all slots with products - const { data: slotsData } = trpc.user.slots.getSlotsWithProducts.useQuery(); + // Get slots data from central store + const slots = useCentralSlotStore((state) => state.slots); + const productSlotsMap = useCentralSlotStore((state) => state.productSlotsMap); // Create slot lookup map - const slotMap = useMemo(() => { + const slotMap = React.useMemo(() => { const map: Record = {}; - slotsData?.slots?.forEach((slot: any) => { + slots?.forEach((slot: any) => { map[slot.id] = slot; }); return map; - }, [slotsData]); + }, [slots]); // Get cart item's slot delivery time if item is in cart const cartSlot = cartItem?.slotId ? slotMap[cartItem.slotId] : null; @@ -86,7 +87,11 @@ const ProductCard: React.FC = ({ // Precompute the next slot and determine display out of stock status const slotId = getQuickestSlot(item.id); - const displayIsOutOfStock = item.isOutOfStock || !slotId; + + // Use isOutOfStock from productSlotsMap (all products now included) + const productSlotInfo = productSlotsMap[item.id]; + const isOutOfStockFromSlots = productSlotInfo?.isOutOfStock; + const displayIsOutOfStock = isOutOfStockFromSlots || !slotId; // if(item.name.startsWith('Mutton Curry Cut')) { // console.log({slotId, displayIsOutOfStock}) @@ -213,4 +218,4 @@ const ProductCard: React.FC = ({ ); }; -export default ProductCard; \ No newline at end of file +export default ProductCard; diff --git a/apps/user-ui/components/ProductDetail.tsx b/apps/user-ui/components/ProductDetail.tsx index c221cd6..ab3f948 100644 --- a/apps/user-ui/components/ProductDetail.tsx +++ b/apps/user-ui/components/ProductDetail.tsx @@ -12,6 +12,7 @@ import { trpc, trpcClient } from '@/src/trpc-client'; import { useAddToCart, useGetCart, useUpdateCartItem, useRemoveFromCart } from '@/hooks/cart-query-hooks'; import { useProductSlotIdentifier } from '@/hooks/useProductSlotIdentifier'; import { useFlashNavigationStore } from '@/components/stores/flashNavigationStore'; +import { useSlots } from '@/src/hooks/prominent-api-hooks'; import FloatingCartBar from './floating-cart-bar'; import { useStoreHeaderStore } from '@/src/store/storeHeaderStore'; import { useCartStore } from '@/src/store/cartStore'; @@ -57,15 +58,22 @@ const ProductDetail: React.FC = ({ productId, isFlashDeliver const { getQuickestSlot } = useProductSlotIdentifier(); const { setShouldNavigateToCart } = useFlashNavigationStore(); const { setAddedToCartProduct } = useCartStore(); + const { data: slotsData } = useSlots(); const sortedDeliverySlots = useMemo(() => { - if (!productDetail?.deliverySlots) return [] - return [...productDetail.deliverySlots].sort((a, b) => { + if (!slotsData?.slots || !productDetail) return [] + + // Filter slots that contain this product + const productSlots = slotsData.slots.filter((slot: any) => + slot.products?.some((p: any) => p.id === productDetail.id) + ) + + return productSlots.sort((a: any, b: any) => { const deliveryDiff = new Date(a.deliveryTime).getTime() - new Date(b.deliveryTime).getTime() if (deliveryDiff !== 0) return deliveryDiff return new Date(a.freezeTime).getTime() - new Date(b.freezeTime).getTime() }) - }, [productDetail?.deliverySlots]) + }, [slotsData, productDetail]) // Find current quantity from cart data const cartItem = productDetail ? cartData?.data?.items?.find((item: any) => item.productId === productDetail.id) : null; @@ -343,13 +351,13 @@ const ProductDetail: React.FC = ({ productId, isFlashDeliver ) : productDetail.isFlashAvailable ? ( productDetail.deliverySlots.length > 0 && handleBuyNow(productDetail.id)} - disabled={productDetail.deliverySlots.length === 0} + onPress={() => sortedDeliverySlots.length > 0 && handleBuyNow(productDetail.id)} + disabled={sortedDeliverySlots.length === 0} > - - {productDetail.deliverySlots.length === 0 ? 'No Slots' : 'Get in 1 Hour'} + + {sortedDeliverySlots.length === 0 ? 'No Slots' : 'Get in 1 Hour'} ) : ( @@ -797,4 +805,4 @@ const ReviewForm = ({ productId, onReviewSubmitted }: ReviewFormProps) => { ); }; -export default ProductDetail; \ No newline at end of file +export default ProductDetail; diff --git a/apps/user-ui/components/QuickDeliveryAddressSelector.tsx b/apps/user-ui/components/QuickDeliveryAddressSelector.tsx index dc28eeb..4d6ac53 100644 --- a/apps/user-ui/components/QuickDeliveryAddressSelector.tsx +++ b/apps/user-ui/components/QuickDeliveryAddressSelector.tsx @@ -6,6 +6,7 @@ import { BottomDialog, MyTouchableOpacity, MyText, tw, theme } from 'common-ui'; import { useAuth } from '@/src/contexts/AuthContext'; import { trpc } from '@/src/trpc-client'; import { useAddressStore } from '@/src/store/addressStore'; +import { useSlots } from '@/src/hooks/prominent-api-hooks'; import dayjs from 'dayjs'; interface QuickDeliveryAddressSelectorProps { @@ -31,13 +32,13 @@ const QuickDeliveryAddressSelector: React.FC const { data: addressesData } = trpc.user.address.getUserAddresses.useQuery(undefined, { enabled: isAuthenticated, }); - const { data: slotsData } = trpc.user.slots.getSlotsWithProducts.useQuery(); + const { data: slotsData } = useSlots(); const defaultAddress = defaultAddressData?.data; const addresses = addressesData?.data || []; // Format time range helper - const formatTimeRange = (deliveryTime: string) => { + const formatTimeRange = (deliveryTime: string | Date) => { const time = dayjs(deliveryTime); const endTime = time.add(1, 'hour'); const startPeriod = time.format('A'); @@ -276,4 +277,4 @@ const QuickDeliveryAddressSelector: React.FC ); }; -export default QuickDeliveryAddressSelector; \ No newline at end of file +export default QuickDeliveryAddressSelector; diff --git a/apps/user-ui/components/cart-page.tsx b/apps/user-ui/components/cart-page.tsx index 8f0077f..adec182 100644 --- a/apps/user-ui/components/cart-page.tsx +++ b/apps/user-ui/components/cart-page.tsx @@ -24,7 +24,7 @@ import TestingPhaseNote from "@/components/TestingPhaseNote"; import dayjs from "dayjs"; import { trpc } from "@/src/trpc-client"; -import { useAllProducts } from "@/src/hooks/prominent-api-hooks"; +import { useCentralProductStore } from '@/src/store/centralProductStore'; import { useGetCart, useUpdateCartItem, useRemoveFromCart } from '@/hooks/cart-query-hooks'; import { useGetEssentialConsts } from '@/src/hooks/prominent-api-hooks'; @@ -81,33 +81,33 @@ export default function CartPage({ isFlashDelivery = false }: CartPageProps) { const { data: couponsRaw, error: couponsError } = trpc.user.coupon.getEligible.useQuery(); const { data: constsData } = useGetEssentialConsts(); - const { data: productsData } = useAllProducts(); + const products = useCentralProductStore((state) => state.products); + const productsById = useCentralProductStore((state) => state.productsById); const cartItems = cartData?.items || []; // Memoized flash-eligible product IDs const flashEligibleProductIds = useMemo(() => { - if (!productsData?.products) return new Set(); + if (!products.length) return new Set(); return new Set( - productsData.products - .filter((product: any) => product.isFlashAvailable) - .map((product: any) => product.id) + products + .filter((product) => product.isFlashAvailable) + .map((product) => product.id) ); - }, [productsData]); + }, [products]); // Base total price without discounts for coupon eligibility check - const baseTotalPrice = useMemo( - () => - cartItems - .filter((item) => !item.product?.isOutOfStock) - .reduce( - (sum, item) => - sum + - (item.product?.price || 0) * (quantities[item.id] || item.quantity), - 0 - ), - [cartItems, quantities] + const baseTotalPrice = useMemo( + () => + cartItems + .filter((item) => !productsById[item.productId]?.isOutOfStock) + .reduce((sum, item) => { + const product = productsById[item.productId]; + const price = product?.price || 0; + return sum + price * (quantities[item.id] || item.quantity); + }, 0), + [cartItems, quantities, productsById] ); const eligibleCoupons = useMemo(() => { @@ -200,13 +200,14 @@ export default function CartPage({ isFlashDelivery = false }: CartPageProps) { [eligibleCoupons, selectedCouponId] ); - const totalPrice = cartItems - .filter((item) => !item.product?.isOutOfStock) - .reduce((sum, item) => { - const quantity = quantities[item.id] || item.quantity; - const price = isFlashDelivery ? (item.product?.flashPrice ?? item.product?.price ?? 0) : (item.product?.price || 0); - return sum + price * quantity; - }, 0); + const totalPrice = cartItems + .filter((item) => !productsById[item.productId]?.isOutOfStock) + .reduce((sum, item) => { + const product = productsById[item.productId]; + const quantity = quantities[item.id] || item.quantity; + const price = isFlashDelivery ? (product?.flashPrice ?? product?.price ?? 0) : (product?.price || 0); + return sum + price * quantity; + }, 0); const dropdownData = useMemo( () => eligibleCoupons?.map((coupon) => { @@ -274,7 +275,7 @@ export default function CartPage({ isFlashDelivery = false }: CartPageProps) { const finalTotalWithDelivery = finalTotal + deliveryCharge; - const hasAvailableItems = cartItems.some(item => !item.product?.isOutOfStock); + const hasAvailableItems = cartItems.some(item => !productsById[item.productId]?.isOutOfStock); useEffect(() => { const initial: Record = {}; @@ -411,10 +412,11 @@ export default function CartPage({ isFlashDelivery = false }: CartPageProps) { const productSlots = getAvailableSlotsForProduct(item.productId); const selectedSlotForItem = selectedSlots[item.id]; const isFlashEligible = isFlashDelivery ? flashEligibleProductIds.has(item.productId) : true; + const product = productsById[item.productId]; // const isAvailable = (productSlots.length > 0 || isFlashDelivery) && !item.product?.isOutOfStock && isFlashEligible; let isAvailable = true; - if(item.product?.isOutOfStock) { + if (product?.isOutOfStock) { isAvailable = false; } else if(isFlashDelivery) { if(!isFlashEligible) { @@ -431,7 +433,7 @@ export default function CartPage({ isFlashDelivery = false }: CartPageProps) { // isAvailable = isFlashEligible; // } const quantity = quantities[item.id] || item.quantity; - const price = isFlashDelivery ? (item.product?.flashPrice ?? item.product?.price ?? 0) : (item.product?.price || 0); + const price = isFlashDelivery ? (product?.flashPrice ?? product?.price ?? 0) : (product?.price || 0); const itemPrice = price * quantity; return ( @@ -439,7 +441,7 @@ export default function CartPage({ isFlashDelivery = false }: CartPageProps) { @@ -447,12 +449,12 @@ export default function CartPage({ isFlashDelivery = false }: CartPageProps) { style={tw`text-sm text-gray-900 flex-1 mr-3`} numberOfLines={2} > - {item.product.name} + {product?.name} {(() => { - const qty = item.product?.productQuantity || 1; - const unit = item.product?.unitNotation || ''; + const qty = product?.productQuantity || 1; + const unit = product?.unitNotation || ''; if (unit?.toLowerCase() === 'kg' && qty < 1) { return `${Math.round(qty * 1000)}g`; } @@ -513,8 +515,8 @@ export default function CartPage({ isFlashDelivery = false }: CartPageProps) { }); } }} - step={item.product.incrementStep} - unit={item.product?.unitNotation} + step={product?.incrementStep} + unit={product?.unitNotation} /> @@ -580,7 +582,7 @@ export default function CartPage({ isFlashDelivery = false }: CartPageProps) { onPress={() => { Alert.alert( "Remove Item", - `Remove ${item.product.name} from cart?`, + `Remove ${product?.name} from cart?`, [ { text: "Cancel", style: "cancel" }, { @@ -631,7 +633,7 @@ export default function CartPage({ isFlashDelivery = false }: CartPageProps) { onPress={() => { Alert.alert( "Remove Item", - `Remove ${item.product.name} from cart?`, + `Remove ${product?.name} from cart?`, [ { text: "Cancel", style: "cancel" }, { @@ -675,8 +677,8 @@ export default function CartPage({ isFlashDelivery = false }: CartPageProps) { style={tw`bg-red-50 self-start px-2 py-1 rounded-md mt-2`} > - {item.product?.isOutOfStock - ? "Out of Stock" + {product?.isOutOfStock + ? "Out of Stock" : isFlashDelivery && !flashEligibleProductIds.has(item.productId) ? "Not available for flash delivery. Please remove" : "No delivery slots available"} @@ -909,7 +911,7 @@ export default function CartPage({ isFlashDelivery = false }: CartPageProps) { onPress={() => { const availableItems = cartItems .filter(item => { - if (item.product?.isOutOfStock) return false; + if (productsById[item.productId]?.isOutOfStock) return false; if (isFlashDelivery) { // Check if product supports flash delivery return flashEligibleProductIds.has(item.productId); @@ -922,8 +924,8 @@ export default function CartPage({ isFlashDelivery = false }: CartPageProps) { if (availableItems.length === 0) { // Determine why no items are available - const outOfStockItems = cartItems.filter(item => item.product?.isOutOfStock); - const inStockItems = cartItems.filter(item => !item.product?.isOutOfStock); + const outOfStockItems = cartItems.filter(item => productsById[item.productId]?.isOutOfStock); + const inStockItems = cartItems.filter(item => !productsById[item.productId]?.isOutOfStock); let errorTitle = "Cannot Proceed"; let errorMessage = ""; @@ -962,7 +964,7 @@ export default function CartPage({ isFlashDelivery = false }: CartPageProps) { // Check if there are items without slots (for regular delivery) if (!isFlashDelivery && availableItems.length < cartItems.length) { - const itemsWithoutSlots = cartItems.filter(item => !selectedSlots[item.id] && !item.product?.isOutOfStock); + const itemsWithoutSlots = cartItems.filter(item => !selectedSlots[item.id] && !productsById[item.productId]?.isOutOfStock); if (itemsWithoutSlots.length > 0) { Alert.alert( "Delivery Slot Required", diff --git a/apps/user-ui/components/checkout-page.tsx b/apps/user-ui/components/checkout-page.tsx index 60a33f1..6be1b95 100644 --- a/apps/user-ui/components/checkout-page.tsx +++ b/apps/user-ui/components/checkout-page.tsx @@ -8,7 +8,7 @@ import AddressForm from '@/src/components/AddressForm'; import { useAuthenticatedRoute } from '@/hooks/useAuthenticatedRoute'; import { trpc } from '@/src/trpc-client'; -import { useAllProducts } from '@/src/hooks/prominent-api-hooks'; +import { useCentralProductStore } from '@/src/store/centralProductStore'; import { useGetCart } from '@/hooks/cart-query-hooks'; import { useGetEssentialConsts } from '@/src/hooks/prominent-api-hooks'; import PaymentAndOrderComponent from '@/components/PaymentAndOrderComponent'; @@ -36,7 +36,8 @@ const CheckoutPage: React.FC = ({ isFlashDelivery = false }) const { data: addresses, refetch: refetchAddresses } = trpc.user.address.getUserAddresses.useQuery(); const { data: slotsData, refetch: refetchSlots } = trpc.user.slots.getSlots.useQuery(); const { data: constsData } = useGetEssentialConsts(); - const { data: productsData } = useAllProducts(); + const products = useCentralProductStore((state) => state.products); + const productsById = useCentralProductStore((state) => state.productsById); useMarkDataFetchers(() => { refetchCart(); @@ -53,14 +54,14 @@ const CheckoutPage: React.FC = ({ isFlashDelivery = false }) const cartItems = cartData?.items || []; // Memoized flash-eligible product IDs - const flashEligibleProductIds = useMemo(() => { - if (!productsData?.products) return new Set(); - return new Set( - productsData.products - .filter((product: any) => product.isFlashAvailable) - .map((product: any) => product.id) - ); - }, [productsData]); + const flashEligibleProductIds = useMemo(() => { + if (!products.length) return new Set(); + return new Set( + products + .filter((product) => product.isFlashAvailable) + .map((product) => product.id) + ); + }, [products]); // Parse slots parameter from URL (format: "1:1,2,3;2:4,5") const selectedSlots = useMemo(() => { @@ -124,10 +125,11 @@ const CheckoutPage: React.FC = ({ isFlashDelivery = false }) const totalPrice = selectedItems - .filter((item) => !item.product?.isOutOfStock) + .filter((item) => !productsById[item.productId]?.isOutOfStock) .reduce( (sum, item) => { - const price = isFlashDelivery ? (item.product?.flashPrice ?? item.product?.price ?? 0) : (item.product?.price || 0); + const product = productsById[item.productId]; + const price = isFlashDelivery ? (product?.flashPrice ?? product?.price ?? 0) : (product?.price || 0); return sum + price * item.quantity; }, 0 diff --git a/apps/user-ui/components/floating-cart-bar.tsx b/apps/user-ui/components/floating-cart-bar.tsx index cbf7e79..fff1dc8 100644 --- a/apps/user-ui/components/floating-cart-bar.tsx +++ b/apps/user-ui/components/floating-cart-bar.tsx @@ -14,7 +14,6 @@ import { theme, updateStatusBarColor, } from "common-ui"; -import { trpc } from "@/src/trpc-client"; import { useGetCart, useUpdateCartItem, @@ -22,8 +21,9 @@ import { useAddToCart, type CartType, } from "@/hooks/cart-query-hooks"; -import { useGetEssentialConsts } from "@/src/api-hooks/essential-consts.api"; +import { useGetEssentialConsts, useSlots } from "@/src/hooks/prominent-api-hooks" import { useProductSlotIdentifier } from "@/hooks/useProductSlotIdentifier"; +import { useCentralProductStore } from "@/src/store/centralProductStore"; import dayjs from "dayjs"; import { LinearGradient } from "expo-linear-gradient"; @@ -36,7 +36,7 @@ interface FloatingCartBarProps { } // Smart time window formatting function -const formatTimeRange = (deliveryTime: string) => { +const formatTimeRange = (deliveryTime: string | Date) => { const time = dayjs(deliveryTime); const endTime = time.add(1, 'hour'); const startPeriod = time.format('A'); @@ -79,7 +79,8 @@ const FloatingCartBar: React.FC = ({ const setIsExpanded = controlledSetIsExpanded ?? setLocalIsExpanded; const { data: cartData, refetch: refetchCart } = useGetCart({}, cartType); const { data: constsData } = useGetEssentialConsts(); - const { data: slotsData } = trpc.user.slots.getSlotsWithProducts.useQuery(); + const { data: slotsData } = useSlots(); + const productsById = useCentralProductStore((state) => state.productsById); const { productSlotsMap } = useProductSlotIdentifier(); const cartItems = cartData?.items || []; const itemCount = cartItems.length; @@ -108,21 +109,21 @@ const FloatingCartBar: React.FC = ({ setQuantities(initial); }, [cartData]); -useEffect(() => { + useEffect(() => { if (!cartItems.length || !slotsData?.slots || !productSlotsMap) return; const itemsToUpdate = cartItems.filter(item => { if (isFlashDelivery || !item.slotId) return false; - const availableSlots = productSlotsMap.get(item.productId) || []; - const isSlotAvailable = availableSlots.includes(item.slotId); + const availableSlots = productSlotsMap[item.productId]?.slots || []; + const isSlotAvailable = availableSlots.some((slot) => slot.id === item.slotId); return !isSlotAvailable; }); itemsToUpdate.forEach((item) => { - const availableSlots = productSlotsMap.get(item.productId) || []; + const availableSlots = productSlotsMap[item.productId]?.slots || []; if (availableSlots.length > 0 && !isFlashDelivery) { - const nearestSlotId = availableSlots[0]; + const nearestSlotId = availableSlots[0].id; removeFromCart.mutate({ itemId: item.id }); addToCartHook.addToCart(item.productId, item.quantity, nearestSlotId); } @@ -135,7 +136,9 @@ useEffect(() => { // Calculate total cart value and free delivery info const totalCartValue = cartItems.reduce( (sum, item) => { - const price = isFlashDelivery ? (item.product.flashPrice ?? item.product.price) : item.product.price; + const product = productsById[item.productId]; + const basePrice = product?.price ?? 0; + const price = isFlashDelivery ? (product?.flashPrice ?? basePrice) : basePrice; return sum + price * item.quantity; }, 0 @@ -256,21 +259,21 @@ useEffect(() => { - + - { + { if (value === 0) { removeFromCart.mutate({ itemId: item.id }); } else { @@ -278,21 +281,20 @@ useEffect(() => { updateCartItem.mutate({ itemId: item.id, quantity: value }); } }} - step={item.product.incrementStep} - showUnits={true} - unit={item.product?.unitNotation} - /> + step={productsById[item.productId]?.incrementStep || 1} + showUnits={true} + unit={productsById[item.productId]?.unitNotation} + /> - {item.slotId && slotsData && productSlotsMap.has(item.productId) && ( + {item.slotId && slotsData && productSlotsMap[item.productId] && ( { - const slot = slotsData.slots.find(s => s.id === slotId); + options={(productSlotsMap[item.productId]?.slots || []).map((slot) => { return { label: slot ? formatTimeRange(slot.deliveryTime) : "N/A", - value: slotId, + value: slot.id, }; })} onValueChange={async (val) => { @@ -325,7 +327,12 @@ useEffect(() => { /> )} - ₹{(isFlashDelivery ? (item.product.flashPrice ?? item.product.price) : item.product.price) * item.quantity} + ₹{(() => { + const product = productsById[item.productId]; + const basePrice = product?.price ?? 0; + const price = isFlashDelivery ? (product?.flashPrice ?? basePrice) : basePrice; + return price * item.quantity; + })()} diff --git a/apps/user-ui/hooks/cart-query-hooks.tsx b/apps/user-ui/hooks/cart-query-hooks.tsx index 4f975f4..ceee1df 100644 --- a/apps/user-ui/hooks/cart-query-hooks.tsx +++ b/apps/user-ui/hooks/cart-query-hooks.tsx @@ -1,16 +1,12 @@ -import { trpc } from '@/src/trpc-client'; import { useAllProducts } from '@/src/hooks/prominent-api-hooks'; +import { useCentralSlotStore } from '@/src/store/centralSlotStore'; import { Alert } from 'react-native'; -import { useState, useEffect } from 'react'; -import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; +import { useQuery, useMutation, useQueryClient, UseQueryResult, UseMutationResult } from '@tanstack/react-query'; import { StorageServiceCasual } from 'common-ui/src/services/StorageServiceCasual'; // Cart type definition export type CartType = "regular" | "flash"; -// const CART_MODE: 'remote' | 'local' = 'remote'; -const CART_MODE: 'remote' | 'local' = 'local'; - const getCartStorageKey = (cartType: CartType = "regular"): string => { return cartType === "flash" ? "flash_cart_items" : "cart_items"; }; @@ -27,15 +23,114 @@ interface ProductSummary { id: number; price: string; incrementStep: number; + isOutOfStock: boolean; + isFlashAvailable: boolean; + name?: string; + flashPrice?: string | null; + images?: string[]; + productQuantity?: number; + unitNotation?: string; + marketPrice?: string | null; } -interface CartItem { +export interface CartItem { id: number; productId: number; quantity: number; addedAt: string; product: ProductSummary; subtotal: number; + slotId: number; +} + +interface CartData { + items: CartItem[]; + totalItems: number; + totalAmount: number; +} + +interface UseGetCartOptions { + refetchOnWindowFocus?: boolean; + enabled?: boolean; +} + +interface UseGetCartReturn { + data: CartData | undefined; + isLoading: boolean; + error: Error | null; + refetch: () => Promise>; + cartItems: CartItem[]; + totalItems: number; + totalPrice: number; + isEmpty: boolean; + hasItems: boolean; +} + +interface AddToCartVariables { + productId: number; + quantity: number; + slotId: number; +} + +interface UpdateCartVariables { + itemId: number; + quantity: number; +} + +interface RemoveCartVariables { + itemId: number; +} + +interface MutationOptions { + onSuccess?: (data: TData, variables: TVariables) => void; + onError?: (error: Error) => void; + showSuccessAlert?: boolean; + showErrorAlert?: boolean; + refetchCart?: boolean; +} + +interface UseAddToCartReturn { + mutate: UseMutationResult['mutate']; + mutateAsync: UseMutationResult['mutateAsync']; + isLoading: boolean; + error: Error | null; + data: LocalCartItem[] | undefined; + addToCart: (productId: number, quantity?: number, slotId?: number, onSettled?: (data: LocalCartItem[] | undefined, error: Error | null) => void) => void; + addToCartAsync: (productId: number, quantity?: number, slotId?: number) => Promise; +} + +interface UseUpdateCartItemReturn { + mutate: UseMutationResult['mutate']; + mutateAsync: UseMutationResult['mutateAsync']; + isLoading: boolean; + error: Error | null; + data: LocalCartItem[] | undefined; + updateCartItem: (itemId: number, quantity: number) => void; + updateCartItemAsync: (itemId: number, quantity: number) => Promise; +} + +interface UseRemoveFromCartReturn { + mutate: UseMutationResult['mutate']; + mutateAsync: UseMutationResult['mutateAsync']; + isLoading: boolean; + error: Error | null; + data: LocalCartItem[] | undefined; + removeFromCart: (itemId: number) => void; + removeFromCartAsync: (itemId: number) => Promise; +} + +interface AllProductsResponse { + products: Array<{ + id: number; + price: number; + incrementStep: number; + marketPrice?: number | null; + name?: string; + flashPrice?: string | null; + images?: string[]; + productQuantity?: number; + unitNotation?: string; + }>; } const getLocalCart = async (cartType: CartType = "regular"): Promise => { @@ -47,8 +142,7 @@ const getLocalCart = async (cartType: CartType = "regular"): Promise => { const key = getCartStorageKey(cartType); await StorageServiceCasual.setItem(key, JSON.stringify(items)); - const fetchedItems = await getLocalCart(cartType); - + await getLocalCart(cartType); }; const getNextCartItemId = (items: LocalCartItem[]): number => { @@ -56,8 +150,7 @@ const getNextCartItemId = (items: LocalCartItem[]): number => { return maxId + 1; }; -const addToLocalCart = async (productId: number, quantity: number, slotId?: number, cartType: CartType = "regular"): Promise => { - +const addToLocalCart = async (productId: number, quantity: number, slotId: number | undefined, cartType: CartType = "regular"): Promise => { const items = await getLocalCart(cartType); const existingIndex = items.findIndex(item => item.productId === productId); @@ -68,13 +161,13 @@ const addToLocalCart = async (productId: number, quantity: number, slotId?: numb } } else { const newId = getNextCartItemId(items); - const cartItem = { + const cartItem: LocalCartItem = { id: newId, productId, quantity, - slotId: slotId ?? 0, // Default to 0 if not provided + slotId: slotId ?? 0, addedAt: new Date().toISOString(), - } + }; items.push(cartItem); } @@ -105,401 +198,198 @@ const clearLocalCart = async (cartType: CartType = "regular"): Promise => await StorageServiceCasual.setItem(key, JSON.stringify([])); }; -export function useGetCart(options?: { - refetchOnWindowFocus?: boolean; - enabled?: boolean; -}, cartType: CartType = "regular") { - if (CART_MODE === 'remote') { - const query = trpc.user.cart.getCart.useQuery(undefined, { - refetchOnWindowFocus: options?.refetchOnWindowFocus ?? true, - enabled: options?.enabled ?? true, - ...options - }); +export function useGetCart(options: UseGetCartOptions = {}, cartType: CartType = "regular"): UseGetCartReturn { + const { data: products } = useAllProducts() as { data: AllProductsResponse | undefined }; + const productSlotsMap = useCentralSlotStore((state) => state.productSlotsMap); + + const query: UseQueryResult = useQuery({ + queryKey: [`local-cart-${cartType}`], + queryFn: async (): Promise => { + const cartItems = await getLocalCart(cartType); - return { - // Original tRPC returns - data: query.data, - isLoading: query.isLoading, - error: query.error, - refetch: query.refetch, + const productMap: Record> = Object.fromEntries( + products?.products?.map((p) => [ + p.id, + { + id: p.id, + price: String(p.price), + incrementStep: p.incrementStep, + marketPrice: p.marketPrice === null || p.marketPrice === undefined ? null : String(p.marketPrice), + name: p.name, + flashPrice: p.flashPrice, + images: p.images, + productQuantity: p.productQuantity, + unitNotation: p.unitNotation, + }, + ]) ?? [] + ); - // Computed properties - cartItems: query.data?.items || [], - totalItems: query.data?.totalItems || 0, - totalPrice: query.data?.totalAmount || 0, + const items: CartItem[] = cartItems + .map((cartItem): CartItem | null => { + const productBasic = productMap[cartItem.productId]; + const productAvailability = productSlotsMap[cartItem.productId]; - // Helper methods - isEmpty: !query.data?.items?.length, - hasItems: Boolean(query.data?.items?.length), - }; - } else { - - const { data: products } = useAllProducts(); - const query = useQuery({ - queryKey: [`local-cart-${cartType}`], - queryFn: async () => { - - const cartItems = await getLocalCart(cartType); - - const productMap = Object.fromEntries( - products?.products?.map((p) => [ - p.id, - { - ...p, - price: String(p.price), - marketPrice: p.marketPrice === null || p.marketPrice === undefined ? null : String(p.marketPrice), - } as ProductSummary, - ]) || [] - ); - - const items: CartItem[] = cartItems.map(cartItem => { - const product = productMap[cartItem.productId]; + if (!productBasic || !productAvailability) return null; - if (!product) return null as any; return { id: cartItem.id, productId: cartItem.productId, quantity: cartItem.quantity, addedAt: cartItem.addedAt, - product, - incrementStep: product.incrementStep, - subtotal: Number(product.price) * cartItem.quantity, + product: { + ...productBasic, + isOutOfStock: productAvailability.isOutOfStock, + isFlashAvailable: productAvailability.isFlashAvailable, + }, + incrementStep: productBasic.incrementStep, + subtotal: Number(productBasic.price) * cartItem.quantity, slotId: cartItem.slotId, }; - }).filter(Boolean) as CartItem[]; - const totalAmount = items.reduce((sum, item) => sum + item.subtotal, 0); + }) + .filter((item): item is CartItem => item !== null); - return { - items, - totalItems: items.length, - totalAmount, - }; - }, - refetchOnWindowFocus: options?.refetchOnWindowFocus ?? true, - enabled: (options?.enabled ?? true) && !!products, + const totalAmount = items.reduce((sum, item) => sum + item.subtotal, 0); + + return { + items, + totalItems: items.length, + totalAmount, + }; + }, + refetchOnWindowFocus: options?.refetchOnWindowFocus ?? true, + enabled: (options?.enabled ?? true) && !!products, + }); + + return { + data: query.data, + isLoading: query.isLoading, + error: query.error, + refetch: query.refetch, + cartItems: query.data?.items ?? [], + totalItems: query.data?.totalItems ?? 0, + totalPrice: query.data?.totalAmount ?? 0, + isEmpty: !(query.data?.items?.length ?? 0), + hasItems: Boolean(query.data?.items?.length), + }; +} + +export function useAddToCart(options: MutationOptions = {}, cartType: CartType = "regular"): UseAddToCartReturn { + const queryClient = useQueryClient(); + + const mutation: UseMutationResult = useMutation({ + mutationFn: async ({ productId, quantity, slotId }: AddToCartVariables): Promise => { + return await addToLocalCart(productId, quantity, slotId, cartType); + }, + onSuccess: (data: LocalCartItem[], variables: AddToCartVariables) => { + queryClient.invalidateQueries({ queryKey: [`local-cart-${cartType}`] }); + if (options?.showSuccessAlert !== false) { + Alert.alert("Success", "Item added to cart!"); + } + options?.onSuccess?.(data, variables); + }, + onError: (error: Error) => { + if (options?.showErrorAlert !== false) { + Alert.alert("Error", error.message || "Failed to add item to cart"); + } + options?.onError?.(error); + }, + }); + + const addToCart = (productId: number, quantity = 1, slotId?: number, onSettled?: (data: LocalCartItem[] | undefined, error: Error | null) => void): void => { + if (slotId == null) { + throw new Error('slotId is required for adding to cart'); + } + mutation.mutate({ productId, quantity, slotId }, { + onSettled: (data: LocalCartItem[] | undefined, error: Error | null) => { + onSettled?.(data, error); + } }); - - return { - data: query.data, - isLoading: query.isLoading, - error: query.error, - refetch: query.refetch, + }; - // Computed properties - cartItems: query.data?.items || [], - totalItems: query.data?.totalItems || 0, - totalPrice: query.data?.totalAmount || 0, - - // Helper methods - isEmpty: !query.data?.items?.length, - hasItems: Boolean(query.data?.items?.length), - }; - } -} - -interface UseAddToCartReturn { - mutate: any; - mutateAsync: any; - isLoading: boolean; - error: any; - data: any; - addToCart: (productId: number, quantity?: number, slotId?: number, onSettled?: (data: any, error: any) => void) => void; - addToCartAsync: (productId: number, quantity?: number, slotId?: number) => Promise; -} - -export function useAddToCart(options?: { - onSuccess?: (data: any, variables: any) => void; - onError?: (error: any) => void; - showSuccessAlert?: boolean; - showErrorAlert?: boolean; - refetchCart?: boolean; -}, cartType: CartType = "regular"): UseAddToCartReturn { - if (CART_MODE === 'remote') { - const utils = trpc.useUtils(); - - const mutation = trpc.user.cart.addToCart.useMutation({ - onSuccess: (data, variables) => { - // Default success handling - if (options?.showSuccessAlert !== false) { - Alert.alert("Success", "Item added to cart!"); - } - - // Auto-refetch cart if requested - if (options?.refetchCart) { - utils.user.cart.getCart.invalidate(); - } - - // Custom success callback - options?.onSuccess?.(data, variables); - }, - onError: (error) => { - // Default error handling - if (options?.showErrorAlert !== false) { - Alert.alert("Error", error.message || "Failed to add item to cart"); - } - - // Custom error callback - options?.onError?.(error); - }, - }) as any; - - const addToCart = (productId: number, quantity = 1, slotId?: number, onSettled?: (data: any, error: any) => void) => { - + return { + mutate: mutation.mutate, + mutateAsync: mutation.mutateAsync, + isLoading: mutation.isPending, + error: mutation.error, + data: mutation.data, + addToCart, + addToCartAsync: (productId: number, quantity = 1, slotId?: number): Promise => { if (slotId == null) { throw new Error('slotId is required for adding to cart'); } - return mutation.mutate({ productId, quantity, slotId }, { - onSettled: (data: any, error: any) => { - onSettled?.(data, error); - } - }); - }; + return mutation.mutateAsync({ productId, quantity, slotId }); + }, + }; +} - return { - // Original mutation returns - mutate: mutation.mutate, - mutateAsync: mutation.mutateAsync, - isLoading: mutation.isPending, - error: mutation.error, - data: mutation.data, +export function useUpdateCartItem(options: MutationOptions = {}, cartType: CartType = "regular"): UseUpdateCartItemReturn { + const queryClient = useQueryClient(); - addToCart, - - addToCartAsync: (productId: number, quantity = 1, slotId?: number) => { - if (slotId == null) { - throw new Error('slotId is required for adding to cart'); - } - return mutation.mutateAsync({ productId, quantity, slotId }); - }, - }; - } else { - - const queryClient = useQueryClient(); - - const mutation = useMutation({ - mutationFn: async ({ productId, quantity, slotId }: { productId: number, quantity: number, slotId: number }) => { - return await addToLocalCart(productId, quantity, slotId, cartType); - }, - onSuccess: (data, variables) => { - queryClient.invalidateQueries({ queryKey: [`local-cart-${cartType}`] }); - if (options?.showSuccessAlert !== false) { - Alert.alert("Success", "Item added to cart!"); - } - options?.onSuccess?.(data, variables); - }, - onError: (error) => { - if (options?.showErrorAlert !== false) { - Alert.alert("Error", error.message || "Failed to add item to cart"); - } - options?.onError?.(error); - }, - }); - - const addToCart = (productId: number, quantity = 1, slotId?: number, onSettled?: (data: any, error: any) => void) => { - - if (slotId == null) { - throw new Error('slotId is required for adding to cart'); + const mutation: UseMutationResult = useMutation({ + mutationFn: async ({ itemId, quantity }: UpdateCartVariables): Promise => { + return await updateLocalCartItem(itemId, quantity, cartType); + }, + onSuccess: (data: LocalCartItem[], variables: UpdateCartVariables) => { + queryClient.invalidateQueries({ queryKey: [`local-cart-${cartType}`] }); + if (options?.showSuccessAlert !== false) { + Alert.alert("Success", "Cart item updated!"); } - return mutation.mutate({ productId, quantity, slotId }, { - onSettled: (data: any, error: any) => { - onSettled?.(data, error); - } - }); - }; + options?.onSuccess?.(data, variables); + }, + onError: (error: Error) => { + if (options?.showErrorAlert !== false) { + Alert.alert("Error", error.message || "Failed to update cart item"); + } + options?.onError?.(error); + }, + }); - return { - mutate: mutation.mutate, - mutateAsync: mutation.mutateAsync, - isLoading: mutation.isPending, - error: mutation.error, - data: mutation.data, - addToCart, - addToCartAsync: (productId: number, quantity = 1, slotId?: number) => { - if (slotId == null) { - throw new Error('slotId is required for adding to cart'); - } - return mutation.mutateAsync({ productId, quantity, slotId }); - }, - }; - } + return { + mutate: mutation.mutate, + mutateAsync: mutation.mutateAsync, + isLoading: mutation.isPending, + error: mutation.error, + data: mutation.data, + updateCartItem: (itemId: number, quantity: number): void => + mutation.mutate({ itemId, quantity }), + updateCartItemAsync: (itemId: number, quantity: number): Promise => + mutation.mutateAsync({ itemId, quantity }), + }; } -export function useUpdateCartItem(options?: { - onSuccess?: (data: any, variables: any) => void; - onError?: (error: any) => void; - showSuccessAlert?: boolean; - showErrorAlert?: boolean; - refetchCart?: boolean; -}, cartType: CartType = "regular") { - if (CART_MODE === 'remote') { - const utils = trpc.useUtils(); +export function useRemoveFromCart(options: MutationOptions = {}, cartType: CartType = "regular"): UseRemoveFromCartReturn { + const queryClient = useQueryClient(); - const mutation = trpc.user.cart.updateCartItem.useMutation({ - onSuccess: (data, variables) => { - // Default success handling - if (options?.showSuccessAlert !== false) { - Alert.alert("Success", "Cart item updated!"); - } + const mutation: UseMutationResult = useMutation({ + mutationFn: async ({ itemId }: RemoveCartVariables): Promise => { + return await removeFromLocalCart(itemId, cartType); + }, + onSuccess: (data: LocalCartItem[], variables: RemoveCartVariables) => { + queryClient.invalidateQueries({ queryKey: [`local-cart-${cartType}`] }); + if (options?.showSuccessAlert !== false) { + Alert.alert("Success", "Item removed from cart!"); + } + options?.onSuccess?.(data, variables); + }, + onError: (error: Error) => { + if (options?.showErrorAlert !== false) { + Alert.alert("Error", error.message || "Failed to remove item from cart"); + } + options?.onError?.(error); + }, + }); - // Auto-refetch cart if requested - if (options?.refetchCart) { - utils.user.cart.getCart.invalidate(); - } - - // Custom success callback - options?.onSuccess?.(data, variables); - }, - onError: (error) => { - // Default error handling - if (options?.showErrorAlert !== false) { - Alert.alert("Error", error.message || "Failed to update cart item"); - } - - // Custom error callback - options?.onError?.(error); - }, - }); - - return { - // Original mutation returns - mutate: mutation.mutate, - mutateAsync: mutation.mutateAsync, - isLoading: mutation.isPending, - error: mutation.error, - data: mutation.data, - - // Helper methods - updateCartItem: (itemId: number, quantity: number) => - mutation.mutate({ itemId, quantity }), - - updateCartItemAsync: (itemId: number, quantity: number) => - mutation.mutateAsync({ itemId, quantity }), - }; - } else { - const queryClient = useQueryClient(); - - const mutation = useMutation({ - mutationFn: async ({ itemId, quantity }: { itemId: number, quantity: number }) => { - return await updateLocalCartItem(itemId, quantity, cartType); - }, - onSuccess: (data, variables) => { - queryClient.invalidateQueries({ queryKey: [`local-cart-${cartType}`] }); - if (options?.showSuccessAlert !== false) { - Alert.alert("Success", "Cart item updated!"); - } - options?.onSuccess?.(data, variables); - }, - onError: (error) => { - if (options?.showErrorAlert !== false) { - Alert.alert("Error", error.message || "Failed to update cart item"); - } - options?.onError?.(error); - }, - }); - - return { - mutate: mutation.mutate, - mutateAsync: mutation.mutateAsync, - isLoading: mutation.isPending, - error: mutation.error, - data: mutation.data, - - updateCartItem: (itemId: number, quantity: number) => - mutation.mutate({ itemId, quantity }), - - updateCartItemAsync: (itemId: number, quantity: number) => - mutation.mutateAsync({ itemId, quantity }), - }; - } -} - -export function useRemoveFromCart(options?: { - onSuccess?: (data: any, variables: any) => void; - onError?: (error: any) => void; - showSuccessAlert?: boolean; - showErrorAlert?: boolean; - refetchCart?: boolean; -}, cartType: CartType = "regular") { - if (CART_MODE === 'remote') { - const utils = trpc.useUtils(); - - const mutation = trpc.user.cart.removeFromCart.useMutation({ - onSuccess: (data, variables) => { - // Default success handling - if (options?.showSuccessAlert !== false) { - Alert.alert("Success", "Item removed from cart!"); - } - - // Auto-refetch cart if requested - if (options?.refetchCart) { - utils.user.cart.getCart.invalidate(); - } - - // Custom success callback - options?.onSuccess?.(data, variables); - }, - onError: (error) => { - // Default error handling - if (options?.showErrorAlert !== false) { - Alert.alert("Error", error.message || "Failed to remove item from cart"); - } - - // Custom error callback - options?.onError?.(error); - }, - }); - - return { - // Original mutation returns - mutate: mutation.mutate, - mutateAsync: mutation.mutateAsync, - isLoading: mutation.isPending, - error: mutation.error, - data: mutation.data, - - // Helper methods - removeFromCart: (itemId: number) => - mutation.mutate({ itemId }), - - removeFromCartAsync: (itemId: number) => - mutation.mutateAsync({ itemId }), - }; - } else { - const queryClient = useQueryClient(); - - const mutation = useMutation({ - mutationFn: async ({ itemId }: { itemId: number }) => { - return await removeFromLocalCart(itemId, cartType); - }, - onSuccess: (data, variables) => { - queryClient.invalidateQueries({ queryKey: [`local-cart-${cartType}`] }); - if (options?.showSuccessAlert !== false) { - Alert.alert("Success", "Item removed from cart!"); - } - options?.onSuccess?.(data, variables); - }, - onError: (error) => { - if (options?.showErrorAlert !== false) { - Alert.alert("Error", error.message || "Failed to remove item from cart"); - } - options?.onError?.(error); - }, - }); - - return { - mutate: mutation.mutate, - mutateAsync: mutation.mutateAsync, - isLoading: mutation.isPending, - error: mutation.error, - data: mutation.data, - - removeFromCart: (itemId: number) => - mutation.mutate({ itemId }), - - removeFromCartAsync: (itemId: number) => - mutation.mutateAsync({ itemId }), - }; - } + return { + mutate: mutation.mutate, + mutateAsync: mutation.mutateAsync, + isLoading: mutation.isPending, + error: mutation.error, + data: mutation.data, + removeFromCart: (itemId: number): void => + mutation.mutate({ itemId }), + removeFromCartAsync: (itemId: number): Promise => + mutation.mutateAsync({ itemId }), + }; } // Export clear cart function for direct use diff --git a/apps/user-ui/hooks/useProductSlotIdentifier.ts b/apps/user-ui/hooks/useProductSlotIdentifier.ts index 9f5292a..69b29ea 100644 --- a/apps/user-ui/hooks/useProductSlotIdentifier.ts +++ b/apps/user-ui/hooks/useProductSlotIdentifier.ts @@ -1,46 +1,28 @@ -import { trpc } from '@/src/trpc-client'; import dayjs from 'dayjs'; +import { useCentralSlotStore } from '@/src/store/centralSlotStore'; export function useProductSlotIdentifier() { - // Fetch all slots with products - const { data: slotsData, isLoading: isProductsLoading } = trpc.user.slots.getSlotsWithProducts.useQuery(); - - - const productSlotsMap = new Map(); - - if (slotsData?.slots) { - const now = dayjs(); - - // Build map of productId to available slot IDs - slotsData.slots.forEach(slot => { - if (dayjs(slot.deliveryTime).isAfter(now)) { - slot.products.forEach(product => { - if (!productSlotsMap.has(product.id)) { - productSlotsMap.set(product.id, []); - } - productSlotsMap.get(product.id)!.push(slot.id); - }); - } - }); - } + // Get slots data from central store + const slots = useCentralSlotStore((state) => state.slots); + const productSlotsMap = useCentralSlotStore((state) => state.productSlotsMap); const getQuickestSlot = (productId: number): number | null => { - - if (!slotsData?.slots) return null; + if (!slots?.length) return null; const now = dayjs(); + const productInfo = productSlotsMap[productId]; + + if (!productInfo?.slots?.length) return null; // Find slots that contain this product and have future delivery time - const availableSlots = slotsData.slots.filter(slot => - slot.products.some(product => product.id === productId) && + const availableSlots = productInfo.slots.filter((slot: any) => dayjs(slot.deliveryTime).isAfter(now) ); - // if(productId === 98) - // console.log(JSON.stringify(slotsData)) + if (availableSlots.length === 0) return null; // Return earliest slot ID (sorted by delivery time) - const earliestSlot = availableSlots.sort((a, b) => + const earliestSlot = availableSlots.sort((a: any, b: any) => dayjs(a.deliveryTime).diff(dayjs(b.deliveryTime)) )[0]; @@ -48,4 +30,4 @@ export function useProductSlotIdentifier() { }; return { getQuickestSlot, productSlotsMap }; -} \ No newline at end of file +} diff --git a/apps/user-ui/metro.config.js b/apps/user-ui/metro.config.js index fbbf1eb..fba7b7c 100755 --- a/apps/user-ui/metro.config.js +++ b/apps/user-ui/metro.config.js @@ -1,6 +1,19 @@ // Learn more on how to setup config for the app: https://docs.expo.dev/guides/config-plugins/#metro-config const { getDefaultConfig } = require('expo/metro-config'); +const path = require('path'); const config = getDefaultConfig(__dirname); +// Add the packages directory to watch folders +config.watchFolders = [ + ...config.watchFolders || [], + path.resolve(__dirname, '../../packages/shared'), +]; + +// Configure module resolution for @packages/* +config.resolver.extraNodeModules = { + ...config.resolver.extraNodeModules, + '@packages/shared': path.resolve(__dirname, '../../packages/shared'), +}; + module.exports = config; diff --git a/apps/user-ui/src/components/AddToCartDialog.tsx b/apps/user-ui/src/components/AddToCartDialog.tsx index d005993..aaa8f79 100644 --- a/apps/user-ui/src/components/AddToCartDialog.tsx +++ b/apps/user-ui/src/components/AddToCartDialog.tsx @@ -5,9 +5,8 @@ import { tw, BottomDialog, MyText, MyTouchableOpacity, Quantifier } from 'common import MaterialIcons from '@expo/vector-icons/MaterialIcons'; import { useCartStore } from '@/src/store/cartStore'; import { useFlashCartStore } from '@/src/store/flashCartStore'; -import { trpc } from '@/src/trpc-client'; import { useAddToCart, useGetCart, useUpdateCartItem, useRemoveFromCart } from '@/hooks/cart-query-hooks'; -import { useGetEssentialConsts } from '@/src/hooks/prominent-api-hooks'; +import { useGetEssentialConsts, useSlots } from '@/src/hooks/prominent-api-hooks'; import dayjs from 'dayjs'; import { SafeAreaView } from 'react-native-safe-area-context'; @@ -31,7 +30,7 @@ export default function AddToCartDialog() { const [selectedSlotId, setSelectedSlotId] = useState(null); const [selectedFlashDelivery, setSelectedFlashDelivery] = useState(false); - const { data: slotsData } = trpc.user.slots.getSlotsWithProducts.useQuery(); + const { data: slotsData } = useSlots(); const { data: cartData } = useGetCart(); const { data: constsData } = useGetEssentialConsts(); // const isFlashDeliveryEnabled = constsData?.isFlashDeliveryEnabled === true; diff --git a/apps/user-ui/src/components/CentralStoreInitializer.tsx b/apps/user-ui/src/components/CentralStoreInitializer.tsx new file mode 100644 index 0000000..faa6b53 --- /dev/null +++ b/apps/user-ui/src/components/CentralStoreInitializer.tsx @@ -0,0 +1,14 @@ +import React from 'react'; +import { useInitializeCentralSlotStore } from '@/src/store/centralSlotStore'; +import { useInitializeCentralProductStore } from '@/src/store/centralProductStore'; + +interface CentralStoreInitializerProps { + children: React.ReactNode; +} + +export default function CentralStoreInitializer({ children }: CentralStoreInitializerProps) { + useInitializeCentralSlotStore(); + useInitializeCentralProductStore(); + + return <>{children}; +} diff --git a/apps/user-ui/src/hooks/prominent-api-hooks.ts b/apps/user-ui/src/hooks/prominent-api-hooks.ts index 42eea86..fab59e8 100644 --- a/apps/user-ui/src/hooks/prominent-api-hooks.ts +++ b/apps/user-ui/src/hooks/prominent-api-hooks.ts @@ -1,7 +1,8 @@ import { useQuery } from '@tanstack/react-query' import axios from 'axios' import { trpc } from '@/src/trpc-client' -import { AllProductsApiType, StoresApiType, SlotsApiType, EssentialConstsApiType } from "@backend/trpc/router"; +import { AllProductsApiType, StoresApiType, SlotsApiType, EssentialConstsApiType, BannersApiType, StoreWithProductsApiType } from "@backend/trpc/router"; +import { CACHE_FILENAMES } from "@packages/shared"; // Local useGetEssentialConsts hook export const useGetEssentialConsts = () => { @@ -15,20 +16,22 @@ type ProductsResponse = AllProductsApiType; type StoresResponse = StoresApiType; type SlotsResponse = SlotsApiType; type EssentialConstsResponse = EssentialConstsApiType; +type BannersResponse = BannersApiType; +type StoreWithProductsResponse = StoreWithProductsApiType; function useCacheUrl(filename: string): string | null { const { data: essentialConsts } = useGetEssentialConsts() - + const assetsDomain = essentialConsts?.assetsDomain const apiCacheKey = essentialConsts?.apiCacheKey - + return assetsDomain && apiCacheKey ? `${assetsDomain}${apiCacheKey}/${filename}` : null } export function useAllProducts() { - const cacheUrl = useCacheUrl('products.json') + const cacheUrl = useCacheUrl(CACHE_FILENAMES.products) return useQuery({ queryKey: ['all-products', cacheUrl], @@ -45,7 +48,7 @@ export function useAllProducts() { } export function useStores() { - const cacheUrl = useCacheUrl('stores.json') + const cacheUrl = useCacheUrl(CACHE_FILENAMES.stores) return useQuery({ queryKey: ['stores', cacheUrl], @@ -62,7 +65,7 @@ export function useStores() { } export function useSlots() { - const cacheUrl = useCacheUrl('slots.json') + const cacheUrl = useCacheUrl(CACHE_FILENAMES.slots) return useQuery({ queryKey: ['slots', cacheUrl], @@ -77,3 +80,46 @@ export function useSlots() { enabled: !!cacheUrl, }) } + +export function useBanners() { + const cacheUrl = useCacheUrl(CACHE_FILENAMES.banners) + + return useQuery({ + queryKey: ['banners', cacheUrl], + queryFn: async () => { + + if (!cacheUrl) { + throw new Error('Cache URL not available') + } + const response = await axios.get(cacheUrl) + return response.data + + }, + staleTime: 60000, // 1 minute + enabled: !!cacheUrl, + }) +} + +export function useStoreWithProducts(storeId: number) { + const { data: essentialConsts } = useGetEssentialConsts() + + const assetsDomain = essentialConsts?.assetsDomain + const apiCacheKey = essentialConsts?.apiCacheKey + + const cacheUrl = assetsDomain && apiCacheKey + ? `${assetsDomain}${apiCacheKey}/stores/${storeId}.json` + : null + + return useQuery({ + queryKey: ['store-with-products', storeId, cacheUrl], + queryFn: async () => { + if (!cacheUrl) { + throw new Error('Cache URL not available') + } + const response = await axios.get(cacheUrl) + return response.data + }, + staleTime: 60000, // 1 minute + enabled: !!cacheUrl, + }) +} diff --git a/apps/user-ui/src/store/centralProductStore.ts b/apps/user-ui/src/store/centralProductStore.ts new file mode 100644 index 0000000..6bccb14 --- /dev/null +++ b/apps/user-ui/src/store/centralProductStore.ts @@ -0,0 +1,41 @@ +import { create } from 'zustand' +import { useEffect } from 'react' +import { useAllProducts } from '@/src/hooks/prominent-api-hooks' +import { AllProductsApiType } from '@backend/trpc/router' + +type Product = AllProductsApiType['products'][number] & { + flashPrice?: number | null +} + +interface CentralProductState { + products: Product[] + productsById: Record + setProducts: (products: Product[]) => void + clearProducts: () => void +} + +export const useCentralProductStore = create((set) => ({ + products: [], + productsById: {}, + setProducts: (products) => { + const productsById: Record = {} + + products.forEach((product) => { + productsById[product.id] = product + }) + + set({ products, productsById }) + }, + clearProducts: () => set({ products: [], productsById: {} }), +})) + +export function useInitializeCentralProductStore() { + const { data: productsData } = useAllProducts() + const setProducts = useCentralProductStore((state) => state.setProducts) + + useEffect(() => { + if (productsData?.products) { + setProducts(productsData.products) + } + }, [productsData, setProducts]) +} diff --git a/apps/user-ui/src/store/centralSlotStore.ts b/apps/user-ui/src/store/centralSlotStore.ts new file mode 100644 index 0000000..7be29eb --- /dev/null +++ b/apps/user-ui/src/store/centralSlotStore.ts @@ -0,0 +1,60 @@ +import { create } from 'zustand'; +import { useSlots } from '@/src/hooks/prominent-api-hooks'; +import { useEffect } from 'react'; +import { SlotsApiType } from "@backend/trpc/router"; + +type Slot = SlotsApiType['slots'][number]; +type ProductAvailability = SlotsApiType['productAvailability'][number]; + +interface ProductSlotInfo { + slots: Slot[]; + isOutOfStock: boolean; + isFlashAvailable: boolean; +} + +interface CentralSlotState { + slots: Slot[]; + productSlotsMap: Record; + setSlotsData: (slots: Slot[], productAvailability: ProductAvailability[]) => void; + clearSlotsData: () => void; +} + +export const useCentralSlotStore = create((set) => ({ + slots: [], + productSlotsMap: {}, + setSlotsData: (slots, productAvailability) => { + const productSlotsMap: Record = {}; + + // First, create entries for ALL products from productAvailability + productAvailability.forEach((product) => { + productSlotsMap[product.id] = { + slots: [], + isOutOfStock: product.isOutOfStock, + isFlashAvailable: product.isFlashAvailable, + }; + }); + + // Then, populate slots for products that appear in delivery slots + slots.forEach((slot) => { + slot.products?.forEach((product) => { + if (productSlotsMap[product.id]) { + productSlotsMap[product.id].slots.push(slot); + } + }); + }); + + set({ slots, productSlotsMap }); + }, + clearSlotsData: () => set({ slots: [], productSlotsMap: {} }), +})); + +export function useInitializeCentralSlotStore() { + const { data: slotsData } = useSlots(); + const setSlotsData = useCentralSlotStore((state) => state.setSlotsData); + + useEffect(() => { + if (slotsData?.slots) { + setSlotsData(slotsData.slots, slotsData.productAvailability || []); + } + }, [slotsData, setSlotsData]); +} diff --git a/apps/user-ui/tsconfig.json b/apps/user-ui/tsconfig.json index d753e48..6f862fc 100755 --- a/apps/user-ui/tsconfig.json +++ b/apps/user-ui/tsconfig.json @@ -18,6 +18,12 @@ ], "common-ui/*": [ "../../packages/ui/*" + ], + "@packages/shared": [ + "../../packages/shared" + ], + "@packages/shared/*": [ + "../../packages/shared/*" ] }, "moduleSuffixes": [ @@ -34,5 +40,6 @@ "**/*.tsx", ".expo/types/**/*.ts", "expo-env.d.ts", + "../../packages/shared" ] } \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 1294227..0af54da 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5673,6 +5673,10 @@ "node": ">=8.0.0" } }, + "node_modules/@packages/shared": { + "resolved": "packages/shared", + "link": true + }, "node_modules/@pkgjs/parseargs": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", @@ -25541,6 +25545,10 @@ } } }, + "packages/shared": { + "name": "@packages/shared", + "version": "1.0.0" + }, "packages/ui": { "name": "common-ui", "version": "1.0.0", diff --git a/packages/shared/index.ts b/packages/shared/index.ts new file mode 100644 index 0000000..dae4213 --- /dev/null +++ b/packages/shared/index.ts @@ -0,0 +1,9 @@ +export const CACHE_FILENAMES = { + products: 'products.json', + stores: 'stores.json', + slots: 'slots.json', + essentialConsts: 'essential-consts.json', + banners: 'banners.json', +} as const + +export type CacheFilename = typeof CACHE_FILENAMES[keyof typeof CACHE_FILENAMES] diff --git a/packages/shared/package.json b/packages/shared/package.json new file mode 100644 index 0000000..2d0a30b --- /dev/null +++ b/packages/shared/package.json @@ -0,0 +1,7 @@ +{ + "name": "@packages/shared", + "version": "1.0.0", + "main": "index.ts", + "types": "index.ts", + "private": true +}