diff --git a/apps/backend/src/admin-apis/product-tags.controller.ts b/apps/backend/src/admin-apis/product-tags.controller.ts index 79ddf06..4466989 100644 --- a/apps/backend/src/admin-apis/product-tags.controller.ts +++ b/apps/backend/src/admin-apis/product-tags.controller.ts @@ -5,6 +5,7 @@ import { eq } from "drizzle-orm"; import { ApiError } from "../lib/api-error"; import { imageUploadS3, generateSignedUrlFromS3Url } from "../lib/s3-client"; import { deleteS3Image } from "../lib/delete-image"; +import { initializeAllStores } from '../stores/store-initializer'; /** * Create a new product tag @@ -56,6 +57,9 @@ export const createTag = async (req: Request, res: Response) => { }) .returning(); + // Reinitialize stores to reflect changes in cache + await initializeAllStores(); + return res.status(201).json({ tag: newTag, message: "Tag created successfully", @@ -172,6 +176,9 @@ export const updateTag = async (req: Request, res: Response) => { .where(eq(productTagInfo.id, parseInt(id))) .returning(); + // Reinitialize stores to reflect changes in cache + await initializeAllStores(); + return res.status(200).json({ tag: updatedTag, message: "Tag updated successfully", @@ -206,6 +213,9 @@ export const deleteTag = async (req: Request, res: Response) => { // Note: This will fail if tag is still assigned to products due to foreign key constraint await db.delete(productTagInfo).where(eq(productTagInfo.id, parseInt(id))); + // Reinitialize stores to reflect changes in cache + await initializeAllStores(); + return res.status(200).json({ message: "Tag deleted successfully", }); diff --git a/apps/backend/src/stores/product-tag-store.ts b/apps/backend/src/stores/product-tag-store.ts index 74750cc..33a7d72 100644 --- a/apps/backend/src/stores/product-tag-store.ts +++ b/apps/backend/src/stores/product-tag-store.ts @@ -1,16 +1,19 @@ // import redisClient from './redis-client'; import redisClient from 'src/lib/redis-client'; import { db } from '../db/db_index'; -import { productTagInfo } from '../db/schema'; -import { eq } from 'drizzle-orm'; +import { productTagInfo, productTags } from '../db/schema'; +import { eq, inArray } from 'drizzle-orm'; import { generateSignedUrlFromS3Url } from 'src/lib/s3-client'; // Tag Type (matches getDashboardTags return) interface Tag { id: number; tagName: string; + tagDescription: string | null; imageUrl: string | null; isDashboardTag: boolean; + relatedStores: number[]; + productIds: number[]; } export async function initializeProductTagStore(): Promise { @@ -22,11 +25,32 @@ export async function initializeProductTagStore(): Promise { .select({ id: productTagInfo.id, tagName: productTagInfo.tagName, + tagDescription: productTagInfo.tagDescription, imageUrl: productTagInfo.imageUrl, isDashboardTag: productTagInfo.isDashboardTag, + relatedStores: productTagInfo.relatedStores, }) .from(productTagInfo); + // Fetch product IDs for each tag + const tagIds = tagsData.map(t => t.id); + const productTagsData = await db + .select({ + tagId: productTags.tagId, + productId: productTags.productId, + }) + .from(productTags) + .where(inArray(productTags.tagId, tagIds)); + + // Group product IDs by tag + const productIdsByTag = new Map(); + for (const pt of productTagsData) { + if (!productIdsByTag.has(pt.tagId)) { + productIdsByTag.set(pt.tagId, []); + } + productIdsByTag.get(pt.tagId)!.push(pt.productId); + } + // Store each tag in Redis for (const tag of tagsData) { const signedImageUrl = tag.imageUrl ? await generateSignedUrlFromS3Url(tag.imageUrl) : null; @@ -34,8 +58,11 @@ export async function initializeProductTagStore(): Promise { const tagObj: Tag = { id: tag.id, tagName: tag.tagName, + tagDescription: tag.tagDescription, imageUrl: signedImageUrl, isDashboardTag: tag.isDashboardTag, + relatedStores: (tag.relatedStores as number[]) || [], + productIds: productIdsByTag.get(tag.id) || [], }; await redisClient.set(`tag:${tag.id}`, JSON.stringify(tagObj)); @@ -112,4 +139,33 @@ export async function getDashboardTags(): Promise { console.error('Error getting dashboard tags:', error); return []; } +} + +export async function getTagsByStoreId(storeId: number): Promise { + try { + // Get all keys matching the pattern "tag:*" + const keys = await redisClient.KEYS('tag:*'); + + if (keys.length === 0) { + return []; + } + + // Get all tags using MGET for better performance + const tagsData = await redisClient.MGET(keys); + + const storeTags: Tag[] = []; + for (const tagData of tagsData) { + if (tagData) { + const tag = JSON.parse(tagData) as Tag; + if (tag.relatedStores.includes(storeId)) { + storeTags.push(tag); + } + } + } + + return storeTags; + } catch (error) { + console.error(`Error getting tags for store ${storeId}:`, error); + return []; + } } \ No newline at end of file diff --git a/apps/backend/src/trpc/user-apis/tags.ts b/apps/backend/src/trpc/user-apis/tags.ts new file mode 100644 index 0000000..5bbbd46 --- /dev/null +++ b/apps/backend/src/trpc/user-apis/tags.ts @@ -0,0 +1,28 @@ +import { router, publicProcedure } from '../trpc-index'; +import { z } from 'zod'; +import { getTagsByStoreId } from '../../stores/product-tag-store'; +import { ApiError } from '../../lib/api-error'; + +export const tagsRouter = router({ + getTagsByStore: publicProcedure + .input(z.object({ + storeId: z.number(), + })) + .query(async ({ input }) => { + const { storeId } = input; + + // Get tags from cache that are related to this store + const tags = await getTagsByStoreId(storeId); + + + return { + tags: tags.map(tag => ({ + id: tag.id, + tagName: tag.tagName, + tagDescription: tag.tagDescription, + imageUrl: tag.imageUrl, + productIds: tag.productIds, + })), + }; + }), +}); diff --git a/apps/backend/src/trpc/user-apis/user-trpc-index.ts b/apps/backend/src/trpc/user-apis/user-trpc-index.ts index 247edee..2d9d41d 100644 --- a/apps/backend/src/trpc/user-apis/user-trpc-index.ts +++ b/apps/backend/src/trpc/user-apis/user-trpc-index.ts @@ -12,6 +12,7 @@ import { userCouponRouter } from './coupon'; import { paymentRouter } from './payments'; import { storesRouter } from './stores'; import { fileUploadRouter } from './file-upload'; +import { tagsRouter } from './tags'; export const userRouter = router({ address: addressRouter, @@ -27,6 +28,7 @@ export const userRouter = router({ payment: paymentRouter, stores: storesRouter, fileUpload: fileUploadRouter, + tags: tagsRouter, }); export type UserRouter = typeof userRouter; \ 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 bc8855a..931fa96 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,5 +1,5 @@ -import React from "react"; -import { View, Dimensions } from "react-native"; +import React, { useState } from "react"; +import { View, Dimensions, ScrollView, TouchableOpacity } from "react-native"; import { useRouter, useLocalSearchParams } from "expo-router"; import { theme, @@ -19,10 +19,46 @@ import FloatingCartBar from "@/components/floating-cart-bar"; const { width: screenWidth } = Dimensions.get("window"); const itemWidth = (screenWidth - 48) / 2; +interface Tag { + id: number; + tagName: string; + productIds?: number[]; +} + +interface ChipProps { + tag: Tag; + isSelected: boolean; + onPress: () => void; +} + +const Chip: React.FC = ({ tag, isSelected, onPress }) => { + const productCount = tag.productIds?.length || 0; + + return ( + + + {tag.tagName} ({productCount}) + + + ); +}; + export default function StoreDetail() { const router = useRouter(); const { id: storeId } = useLocalSearchParams(); const storeIdNum = parseInt(storeId as string); + const [selectedTagId, setSelectedTagId] = useState(null); const { data: storeData, isLoading, refetch, error } = trpc.user.stores.getStoreWithProducts.useQuery( @@ -30,6 +66,21 @@ export default function StoreDetail() { { enabled: !!storeIdNum } ); + const { data: tagsData, isLoading: isLoadingTags } = + trpc.user.tags.getTagsByStore.useQuery( + { storeId: storeIdNum }, + { enabled: !!storeIdNum } + ); + + // Filter products based on selected tag + const filteredProducts = selectedTagId + ? storeData?.products.filter(product => { + const selectedTag = tagsData?.tags.find(t => t.id === selectedTagId); + return selectedTag?.productIds?.includes(product.id) ?? false; + }) || [] + : storeData?.products || []; + + useManualRefresh(() => { refetch(); }); @@ -63,7 +114,7 @@ export default function StoreDetail() { return ( ( )} - - - - Products from this Store - + {/* Tags Section */} + {tagsData && tagsData.tags.length > 0 && ( + + {tagsData.tags.map((tag) => ( + setSelectedTagId(selectedTagId === tag.id ? null : tag.id)} + /> + ))} + + )} + + + + + + {selectedTagId + ? `${tagsData?.tags.find(t => t.id === selectedTagId)?.tagName} items` + : `${filteredProducts.length} products`} + + + {selectedTagId && ( + setSelectedTagId(null)} + style={tw`flex-row items-center`} + > + + Clear + + + + )} } diff --git a/packages/ui/index.ts b/packages/ui/index.ts index 150fea9..a75fab3 100755 --- a/packages/ui/index.ts +++ b/packages/ui/index.ts @@ -64,8 +64,8 @@ const isDevMode = Constants.executionEnvironment !== "standalone"; // const BASE_API_URL = 'http://10.0.2.2:4000'; // const BASE_API_URL = 'http://192.168.100.101:4000'; // const BASE_API_URL = 'http://192.168.1.7:4000'; -// let BASE_API_URL = "https://mf.freshyo.in"; - let BASE_API_URL = 'http://192.168.100.104:4000'; +let BASE_API_URL = "https://mf.freshyo.in"; + // let BASE_API_URL = 'http://192.168.100.104:4000'; // let BASE_API_URL = 'http://192.168.29.176:4000'; // if(isDevMode) {