-
My Orders
-
- {isLoading ? (
+ if (isLoading) {
+ return (
+
+
{t.myOrders}
@@ -78,150 +129,110 @@ export default function OrdersPageClient({ locale }: OrdersPageProps) {
))}
- ) : isError ? (
+
+ )
+ }
+
+ if (isError) {
+ return (
+
+
{t.myOrders}
-
Failed to load orders. Please try again later.
+
{t.loadError}
+ {error &&
{error.message}
}
- ) : !orders || orders.length === 0 ? (
-
You have no orders yet.
- ) : (
-
-
- Active Orders ({activeOrders.length})
- Completed Orders ({completedOrders.length})
-
+
+ )
+ }
-
- {activeOrders.length === 0 ? (
- You have no active orders.
- ) : (
-
- {activeOrders.map((order) => (
-
-
-
-
Order #{order.id}
- {getStatusBadge(order.status)}
-
-
-
- Ordered: {new Date(order.created_at).toLocaleDateString()}
-
- {order.estimated_delivery && (
-
Est. Delivery: {order.estimated_delivery}
- )}
-
-
- {order.items?.map((item) => (
-
- {item.product?.image && (
-
- )}
-
-
{item.product?.name}
-
Qty: {item.quantity}
-
-
- ))}
-
-
-
- Total
- {order.total_formatted || `$${order.total}`}
-
-
-
-
-
-
-
- ))}
-
- )}
-
+ if (!orders || orders.length === 0) {
+ return (
+
+ )
+ }
-
- {completedOrders.length === 0 ? (
- You have no completed orders.
- ) : (
-
- {completedOrders.map((order) => (
-
-
-
-
Order #{order.id}
- {getStatusBadge(order.status)}
-
-
-
- Ordered: {new Date(order.created_at).toLocaleDateString()}
-
- {order.updated_at && (
-
- Completed: {new Date(order.updated_at).toLocaleDateString()}
-
- )}
-
-
- {order.items?.map((item) => (
-
- {item.product?.image && (
-
- )}
-
-
{item.product?.name}
-
Qty: {item.quantity}
-
-
- ))}
-
-
-
- Total
- {order.total_formatted || `$${order.total}`}
-
-
-
-
- ))}
-
- )}
-
-
- )}
+ return (
+
+
{t.myOrders}
+
+
+
+
+ {t.activeOrders} ({activeOrders.length})
+
+
+ {t.completedOrders} ({completedOrders.length})
+
+
+
+
+ {activeOrders.length === 0 ? (
+
+ ) : (
+
+ {activeOrders.map((order) => (
+
+ ))}
+
+ )}
+
+
+
+ {completedOrders.length === 0 ? (
+
+
{t.noCompletedOrders}
+
+ ) : (
+
+ {completedOrders.map((order) => (
+
+ ))}
+
+ )}
+
+
)
}
+
+interface OrderCardProps {
+ order: Order
+ onCancel: (order: Order) => void
+ isCancelling: boolean
+ getStatusBadge: (status: Order["status"]) => React.ReactNode
+ translations: any
+ showCancelButton: boolean
+}
+
+function OrderCard({
+ order,
+ onCancel,
+ isCancelling,
+ getStatusBadge,
+ translations: t,
+ showCancelButton,
+}: OrderCardProps) {
+ const canCancel =
+ showCancelButton && order.status !== "shipped" && order.status !== "delivered" && order.status !== "cancelled"
+
+ return (
+
+
+
+
+ {t.orderNumber}{order.id}
+
+ {getStatusBadge(order.status)}
+
+
+
+
+ {t.ordered}: {new Date(order.created_at).toLocaleDateString()}
+
+ {order.estimated_delivery && (
+
+ {t.estimatedDelivery}: {order.estimated_delivery}
+
+ )}
+ {!showCancelButton && order.updated_at && (
+
+ {t.completed}: {new Date(order.updated_at).toLocaleDateString()}
+
+ )}
+
+
+
+ {order.items?.map((item) => (
+
+ {item.product?.image && (
+
+ )}
+
+
{item.product?.name}
+
+ {t.quantity}: {item.quantity}
+
+
+
+ ))}
+
+
+
+
+ {t.total}
+ {order.total_formatted || `$${order.total}`}
+
+
+
+
+ {canCancel && (
+
+ onCancel(order)}
+ disabled={isCancelling}
+ className="w-full"
+ >
+ {t.cancelOrder}
+
+
+ )}
+
+ )
+}
\ No newline at end of file
diff --git a/app/[locale]/product/[slug]/ProductPageContent.tsx b/app/[locale]/product/[slug]/ProductPageContent.tsx
index 2fd6c70..442c495 100644
--- a/app/[locale]/product/[slug]/ProductPageContent.tsx
+++ b/app/[locale]/product/[slug]/ProductPageContent.tsx
@@ -7,73 +7,105 @@ import { Minus, Plus, Heart, ShoppingCart, Store } from "lucide-react"
import { Button } from "@/components/ui/button"
import { Card } from "@/components/ui/card"
import { Separator } from "@/components/ui/separator"
-import { Badge } from "@/components/ui/badge"
import { Avatar, AvatarFallback } from "@/components/ui/avatar"
-import placeholder from "@/public/jb.webp"
-import { useProduct, useCategories } from "@/lib/hooks"
import { Skeleton } from "@/components/ui/skeleton"
+import { useProductsBySlug } from "@/lib/hooks/useProducts"
+import { useAddToCart, useUpdateCartItemQuantity, useCart } from "@/lib/hooks/useCart"
+import { toast } from "sonner"
interface ProductDetailProps {
slug: string
}
const ProductPageContent = ({ slug }: ProductDetailProps) => {
- const [isClient, setIsClient] = useState(false)
const [selectedImage, setSelectedImage] = useState(0)
const [quantity, setQuantity] = useState(1)
const [isFavorite, setIsFavorite] = useState(false)
- const [isInCart, setIsInCart] = useState(false)
- const [isLoading, setIsLoading] = useState(false)
- const { data: product, isLoading: productLoading, error } = useProduct(slug)
- const { data: categoriesData } = useCategories()
-
- if (!isClient) {
- typeof window !== "undefined" && setIsClient(true)
- }
+ // Get product data
+ const { data: product, isLoading: productLoading, error } = useProductsBySlug(slug)
+
+ // Get cart data to check if product is already in cart
+ const { data: cartData } = useCart()
+
+ // Cart mutations
+ const addToCartMutation = useAddToCart()
+ const updateCartMutation = useUpdateCartItemQuantity()
const t = {
- addToCart: "Add to Cart",
- goToCart: "Go to Cart",
- price: "Price:",
- aboutProduct: "About Product",
- brand: "Brand",
- model: "Model",
- description: "Product Description",
- recommended: "Recommended Products",
- store: "Store",
- writeToStore: "Write to Store",
- color: "Color:",
+ addToCart: "Sebede goş",
+ goToCart: "Sebede git",
+ price: "Bahasy:",
+ aboutProduct: "Haryt barada",
+ brand: "Marka",
+ stock: "Mukdary",
+ description: "Düşündiriş",
+ store: "Dükan",
+ writeToStore: "Dükana ýaz",
+ color: "Reňk:",
+ category: "Kategoriýa:",
+ barcode: "Barkod:",
+ addedToCart: "Sebede goşuldy",
+ updatedCart: "Sebe täzelendi",
+ error: "Ýalňyşlyk ýüze çykdy",
}
+ // Check if product is in cart
+ const cartItem = cartData?.data?.find((item: any) => item.product?.id === product?.id)
+ const isInCart = !!cartItem
+
const handleAddToCart = async () => {
- setIsLoading(true)
+ if (!product?.id) return
+
try {
- // TODO: implement cart API call
- await new Promise((resolve) => setTimeout(resolve, 500))
- setIsInCart(true)
- } finally {
- setIsLoading(false)
+ await addToCartMutation.mutateAsync({
+ productId: product.id,
+ quantity: quantity,
+ })
+
+ toast.success(t.addedToCart, {
+ description: `${product.name} sebede goşuldy`,
+ })
+ } catch (error) {
+ console.error("Add to cart error:", error)
+ toast.error(t.error, {
+ description: "Haryt sebede goşup bolmady",
+ })
}
}
const handleQuantityChange = async (newQuantity: number) => {
- if (newQuantity < 1) return
- setIsLoading(true)
- try {
- setQuantity(newQuantity)
- // TODO: implement cart quantity update API call
- await new Promise((resolve) => setTimeout(resolve, 300))
- } finally {
- setIsLoading(false)
+ if (newQuantity < 1 || !product?.id) return
+ if (newQuantity > product.stock) return
+
+ setQuantity(newQuantity)
+
+ // If product is already in cart, update it
+ if (isInCart) {
+ try {
+ await updateCartMutation.mutateAsync({
+ productId: product.id,
+ quantity: newQuantity,
+ })
+
+ toast.success(t.updatedCart, {
+ description: `Mukdar: ${newQuantity}`,
+ })
+ } catch (error) {
+ console.error("Update cart error:", error)
+ toast.error(t.error, {
+ description: "Mukdar täzelenip bolmady",
+ })
+ }
}
}
const handleToggleFavorite = () => {
setIsFavorite(!isFavorite)
- // TODO: implement favorites API call
+ // TODO: Implement favorites API
}
+ // Loading state
if (productLoading) {
return (
@@ -95,14 +127,21 @@ const ProductPageContent = ({ slug }: ProductDetailProps) => {
)
}
+ // Error state
if (error || !product) {
return (
-
Product not found
+
Haryt tapylmady
+
Bu haryt ýok ýa-da aýryldy
)
}
+ // Extract image URLs from media array
+ const imageUrls = product.media?.map(m => m.images_800x800 || m.images_720x720 || m.thumbnail) || []
+
+ const isLoading = addToCartMutation.isPending || updateCartMutation.isPending
+
return (
@@ -110,42 +149,37 @@ const ProductPageContent = ({ slug }: ProductDetailProps) => {
- {product.labels && product.labels.length > 0 && (
-
- {product.labels.map((label) => (
-
- {label.text}
-
- ))}
+ {imageUrls.length > 0 ? (
+
+ ) : (
+
+ Surat ýok
)}
-
{/* Thumbnail Images */}
- {product.images && product.images.length > 1 && (
+ {imageUrls.length > 1 && (
- {product.images.map((image, index) => (
+ {imageUrls.map((image, index) => (
setSelectedImage(index)}
- className={`relative w-16 h-16 rounded overflow-hidden border ${
- selectedImage === index ? "border-black" : "border-transparent"
+ className={`relative w-16 h-16 flex-shrink-0 rounded overflow-hidden border-2 transition-all ${
+ selectedImage === index
+ ? "border-primary ring-2 ring-primary/20"
+ : "border-gray-200 hover:border-gray-300"
}`}
>
@@ -160,60 +194,121 @@ const ProductPageContent = ({ slug }: ProductDetailProps) => {
{product.name}
- {product.category && (
-
-
Category: {product.category}
+ {product.categories && product.categories.length > 0 && (
+
+ {product.categories.map((cat, idx) => (
+
+ {cat.name}
+
+ ))}
)}
{/* Product Info Table */}
-
+
{t.aboutProduct}
- {product.brand && (
+ {product.brand?.name && (
<>
{t.brand}
- {product.brand}
+ {product.brand.name}
>
)}
+
{product.stock !== undefined && (
<>
- Stock
- {product.stock}
+ {t.stock}
+
+ {product.stock === 0 ? 'Ýok' : product.stock}
+
>
)}
+
+ {product.barcode && (
+ <>
+
+ {t.barcode}
+ {product.barcode}
+
+
+ >
+ )}
+
+ {product.colour && (
+ <>
+
+ {t.color}
+ {product.colour}
+
+
+ >
+ )}
+
+ {product.properties && product.properties.length > 0 && (
+ <>
+ {product.properties.map((prop, idx) => (
+ prop.value && (
+
+
+ {prop.name}
+ {prop.value}
+
+ {idx < product.properties.length - 1 &&
}
+
+ )
+ ))}
+ >
+ )}
{/* Description */}
{product.description && (
-
+
{t.description}
- {product.description}
-
+
+
)}
{/* Price & Actions Sidebar */}
-
-
-
+
+
+
{t.price}
-
${product.price}
+
+
+ {product.price_amount} TMT
+
+ {product.old_price_amount && parseFloat(product.old_price_amount) > 0 && (
+
+ {product.old_price_amount} TMT
+
+ )}
+
-
+
{isInCart ? (
-
+ <>
-
+
{t.goToCart}
@@ -225,31 +320,33 @@ const ProductPageContent = ({ slug }: ProductDetailProps) => {
size="icon"
onClick={() => handleQuantityChange(quantity - 1)}
disabled={quantity === 1 || isLoading}
- className="rounded-xl bg-blue-50 flex-shrink-0"
+ className="rounded-xl h-12 w-12"
>
-
{quantity}
+
+ {quantity}
+
handleQuantityChange(quantity + 1)}
- disabled={isLoading}
- className="rounded-xl bg-blue-50 flex-shrink-0"
+ disabled={isLoading || quantity >= product.stock}
+ className="rounded-xl h-12 w-12"
>
-
+ >
) : (
- {t.addToCart}
+ {isLoading ? "Goşulýar..." : product.stock === 0 ? "Haryt ýok" : t.addToCart}
)}
@@ -257,36 +354,48 @@ const ProductPageContent = ({ slug }: ProductDetailProps) => {
variant="outline"
size="lg"
onClick={handleToggleFavorite}
- className={`w-full rounded-xl ${
- isFavorite ? "bg-red-50 border-red-200 hover:bg-red-100" : "bg-blue-50"
+ className={`w-full rounded-xl transition-all ${
+ isFavorite
+ ? "bg-red-50 border-red-300 hover:bg-red-100"
+ : "hover:bg-gray-50"
}`}
>
-
+
- {/* Seller Card */}
-
-
-
-
-
-
-
-
-
{t.store}
-
Official Store
+ {/* Store/Channel Card */}
+ {product.channel && product.channel.length > 0 && (
+
+
+
+
+
+
+
+
+
{t.store}
+
{product.channel[0].name}
+
-
-
- {t.writeToStore}
-
-
+
+ {t.writeToStore}
+
+
+ )}
)
}
-export default ProductPageContent
+export default ProductPageContent
\ No newline at end of file
diff --git a/components/ProductCard.tsx b/components/ProductCard.tsx
index 42503a9..13bbd0f 100644
--- a/components/ProductCard.tsx
+++ b/components/ProductCard.tsx
@@ -1,13 +1,15 @@
"use client";
import { useState, MouseEvent } from "react";
-
import Link from "next/link";
import { Button } from "@/components/ui/button";
import { Card, CardContent } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
-import { Heart, HeartOff, Minus, Plus } from "lucide-react";
+import { Heart, Minus, Plus } from "lucide-react";
import Image, { StaticImageData } from "next/image";
+import { useAddToFavorites, useRemoveFromFavorites } from "@/lib/hooks";
+import { useToast } from "@/hooks/use-toast";
+
type ProductCardProps = {
id: number;
name: string;
@@ -43,10 +45,52 @@ export default function ProductCard({
const [cart, setCart] = useState(false);
const [count, setCount] = useState(1);
+ const { toast } = useToast();
+ const { mutate: addToFavorites, isPending: isAdding } = useAddToFavorites();
+ const { mutate: removeFromFavorites, isPending: isRemoving } =
+ useRemoveFromFavorites();
+
const handleFavorite = (e: MouseEvent) => {
e.preventDefault();
e.stopPropagation();
- setFavorite((prev) => !prev);
+
+ const newFavoriteState = !favorite;
+
+ if (newFavoriteState) {
+ // Добавляем в избранное
+ addToFavorites(id, {
+ onSuccess: () => {
+ setFavorite(true);
+ toast({
+ title: "Товар добавлен в избранное",
+ });
+ },
+ onError: (error) => {
+ toast({
+ title: "Ошибка",
+ description: error.message,
+ variant: "destructive",
+ });
+ },
+ });
+ } else {
+ // Удаляем из избранного
+ removeFromFavorites(id, {
+ onSuccess: () => {
+ setFavorite(false);
+ toast({
+ title: "Товар удален из избранного",
+ });
+ },
+ onError: (error) => {
+ toast({
+ title: "Ошибка",
+ description: error.message,
+ variant: "destructive",
+ });
+ },
+ });
+ }
};
const handleAddToCart = (e: MouseEvent) => {
@@ -67,21 +111,23 @@ export default function ProductCard({
setCount((c) => (c > 1 ? c - 1 : c));
};
+ const isPending = isAdding || isRemoving;
+
return (
{/* Image Section */}
-
+
{images?.[0] && (
)}
@@ -89,7 +135,8 @@ export default function ProductCard({
{/* Favorite Button */}
{favorite ? (
@@ -125,7 +172,7 @@ export default function ProductCard({
{name}
- {/* Buttons */}
+ {/* Buttons - закомментированы в оригинале */}
{/* {button && (
{!cart ? (
diff --git a/components/home/CategoryGrid.tsx b/components/home/CategoryGrid.tsx
index ee40920..7de50c4 100644
--- a/components/home/CategoryGrid.tsx
+++ b/components/home/CategoryGrid.tsx
@@ -47,7 +47,13 @@ export default function CategoryGrid({ categories, isLoading, isError, locale, t
-
+
+
{cat.name}
diff --git a/lib/api.ts b/lib/api.ts
index 2af6327..f760ca1 100644
--- a/lib/api.ts
+++ b/lib/api.ts
@@ -22,7 +22,6 @@ const removeTokenFromCookie = (name: string): void => {
}
const getToken = (): string | null => {
- // Check cookies first (more secure)
const authToken = getTokenFromCookie("authToken")
if (authToken) return authToken
@@ -32,6 +31,17 @@ const getToken = (): string | null => {
return null
}
+/**
+ * Map internal locale codes to API language codes
+ */
+const localeToApiLang = (locale: string): string => {
+ const mapping: Record = {
+ tm: "tk",
+ ru: "ru",
+ }
+ return mapping[locale] || locale
+}
+
/**
* Centralized API client with interceptors
*/
@@ -68,13 +78,27 @@ class APIClient {
config.headers.Authorization = `Bearer ${token}`
}
- // Add language parameter if i18n is available
- if (typeof window !== "undefined" && (window as any).i18n) {
- const lang = (window as any).i18n.language || "tm"
- const url = config.url || ""
- config.url = `${url}${url.includes("?") ? "&" : "?"}lang=${lang}`
+ // Add language parameter
+ let lang = "tk" // default fallback
+
+ if (typeof window !== "undefined") {
+ // Try to get from i18n
+ if ((window as any).i18n?.language) {
+ lang = localeToApiLang((window as any).i18n.language)
+ }
+ // Try to get from pathname as fallback
+ else {
+ const pathLocale = window.location.pathname.split("/")[1]
+ if (pathLocale === "tm" || pathLocale === "ru") {
+ lang = localeToApiLang(pathLocale)
+ }
+ }
}
+ const url = config.url || ""
+ const separator = url.includes("?") ? "&" : "?"
+ config.url = `${url}${separator}lang=${lang}`
+
return config
},
(error) => Promise.reject(error)
@@ -89,7 +113,6 @@ class APIClient {
// Handle 401 errors
if (error.response?.status === 401 && !originalRequest._retry) {
if (this.isRefreshing) {
- // Queue requests while refreshing
return new Promise((resolve, reject) => {
this.failedQueue.push({ resolve, reject })
})
@@ -101,7 +124,6 @@ class APIClient {
this.isRefreshing = true
try {
- // Attempt to get guest token
const guestTokenResponse = await axios.post(
`${this.baseUrl}/api/v1/auth/guest-token`,
{},
@@ -203,10 +225,7 @@ class APIClient {
}
}
-// Export singleton instance
export const apiClient = new APIClient()
-
-// Export helper functions
export const setAuthToken = (token: string) => apiClient.setAuthToken(token)
export const setGuestToken = (token: string) => apiClient.setGuestToken(token)
export const clearAuthToken = () => apiClient.clearAuthToken()
\ No newline at end of file
diff --git a/lib/hooks/index.ts b/lib/hooks/index.ts
index 5162307..171fa28 100644
--- a/lib/hooks/index.ts
+++ b/lib/hooks/index.ts
@@ -9,7 +9,12 @@ export * from "./useOpenStore"
export * from "./useRegions"
export * from "./useAddresses"
export * from "./usePaymentTypes"
-export * from "./useCategories"
+
+
+
+
+
+
export * from "./useMedia"
export * from "./useCollections"
diff --git a/lib/hooks/useAddresses.ts b/lib/hooks/useAddresses.ts
index 39786eb..e69de29 100644
--- a/lib/hooks/useAddresses.ts
+++ b/lib/hooks/useAddresses.ts
@@ -1,14 +0,0 @@
-import { useQuery } from "@tanstack/react-query"
-import { apiClient } from "@/lib/api"
-import type { Address } from "@/lib/types/api"
-
-export function useAddresses() {
- return useQuery({
- queryKey: ["addresses"],
- queryFn: async () => {
- const response = await apiClient.get("/api/v1/addresses")
- return response.data
- },
- staleTime: 1000 * 60 * 30, // 30 minutes
- })
-}
diff --git a/lib/hooks/useCart.ts b/lib/hooks/useCart.ts
index 05dcd00..27c6c35 100644
--- a/lib/hooks/useCart.ts
+++ b/lib/hooks/useCart.ts
@@ -1,16 +1,61 @@
-import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"
+import { useQuery, useMutation, useQueryClient, UseQueryOptions } from "@tanstack/react-query"
import { apiClient } from "@/lib/api"
import type { Cart, CartItem } from "@/lib/types/api"
-export function useCart() {
+interface CartResponse {
+ message: string
+ data: CartItem[]
+ errorDetails?: string
+}
+
+// Transform response to handle HTML/malformed responses
+function transformCartResponse(response: any): CartResponse {
+ if (
+ typeof response === "string" &&
+ (response.trim().startsWith(">) {
return useQuery({
queryKey: ["cart"],
queryFn: async () => {
- const response = await apiClient.get("/api/v1/carts")
- return response.data
+ const response = await apiClient.get("/carts")
+ return transformCartResponse(response.data)
},
- staleTime: 0, // Always fetch fresh
+ refetchInterval: 5000, // Poll every 5 seconds like RTK
+ refetchOnMount: true,
+ refetchOnWindowFocus: false,
+ refetchOnReconnect: true,
+ staleTime: 0,
retry: 1,
+ ...options,
})
}
@@ -19,17 +64,38 @@ export function useAddToCart() {
return useMutation({
mutationFn: async ({ productId, quantity = 1 }: { productId: number; quantity?: number }) => {
- const response = await apiClient.post("/api/v1/carts", {
- product_id: productId,
- quantity,
+ const params = new URLSearchParams({
+ product_id: String(productId),
+ product_quantity: String(quantity),
})
- return response.data
+
+ 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
+ }
+
+ if (typeof response.data === "string") {
+ try {
+ 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" }
+ }
+ }
+
+ return { message: "success", data: "Added to cart" }
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["cart"] })
},
onError: (error: any) => {
- console.error("[v0] Add to cart error:", error.response?.data?.message || error.message)
+ console.error("Add to cart error:", error.response?.data?.message || error.message)
},
})
}
@@ -38,8 +104,66 @@ export function useRemoveFromCart() {
const queryClient = useQueryClient()
return useMutation({
- mutationFn: async (itemId: number) => {
- await apiClient.delete(`/api/v1/carts/${itemId}`)
+ mutationFn: async (productId: number) => {
+ 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
+ }
+
+ if (typeof response.data === "string") {
+ try {
+ const parsed = JSON.parse(response.data)
+ return parsed.data || []
+ } catch (error) {
+ console.error("Failed to parse cart response:", error)
+ return []
+ }
+ }
+
+ return []
+ },
+ onSuccess: () => {
+ queryClient.invalidateQueries({ queryKey: ["cart"] })
+ },
+ onError: (error: any) => {
+ console.error("Remove from cart error:", error.response?.data?.message || error.message)
+ },
+ })
+}
+
+export function useCleanCart() {
+ const queryClient = useQueryClient()
+
+ return useMutation({
+ mutationFn: async () => {
+ const response = await apiClient.delete("/carts", {
+ headers: {
+ "Content-Type": "application/x-www-form-urlencoded",
+ },
+ })
+
+ if (typeof response.data === "object" && response.data.data) {
+ return response.data.data
+ }
+
+ if (typeof response.data === "string") {
+ try {
+ const parsed = JSON.parse(response.data)
+ return parsed.data || []
+ } catch (error) {
+ console.error("Failed to parse cart response:", error)
+ return []
+ }
+ }
+
+ return []
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["cart"] })
@@ -51,15 +175,40 @@ export function useUpdateCartItemQuantity() {
const queryClient = useQueryClient()
return useMutation({
- mutationFn: async ({ itemId, quantity }: { itemId: number; quantity: number }) => {
- const response = await apiClient.patch(`/api/v1/carts/${itemId}`, {
- quantity,
+ mutationFn: async ({ productId, quantity }: { productId: number; quantity: number }) => {
+ const params = new URLSearchParams({
+ product_id: String(productId),
+ product_quantity: String(quantity),
})
- return response.data
+
+ 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
+ }
+
+ if (typeof response.data === "string") {
+ try {
+ 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" }
+ }
+ }
+
+ return { message: "success", data: "Updated cart" }
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["cart"] })
},
+ onError: (error: any) => {
+ console.error("API update failed:", error.response?.data?.message || error.message)
+ },
})
}
@@ -86,7 +235,36 @@ export function useCreateOrder() {
queryClient.invalidateQueries({ queryKey: ["orders"] })
},
onError: (error: any) => {
- console.error("[v0] Create order error:", error.response?.data?.message || error.message)
+ console.error("Create order error:", error.response?.data?.message || error.message)
},
})
}
+
+
+import type { Region } from "@/lib/types/api"
+
+export function useRegions() {
+ return useQuery({
+ queryKey: ["regions"],
+ queryFn: async () => {
+ const response = await apiClient.get("/api/v1/provinces")
+ return response.data
+ },
+ staleTime: 1000 * 60 * 60, // 1 hour
+ })
+}
+
+
+
+import type { Address } from "@/lib/types/api"
+
+export function useAddresses() {
+ return useQuery({
+ queryKey: ["addresses"],
+ queryFn: async () => {
+ const response = await apiClient.get("/api/v1/addresses")
+ return response.data
+ },
+ staleTime: 1000 * 60 * 30, // 30 minutes
+ })
+}
diff --git a/lib/hooks/useCategories.ts b/lib/hooks/useCategories.ts
index dc55307..cb33fe5 100644
--- a/lib/hooks/useCategories.ts
+++ b/lib/hooks/useCategories.ts
@@ -92,6 +92,8 @@ export function useAllCategoryProducts(
})
}
+
+
// Get products from category and children WITH pagination (mimics RTK getAllCategoryProductsPaginated)
export function useAllCategoryProductsPaginated(
category: Category | undefined,
@@ -155,4 +157,5 @@ export function useAllCategoryProductsPaginated(
},
enabled: options?.enabled !== false && !!category,
})
-}
\ No newline at end of file
+}
+
diff --git a/lib/hooks/useFavorites.ts b/lib/hooks/useFavorites.ts
index b057f2b..69398e4 100644
--- a/lib/hooks/useFavorites.ts
+++ b/lib/hooks/useFavorites.ts
@@ -1,42 +1,112 @@
-import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"
-import { apiClient } from "@/lib/api"
-import type { Favorite } from "@/lib/types/api"
+import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
+import { apiClient } from "@/lib/api";
+import type { Favorite } from "@/lib/types/api";
+
+// Response tiplerini tanımlayalım
+interface FavoritesResponse {
+ data?: Favorite[];
+ [key: string]: any;
+}
+
+interface FavoriteActionResponse {
+ data?: string | Favorite[];
+ [key: string]: any;
+}
+
+// Response'u transform eden yardımcı fonksiyon
+function transformFavoritesResponse(response: any): Favorite[] {
+ if (typeof response === "object" && response.data) {
+ return response.data;
+ }
+
+ if (typeof response === "string") {
+ try {
+ const parsed = JSON.parse(response);
+ return parsed.data || [];
+ } catch (error) {
+ console.error("Failed to parse favorites response:", error);
+ return [];
+ }
+ }
+
+ return [];
+}
+
+function transformActionResponse(response: any, defaultValue: string): string {
+ if (typeof response === "object" && response.data) {
+ return response.data;
+ }
+
+ if (typeof response === "string") {
+ try {
+ const parsed = JSON.parse(response);
+ return parsed.data || defaultValue;
+ } catch (error) {
+ if (response.includes("")) {
+ return defaultValue;
+ }
+ console.error(`Failed to parse favorite response:`, error);
+ return defaultValue;
+ }
+ }
+
+ return defaultValue;
+}
export function useFavorites() {
return useQuery({
queryKey: ["favorites"],
queryFn: async () => {
- const response = await apiClient.get("/favorites")
- return response.data
+ const response = await apiClient.get("/favorites");
+ return transformFavoritesResponse(response.data);
},
staleTime: 1000 * 60 * 5,
retry: 1,
- })
+ });
}
export function useAddToFavorites() {
- const queryClient = useQueryClient()
+ const queryClient = useQueryClient();
return useMutation({
mutationFn: async (productId: number) => {
- const response = await apiClient.post("/favorites", { product_id: productId })
- return response.data
+ const formData = new URLSearchParams({
+ product_id: productId.toString(),
+ });
+
+ const response = await apiClient.post("/favorites", formData, {
+ headers: {
+ "Content-Type": "application/x-www-form-urlencoded",
+ },
+ });
+
+ return transformActionResponse(response.data, "Added");
},
onSuccess: () => {
- queryClient.invalidateQueries({ queryKey: ["favorites"] })
+ queryClient.invalidateQueries({ queryKey: ["favorites"] });
},
- })
+ });
}
export function useRemoveFromFavorites() {
- const queryClient = useQueryClient()
+ const queryClient = useQueryClient();
return useMutation({
mutationFn: async (productId: number) => {
- await apiClient.delete(`/favorites/${productId}`)
+ const formData = new URLSearchParams({
+ product_id: productId.toString(),
+ });
+
+ const response = await apiClient.post("/favorites", formData, {
+ headers: {
+ "Content-Type": "application/x-www-form-urlencoded",
+ },
+ });
+
+ return transformActionResponse(response.data, "Removed");
},
onSuccess: () => {
- queryClient.invalidateQueries({ queryKey: ["favorites"] })
+ queryClient.invalidateQueries({ queryKey: ["favorites"] });
},
- })
+ });
}
diff --git a/lib/hooks/useOrders.ts b/lib/hooks/useOrders.ts
index 23e653b..45b06bb 100644
--- a/lib/hooks/useOrders.ts
+++ b/lib/hooks/useOrders.ts
@@ -1,59 +1,165 @@
-import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"
-import { apiClient } from "@/lib/api"
-import type { Order, PaginatedResponse } from "@/lib/types/api"
+import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
+import { apiClient } from "@/lib/api";
+import type { Order, PaginatedResponse } from "@/lib/types/api";
+// Response tiplerini tanımlayalım
+interface OrderResponse {
+ data?: Order | Order[];
+ [key: string]: any;
+}
+
+interface OrderActionResponse {
+ data?: string | Order;
+ [key: string]: any;
+}
+
+// Response'u transform eden yardımcı fonksiyonlar
+function transformOrdersResponse(response: any): Order[] {
+ if (typeof response === "object" && response.data) {
+ return Array.isArray(response.data) ? response.data : [];
+ }
+ return [];
+}
+
+function transformOrderResponse(response: any): Order | null {
+ if (typeof response === "object" && response.data) {
+ return response.data;
+ }
+ return null;
+}
+
+function transformOrderActionResponse(
+ response: any,
+ defaultValue: string
+): string | Order {
+ if (response && response.data) {
+ return response.data;
+ }
+ return defaultValue;
+}
+
+function isHtmlResponse(response: any): boolean {
+ return typeof response === "string" && response.includes("");
+}
+
+// Orders list query
export function useOrders(options?: { page?: number; perPage?: number }) {
return useQuery({
queryKey: ["orders", options?.page],
queryFn: async () => {
- const response = await apiClient.get>("/orders", {
+ const response = await apiClient.get("/orders", {
params: {
page: options?.page || 1,
per_page: options?.perPage || 20,
},
- })
- return response.data.data || response.data
+ });
+ return transformOrdersResponse(response.data);
},
staleTime: 1000 * 60 * 5,
retry: 1,
- })
+ });
}
+// Single order query
export function useOrder(id: number | string) {
return useQuery({
queryKey: ["order", id],
queryFn: async () => {
- const response = await apiClient.get(`/orders/${id}`)
- return response.data
+ const response = await apiClient.get(`/orders/${id}`);
+ return transformOrderResponse(response.data);
},
enabled: !!id,
- })
+ staleTime: 1000 * 60 * 5,
+ retry: 1,
+ });
}
+// Order times query
+export function useOrderTimes() {
+ return useQuery({
+ queryKey: ["order-times"],
+ queryFn: async () => {
+ const response = await apiClient.get("/order-time");
+ return transformOrdersResponse(response.data);
+ },
+ staleTime: 1000 * 60 * 10,
+ retry: 1,
+ });
+}
+
+// Order payments query
+export function useOrderPayments() {
+ return useQuery({
+ queryKey: ["order-payments"],
+ queryFn: async () => {
+ const response = await apiClient.get("/order-payments");
+ return transformOrdersResponse(response.data);
+ },
+ staleTime: 1000 * 60 * 10,
+ retry: 1,
+ });
+}
+
+// Create/Place order mutation
+export function useCreateOrder() {
+ const queryClient = useQueryClient();
+
+ return useMutation({
+ mutationFn: async (orderData: Record) => {
+ try {
+ const formData = new URLSearchParams();
+
+ // Convert orderData to URLSearchParams
+ Object.entries(orderData).forEach(([key, value]) => {
+ if (value !== null && value !== undefined) {
+ formData.append(key, String(value));
+ }
+ });
+
+ const response = await apiClient.post("/orders", formData, {
+ headers: {
+ "Content-Type": "application/x-www-form-urlencoded",
+ },
+ validateStatus: (status) => status >= 200 && status < 300,
+ });
+
+ // Check for HTML response
+ if (isHtmlResponse(response.data)) {
+ throw new Error(
+ "Server returned HTML instead of expected response format"
+ );
+ }
+
+ return transformOrderActionResponse(response.data, "Order placed");
+ } catch (error: any) {
+ // Handle HTML error response
+ if (error.response && isHtmlResponse(error.response.data)) {
+ throw new Error(
+ "Server returned HTML instead of expected response format"
+ );
+ }
+ throw error;
+ }
+ },
+ onSuccess: () => {
+ queryClient.invalidateQueries({ queryKey: ["orders"] });
+ queryClient.invalidateQueries({ queryKey: ["cart"] });
+ },
+ });
+}
+
+// Cancel order mutation
export function useCancelOrder() {
- const queryClient = useQueryClient()
+ const queryClient = useQueryClient();
return useMutation({
mutationFn: async (orderId: number) => {
- await apiClient.post(`/orders/${orderId}/cancel`, {})
+ const response = await apiClient.delete(`/orders/${orderId}`);
+ return transformOrderActionResponse(response.data, "Order cancelled");
},
- onSuccess: () => {
- queryClient.invalidateQueries({ queryKey: ["orders"] })
+ onSuccess: (_, orderId) => {
+ queryClient.invalidateQueries({ queryKey: ["orders"] });
+ queryClient.invalidateQueries({ queryKey: ["order", orderId] });
},
- })
-}
-
-export function useCreateOrder() {
- const queryClient = useQueryClient()
-
- return useMutation({
- mutationFn: async (orderData: any) => {
- const response = await apiClient.post("/orders", orderData)
- return response.data
- },
- onSuccess: () => {
- queryClient.invalidateQueries({ queryKey: ["orders"] })
- queryClient.invalidateQueries({ queryKey: ["cart"] })
- },
- })
+ });
}
diff --git a/lib/hooks/useProducts.ts b/lib/hooks/useProducts.ts
index c309029..a044c04 100644
--- a/lib/hooks/useProducts.ts
+++ b/lib/hooks/useProducts.ts
@@ -1,50 +1,216 @@
-import { useQuery } from "@tanstack/react-query"
-import { apiClient } from "@/lib/api"
-import type { Product, PaginatedResponse } from "@/lib/types/api"
+import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
+import { apiClient } from "@/lib/api";
+import type { Review, Product, PaginatedResponse } from "@/lib/types/api";
-interface UseProductsOptions {
- enabled?: boolean
- staleTime?: number
- page?: number
- perPage?: number
+// Get single review by ID
+export function useReview(
+ reviewId: number | string,
+ options?: { enabled?: boolean }
+) {
+ return useQuery({
+ queryKey: ["review", reviewId],
+ queryFn: async () => {
+ const response = await apiClient.get(`/reviews/${reviewId}`);
+ return response.data;
+ },
+ enabled: options?.enabled !== false && !!reviewId,
+ staleTime: 1000 * 60 * 10,
+ });
+}
+
+// Get all reviews with pagination
+export function useReviews(options?: {
+ enabled?: boolean;
+ page?: number;
+ limit?: number;
+}) {
+ return useQuery({
+ queryKey: ["reviews", options?.page, options?.limit],
+ queryFn: async () => {
+ const response = await apiClient.get>(
+ `/reviews`,
+ {
+ params: {
+ page: options?.page || 1,
+ limit: options?.limit,
+ },
+ }
+ );
+ return {
+ data: response.data.data || [],
+ pagination: response.data.pagination || {},
+ };
+ },
+ enabled: options?.enabled !== false,
+ staleTime: 1000 * 60 * 5,
+ });
+}
+
+// Get related reviews for a review
+export function useRelatedReviews(
+ reviewId: number | string,
+ options?: { enabled?: boolean }
+) {
+ return useQuery({
+ queryKey: ["review", reviewId, "related"],
+ queryFn: async () => {
+ const response = await apiClient.get>(
+ `/reviews/${reviewId}/related`
+ );
+ return response.data.data || response.data;
+ },
+ enabled: options?.enabled !== false && !!reviewId,
+ staleTime: 1000 * 60 * 15,
+ });
}
export function useProducts(options?: UseProductsOptions) {
return useQuery({
queryKey: ["products", options?.page, options?.perPage],
queryFn: async () => {
- const response = await apiClient.get>("/products", {
- params: {
- page: options?.page || 1,
- per_page: options?.perPage || 20,
- },
- })
- return response.data.data || response.data
+ const response = await apiClient.get>(
+ "/products",
+ {
+ params: {
+ page: options?.page || 1,
+ per_page: options?.perPage || 20,
+ },
+ }
+ );
+ return response.data.data || response.data;
},
- staleTime: options?.staleTime ?? 1000 * 60 * 5, // 5 minutes
+ staleTime: options?.staleTime ?? 1000 * 60 * 5,
enabled: options?.enabled !== false,
- })
+ });
}
-export function useProduct(id: number | string, options?: { enabled?: boolean }) {
+// Get single product by ID (for review context)
+export function useProduct(
+ productId: number | string,
+ options?: { enabled?: boolean }
+) {
return useQuery({
- queryKey: ["product", id],
+ queryKey: ["product", productId],
queryFn: async () => {
- const response = await apiClient.get(`/products/${id}`)
- return response.data
+ const response = await apiClient.get(`/products/${productId}`);
+ return response.data;
},
- staleTime: 1000 * 60 * 10, // 10 minutes
- enabled: options?.enabled !== false && !!id,
- })
+ enabled: options?.enabled !== false && !!productId,
+ staleTime: 1000 * 60 * 10,
+ });
}
-export function useProductsBySlug(slug: string, options?: { enabled?: boolean }) {
+export function useProductsBySlug(
+ slug: string,
+ options?: { enabled?: boolean }
+) {
return useQuery({
queryKey: ["products", "slug", slug],
queryFn: async () => {
- const response = await apiClient.get(`/products/${slug}`)
- return response.data
+ const response = await apiClient.get(`/products/${slug}`);
+ // API returns { message: "success", data: {...} }
+ return response.data.data || response.data;
},
enabled: options?.enabled !== false && !!slug,
- })
+ staleTime: 1000 * 60 * 10,
+ });
+}
+
+// Submit review mutation
+export function useSubmitReview() {
+ const queryClient = useQueryClient();
+
+ return useMutation({
+ mutationFn: async ({
+ productId,
+ rating,
+ title,
+ source,
+ }: {
+ productId: number | string;
+ rating: number;
+ title: string;
+ source: string;
+ }) => {
+ const response = await apiClient.post(
+ `/products/${productId}/reviews`,
+ { rating, title, source },
+ {
+ headers: {
+ "Content-Type": "application/json",
+ },
+ }
+ );
+ return response.data;
+ },
+ onSuccess: (_, variables) => {
+ queryClient.invalidateQueries({
+ queryKey: ["reviews", "product", variables.productId],
+ });
+ queryClient.invalidateQueries({
+ queryKey: ["product", variables.productId],
+ });
+ queryClient.invalidateQueries({
+ queryKey: ["reviews"],
+ });
+ },
+ });
+}
+
+// Update review mutation
+export function useUpdateReview() {
+ const queryClient = useQueryClient();
+
+ return useMutation({
+ mutationFn: async ({
+ reviewId,
+ rating,
+ title,
+ source,
+ }: {
+ reviewId: number | string;
+ rating?: number;
+ title?: string;
+ source?: string;
+ }) => {
+ const response = await apiClient.put(
+ `/reviews/${reviewId}`,
+ { rating, title, source },
+ {
+ headers: {
+ "Content-Type": "application/json",
+ },
+ }
+ );
+ return response.data;
+ },
+ onSuccess: (data, variables) => {
+ queryClient.invalidateQueries({
+ queryKey: ["review", variables.reviewId],
+ });
+ queryClient.invalidateQueries({
+ queryKey: ["reviews"],
+ });
+ },
+ });
+}
+
+// Delete review mutation
+export function useDeleteReview() {
+ const queryClient = useQueryClient();
+
+ return useMutation({
+ mutationFn: async (reviewId: number | string) => {
+ const response = await apiClient.delete(`/reviews/${reviewId}`);
+ return response.data;
+ },
+ onSuccess: (_, reviewId) => {
+ queryClient.invalidateQueries({
+ queryKey: ["review", reviewId],
+ });
+ queryClient.invalidateQueries({
+ queryKey: ["reviews"],
+ });
+ },
+ });
}
diff --git a/lib/hooks/useRegions.ts b/lib/hooks/useRegions.ts
index 61cfb30..e69de29 100644
--- a/lib/hooks/useRegions.ts
+++ b/lib/hooks/useRegions.ts
@@ -1,14 +0,0 @@
-import { useQuery } from "@tanstack/react-query"
-import { apiClient } from "@/lib/api"
-import type { Region } from "@/lib/types/api"
-
-export function useRegions() {
- return useQuery({
- queryKey: ["regions"],
- queryFn: async () => {
- const response = await apiClient.get("/api/v1/regions")
- return response.data
- },
- staleTime: 1000 * 60 * 60, // 1 hour
- })
-}
diff --git a/lib/types/api.ts b/lib/types/api.ts
index 93f0b19..6744427 100644
--- a/lib/types/api.ts
+++ b/lib/types/api.ts
@@ -4,23 +4,65 @@
*/
// Product Types
+export interface ProductMedia {
+ thumbnail: string
+ images_400x400: string
+ images_720x720: string
+ images_800x800: string
+ images_1200x1200: string
+}
+
+export interface ProductProperty {
+ attribute_id: number
+ name: string
+ value: string
+}
+
+export interface ProductReviews {
+ count: number
+ rating: string
+}
+
export interface Product {
id: number
+ parent_id: number | null
name: string
- slug?: string
- price: number
+ slug: string
description: string
- image: string
- images?: string[]
- category: string
- brand?: string
- stock?: number
- rating?: number
- reviews_count?: number
- is_favorite?: boolean
- is_in_cart?: boolean
- labels?: Array<{ text: string; bg_color: string }>
- struct_price_text?: string
+ sku: string | null
+ barcode: string
+ stock: number
+ price_amount: string
+ old_price_amount: string | null
+ backorder: string
+ weight_value: number | null
+ weight_unit: string | null
+ height_value: number | null
+ height_unit: string | null
+ media: ProductMedia[]
+ created_at: string
+ seo_title: string | null
+ seo_description: string | null
+ colour: string | null
+ size: string | null
+ available_colors: string[]
+ available_sizes: string[]
+ brand: {
+ id: number | null
+ name: string | null
+ }
+ channel: Array<{
+ id: number
+ name: string
+ }>
+ properties: ProductProperty[]
+ variations: any[]
+ reviews: ProductReviews
+ reviews_resources: any[]
+ categories: Array<{
+ id: number
+ name: string
+ }>
}
// Category Types
@@ -193,3 +235,5 @@ export interface CreateOrderPayload {
region: string
note?: string
}
+
+
diff --git a/next.config.ts b/next.config.ts
index 18486a1..eb1d895 100644
--- a/next.config.ts
+++ b/next.config.ts
@@ -12,8 +12,13 @@ const nextConfig: NextConfig = {
},
images: {
unoptimized: true,
+ remotePatterns: [
+ {
+ protocol: "https",
+ hostname: "shop.post.tm",
+ },
+ ],
},
- /* config options here */
}
export default withNextIntl(nextConfig)