diff --git a/src/app/[locale]/product/[product]/page.tsx b/src/app/[locale]/product/[product]/page.tsx index 475d8b3..099695e 100644 --- a/src/app/[locale]/product/[product]/page.tsx +++ b/src/app/[locale]/product/[product]/page.tsx @@ -22,11 +22,11 @@ export async function generateMetadata({ return { title: `${res.data.name} - Gastro Market`, description: - res.data.description || `Gastro Market mahsuloti: ${res.data.name}`, + res.data.short_name || `Gastro Market mahsuloti: ${res.data.name}`, openGraph: { title: `${res.data.name} - Gastro Market`, description: - res.data.description || `Gastro Market mahsuloti: ${res.data.name}`, + res.data.short_name || `Gastro Market mahsuloti: ${res.data.name}`, type: 'website', images: [ { @@ -45,7 +45,7 @@ export async function generateMetadata({ card: 'summary_large_image', title: `${res.data.name} - Gastro Market`, description: - res.data.description || `Gastro Market mahsuloti: ${res.data.name}`, + res.data.short_name || `Gastro Market mahsuloti: ${res.data.name}`, images: res.data.images && res.data.images.length > 0 ? [ diff --git a/src/features/about/ui/AboutPage.tsx b/src/features/about/ui/AboutPage.tsx index eed13cf..30aad5c 100644 --- a/src/features/about/ui/AboutPage.tsx +++ b/src/features/about/ui/AboutPage.tsx @@ -90,7 +90,7 @@ export function PartnershipForm() { form.reset(); }, onError: () => { - toast.error('Xatolik yuz berdi', { + toast.error(t('Xatolik yuz berdi'), { richColors: true, position: 'top-center', }); diff --git a/src/features/cart/lib/api.ts b/src/features/cart/lib/api.ts index 869e86a..ed6c3bc 100644 --- a/src/features/cart/lib/api.ts +++ b/src/features/cart/lib/api.ts @@ -2,7 +2,7 @@ import httpClient from '@/shared/config/api/httpClient'; import { API_URLS } from '@/shared/config/api/URLs'; import { AxiosResponse } from 'axios'; -interface CartItem { +export interface CartItem { id: string; cart_item: { id: number; diff --git a/src/features/cart/ui/OrderPage.tsx b/src/features/cart/ui/OrderPage.tsx index 136b419..add9de4 100644 --- a/src/features/cart/ui/OrderPage.tsx +++ b/src/features/cart/ui/OrderPage.tsx @@ -25,6 +25,7 @@ import { SelectValue, } from '@/shared/ui/select'; import { Textarea } from '@/shared/ui/textarea'; +import { userStore } from '@/widgets/welcome/lib/hook'; import { zodResolver } from '@hookform/resolvers/zod'; import { Map, @@ -72,6 +73,7 @@ const deliveryTimeSlots = [ const OrderPage = () => { const t = useTranslations(); + const { user } = userStore(); const form = useForm>({ resolver: zodResolver(orderForm), defaultValues: { @@ -103,7 +105,7 @@ const OrderPage = () => { queryClinet.refetchQueries({ queryKey: ['cart_items'] }); } else { - toast.error('Xatolik yuz berdi', { + toast.error(t('Xatolik yuz berdi'), { richColors: true, position: 'top-center', }); @@ -235,7 +237,7 @@ const OrderPage = () => { function onSubmit(value: z.infer) { if (!cartItems || cartItems.length === 0) { - toast.error("Savatcha bo'sh", { + toast.error(t("Savatcha bo'sh"), { richColors: true, position: 'top-center', }); @@ -244,7 +246,7 @@ const OrderPage = () => { // Yetkazib berish vaqtini tekshirish if (!deliveryDate) { - toast.error('Yetkazib berish sanasini tanlang', { + toast.error(t('Yetkazib berish sanasini tanlang'), { richColors: true, position: 'top-center', }); @@ -252,7 +254,7 @@ const OrderPage = () => { } if (!selectedTimeSlot) { - toast.error('Yetkazib berish vaqtini tanlang', { + toast.error(t('Yetkazib berish vaqtini tanlang'), { richColors: true, position: 'top-center', }); @@ -276,25 +278,31 @@ const OrderPage = () => { product_price: item.product.prices![0].price, warehouse_code: 'wh1', })); - - mutate({ - order: [ - { - filial_code: 'dodge', - delivery_date: formatDate.format(deliveryDate, 'DD.MM.YYYY'), - room_code: '100', - deal_time: formatDate.format(deliveryDate, 'DD.MM.YYYY'), - robot_code: 'r2', - status: 'B#N', - sales_manager_code: '1', - person_code: '12345678', - currency_code: '860', - owner_person_code: '1234567', - note: value.comment, - order_products: order_products, - }, - ], - }); + if (user) { + mutate({ + order: [ + { + filial_code: 'dodge', + delivery_date: formatDate.format(deliveryDate, 'DD.MM.YYYY'), + room_code: '100', + deal_time: formatDate.format(deliveryDate, 'DD.MM.YYYY'), + robot_code: 'r2', + status: 'B#N', + sales_manager_code: '1', + person_code: user?.person_id.toString(), + currency_code: '860', + owner_person_code: user?.person_id.toString(), + note: value.comment, + order_products: order_products, + }, + ], + }); + } else { + toast.error(t('Xatolik yuz berdi'), { + richColors: true, + position: 'top-center', + }); + } } if (orderSuccess) { diff --git a/src/features/favourite/ui/Favourite.tsx b/src/features/favourite/ui/Favourite.tsx index 3d906c9..af07974 100644 --- a/src/features/favourite/ui/Favourite.tsx +++ b/src/features/favourite/ui/Favourite.tsx @@ -110,7 +110,7 @@ export default function Favourite() { {favourite && !isLoading && favourite?.results - .filter((product) => product.is_active) + .filter((product) => product.state === 'A') .map((product) => ( ))} diff --git a/src/features/product/ui/Product.tsx b/src/features/product/ui/Product.tsx index 0ac40a4..bf287d3 100644 --- a/src/features/product/ui/Product.tsx +++ b/src/features/product/ui/Product.tsx @@ -5,7 +5,6 @@ import { product_api } from '@/shared/config/api/product/api'; import { BASE_URL } from '@/shared/config/api/URLs'; import { useCartId } from '@/shared/hooks/cartId'; import formatPrice from '@/shared/lib/formatPrice'; -import { Card } from '@/shared/ui/card'; import { Carousel, CarouselContent, @@ -27,432 +26,284 @@ import { toast } from 'sonner'; const ProductDetail = () => { const t = useTranslations(); - const [quantity, setQuantity] = useState(1); - const { product } = useParams(); + const { product } = useParams<{ product: string }>(); const queryClient = useQueryClient(); - const [selectedImage, setSelectedImage] = useState(0); - const debounceRef = useRef(null); const { cart_id } = useCartId(); + const [quantity, setQuantity] = useState(1); + const [selectedImage, setSelectedImage] = useState(0); + const debounceRef = useRef(null); + + /* ---------------- CART ITEMS ---------------- */ const { data: cartItems } = useQuery({ queryKey: ['cart_items', cart_id], queryFn: () => cart_api.get_cart_items(cart_id!), enabled: !!cart_id, }); + /* ---------------- PRODUCT DETAIL ---------------- */ const { data, isLoading } = useQuery({ queryKey: ['product_detail', product], - queryFn: () => { - if (product) return product_api.detail(product.toString()); - }, - select(data) { - return data?.data; - }, + queryFn: () => product_api.detail(product), + select: (res) => res.data, enabled: !!product, }); - const { data: recomendation, isLoading: proLoad } = useQuery({ + /* ---------------- RECOMMENDATION ---------------- */ + const { data: recomendation, isLoading: recLoad } = useQuery({ queryKey: ['product_list'], queryFn: () => product_api.list({ page: 1, page_size: 12 }), - select(data) { - return data.data.results; - }, + select: (res) => res.data.results, }); + /* ---------------- PRICE ---------------- */ + const price = Number(data?.prices?.[0]?.price || 0); + + /* ---------------- SYNC CART QUANTITY ---------------- */ useEffect(() => { if (!data || !cartItems) return; - const item = cartItems?.data.cart_item.find( - (item) => Number(item.product.id) === data?.id, + const item = cartItems.data.cart_item.find( + (i) => Number(i.product.id) === data.id, ); - if (item) { - setQuantity(item.quantity); - } else { - setQuantity(1); - } + setQuantity(item ? item.quantity : 1); }, [data, cartItems]); + /* ---------------- DEBOUNCE UPDATE ---------------- */ useEffect(() => { - if (!cart_id || !data || !cartItems) return; - if (quantity <= 0) return; + if (!cart_id || !data || !cartItems || quantity <= 0) return; - const cartItem = cartItems?.data?.cart_item.find( - (item) => Number(item.product.id) === data?.id, + const cartItem = cartItems.data.cart_item.find( + (i) => Number(i.product.id) === data.id, ); - if (!cartItem) return; - if (cartItem.quantity === quantity) return; + if (!cartItem || cartItem.quantity === quantity) return; if (debounceRef.current) clearTimeout(debounceRef.current); debounceRef.current = setTimeout(() => { updateCartItem({ - body: { quantity }, cart_item_id: cartItem.id.toString(), + body: { quantity }, }); }, 500); return () => { if (debounceRef.current) clearTimeout(debounceRef.current); }; - }, [quantity, cart_id, data, cartItems]); + }, [quantity]); - const { mutate } = useMutation({ + /* ---------------- ADD TO CART ---------------- */ + const { mutate: addToCart } = useMutation({ mutationFn: (body: { product: string; quantity: number; cart: string }) => cart_api.cart_item(body), onSuccess: () => { queryClient.refetchQueries({ queryKey: ['cart_items'] }); - toast.success(t("Mahsulot savatga qo'shildi"), { - richColors: true, - position: 'top-center', - }); + toast.success(t("Mahsulot savatga qo'shildi"), { richColors: true }); }, onError: (err: AxiosError) => { - const detail = (err.response?.data as { detail: string }).detail; - toast.error(detail || err.message, { - richColors: true, - position: 'top-center', - }); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const msg = (err.response?.data as any)?.detail || err.message; + toast.error(msg, { richColors: true }); }, }); const { mutate: updateCartItem } = useMutation({ - mutationFn: ({ - body, - cart_item_id, - }: { - body: { quantity: number }; + mutationFn: (payload: { cart_item_id: string; - }) => cart_api.update_cart_item({ body, cart_item_id }), - onSuccess: () => { - queryClient.refetchQueries({ queryKey: ['cart_items'] }); - }, - onError: (err: AxiosError) => { - toast.error(err.message, { richColors: true, position: 'top-center' }); - }, + body: { quantity: number }; + }) => cart_api.update_cart_item(payload), + onSuccess: () => queryClient.refetchQueries({ queryKey: ['cart_items'] }), }); + /* ---------------- FAVOURITE ---------------- */ const favouriteMutation = useMutation({ - mutationFn: (productId: string) => product_api.favourite(productId), + mutationFn: (id: string) => product_api.favourite(id), onSuccess: () => { - queryClient.refetchQueries({ queryKey: ['product_list'] }); - queryClient.refetchQueries({ queryKey: ['product_detail', product] }); + queryClient.invalidateQueries({ queryKey: ['product_detail'] }); + queryClient.invalidateQueries({ queryKey: ['product_list'] }); }, }); - const handleQuantityChange = (type: string) => { - if (type === 'increase') { - setQuantity(quantity + 1); - } else if (type === 'decrease' && quantity > 1) { - setQuantity(quantity - 1); - } - }; - + /* ---------------- HANDLERS ---------------- */ const handleAddToCart = () => { if (!data || !cart_id) return; const cartItem = cartItems?.data.cart_item.find( - (e) => Number(e.product.id) === data.id, + (i) => Number(i.product.id) === data.id, ); if (cartItem) { updateCartItem({ - body: { quantity: quantity }, cart_item_id: cartItem.id.toString(), + body: { quantity }, }); } else { - mutate({ + addToCart({ product: String(data.id), cart: cart_id, - quantity: quantity, + quantity, }); } }; + /* ---------------- LOADING ---------------- */ if (isLoading) { return ( -
-
-
-
-
-
-
-
-
-
-
-
+
+
); } + /* ===================== RENDER ===================== */ return ( -
-
-
-
-
-
- {data && ( - 0 - ? data.images[selectedImage].image - : data.image || '/placeholder.svg' - } - alt={data.name} - className="w-full h-[400px] object-contain" - /> - )} -
+
+
+ {/* IMAGES */} +
+ {data?.name - - - {data && data.images.length > 0 ? ( - data.images.map((img, index) => ( - - - - )) - ) : ( - - - - )} - - -
- - {/* Product Info */} -
-

- {data?.name} -

- - {/* Price */} -
-
- - {data && formatPrice(data.price, true)} - -
-
- - {/* Description */} -

{data?.description}

- - {/* Quantity Selector */} -
- -
-
- - { - const v = e.target.value; - if (!/^\d*$/.test(v)) return; - const num = Number(v); - if (num > 0) { - setQuantity(num); - } - }} - inputMode="numeric" - className="w-14 h-12 border-none text-center text-sm !p-0 focus-visible:ring-0" - /> - -
- - {t('Jami')}:{' '} - - {data && formatPrice(data.price * quantity, true)} - - -
-
- - {/* Action Buttons */} -
- - -
- - {/* Features */} -
-
- - - {t('Bepul yetkazib berish')} - -
-
- - {t('Kafolat')} -
-
-
-
-
- - {/* Specifications */} -
-

{t('Xususiyatlari')}

-
-
- {t('Qadoq turi')}: - - {data?.unity.name} - -
- {data?.brand && ( -
- {t('Brandi')}: - {data?.brand} -
- )} - {data?.manufacturer && ( -
- - {t('Ishlab chiqaruvchi')}: - - - {data?.manufacturer} - -
- )} - {data?.volume && ( -
- {t('Hajmi')}: - {data?.volume} -
- )} -
-
- - {/* Related Products */} -
-

- {t("O'xshash mahsulotlar")} -

- - - {proLoad && - Array.from({ length: 6 }).map((__, index) => ( - - - - - - - - - ))} - {recomendation && - !proLoad && - recomendation - .filter((product) => product.state === 'A') - .map((product) => ( - - - - ))} + + + + ))} - - -
+ + {/* INFO */} +
+

{data?.name}

+ +
+ {formatPrice(price, true)} +
+ +

{data?.short_name}

+ + {/* QUANTITY */} +
+ + + { + const v = Number(e.target.value); + if (v > 0) setQuantity(v); + }} + className="w-16 text-center" + /> + + +
+ +
+ {t('Jami')}: {formatPrice(price * quantity, true)} +
+ + {/* ACTIONS */} +
+ + + +
+ + {/* FEATURES */} +
+
+ + {t('Bepul yetkazib berish')} +
+
+ + {t('Kafolat')} +
+
+
+
+ + {/* RELATED */} +
+

{t("O'xshash mahsulotlar")}

+ + + + {recLoad && + Array.from({ length: 6 }).map((_, i) => ( + + + + ))} + + {recomendation + ?.filter((p) => p.state === 'A') + .map((p) => ( + + + + ))} + + + + +
); diff --git a/src/features/profile/lib/order.ts b/src/features/profile/lib/order.ts index 3bf7f09..1f5ab5d 100644 --- a/src/features/profile/lib/order.ts +++ b/src/features/profile/lib/order.ts @@ -1,16 +1,16 @@ // store/orderStore.ts +import { CartItem } from '@/features/cart/lib/api'; import { create } from 'zustand'; -import { OrderListRes } from './api'; type State = { - order: OrderListRes | null; + order: CartItem | null; }; type Actions = { - setOrder: (order: OrderListRes) => void; + setOrder: (order: CartItem) => void; }; -const getInitialOrder = (): OrderListRes | null => { +const getInitialOrder = (): CartItem | null => { if (typeof window === 'undefined') return null; // SSR check const stored = localStorage.getItem('order'); if (!stored) return null; @@ -24,7 +24,7 @@ const getInitialOrder = (): OrderListRes | null => { const useOrderStore = create((set) => ({ order: getInitialOrder(), - setOrder: (order: OrderListRes) => { + setOrder: (order: CartItem) => { if (typeof window !== 'undefined') { localStorage.setItem('order', JSON.stringify(order)); } diff --git a/src/features/profile/ui/History.tsx b/src/features/profile/ui/History.tsx index d3daedd..7929df7 100644 --- a/src/features/profile/ui/History.tsx +++ b/src/features/profile/ui/History.tsx @@ -18,12 +18,10 @@ import { useTranslations } from 'next-intl'; import { useSearchParams } from 'next/navigation'; import { useEffect, useState } from 'react'; import { order_api, OrderListRes } from '../lib/api'; -import useOrderStore from '../lib/order'; const HistoryTabs = () => { const t = useTranslations(); const searchParams = useSearchParams(); - const { setOrder } = useOrderStore(); const [page, setPage] = useState(1); const PAGE_SIZE = 36; const router = useRouter(); @@ -218,7 +216,7 @@ const HistoryTabs = () => { size="sm" onClick={() => { router.push('/profile/refresh-order'); - setOrder(order); + // setOrder(order); }} className="bg-transparent gap-1 md:gap-2 text-xs md:text-sm h-8 md:h-9 px-2 md:px-3" > diff --git a/src/features/profile/ui/RefreshOrder.tsx b/src/features/profile/ui/RefreshOrder.tsx index 8f315c8..df8ceb5 100644 --- a/src/features/profile/ui/RefreshOrder.tsx +++ b/src/features/profile/ui/RefreshOrder.tsx @@ -73,15 +73,12 @@ const RefreshOrder = () => { const t = useTranslations(); const queryClient = useQueryClient(); - const initialCartItems = initialValues?.items.map((item) => ({ + const initialCartItems = initialValues?.cart_item.map((item) => ({ id: item.id, product_id: item.product.id, product_name: item.product.name, - product_price: item.price, - product_image: - item.product.image || - item.product.images?.[0]?.image || - '/placeholder.svg', + product_price: item.product.prices[0].price, + product_image: item.product.images[0].image || '/placeholder.svg', quantity: item.quantity, })); @@ -90,7 +87,7 @@ const RefreshOrder = () => { const form = useForm>({ resolver: zodResolver(orderForm), defaultValues: { - comment: initialValues?.comment, + comment: '', lat: '41.311081', long: '69.240562', }, @@ -100,7 +97,7 @@ const RefreshOrder = () => { const subtotal = cartItems ? cartItems.reduce( - (sum, item) => sum + item.product_price * item.quantity, + (sum, item) => sum + Number(item.product_price) * item.quantity, 0, ) : 0; @@ -114,7 +111,7 @@ const RefreshOrder = () => { queryClient.refetchQueries({ queryKey: ['cart_items'] }); }, onError: () => { - toast.error('Xatolik yuz berdi', { + toast.error(t('Xatolik yuz berdi'), { richColors: true, position: 'top-center', }); @@ -205,7 +202,7 @@ const RefreshOrder = () => { const onSubmit = (value: z.infer) => { if (!deliveryDate) { - toast.error('Yetkazib berish sanasini tanlang', { + toast.error(t('Yetkazib berish sanasini tanlang'), { richColors: true, position: 'top-center', }); @@ -213,7 +210,7 @@ const RefreshOrder = () => { } if (!selectedTimeSlot) { - toast.error('Yetkazib berish vaqtini tanlang', { + toast.error(t('Yetkazib berish vaqtini tanlang'), { richColors: true, position: 'top-center', }); @@ -221,25 +218,48 @@ const RefreshOrder = () => { } if (initialValues === null) { - toast.error('Savatcha bo‘sh', { + toast.error(t('Savatcha bo‘sh'), { richColors: true, position: 'top-center', }); return; } - const items = initialValues.items.map((item) => ({ - product_id: Number(item.product.id), - quantity: item.quantity, - })); + const order_products = initialValues.cart_item + .filter( + (item) => + item.product.prices && + item.product.prices.length > 0 && + item.product.prices[0].price_type?.code && + item.product.prices[0].price, + ) + .map((item) => ({ + inventory_kind: 'G', + product_code: item.product.code, + on_balance: 'Y', + order_quant: item.quantity, + price_type_code: item.product.prices![0].price_type.code, + product_price: item.product.prices![0].price, + warehouse_code: 'wh1', + })); mutate({ - comment: value.comment, - items: items, - long: Number(value.long), - lat: Number(value.lat), - date: formatDate.format(deliveryDate, 'YYYY-MM-DD'), - time: selectedTimeSlot, + order: [ + { + filial_code: 'dodge', + delivery_date: formatDate.format(deliveryDate, 'DD.MM.YYYY'), + room_code: '100', + deal_time: formatDate.format(deliveryDate, 'DD.MM.YYYY'), + robot_code: 'r2', + status: 'B#N', + sales_manager_code: '1', + person_code: '12345678', + currency_code: '860', + owner_person_code: '1234567', + note: value.comment, + order_products: order_products, + }, + ], }); }; @@ -513,7 +533,7 @@ const RefreshOrder = () => {

{formatPrice( - item.product_price * item.quantity, + Number(item.product_price) * item.quantity, true, )}

diff --git a/src/features/search/ui/Search.tsx b/src/features/search/ui/Search.tsx index 828817b..49b757d 100644 --- a/src/features/search/ui/Search.tsx +++ b/src/features/search/ui/Search.tsx @@ -1,10 +1,7 @@ 'use client'; import { product_api } from '@/shared/config/api/product/api'; -import { - ProductListResult, - SearchDataPro, -} from '@/shared/config/api/product/type'; +import { ProductListResult } from '@/shared/config/api/product/type'; import { useRouter } from '@/shared/config/i18n/navigation'; import { Input } from '@/shared/ui/input'; import { ProductCard } from '@/widgets/categories/ui/product-card'; @@ -18,65 +15,73 @@ const SearchResult = () => { const router = useRouter(); const t = useTranslations(); const searchParams = useSearchParams(); - const query = searchParams.get('q') || ''; - const [searchRes, setSearchRes] = useState< - ProductListResult[] | SearchDataPro[] | [] - >([]); - const { data: product } = useQuery({ + const query = searchParams.get('q') || ''; + const [inputValue, setInputValue] = useState(query); + + /* 🔹 Input va URL sync */ + useEffect(() => { + setInputValue(query); + }, [query]); + + /* 🔹 Default product list */ + const { data: productList, isLoading: listLoading } = useQuery({ queryKey: ['product_list'], queryFn: () => product_api.list({ page: 1, page_size: 12 }), - select(data) { - return data.data.results; - }, + select: (res) => res.data.results, + enabled: !query, }); - const { data, isLoading } = useQuery({ + /* 🔹 Search query */ + const { data: searchList, isLoading: searchLoading } = useQuery({ queryKey: ['search', query], - queryFn: () => product_api.search({ search: query, page: 1, page_szie: 5 }), - select(data) { - return data.data.products; + queryFn: () => + product_api.search({ + search: query, + page: 1, + page_size: 12, + }), + select: (res) => { + return res.data.products; }, enabled: !!query, }); - useEffect(() => { - if (data) { - setSearchRes(data); - } else if (query.length === 0 && product && product.length > 0) { - setSearchRes(product); - } else { - setSearchRes([]); - } - }, [product, data]); + const data = query ? (searchList ?? []) : (productList ?? []); + const isLoading = query ? searchLoading : listLoading; + /* 🔹 Handlers */ const handleSearch = (value: string) => { + setInputValue(value); + if (!value.trim()) { router.push('/search'); return; } + router.push(`/search?q=${encodeURIComponent(value)}`); }; const clearSearch = () => { + setInputValue(''); router.push('/search'); }; return (
- {/* Search input (mobile) */} -
+ {/* 🔍 Search input */} +
handleSearch(e.target.value)} className="w-full pl-10 pr-10 h-12" /> - {query && ( + {inputValue && (
+ {/* 🔹 Content */} {isLoading ? ( -
{t('Yuklanmoqda')}
- ) : searchRes && searchRes.length > 0 ? ( +
{t('Yuklanmoqda')}...
+ ) : data.length > 0 ? (
- {searchRes - .filter((product) => product.is_active) - .map((product) => ( - - ))} + {data.map((products) => ( + + ))}
) : (
{t('Natija topilmadi')}
diff --git a/src/shared/config/api/URLs.ts b/src/shared/config/api/URLs.ts index cdc33fd..35cdddd 100644 --- a/src/shared/config/api/URLs.ts +++ b/src/shared/config/api/URLs.ts @@ -22,4 +22,5 @@ export const API_URLS = { CreateOrder: `${API_V}orders/order/create/`, OrderList: `${API_V}orders/order/list/`, Refresh_Token: `${API_V}accounts/refresh/token/`, + Get_Me: `${API_V}accounts/me/`, }; diff --git a/src/shared/config/api/product/api.ts b/src/shared/config/api/product/api.ts index fd2abde..7eff952 100644 --- a/src/shared/config/api/product/api.ts +++ b/src/shared/config/api/product/api.ts @@ -42,7 +42,7 @@ export const product_api = { async search(params: { search?: string; page?: number; - page_szie?: number; + page_size?: number; }): Promise> { const res = await httpClient.get(`${API_URLS.Search_Product}`, { params }); return res; diff --git a/src/shared/config/api/product/type.ts b/src/shared/config/api/product/type.ts index 3701fa4..56f7c3f 100644 --- a/src/shared/config/api/product/type.ts +++ b/src/shared/config/api/product/type.ts @@ -41,27 +41,35 @@ export interface ProductListResult { } export interface ProductDetail { - brand: string; - description: string; - expires_date: null | string; id: number; - image: string; - images: { - id: string; - image: string; - }[]; - is_active: boolean; + images: { id: number; image: string }[]; liked: boolean; - manufacturer: string; - min_quantity: number; + meansurement: null | string; + inventory_id: null | string; + product_id: string; + code: string; name: string; - price: number; - return_date: null | string; - volume: string; - unity: { - id: string; - name: string; - }; + short_name: string; + weight_netto: null | string; + weight_brutto: null | string; + litr: null | string; + box_type_code: null | string; + box_quant: null | string; + groups: number[]; + state: 'A' | 'P'; + barcodes: string; + article_code: null | string; + marketing_group_code: null | string; + inventory_kinds: { id: number; name: string }[]; + sector_codes: { id: number; code: string }[]; + prices: { + id: number; + price: string; + price_type: { + id: number; + name: string; + }; + }[]; } export interface SearchData { @@ -69,18 +77,7 @@ export interface SearchData { } export interface SearchDataPro { - id: number; - name: string; - image: string; - price: number; - description: string; - liked: boolean; - unity: { - id: string; - name: string; - }; - min_quantity: number; - is_active: boolean; + products: ProductListResult; } export interface FavouriteProduct { @@ -95,21 +92,32 @@ export interface FavouriteProduct { export interface FavouriteProductRes { id: number; - name: string; - image: string; - price: number; - description: string; - unity: { - id: string; - name: string; - }; - min_quantity: number; - is_active: boolean; + images: { id: number; image: string }[]; liked: boolean; - brand: null | string; - return_date: null | string; - expires_date: null | string; - manufacturer: null | string; - volume: null | string; - images: { id: string; image: string }[]; + meansurement: null | string; + inventory_id: null | string; + product_id: string; + code: string; + name: string; + short_name: string; + weight_netto: null | string; + weight_brutto: null | string; + litr: null | string; + box_type_code: null | string; + box_quant: null | string; + groups: number[]; + state: 'A' | 'P'; + barcodes: string; + article_code: null | string; + marketing_group_code: null | string; + inventory_kinds: { id: number; name: string }[]; + sector_codes: { id: number; code: string }[]; + prices: { + id: number; + price: string; + price_type: { + id: number; + name: string; + }; + }[]; } diff --git a/src/shared/config/i18n/messages/ru.json b/src/shared/config/i18n/messages/ru.json index 35f0896..fa54daf 100644 --- a/src/shared/config/i18n/messages/ru.json +++ b/src/shared/config/i18n/messages/ru.json @@ -128,12 +128,22 @@ "Tez yetkazib berish 1-2 kun ichida": "Быстрая доставка в течение 1-2 дней", "Xavfsiz to'lov usullari": "Безопасные способы оплаты", "Buyurtma qabul qilindi!": "Заказ принят!", + "Xatolik yuz berdi": "Произошла ошибка", "Buyurtma raqami": "Номер заказа", "Buyurtmangiz muvaffaqiyatli qabul qilindi": "Ваш заказ успешно принят.", "Bosh sahifaga qaytish": "Вернуться на главную", "Ma'lumotlaringizni to'ldiring": "Заполните ваши данные", "Shaxsiy ma'lumotlar": "Личные данные", "Ism": "Имя", + "Savatcha bo'sh": "Корзина пустая", + "Yetkazib berish sanasini tanlang": "Выберите дату доставки", + "Yetkazib berish vaqtini tanlang": "Выберите время доставки", + "Yetkazib berish vaqti": "Время доставки", + "Tanlangan yetkazib berish vaqti": "Выбранное время доставки", + "Vaqt oralig'i": "Интервал времени", + "Vaqtni tanlang": "Выберите время", + "Sanani tanlang": "Выберите дату", + "Yetkazib berish sanasi": "Дата доставки", "Ismingiz": "Ваше имя", "Familiya": "Имя Фамилия", "Familiyangiz": "Ваше имя и фамилия", diff --git a/src/shared/config/i18n/messages/uz.d.json.ts b/src/shared/config/i18n/messages/uz.d.json.ts index af3e082..eda7fdd 100644 --- a/src/shared/config/i18n/messages/uz.d.json.ts +++ b/src/shared/config/i18n/messages/uz.d.json.ts @@ -163,7 +163,16 @@ declare const messages: { 'Majburiy maydon': 'Majburiy maydon'; 'Xato raqam kiritildi': 'Xato raqam kiritildi'; Orqaga: 'Orqaga'; - + 'Xatolik yuz berdi': 'Xatolik yuz berdi'; + "Savatcha bo'sh": "Savatcha bo'sh"; + 'Sanani tanlang': 'Sanani tanlang'; + 'Vaqtni tanlang': 'Vaqtni tanlang'; + 'Yetkazib berish sanasi': 'Yetkazib berish sanasi'; + 'Yetkazib berish sanasini tanlang': 'Yetkazib berish sanasini tanlang'; + 'Yetkazib berish vaqtini tanlang': 'Yetkazib berish vaqtini tanlang'; + 'Tanlangan yetkazib berish vaqti': 'Tanlangan yetkazib berish vaqti'; + 'Yetkazib berish vaqti': 'Yetkazib berish vaqti'; + "Vaqt oralig'i": "Vaqt oralig'i"; "Sevimlilar bo'sh": "Sevimlilar bo'sh"; "Hali hech qanday mahsulotni sevimlilarga qo'shmadingiz": "Hali hech qanday mahsulotni sevimlilarga qo'shmadingiz. Mahsulotlar ro'yxatiga o'ting va yoqqan mahsulotlaringizni saqlang."; 'Sevimli mahsulotlar': 'Sevimli mahsulotlar'; diff --git a/src/shared/config/i18n/messages/uz.json b/src/shared/config/i18n/messages/uz.json index 9e422a9..9bc2dac 100644 --- a/src/shared/config/i18n/messages/uz.json +++ b/src/shared/config/i18n/messages/uz.json @@ -160,7 +160,16 @@ "Majburiy maydon": "Majburiy maydon", "Xato raqam kiritildi": "Xato raqam kiritildi", "Orqaga": "Orqaga", - + "Xatolik yuz berdi": "Xatolik yuz berdi", + "Savatcha bo'sh": "Savatcha bo'sh", + "Sanani tanlang": "Sanani tanlang", + "Vaqtni tanlang": "Vaqtni tanlang", + "Yetkazib berish sanasi": "Yetkazib berish sanasi", + "Yetkazib berish sanasini tanlang": "Yetkazib berish sanasini tanlang", + "Yetkazib berish vaqtini tanlang": "Yetkazib berish vaqtini tanlang", + "Tanlangan yetkazib berish vaqti": "Tanlangan yetkazib berish vaqti", + "Yetkazib berish vaqti": "Yetkazib berish vaqti", + "Vaqt oralig'i": "Vaqt oralig'i", "Sevimlilar bo'sh": "Sevimlilar bo'sh", "Hali hech qanday mahsulotni sevimlilarga qo'shmadingiz": "Hali hech qanday mahsulotni sevimlilarga qo'shmadingiz. Mahsulotlar ro'yxatiga o'ting va yoqqan mahsulotlaringizni saqlang.", "Sevimli mahsulotlar": "Sevimli mahsulotlar", diff --git a/src/widgets/categories/lib/hook.ts b/src/widgets/categories/lib/hook.ts new file mode 100644 index 0000000..2adc6e0 --- /dev/null +++ b/src/widgets/categories/lib/hook.ts @@ -0,0 +1,19 @@ +import { + FavouriteProductRes, + ProductListResult, +} from '@/shared/config/api/product/type'; +import { create } from 'zustand'; + +type State = { + product: ProductListResult | FavouriteProductRes | null; +}; + +type Actions = { + setProduct: (product: ProductListResult | FavouriteProductRes) => void; +}; + +export const useProductStore = create((set) => ({ + product: null, + setProduct: (product: ProductListResult | FavouriteProductRes) => + set(() => ({ product })), +})); diff --git a/src/widgets/categories/ui/animation.tsx b/src/widgets/categories/ui/animation.tsx index b5333bf..51deb67 100644 --- a/src/widgets/categories/ui/animation.tsx +++ b/src/widgets/categories/ui/animation.tsx @@ -186,9 +186,9 @@ const Animation: React.FC = ({
(0); const router = useRouter(); const queryClient = useQueryClient(); @@ -101,6 +106,7 @@ export function ProductCard({ onSuccess: () => { queryClient.refetchQueries({ queryKey: ['product_list'] }); + queryClient.refetchQueries({ queryKey: ['list'] }); queryClient.refetchQueries({ queryKey: ['favourite_product'] }); queryClient.refetchQueries({ queryKey: ['search'] }); queryClient.refetchQueries({ queryKey: ['product_detail', product] }); @@ -126,7 +132,7 @@ export function ProductCard({ if (cartItemId) { updateCartItem({ body: { quantity: newQty }, - cart_item_id: cartItemId, + cart_item_id: cartItemId.toString(), }); } } @@ -141,21 +147,21 @@ export function ProductCard({ const newQty = currentQty - 1; const cartItemId = cartItems.data.cart_item.find( - (item) => Number(item.product_id) === product.id, + (item) => Number(item.product.id) === product.id, )?.id; if (!cartItemId) return; if (newQty <= 0) { setQuantity(0); - deleteCartItem({ cart_item_id: cartItemId }); + deleteCartItem({ cart_item_id: cartItemId.toString() }); return; } setQuantity(newQty); updateCartItem({ body: { quantity: newQty }, - cart_item_id: cartItemId, + cart_item_id: cartItemId.toString(), }); }; @@ -176,6 +182,7 @@ export function ProductCard({ onClick={(e) => { e.stopPropagation(); router.push(`/product/${product.id}`); + setProduct(product); }} className="group relative p-0 h-full flex flex-col overflow-hidden border border-slate-200 bg-white shadow-sm hover:shadow-lg transition-all rounded-xl sm:rounded-2xl hover:border-green-400" > @@ -299,14 +306,14 @@ export function ProductCard({ if (!cartItems) return; const cartItemId = cartItems.data.cart_item.find( - (item) => Number(item.product_id) === product.id, + (item) => Number(item.product.id) === product.id, )?.id; if (!cartItemId) return; // ❗ 0 bo‘lsa — DELETE (darhol) if (num === 0) { - deleteCartItem({ cart_item_id: cartItemId }); + deleteCartItem({ cart_item_id: cartItemId.toString() }); return; } @@ -314,7 +321,7 @@ export function ProductCard({ debounceRef.current = setTimeout(() => { updateCartItem({ body: { quantity: num }, - cart_item_id: cartItemId, + cart_item_id: cartItemId.toString(), }); }, 500); }} diff --git a/src/widgets/navbar/ui/SearchResult.tsx b/src/widgets/navbar/ui/SearchResult.tsx index 0aef852..c1dbcb5 100644 --- a/src/widgets/navbar/ui/SearchResult.tsx +++ b/src/widgets/navbar/ui/SearchResult.tsx @@ -1,3 +1,6 @@ +'use client'; + +import LogosProduct from '@/assets/product.png'; import { product_api } from '@/shared/config/api/product/api'; import { ProductListResult, @@ -11,7 +14,6 @@ import { useQuery } from '@tanstack/react-query'; import { PackageOpen } from 'lucide-react'; import { useTranslations } from 'next-intl'; import Image from 'next/image'; -import { Fragment, useEffect, useState } from 'react'; type SearchResultProps = { query: string; @@ -20,55 +22,54 @@ type SearchResultProps = { export const SearchResult = ({ query }: SearchResultProps) => { const router = useRouter(); const t = useTranslations(); - const [searchRes, setSearchRes] = useState< - ProductListResult[] | SearchDataPro[] | [] - >([]); - const { data: product } = useQuery({ + /* 🔹 Default products - query bo'lmaganda */ + const { data: products, isLoading: isLoadingDefault } = useQuery< + ProductListResult[] + >({ queryKey: ['product_list'], - queryFn: () => product_api.list({ page: 1, page_size: 99 }), - select(data) { - return data.data.results; + queryFn: async () => { + const res = await product_api.list({ page: 1, page_size: 10 }); + return res.data.results; }, + enabled: !query, }); - const { data, isLoading } = useQuery({ + /* 🔹 Search - query bo'lganda */ + const { data: searchData, isLoading: isLoadingSearch } = useQuery< + ProductListResult[] + >({ queryKey: ['search', query], - queryFn: () => product_api.search({ search: query, page: 1, page_szie: 5 }), - select(data) { - return data.data.products; + queryFn: async () => { + const res = await product_api.search({ + search: query, + page: 1, + page_size: 10, + }); + // API response strukturasiga qarab to'g'rilash kerak + // Agar res.data.products array bo'lsa: + + return Array.isArray(res.data.products) + ? res.data.products + : (res.data.products as SearchDataPro[]).map( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (item: any) => item.products || item, + ); }, - enabled: !!query, + enabled: !!query && query.trim().length > 0, }); - useEffect(() => { - if (data) { - setSearchRes(data); - } else if (product && product.length > 0) { - setSearchRes(product); - } else { - setSearchRes([]); - } - }, [product, data]); - - if (searchRes && searchRes.length === 0) { - return ( -
- -

- {t('Hech narsa topilmadi')} -

-
- ); - } + const isLoading = query ? isLoadingSearch : isLoadingDefault; + const list: ProductListResult[] = query + ? (searchData ?? []) + : (products ?? []); if (isLoading) { return (
{Array.from({ length: 5 }).map((_, i) => ( -
+
-
@@ -79,45 +80,62 @@ export const SearchResult = ({ query }: SearchResultProps) => { ); } + if (!list.length) { + return ( +
+ +

+ {query ? t('Hech narsa topilmadi') : t('Mahsulotlar mavjud emas')} +

+
+ ); + } + return (
-

- {query.length > 0 ? t('Qidiruv natijalari') : t('Tavsiya etiladi')} +

+ {query ? t('Qidiruv natijalari') : t('Tavsiya etiladi')}

- {searchRes && - searchRes - .filter((product) => product.is_active) - .slice(0, 5) - .map((product, index) => ( - -
router.push(`/product/${product.id}`)} - > - {product.name} + {list + .filter((item) => item.state === 'A') + .slice(0, 5) + .map((product) => { + const image = + product.images.length > 0 + ? product?.images[0].image?.includes(BASE_URL) + ? product.images[0].image + : BASE_URL + product.images[0].image + : LogosProduct; + const price = product.prices?.[0]?.price; -
-

- {product.name} + return ( +

router.push(`/product/${product.id}`)} + > + {product.name} + +
+

+ {product.name} +

+ {price && ( +

+ {formatPrice(price)}

-

- {formatPrice(product.price)} -

-
+ )}
- - ))} +
+ ); + })}
); }; diff --git a/src/widgets/navbar/ui/index.tsx b/src/widgets/navbar/ui/index.tsx index 2a1a213..ebaa29b 100644 --- a/src/widgets/navbar/ui/index.tsx +++ b/src/widgets/navbar/ui/index.tsx @@ -17,7 +17,9 @@ import { SheetTitle, SheetTrigger, } from '@/shared/ui/sheet'; +import { banner_api } from '@/widgets/welcome/lib/api'; import { categoryList } from '@/widgets/welcome/lib/data'; +import { userStore } from '@/widgets/welcome/lib/hook'; import { PopoverTrigger } from '@radix-ui/react-popover'; import { useMutation, useQuery } from '@tanstack/react-query'; import { @@ -53,6 +55,7 @@ const Navbar = () => { const { cart_id } = useCartId(); const [cartQuenty, setCartQuenty] = useState(0); const { setCartId } = useCartId(); + const { setUser } = userStore(); const { mutate: cart } = useMutation({ mutationFn: () => cart_api.create(), @@ -61,6 +64,17 @@ const Navbar = () => { }, }); + const { data: me } = useQuery({ + queryKey: ['get_me'], + queryFn: () => banner_api.getMe(), + }); + + useEffect(() => { + if (me) { + setUser(me.data); + } + }, [me]); + useEffect(() => { if (token) { cart(); diff --git a/src/widgets/welcome/lib/api.ts b/src/widgets/welcome/lib/api.ts index 79459bd..bc3a5d6 100644 --- a/src/widgets/welcome/lib/api.ts +++ b/src/widgets/welcome/lib/api.ts @@ -3,9 +3,36 @@ import { API_URLS } from '@/shared/config/api/URLs'; import { AxiosResponse } from 'axios'; import { BannerRes } from './type'; +export interface UserRes { + address: null | string; + created_at: string; + date_joined: string; + email: string; + first_name: string; + gender: 'M' | 'F'; + groups: []; + id: null; + is_active: boolean; + is_staff: boolean; + is_superuser: boolean; + last_login: null | string; + last_name: string; + middle_name: null | string; + password: string; + person_id: number; + region: null | string; + tg_id: null | string; + user_permissions: []; + username: string; +} + export const banner_api = { async getBanner(): Promise> { const res = await httpClient.get(API_URLS.Banner); return res; }, + async getMe(): Promise> { + const res = await httpClient.get(API_URLS.Get_Me); + return res; + }, }; diff --git a/src/widgets/welcome/lib/hook.ts b/src/widgets/welcome/lib/hook.ts new file mode 100644 index 0000000..9ac62f4 --- /dev/null +++ b/src/widgets/welcome/lib/hook.ts @@ -0,0 +1,15 @@ +import { create } from 'zustand'; +import { UserRes } from './api'; + +type State = { + user: UserRes | null; +}; + +type Actions = { + setUser: (qty: UserRes) => void; +}; + +export const userStore = create((set) => ({ + user: null, + setUser: (user: UserRes) => set(() => ({ user })), +})); diff --git a/src/widgets/welcome/ui/index.tsx b/src/widgets/welcome/ui/index.tsx index 38655a9..f498cf3 100644 --- a/src/widgets/welcome/ui/index.tsx +++ b/src/widgets/welcome/ui/index.tsx @@ -1,10 +1,13 @@ 'use client'; +import CategoryImage from '@/assets/water-bottle.png'; import { category_api } from '@/shared/config/api/category/api'; +import { product_api } from '@/shared/config/api/product/api'; import { BASE_URL } from '@/shared/config/api/URLs'; import { Link } from '@/shared/config/i18n/navigation'; import { AspectRatio } from '@/shared/ui/aspect-ratio'; import { Button } from '@/shared/ui/button'; +import { Card } from '@/shared/ui/card'; import { Carousel, CarouselContent, @@ -13,13 +16,13 @@ import { } from '@/shared/ui/carousel'; import { Skeleton } from '@/shared/ui/skeleton'; import { CategoryCarousel } from '@/widgets/categories/ui/category-carousel'; +import { ProductCard } from '@/widgets/categories/ui/product-card'; import { useQuery } from '@tanstack/react-query'; import { AlertCircle, ChevronLeft, ChevronRight } from 'lucide-react'; import Image from 'next/image'; import { useState } from 'react'; import 'swiper/css'; import { banner_api } from '../lib/api'; -import CategoryImage from '@/assets/water-bottle.png'; const Welcome = () => { const [api, setApi] = useState(); @@ -65,6 +68,18 @@ const Welcome = () => { apiCat?.scrollNext(); }; + const { data: product, isLoading: productLoading } = useQuery({ + queryKey: ['list'], + queryFn: () => + product_api.list({ + page: 1, + page_size: 16, + }), + select(data) { + return data.data; + }, + }); + return ( <>
@@ -166,6 +181,39 @@ const Welcome = () => {
+
+ + + {productLoading && + Array.from({ length: 6 }).map((__, index) => ( + + + + + + + + + ))} + {product && + !isLoading && + product.results + .filter((product) => product.state === 'A') + .map((product) => ( + + + + ))} + + +
+ {category && category .slice(0, 6)