From 6d0064b106df3e08fbd352a42b90c204bf638676 Mon Sep 17 00:00:00 2001 From: Jelaletdin12 Date: Mon, 15 Dec 2025 17:55:34 +0500 Subject: [PATCH] added empty pages --- app/[locale]/cart/page.tsx | 16 +- app/[locale]/favorites/page.tsx | 9 +- components/layout/Header.tsx | 20 +- components/layout/MobileBar.tsx | 92 ++-- components/layout/ui/ActionButtons.tsx | 7 +- components/ui/checkbox.tsx | 2 +- components/ui/slider.tsx | 2 +- features/cart/components/EmptyCart.tsx | 56 ++- features/cart/hooks/useCart.ts | 408 +++++++++++++----- .../components/CategoryFiltersSheet.tsx | 2 +- .../components/CategoryPageClient.tsx | 4 +- .../components/CollectionFiltersSheet.tsx | 2 +- .../components/CollectionPageClient.tsx | 4 +- .../favorites/components/EmptyFavorites.tsx | 56 ++- features/home/components/ProductCard.tsx | 2 +- features/orders/components/EmptyOrders.tsx | 56 ++- features/orders/components/OrderPage.tsx | 9 +- .../components/ProductReviewsSection.tsx | 13 +- .../components/RelatedProductsSection.tsx | 7 +- features/products/components/ReviewModal.tsx | 23 +- i18n/messages/ru.json | 18 +- i18n/messages/tm.json | 19 +- 22 files changed, 531 insertions(+), 296 deletions(-) diff --git a/app/[locale]/cart/page.tsx b/app/[locale]/cart/page.tsx index d8f6d67..b3fd3e1 100644 --- a/app/[locale]/cart/page.tsx +++ b/app/[locale]/cart/page.tsx @@ -14,7 +14,7 @@ import { userStore } from "@/features/profile/userStore"; import { useTranslations } from "next-intl"; import { useRouter } from "next/navigation"; import type { DeliveryType, PaymentType } from "@/lib/types/api"; - +import EmptyCart from "@/features/cart/components/EmptyCart"; export default function CartPage() { const [isClient, setIsClient] = useState(false); const [paymentType, setPaymentType] = useState(null); @@ -129,21 +129,11 @@ export default function CartPage() { if (!isClient) return null; - if (isLoading) { - return ( -
-

{t("common.loading")}

-
- ); - } + if (isError || cartItems.length === 0) { return ( -
-

- {t("cart_empty")} -

-
+ ); } diff --git a/app/[locale]/favorites/page.tsx b/app/[locale]/favorites/page.tsx index f2f1df1..a6d2e2b 100644 --- a/app/[locale]/favorites/page.tsx +++ b/app/[locale]/favorites/page.tsx @@ -5,7 +5,7 @@ import { Skeleton } from "@/components/ui/skeleton"; import { useTranslations } from "next-intl"; import ProductCard from "@/features/home/components/ProductCard"; import type { Favorite } from "@/lib/types/api"; - +import EmptyFavorites from "@/features/favorites/components/EmptyFavorites"; export default function FavoritesPage() { const t = useTranslations(); const { data: favorites, isLoading, isError } = useFavorites(); @@ -25,12 +25,7 @@ export default function FavoritesPage() { if (isError || !favorites || favorites.length === 0) { return ( -
-

{t("favorite_products")}

-
-

{t("empty_favorites")}

-
-
+ ); } diff --git a/components/layout/Header.tsx b/components/layout/Header.tsx index 68c4a88..8173c60 100644 --- a/components/layout/Header.tsx +++ b/components/layout/Header.tsx @@ -12,10 +12,9 @@ import AuthDialog from "./ui/AuthDialog"; import ActionButtons from "./ui/ActionButtons"; import LanguageSelector from "./ui/LanguageSelector"; import MobileBottomNav from "./MobileBar"; -import { useAuthStatus, useLogout } from "@/lib/hooks/useAuth"; +import { useAuthStatus } from "@/lib/hooks/useAuth"; import { useTranslations } from "next-intl"; import { CategoryIcon } from "../icons"; -import { useRouter } from "next/navigation"; interface HeaderProps { locale?: string; @@ -27,10 +26,10 @@ export default function Header({ locale = "ru" }: HeaderProps) { const [isMobileSearchOpen, setIsMobileSearchOpen] = useState(false); const [isLoginOpen, setIsLoginOpen] = useState(false); const t = useTranslations(); - const router = useRouter(); + const { isAuthenticated, isLoading } = useAuthStatus(); - const { mutate: logout, isPending: isLoggingOut } = useLogout(); + useEffect(() => { setIsClient(true); @@ -44,9 +43,7 @@ export default function Header({ locale = "ru" }: HeaderProps) { } }, [isAuthenticated, locale]); - const handleLogout = useCallback(() => { - logout(); - }, [logout]); + const toggleCategoryMenu = useCallback(() => { setIsCategoryOpen((prev) => !prev); @@ -56,9 +53,7 @@ export default function Header({ locale = "ru" }: HeaderProps) { setIsCategoryOpen(false); }, []); - const handleProfileClick = useCallback(() => { - router.push(`/${locale}/me`); - }, [router, locale]); + if (!isClient) return null; @@ -133,7 +128,10 @@ export default function Header({ locale = "ru" }: HeaderProps) { setIsLoginOpen(true)} + onLoginClick={() => { + console.log('[Header] Opening login dialog'); + setIsLoginOpen(true); + }} /> ); diff --git a/components/layout/MobileBar.tsx b/components/layout/MobileBar.tsx index 79d68d4..f403b75 100644 --- a/components/layout/MobileBar.tsx +++ b/components/layout/MobileBar.tsx @@ -12,10 +12,13 @@ import { SheetTitle, } from "@/components/ui/sheet"; import { ScrollArea } from "@/components/ui/scroll-area"; -import { useCategories, useCart, useFavorites, useOrders } from "@/lib/hooks"; +import { useCategories, useFavorites, useOrders } from "@/lib/hooks"; +import { useCartCount } from "@/features/cart/hooks/useCart"; import { useRouter } from "next/navigation"; import { useAuthStatus } from "@/lib/hooks/useAuth"; import { useTranslations } from "next-intl"; +import AuthDialog from "./ui/AuthDialog"; + interface MobileBottomNavProps { locale?: string; translations?: { @@ -36,29 +39,33 @@ export default function MobileBottomNav({ }: MobileBottomNavProps) { const [isClient, setIsClient] = useState(false); const [isCategoryOpen, setIsCategoryOpen] = useState(false); - const t = useTranslations(); - // AUTH STATE DIRECTLY FROM HOOK - NOT FROM PROPS + const [isLoginOpen, setIsLoginOpen] = useState(false); + const t = useTranslations(); + const { isAuthenticated, isLoading: authLoading } = useAuthStatus(); - + const { data: categories = [] } = useCategories(); - const { data: cartData } = useCart(); + + // OPTIMIZED: Use event-driven cart count instead of full cart data + const cartCount = useCartCount(); + const { data: favoritesData } = useFavorites(); const { data: ordersData } = useOrders(); const router = useRouter(); - - useEffect(() => { setIsClient(true); }, []); - - const handleProfileClick = (e: React.MouseEvent) => { e.preventDefault(); e.stopPropagation(); - - + + console.log("[MobileBottomNav] Profile clicked", { + authLoading, + isAuthenticated, + hasOnLoginClick: !!onLoginClick, + }); if (authLoading) { return; @@ -68,7 +75,11 @@ export default function MobileBottomNav({ router.push(`/${locale}/me`); } else { if (onLoginClick) { + console.log("[MobileBottomNav] Calling parent onLoginClick"); onLoginClick(); + } else { + console.log("[MobileBottomNav] Using local login dialog"); + setIsLoginOpen(true); } } }; @@ -109,14 +120,18 @@ export default function MobileBottomNav({ >
- - {favoritesData?.length || 0} - + {(favoritesData?.length || 0) > 0 && ( + + {favoritesData?.length} + + )}
- {t("common.favorites")} + + {t("common.favorites")} + {/* Orders Button */} @@ -128,17 +143,19 @@ export default function MobileBottomNav({ >
- - {ordersData?.length || 0} - + {(ordersData?.length || 0) > 0 && ( + + {ordersData?.length} + + )}
{t("common.orders")} - {/* Cart Button */} + {/* Cart Button - OPTIMIZED */} @@ -167,7 +186,11 @@ export default function MobileBottomNav({ > - {authLoading ? "..." : (isAuthenticated ? t("profile") : t("login"))} + {authLoading + ? "..." + : isAuthenticated + ? t("common.profile") + : t("common.login")} @@ -212,6 +235,9 @@ export default function MobileBottomNav({ + + {/* Local Auth Dialog */} + setIsLoginOpen(false)} /> ); -} \ No newline at end of file +} diff --git a/components/layout/ui/ActionButtons.tsx b/components/layout/ui/ActionButtons.tsx index 61c89c4..9d8f3f5 100644 --- a/components/layout/ui/ActionButtons.tsx +++ b/components/layout/ui/ActionButtons.tsx @@ -12,7 +12,7 @@ import { DropdownMenuItem, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; -import { useCart, useFavorites, useOrders } from "@/lib/hooks"; +import { useCart, useFavorites, useOrders, useCartCount } from "@/lib/hooks"; import { Skeleton } from "@/components/ui/skeleton"; import { useTranslations } from "next-intl"; import { useLogout } from "@/lib/hooks/useAuth"; @@ -53,10 +53,7 @@ export default function ActionButtons({ const { data: ordersData, isLoading: ordersLoading } = useOrders(); // Calculate cart count from cart items array - const cartCount = useMemo(() => { - if (!cartData?.data) return 0; - return cartData.data.length; - }, [cartData]); +const cartCount = useCartCount() // Calculate favorites count const favoritesCount = useMemo(() => { diff --git a/components/ui/checkbox.tsx b/components/ui/checkbox.tsx index cb0b07b..4fa36cd 100644 --- a/components/ui/checkbox.tsx +++ b/components/ui/checkbox.tsx @@ -14,7 +14,7 @@ function Checkbox({ diff --git a/features/cart/components/EmptyCart.tsx b/features/cart/components/EmptyCart.tsx index 1441ad6..f53bdc2 100644 --- a/features/cart/components/EmptyCart.tsx +++ b/features/cart/components/EmptyCart.tsx @@ -1,32 +1,30 @@ -import { ShoppingCart } from "lucide-react" -import { Button } from "@/components/ui/button" -import Link from "next/link" +import { Button } from "@/components/ui/button"; +import { ShoppingCart } from "lucide-react"; +import { useTranslations } from "next-intl"; +import { useRouter } from "next/navigation"; -interface EmptyCartProps { - locale?: string - message?: string - actionText?: string - actionHref?: string -} - -export default function EmptyCart({ - locale = "ru", - message = "Your cart is empty", - actionText = "Start Shopping", - actionHref = "/", -}: EmptyCartProps) { +export default function EmptyCart() { + const t=useTranslations(); + const router=useRouter(); return ( -
- -

{message}

-

- {locale === "ru" - ? "Добавьте товары в корзину, чтобы начать покупки" - : "Add items to your cart to start shopping"} -

- - - +
+
+
+ +
+ +

+ {t("cart_empty")} +

+ +

+ {t("cart_empty_message")} +

+ + +
- ) -} + ); +} \ No newline at end of file diff --git a/features/cart/hooks/useCart.ts b/features/cart/hooks/useCart.ts index 05cded9..9da5d48 100644 --- a/features/cart/hooks/useCart.ts +++ b/features/cart/hooks/useCart.ts @@ -1,148 +1,277 @@ -import { useQuery, useMutation, useQueryClient, UseQueryOptions } from "@tanstack/react-query" -import { apiClient } from "@/lib/api" -import type { CartItem } from "@/lib/types/api" +import { + useQuery, + useMutation, + useQueryClient, + UseQueryOptions, +} from "@tanstack/react-query"; +import { apiClient } from "@/lib/api"; +import type { CartItem } from "@/lib/types/api"; +import { useEffect } from "react"; interface CartResponse { - message: string - data: CartItem[] - errorDetails?: string + message: string; + data: CartItem[]; + errorDetails?: string; } -// Transform response to handle HTML/malformed responses +// Event emitter for cross-component cart updates +class CartEventEmitter { + private listeners: Set<() => void> = new Set(); + + subscribe(callback: () => void) { + this.listeners.add(callback); + return () => this.listeners.delete(callback); + } + + emit() { + this.listeners.forEach((cb) => cb()); + } +} + +export const cartEvents = new CartEventEmitter(); + function transformCartResponse(response: any): CartResponse { if ( typeof response === "string" && - (response.trim().startsWith(">) { - return useQuery({ + const queryClient = useQueryClient(); + + const query = useQuery({ queryKey: ["cart"], queryFn: async () => { - const response = await apiClient.get("/carts") - return transformCartResponse(response.data) + const response = await apiClient.get("/carts"); + return transformCartResponse(response.data); }, - refetchInterval: 10000, // Increased to 10 seconds (less aggressive) - refetchOnMount: true, - refetchOnWindowFocus: true, // Enable to catch updates on tab focus - refetchOnReconnect: true, - staleTime: 5000, // Data considered fresh for 5 seconds + // REMOVED: Aggressive polling + // ADDED: Smart refetching only when needed + refetchOnMount: false, // Don't refetch on every mount + refetchOnWindowFocus: false, // Don't refetch on tab focus + refetchOnReconnect: true, // Only refetch on reconnect + staleTime: Infinity, // Data never goes stale automatically + gcTime: 1000 * 60 * 5, // Cache for 5 minutes retry: 2, retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 10000), ...options, - }) + }); + + // Subscribe to cart events for cross-component updates + useEffect(() => { + const unsubscribe = cartEvents.subscribe(() => { + // Only update cache, don't refetch + queryClient.invalidateQueries({ + queryKey: ["cart"], + refetchType: "none", + }); + }); + return unsubscribe; + }, [queryClient]); + + return query; } export function useAddToCart() { - const queryClient = useQueryClient() + const queryClient = useQueryClient(); return useMutation({ - mutationFn: async ({ productId, quantity = 1 }: { productId: number; quantity?: number }) => { + mutationFn: async ({ + productId, + quantity = 1, + }: { + productId: number; + quantity?: number; + }) => { const params = new URLSearchParams({ product_id: String(productId), product_quantity: String(quantity), - }) + }); const response = await apiClient.post("/carts", params.toString(), { headers: { "Content-Type": "application/x-www-form-urlencoded", }, - }) + }); if (typeof response.data === "object" && response.data.data) { - return response.data + return response.data; } if (typeof response.data === "string") { try { - const parsed = JSON.parse(response.data) - return parsed + const parsed = JSON.parse(response.data); + return parsed; } catch (error) { - console.error("Failed to parse add to cart response:", error) - return { message: "success", data: "Added to cart" } + console.error("Failed to parse add to cart response:", error); + return { message: "success", data: "Added to cart" }; } } - return { message: "success", data: "Added to cart" } + return { message: "success", data: "Added to cart" }; + }, + onMutate: async ({ productId, quantity }) => { + // Cancel outgoing refetches + await queryClient.cancelQueries({ queryKey: ["cart"] }); + + // Snapshot previous value + const previousCart = queryClient.getQueryData(["cart"]); + + // Optimistically update cart + queryClient.setQueryData(["cart"], (old) => { + if (!old) return old; + + const existingItem = old.data.find( + (item: any) => item.product?.id === productId + ); + + if (existingItem) { + // Update existing item quantity + return { + ...old, + data: old.data.map((item: any) => + item.product?.id === productId + ? { + ...item, + product_quantity: item.product_quantity + quantity, + } + : item + ), + }; + } else { + // Add new item (we don't have full product data, so we add placeholder) + return { + ...old, + data: [ + ...old.data, + { + product: { id: productId }, + product_quantity: quantity, + } as any, + ], + }; + } + }); + + // Notify other components + cartEvents.emit(); + + return { previousCart }; + }, + onError: (error, variables, context) => { + // Rollback on error + if (context?.previousCart) { + queryClient.setQueryData(["cart"], context.previousCart); + cartEvents.emit(); + } + console.error("Add to cart error:", error); }, onSuccess: () => { - // Invalidate but don't refetch immediately (let polling handle it) - queryClient.invalidateQueries({ queryKey: ["cart"], refetchType: 'none' }) + // Silently refetch in background to sync with server + queryClient.invalidateQueries({ + queryKey: ["cart"], + refetchType: "active", // Only refetch if actively being watched + }); }, - onError: (error: any) => { - console.error("Add to cart error:", error.response?.data?.message || error.message) - }, - }) + }); } export function useRemoveFromCart() { - const queryClient = useQueryClient() + const queryClient = useQueryClient(); return useMutation({ mutationFn: async (productId: number) => { - const params = new URLSearchParams({ product_id: String(productId) }) + const params = new URLSearchParams({ product_id: String(productId) }); const response = await apiClient.patch("/carts", params.toString(), { headers: { "Content-Type": "application/x-www-form-urlencoded", }, - }) + }); if (typeof response.data === "object" && response.data.data) { - return response.data.data + return response.data.data; } if (typeof response.data === "string") { try { - const parsed = JSON.parse(response.data) - return parsed.data || [] + const parsed = JSON.parse(response.data); + return parsed.data || []; } catch (error) { - console.error("Failed to parse cart response:", error) - return [] + console.error("Failed to parse cart response:", error); + return []; } } - return [] + return []; + }, + onMutate: async (productId) => { + await queryClient.cancelQueries({ queryKey: ["cart"] }); + + const previousCart = queryClient.getQueryData(["cart"]); + + queryClient.setQueryData(["cart"], (old) => { + if (!old) return old; + return { + ...old, + data: old.data.filter((item: any) => item.product?.id !== productId), + }; + }); + + cartEvents.emit(); + + return { previousCart }; + }, + onError: (error, variables, context) => { + if (context?.previousCart) { + queryClient.setQueryData(["cart"], context.previousCart); + cartEvents.emit(); + } + console.error("Remove from cart error:", error); }, onSuccess: () => { - // Immediate refetch after removal - queryClient.invalidateQueries({ queryKey: ["cart"] }) + queryClient.invalidateQueries({ + queryKey: ["cart"], + refetchType: "active", + }); }, - onError: (error: any) => { - console.error("Remove from cart error:", error.response?.data?.message || error.message) - }, - }) + }); } export function useCleanCart() { - const queryClient = useQueryClient() + const queryClient = useQueryClient(); return useMutation({ mutationFn: async () => { @@ -150,98 +279,171 @@ export function useCleanCart() { headers: { "Content-Type": "application/x-www-form-urlencoded", }, - }) + }); if (typeof response.data === "object" && response.data.data) { - return response.data.data + return response.data.data; } if (typeof response.data === "string") { try { - const parsed = JSON.parse(response.data) - return parsed.data || [] + const parsed = JSON.parse(response.data); + return parsed.data || []; } catch (error) { - console.error("Failed to parse cart response:", error) - return [] + console.error("Failed to parse cart response:", error); + return []; } } - return [] + return []; + }, + onMutate: async () => { + await queryClient.cancelQueries({ queryKey: ["cart"] }); + + const previousCart = queryClient.getQueryData(["cart"]); + + queryClient.setQueryData(["cart"], (old) => { + if (!old) return old; + return { ...old, data: [] }; + }); + + cartEvents.emit(); + + return { previousCart }; + }, + onError: (error, variables, context) => { + if (context?.previousCart) { + queryClient.setQueryData(["cart"], context.previousCart); + cartEvents.emit(); + } }, onSuccess: () => { - queryClient.invalidateQueries({ queryKey: ["cart"] }) + queryClient.invalidateQueries({ queryKey: ["cart"] }); }, - }) + }); } export function useUpdateCartItemQuantity() { - const queryClient = useQueryClient() + const queryClient = useQueryClient(); return useMutation({ - mutationFn: async ({ productId, quantity }: { productId: number; quantity: number }) => { + mutationFn: async ({ + productId, + quantity, + }: { + productId: number; + quantity: number; + }) => { const params = new URLSearchParams({ product_id: String(productId), product_quantity: String(quantity), - }) + }); const response = await apiClient.post("/carts", params.toString(), { headers: { "Content-Type": "application/x-www-form-urlencoded", }, - timeout: 15000, // 15 second timeout - }) + timeout: 15000, + }); if (typeof response.data === "object" && response.data.data) { - return response.data + return response.data; } if (typeof response.data === "string") { try { - const parsed = JSON.parse(response.data) - return parsed + const parsed = JSON.parse(response.data); + return parsed; } catch (error) { - console.error("Failed to parse update cart response:", error) - return { message: "success", data: "Updated cart" } + console.error("Failed to parse update cart response:", error); + return { message: "success", data: "Updated cart" }; } } - return { message: "success", data: "Updated cart" } + return { message: "success", data: "Updated cart" }; + }, + onMutate: async ({ productId, quantity }) => { + await queryClient.cancelQueries({ queryKey: ["cart"] }); + + const previousCart = queryClient.getQueryData(["cart"]); + + queryClient.setQueryData(["cart"], (old) => { + if (!old) return old; + return { + ...old, + data: old.data.map((item: any) => + item.product?.id === productId + ? { ...item, product_quantity: quantity } + : item + ), + }; + }); + + cartEvents.emit(); + + return { previousCart }; + }, + onError: (error, variables, context) => { + if (context?.previousCart) { + queryClient.setQueryData(["cart"], context.previousCart); + cartEvents.emit(); + } + console.error("API update failed:", error); + throw error; }, onSuccess: () => { - // Invalidate but don't refetch immediately (let optimistic update handle it) - queryClient.invalidateQueries({ queryKey: ["cart"], refetchType: 'none' }) + // Background sync + queryClient.invalidateQueries({ + queryKey: ["cart"], + refetchType: "none", // Don't refetch, trust optimistic update + }); }, - onError: (error: any) => { - console.error("API update failed:", error.response?.data?.message || error.message) - throw error // Re-throw to trigger retry mechanism - }, - }) + }); } export function useCreateOrder() { - const queryClient = useQueryClient() + const queryClient = useQueryClient(); return useMutation({ mutationFn: async (payload: { - customer_name?: string - customer_phone: string - customer_address: string - shipping_method: string - payment_type_id: number - delivery_time?: string - delivery_at?: string - region: string - note?: string + customer_name?: string; + customer_phone: string; + customer_address: string; + shipping_method: string; + payment_type_id: number; + delivery_time?: string; + delivery_at?: string; + region: string; + note?: string; }) => { - const response = await apiClient.post("/orders", payload) - return response.data + const response = await apiClient.post("/orders", payload); + return response.data; }, onSuccess: () => { - queryClient.invalidateQueries({ queryKey: ["cart"] }) - queryClient.invalidateQueries({ queryKey: ["orders"] }) + // Clear cart after successful order + queryClient.setQueryData(["cart"], (old) => { + if (!old) return old; + return { ...old, data: [] }; + }); + cartEvents.emit(); + queryClient.invalidateQueries({ queryKey: ["orders"] }); }, onError: (error: any) => { - console.error("Create order error:", error.response?.data?.message || error.message) + console.error( + "Create order error:", + error.response?.data?.message || error.message + ); }, - }) -} \ No newline at end of file + }); +} + +// Hook to get cart count for badges +export function useCartCount() { + const { data } = useCart(); + return ( + data?.data?.reduce( + (sum: number, item: any) => sum + (item.product_quantity || 0), + 0 + ) || 0 + ); +} diff --git a/features/category/components/CategoryFiltersSheet.tsx b/features/category/components/CategoryFiltersSheet.tsx index 25afd98..d25a563 100644 --- a/features/category/components/CategoryFiltersSheet.tsx +++ b/features/category/components/CategoryFiltersSheet.tsx @@ -28,7 +28,7 @@ export default function CategoryFiltersSheet({ - +
+
+
+ +
+ +

+ {t("favorites_empty")} +

+ +

+ {t("favorites_empty_message")} +

+ + +
- ) -} + ); +} \ No newline at end of file diff --git a/features/home/components/ProductCard.tsx b/features/home/components/ProductCard.tsx index 0523e89..5564c7a 100644 --- a/features/home/components/ProductCard.tsx +++ b/features/home/components/ProductCard.tsx @@ -306,7 +306,7 @@ export default function ProductCard({ ) : ( )} diff --git a/features/orders/components/EmptyOrders.tsx b/features/orders/components/EmptyOrders.tsx index c434bbf..b158a21 100644 --- a/features/orders/components/EmptyOrders.tsx +++ b/features/orders/components/EmptyOrders.tsx @@ -1,32 +1,30 @@ -import { Package } from "lucide-react" -import { Button } from "@/components/ui/button" -import Link from "next/link" +import { Button } from "@/components/ui/button"; +import { ShoppingCart } from "lucide-react"; +import { useTranslations } from "next-intl"; +import { useRouter } from "next/navigation"; -interface EmptyOrdersProps { - locale?: string - message?: string - actionText?: string - actionHref?: string -} - -export default function EmptyOrders({ - locale = "ru", - message = "No orders yet", - actionText = "Start Shopping", - actionHref = "/", -}: EmptyOrdersProps) { +export default function EmptyOrders() { + const t=useTranslations(); + const router=useRouter(); return ( -
- -

{message}

-

- {locale === "ru" - ? "У вас еще нет заказов. Начните покупки прямо сейчас!" - : "You haven't placed any orders yet. Start shopping now!"} -

- - - +
+
+
+ +
+ +

+ {t("orders_empty")} +

+ +

+ {t("orders_empty_message")} +

+ + +
- ) -} + ); +} \ No newline at end of file diff --git a/features/orders/components/OrderPage.tsx b/features/orders/components/OrderPage.tsx index 4b45a19..892e163 100644 --- a/features/orders/components/OrderPage.tsx +++ b/features/orders/components/OrderPage.tsx @@ -28,7 +28,7 @@ import { useToast } from "@/hooks/use-toast"; import { useOrders, useCancelOrder } from "@/lib/hooks"; import { useTranslations } from "next-intl"; import type { Order } from "@/lib/types/api"; - +import EmptyOrders from "./EmptyOrders"; interface OrdersPageClientProps { locale: string; } @@ -181,12 +181,7 @@ export default function OrdersPageClient({ locale }: OrdersPageClientProps) { if (isError || !orders || orders.length === 0) { return ( -
-

{t("my_orders")}

-
-

{t("no_orders")}

-
-
+ ); } diff --git a/features/products/components/ProductReviewsSection.tsx b/features/products/components/ProductReviewsSection.tsx index af7b1e8..823cf89 100644 --- a/features/products/components/ProductReviewsSection.tsx +++ b/features/products/components/ProductReviewsSection.tsx @@ -3,6 +3,7 @@ import { Card } from "@/components/ui/card"; import { Button } from "@/components/ui/button"; import { Separator } from "@/components/ui/separator"; import { Skeleton } from "@/components/ui/skeleton"; +import { useTranslations } from "next-intl"; interface Review { id: number; @@ -41,11 +42,13 @@ export function ProductReviewsSection({ ); }; + const t= useTranslations(); + return ( -
+
-

Customer Reviews

+

{t("customer_reviews")}

{renderStars(Math.round(averageRating))} @@ -53,9 +56,9 @@ export function ProductReviewsSection({
-
@@ -83,7 +86,7 @@ export function ProductReviewsSection({
) : (
- No reviews yet. Be the first to review this product! + {t("no_reviews")}
)}
diff --git a/features/products/components/RelatedProductsSection.tsx b/features/products/components/RelatedProductsSection.tsx index e8b30f2..c43b5cd 100644 --- a/features/products/components/RelatedProductsSection.tsx +++ b/features/products/components/RelatedProductsSection.tsx @@ -1,5 +1,5 @@ import ProductCard from "@/features/home/components/ProductCard"; - +import {useTranslations} from "next-intl"; interface RelatedProduct { id: number; slug: string; @@ -30,12 +30,13 @@ interface RelatedProductsSectionProps { export function RelatedProductsSection({ products, }: RelatedProductsSectionProps) { + const t = useTranslations(); if (!products || products.length === 0) return null; return (
-

Related Products

-
+

{t("related_products")}

+
{products.slice(0, 4).map((product) => { // Extract image URLs from media const images = diff --git a/features/products/components/ReviewModal.tsx b/features/products/components/ReviewModal.tsx index 3eaecc9..b2e90a0 100644 --- a/features/products/components/ReviewModal.tsx +++ b/features/products/components/ReviewModal.tsx @@ -9,6 +9,7 @@ import { } from "@/components/ui/dialog"; import { Button } from "@/components/ui/button"; import { Textarea } from "@/components/ui/textarea"; +import { useTranslations } from "next-intl"; interface ReviewModalProps { open: boolean; @@ -27,6 +28,8 @@ export function ReviewModal({ const [text, setText] = useState(""); const [hoveredStar, setHoveredStar] = useState(0); + const t = useTranslations(); + const handleClose = () => { onOpenChange(false); setRating(0); @@ -63,29 +66,29 @@ export function ReviewModal({ - Write a Review + {t("write_review")} - Share your experience with this product + {t("share_experience")}
- + {renderStars()}