This commit is contained in:
shafi54 2026-02-06 01:57:16 +05:30
parent 9b38a3678b
commit 71a8dece86
6 changed files with 192 additions and 12 deletions

View file

@ -5,6 +5,7 @@ import { eq } from "drizzle-orm";
import { ApiError } from "../lib/api-error"; import { ApiError } from "../lib/api-error";
import { imageUploadS3, generateSignedUrlFromS3Url } from "../lib/s3-client"; import { imageUploadS3, generateSignedUrlFromS3Url } from "../lib/s3-client";
import { deleteS3Image } from "../lib/delete-image"; import { deleteS3Image } from "../lib/delete-image";
import { initializeAllStores } from '../stores/store-initializer';
/** /**
* Create a new product tag * Create a new product tag
@ -56,6 +57,9 @@ export const createTag = async (req: Request, res: Response) => {
}) })
.returning(); .returning();
// Reinitialize stores to reflect changes in cache
await initializeAllStores();
return res.status(201).json({ return res.status(201).json({
tag: newTag, tag: newTag,
message: "Tag created successfully", message: "Tag created successfully",
@ -172,6 +176,9 @@ export const updateTag = async (req: Request, res: Response) => {
.where(eq(productTagInfo.id, parseInt(id))) .where(eq(productTagInfo.id, parseInt(id)))
.returning(); .returning();
// Reinitialize stores to reflect changes in cache
await initializeAllStores();
return res.status(200).json({ return res.status(200).json({
tag: updatedTag, tag: updatedTag,
message: "Tag updated successfully", 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 // 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))); await db.delete(productTagInfo).where(eq(productTagInfo.id, parseInt(id)));
// Reinitialize stores to reflect changes in cache
await initializeAllStores();
return res.status(200).json({ return res.status(200).json({
message: "Tag deleted successfully", message: "Tag deleted successfully",
}); });

View file

@ -1,16 +1,19 @@
// import redisClient from './redis-client'; // import redisClient from './redis-client';
import redisClient from 'src/lib/redis-client'; import redisClient from 'src/lib/redis-client';
import { db } from '../db/db_index'; import { db } from '../db/db_index';
import { productTagInfo } from '../db/schema'; import { productTagInfo, productTags } from '../db/schema';
import { eq } from 'drizzle-orm'; import { eq, inArray } from 'drizzle-orm';
import { generateSignedUrlFromS3Url } from 'src/lib/s3-client'; import { generateSignedUrlFromS3Url } from 'src/lib/s3-client';
// Tag Type (matches getDashboardTags return) // Tag Type (matches getDashboardTags return)
interface Tag { interface Tag {
id: number; id: number;
tagName: string; tagName: string;
tagDescription: string | null;
imageUrl: string | null; imageUrl: string | null;
isDashboardTag: boolean; isDashboardTag: boolean;
relatedStores: number[];
productIds: number[];
} }
export async function initializeProductTagStore(): Promise<void> { export async function initializeProductTagStore(): Promise<void> {
@ -22,11 +25,32 @@ export async function initializeProductTagStore(): Promise<void> {
.select({ .select({
id: productTagInfo.id, id: productTagInfo.id,
tagName: productTagInfo.tagName, tagName: productTagInfo.tagName,
tagDescription: productTagInfo.tagDescription,
imageUrl: productTagInfo.imageUrl, imageUrl: productTagInfo.imageUrl,
isDashboardTag: productTagInfo.isDashboardTag, isDashboardTag: productTagInfo.isDashboardTag,
relatedStores: productTagInfo.relatedStores,
}) })
.from(productTagInfo); .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<number, number[]>();
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 // Store each tag in Redis
for (const tag of tagsData) { for (const tag of tagsData) {
const signedImageUrl = tag.imageUrl ? await generateSignedUrlFromS3Url(tag.imageUrl) : null; const signedImageUrl = tag.imageUrl ? await generateSignedUrlFromS3Url(tag.imageUrl) : null;
@ -34,8 +58,11 @@ export async function initializeProductTagStore(): Promise<void> {
const tagObj: Tag = { const tagObj: Tag = {
id: tag.id, id: tag.id,
tagName: tag.tagName, tagName: tag.tagName,
tagDescription: tag.tagDescription,
imageUrl: signedImageUrl, imageUrl: signedImageUrl,
isDashboardTag: tag.isDashboardTag, isDashboardTag: tag.isDashboardTag,
relatedStores: (tag.relatedStores as number[]) || [],
productIds: productIdsByTag.get(tag.id) || [],
}; };
await redisClient.set(`tag:${tag.id}`, JSON.stringify(tagObj)); await redisClient.set(`tag:${tag.id}`, JSON.stringify(tagObj));
@ -113,3 +140,32 @@ export async function getDashboardTags(): Promise<Tag[]> {
return []; return [];
} }
} }
export async function getTagsByStoreId(storeId: number): Promise<Tag[]> {
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 [];
}
}

View file

@ -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,
})),
};
}),
});

View file

@ -12,6 +12,7 @@ import { userCouponRouter } from './coupon';
import { paymentRouter } from './payments'; import { paymentRouter } from './payments';
import { storesRouter } from './stores'; import { storesRouter } from './stores';
import { fileUploadRouter } from './file-upload'; import { fileUploadRouter } from './file-upload';
import { tagsRouter } from './tags';
export const userRouter = router({ export const userRouter = router({
address: addressRouter, address: addressRouter,
@ -27,6 +28,7 @@ export const userRouter = router({
payment: paymentRouter, payment: paymentRouter,
stores: storesRouter, stores: storesRouter,
fileUpload: fileUploadRouter, fileUpload: fileUploadRouter,
tags: tagsRouter,
}); });
export type UserRouter = typeof userRouter; export type UserRouter = typeof userRouter;

View file

@ -1,5 +1,5 @@
import React from "react"; import React, { useState } from "react";
import { View, Dimensions } from "react-native"; import { View, Dimensions, ScrollView, TouchableOpacity } from "react-native";
import { useRouter, useLocalSearchParams } from "expo-router"; import { useRouter, useLocalSearchParams } from "expo-router";
import { import {
theme, theme,
@ -19,10 +19,46 @@ import FloatingCartBar from "@/components/floating-cart-bar";
const { width: screenWidth } = Dimensions.get("window"); const { width: screenWidth } = Dimensions.get("window");
const itemWidth = (screenWidth - 48) / 2; 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<ChipProps> = ({ tag, isSelected, onPress }) => {
const productCount = tag.productIds?.length || 0;
return (
<TouchableOpacity
onPress={onPress}
style={tw`px-4 py-2 rounded-lg border ${
isSelected
? 'bg-brand500 border-brand500'
: 'bg-white border-brand500'
}`}
>
<MyText
style={tw`font-medium text-sm ${
isSelected ? 'text-white' : 'text-brand500'
}`}
>
{tag.tagName} ({productCount})
</MyText>
</TouchableOpacity>
);
};
export default function StoreDetail() { export default function StoreDetail() {
const router = useRouter(); const router = useRouter();
const { id: storeId } = useLocalSearchParams(); const { id: storeId } = useLocalSearchParams();
const storeIdNum = parseInt(storeId as string); const storeIdNum = parseInt(storeId as string);
const [selectedTagId, setSelectedTagId] = useState<number | null>(null);
const { data: storeData, isLoading, refetch, error } = const { data: storeData, isLoading, refetch, error } =
trpc.user.stores.getStoreWithProducts.useQuery( trpc.user.stores.getStoreWithProducts.useQuery(
@ -30,6 +66,21 @@ export default function StoreDetail() {
{ enabled: !!storeIdNum } { 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(() => { useManualRefresh(() => {
refetch(); refetch();
}); });
@ -63,7 +114,7 @@ export default function StoreDetail() {
return ( return (
<View style={tw`flex-1 bg-gray-50 relative`}> <View style={tw`flex-1 bg-gray-50 relative`}>
<MyFlatList <MyFlatList
data={storeData?.products || []} data={filteredProducts}
numColumns={2} numColumns={2}
renderItem={({ item }) => ( renderItem={({ item }) => (
<ProductCard <ProductCard
@ -109,12 +160,45 @@ export default function StoreDetail() {
</MyText> </MyText>
)} )}
</View> </View>
<View style={tw`flex-row items-center mt-6 mb-2`}> {/* Tags Section */}
{tagsData && tagsData.tags.length > 0 && (
<ScrollView
horizontal
showsHorizontalScrollIndicator={false}
contentContainerStyle={tw`gap-2 mt-6`}
>
{tagsData.tags.map((tag) => (
<Chip
key={tag.id}
tag={tag}
isSelected={selectedTagId === tag.id}
onPress={() => setSelectedTagId(selectedTagId === tag.id ? null : tag.id)}
/>
))}
</ScrollView>
)}
<View style={tw`flex-row items-center justify-between mt-6 mb-2`}>
<View style={tw`flex-row items-center`}>
<MaterialIcons name="grid-view" size={20} color="#374151" /> <MaterialIcons name="grid-view" size={20} color="#374151" />
<MyText style={tw`text-lg font-bold text-gray-900 ml-2`}> <MyText style={tw`text-lg font-bold text-gray-900 ml-2`}>
Products from this Store {selectedTagId
? `${tagsData?.tags.find(t => t.id === selectedTagId)?.tagName} items`
: `${filteredProducts.length} products`}
</MyText> </MyText>
</View> </View>
{selectedTagId && (
<TouchableOpacity
onPress={() => setSelectedTagId(null)}
style={tw`flex-row items-center`}
>
<MyText style={tw`text-brand500 text-sm font-medium mr-1`}>
Clear
</MyText>
<MaterialIcons name="close" size={16} color={theme.colors.brand500} />
</TouchableOpacity>
)}
</View>
</View> </View>
} }
/> />

View file

@ -64,8 +64,8 @@ const isDevMode = Constants.executionEnvironment !== "standalone";
// const BASE_API_URL = 'http://10.0.2.2:4000'; // 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.100.101:4000';
// const BASE_API_URL = 'http://192.168.1.7:4000'; // const BASE_API_URL = 'http://192.168.1.7:4000';
// let BASE_API_URL = "https://mf.freshyo.in"; 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.100.104:4000';
// let BASE_API_URL = 'http://192.168.29.176:4000'; // let BASE_API_URL = 'http://192.168.29.176:4000';
// if(isDevMode) { // if(isDevMode) {