cleaned code from logs and some comments

This commit is contained in:
Jelaletdin12
2025-12-19 18:14:29 +05:00
parent 0fb4e2765c
commit cdc9fa686f
45 changed files with 368 additions and 501 deletions

View File

@@ -8,4 +8,6 @@
5. Review feed back yazylyan yer bamy bolmalymy 5. Review feed back yazylyan yer bamy bolmalymy
6. Open Store api field ler nahili bolmaly. 6. Open Store api field ler nahili bolmaly.
7. Delivery type soramaly, type lar yok

View File

@@ -10,11 +10,11 @@ import {
useRegions, useRegions,
usePaymentTypes, usePaymentTypes,
} from "@/lib/hooks"; } from "@/lib/hooks";
import { userStore } from "@/features/profile/userStore";
import { useTranslations } from "next-intl"; import { useTranslations } from "next-intl";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import type { DeliveryType, PaymentType } from "@/lib/types/api"; import type { DeliveryType, PaymentType } from "@/lib/types/api";
import EmptyCart from "@/features/cart/components/EmptyCart"; import EmptyCart from "@/features/cart/components/EmptyCart";
export default function CartPage() { export default function CartPage() {
const [isClient, setIsClient] = useState(false); const [isClient, setIsClient] = useState(false);
const [paymentType, setPaymentType] = useState<PaymentType | null>(null); const [paymentType, setPaymentType] = useState<PaymentType | null>(null);
@@ -38,14 +38,6 @@ export default function CartPage() {
useEffect(() => { useEffect(() => {
setIsClient(true); setIsClient(true);
// Get user data from store if available
const orderData = userStore.getOrderData();
if (orderData) {
if (orderData.customer_name) setName(orderData.customer_name);
if (orderData.customer_last_name) setLastName(orderData.customer_last_name);
if (orderData.customer_phone) setPhone(orderData.customer_phone);
}
}, []); }, []);
const regionGroups = useMemo(() => { const regionGroups = useMemo(() => {
@@ -92,7 +84,13 @@ export default function CartPage() {
}; };
const handleCompleteOrder = () => { const handleCompleteOrder = () => {
if (!selectedRegion || !selectedProvince || !paymentType || !phone || !name) { if (
!selectedRegion ||
!selectedProvince ||
!paymentType ||
!phone ||
!name
) {
console.warn("Missing required fields for order"); console.warn("Missing required fields for order");
return; return;
} }
@@ -102,13 +100,6 @@ export default function CartPage() {
); );
if (!selectedProvinceData) return; if (!selectedProvinceData) return;
const orderData = userStore.getOrderData();
if (!orderData) {
console.error("User data not found");
router.push("/");
return;
}
createOrder( createOrder(
{ {
customer_name: name, customer_name: name,
@@ -129,17 +120,15 @@ export default function CartPage() {
if (!isClient) return null; if (!isClient) return null;
if (isError || cartItems.length === 0) { if (isError || cartItems.length === 0) {
return ( return <EmptyCart />;
<EmptyCart/>
);
} }
return ( return (
<div className=" mx-auto px-2 md:px-4 lg:px-6 mb-18"> <div className=" mx-auto px-2 md:px-4 lg:px-6 mb-18">
<h1 className="text-xl md:text-2xl lg:text-3xl font-bold mb-4 md:mb-6 pt-3">{t("cart")}</h1> <h1 className="text-xl md:text-2xl lg:text-3xl font-bold mb-4 md:mb-6 pt-3">
{t("cart")}
</h1>
<div className="flex flex-col md:flex-row gap-6"> <div className="flex flex-col md:flex-row gap-6">
<div className="flex-1"> <div className="flex-1">
@@ -234,4 +223,4 @@ export default function CartPage() {
</div> </div>
</div> </div>
); );
} }

View File

@@ -22,7 +22,6 @@ export async function generateMetadata({ params }: Props): Promise<Metadata> {
} }
export async function generateStaticParams() { export async function generateStaticParams() {
// Generate static params for popular categories
const categories = ["electronics", "clothing", "home-garden"] const categories = ["electronics", "clothing", "home-garden"]
return categories.map((slug) => ({ slug })) return categories.map((slug) => ({ slug }))
} }

View File

@@ -22,7 +22,6 @@ export async function generateMetadata({ params }: Props): Promise<Metadata> {
} }
export async function generateStaticParams() { export async function generateStaticParams() {
// Generate static params for popular collections
const collections = ["new-arrivals", "best-sellers", "featured"] const collections = ["new-arrivals", "best-sellers", "featured"]
return collections.map((slug) => ({ slug })) return collections.map((slug) => ({ slug }))
} }

View File

@@ -15,6 +15,7 @@ import {
} from "@/components/ui/card"; } from "@/components/ui/card";
import { useOpenStore } from "@/lib/hooks"; import { useOpenStore } from "@/lib/hooks";
import { toast } from "sonner"; import { toast } from "sonner";
import { useTranslations } from "next-intl";
interface OpenStorePageProps { interface OpenStorePageProps {
locale?: string; locale?: string;
@@ -68,56 +69,39 @@ export default function OpenStorePage({
const [fileName, setFileName] = useState(""); const [fileName, setFileName] = useState("");
const { mutate: submitOpenStore, isPending: loading } = useOpenStore(); const { mutate: submitOpenStore, isPending: loading } = useOpenStore();
const t = translations || { const t = useTranslations();
title: "Форма подачи заявления на открытие магазина",
firstName: "Имя",
lastName: "Фамилия",
email: "Email",
phone: "Телефон",
uploadPatent: "Загрузите патент на розничную торговлю (PDF, JPG)",
submit: "Отправить",
selectedFile: "Выбранный файл",
firstNameRequired: "Имя обязательно",
lastNameRequired: "Фамилия обязательна",
emailInvalid: "Некорректный email",
phoneInvalid: "Некорректный номер телефона",
fileRequired: "Патент обязателен",
fileSizeError: "Файл слишком большой (макс. 25MB)",
fileTypeError: "Только PDF и JPG документы",
};
const validateForm = (): boolean => { const validateForm = (): boolean => {
const newErrors: FormErrors = {}; const newErrors: FormErrors = {};
if (!formData.firstName.trim()) { if (!formData.firstName.trim()) {
newErrors.firstName = t.firstNameRequired; newErrors.firstName = t("firstNameRequired");
} }
if (!formData.lastName.trim()) { if (!formData.lastName.trim()) {
newErrors.lastName = t.lastNameRequired; newErrors.lastName = t("lastNameRequired");
} }
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(formData.email)) { if (!emailRegex.test(formData.email)) {
newErrors.email = t.emailInvalid; newErrors.email = t("emailInvalid");
} }
const phoneRegex = /^\+?[0-9]{6,15}$/; const phoneRegex = /^\+?[0-9]{6,15}$/;
if (!phoneRegex.test(formData.phone)) { if (!phoneRegex.test(formData.phone)) {
newErrors.phone = t.phoneInvalid; newErrors.phone = t("phoneInvalid");
} }
if (!formData.file) { if (!formData.file) {
newErrors.file = t.fileRequired; newErrors.file = t("fileRequired");
} else { } else {
const allowedTypes = ["image/jpeg", "image/jpg", "application/pdf"]; const allowedTypes = ["image/jpeg", "image/jpg", "application/pdf"];
if (!allowedTypes.includes(formData.file.type)) { if (!allowedTypes.includes(formData.file.type)) {
newErrors.file = t.fileTypeError; newErrors.file = t("fileTypeError");
} }
if (formData.file.size > 25 * 1024 * 1024) { if (formData.file.size > 25 * 1024 * 1024) {
newErrors.file = t.fileSizeError; newErrors.file = t("fileSizeError");
} }
} }
@@ -160,9 +144,8 @@ export default function OpenStorePage({
}, },
{ {
onSuccess: () => { onSuccess: () => {
toast.success("Your store request has been submitted successfully"); toast.success(t("submit_success"));
setFormData({ setFormData({
firstName: "", firstName: "",
lastName: "", lastName: "",
@@ -173,7 +156,7 @@ export default function OpenStorePage({
setFileName(""); setFileName("");
}, },
onError: (error: any) => { onError: (error: any) => {
toast.error(error?.message || "Failed to submit store request"); toast.error(error?.message || t("submit_error"));
}, },
} }
); );
@@ -184,7 +167,7 @@ export default function OpenStorePage({
<div className=" bg-gray-50 flex items-center justify-center p-4"> <div className=" bg-gray-50 flex items-center justify-center p-4">
<Card className="w-full max-w-md shadow-lg"> <Card className="w-full max-w-md shadow-lg">
<CardHeader> <CardHeader>
<CardTitle className="text-2xl text-center">{t.title}</CardTitle> <CardTitle className="text-2xl text-center">{t("title")}</CardTitle>
<CardDescription className="text-center"> <CardDescription className="text-center">
Заполните форму для подачи заявления Заполните форму для подачи заявления
</CardDescription> </CardDescription>
@@ -193,7 +176,7 @@ export default function OpenStorePage({
<form onSubmit={handleSubmit} className="space-y-4"> <form onSubmit={handleSubmit} className="space-y-4">
{/* First Name */} {/* First Name */}
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="firstName">{t.firstName}</Label> <Label htmlFor="firstName">{t("firstName")}</Label>
<Input <Input
id="firstName" id="firstName"
name="firstName" name="firstName"
@@ -208,7 +191,7 @@ export default function OpenStorePage({
{/* Last Name */} {/* Last Name */}
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="lastName">{t.lastName}</Label> <Label htmlFor="lastName">{t("lastName")}</Label>
<Input <Input
id="lastName" id="lastName"
name="lastName" name="lastName"
@@ -223,7 +206,7 @@ export default function OpenStorePage({
{/* Email */} {/* Email */}
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="email">{t.email}</Label> <Label htmlFor="email">{t("email")}</Label>
<Input <Input
id="email" id="email"
name="email" name="email"
@@ -239,7 +222,7 @@ export default function OpenStorePage({
{/* Phone */} {/* Phone */}
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="phone">{t.phone}</Label> <Label htmlFor="phone">{t("phone")}</Label>
<Input <Input
id="phone" id="phone"
name="phone" name="phone"
@@ -255,7 +238,7 @@ export default function OpenStorePage({
{/* File Upload */} {/* File Upload */}
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="file">{t.uploadPatent}</Label> <Label htmlFor="file">{t("uploadPatent")}</Label>
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<Input <Input
id="file" id="file"
@@ -267,15 +250,15 @@ export default function OpenStorePage({
<Button <Button
type="button" type="button"
variant="outline" variant="outline"
className="w-full bg-transparent" className="w-full bg-transparent cursor-pointer"
onClick={() => document.getElementById("file")?.click()} onClick={() => document.getElementById("file")?.click()}
> >
<Upload className="mr-2 h-4 w-4" /> <Upload className="mr-2 h-4 w-4" />
{t.uploadPatent} {t("uploadPatent")}
</Button> </Button>
{fileName && ( {fileName && (
<p className="text-sm text-gray-600"> <p className="text-sm text-gray-600">
{t.selectedFile}: {fileName} {t("selectedFile")}: {fileName}
</p> </p>
)} )}
{errors.file && ( {errors.file && (
@@ -290,7 +273,7 @@ export default function OpenStorePage({
className="w-full cursor-pointer bg-[#005bff] hover:bg-[#0041c4]" className="w-full cursor-pointer bg-[#005bff] hover:bg-[#0041c4]"
disabled={loading} disabled={loading}
> >
{loading ? "Загрузка..." : t.submit} {loading ? "Загрузка..." : t("submit")}
</Button> </Button>
</form> </form>
</CardContent> </CardContent>

View File

@@ -23,7 +23,6 @@ export async function generateMetadata({ params }: Props): Promise<Metadata> {
} }
export async function generateStaticParams() { export async function generateStaticParams() {
// Generate static params for popular products
return [{ slug: "nike-air-max" }, { slug: "adidas-ultraboost" }]; return [{ slug: "nike-air-max" }, { slug: "adidas-ultraboost" }];
} }

View File

@@ -3,7 +3,7 @@
import { useState, useEffect, useCallback } from "react"; import { useState, useEffect, useCallback } from "react";
import Link from "next/link"; import Link from "next/link";
import Image from "next/image"; import Image from "next/image";
import { X, Search, Store, User as UserIcon } from "lucide-react"; import { X, Search, User as UserIcon } from "lucide-react";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import Logo from "@/public/logo.webp"; import Logo from "@/public/logo.webp";
import CategoryMenu from "./ui/CategoryMenu"; import CategoryMenu from "./ui/CategoryMenu";
@@ -70,14 +70,14 @@ export default function Header({ locale = "ru" }: HeaderProps) {
<Button <Button
onClick={toggleCategoryMenu} onClick={toggleCategoryMenu}
className="hidden gap-2 rounded-lg font-bold lg:flex hover:bg-[#005bff] bg-[#005bff] text-white" className="cursor-pointer hidden gap-2 rounded-lg font-bold lg:flex hover:bg-[#005bff] bg-[#005bff] text-white"
size="lg" size="lg"
> >
{isCategoryOpen ? <X className="h-5 w-5" /> : <CategoryIcon />} {isCategoryOpen ? <X className="h-5 w-5" /> : <CategoryIcon />}
{t("common.catalog")} {t("common.catalog")}
</Button> </Button>
<div className="flex items-center gap-2 sm:hidden"> <div className="flex items-center gap-2 sm:hidden cursor-pointer">
<Button <Button
variant="ghost" variant="ghost"
size="icon" size="icon"

View File

@@ -46,7 +46,6 @@ export default function MobileBottomNav({
const { data: categories = [] } = useCategories(); const { data: categories = [] } = useCategories();
// OPTIMIZED: Use event-driven cart count instead of full cart data
const cartCount = useCartCount(); const cartCount = useCartCount();
const { data: favoritesData } = useFavorites(); const { data: favoritesData } = useFavorites();
@@ -61,12 +60,6 @@ export default function MobileBottomNav({
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
console.log("[MobileBottomNav] Profile clicked", {
authLoading,
isAuthenticated,
hasOnLoginClick: !!onLoginClick,
});
if (authLoading) { if (authLoading) {
return; return;
} }
@@ -75,10 +68,8 @@ export default function MobileBottomNav({
router.push(`/${locale}/me`); router.push(`/${locale}/me`);
} else { } else {
if (onLoginClick) { if (onLoginClick) {
console.log("[MobileBottomNav] Calling parent onLoginClick");
onLoginClick(); onLoginClick();
} else { } else {
console.log("[MobileBottomNav] Using local login dialog");
setIsLoginOpen(true); setIsLoginOpen(true);
} }
} }
@@ -86,7 +77,6 @@ export default function MobileBottomNav({
const handleNavigation = (path: string) => (e: React.MouseEvent) => { const handleNavigation = (path: string) => (e: React.MouseEvent) => {
e.preventDefault(); e.preventDefault();
console.log("[MobileBottomNav] Navigating to:", path);
router.push(path); router.push(path);
}; };
@@ -103,7 +93,6 @@ export default function MobileBottomNav({
size="sm" size="sm"
className="flex-col gap-0.5 h-auto px-2 py-2" className="flex-col gap-0.5 h-auto px-2 py-2"
onClick={() => { onClick={() => {
console.log("[MobileBottomNav] Catalog clicked");
setIsCategoryOpen(true); setIsCategoryOpen(true);
}} }}
> >

View File

@@ -122,7 +122,7 @@ const cartCount = useCartCount()
<Button <Button
variant="ghost" variant="ghost"
size="sm" size="sm"
className="flex-col gap-0.5 h-auto px-2 py-2" className="flex-col cursor-pointer gap-0.5 h-auto px-2 py-2"
> >
<ProfileIcon /> <ProfileIcon />
<span className="text-xs text-gray-700">{t("profile")}</span> <span className="text-xs text-gray-700">{t("profile")}</span>
@@ -143,7 +143,7 @@ const cartCount = useCartCount()
<Button <Button
variant="ghost" variant="ghost"
size="sm" size="sm"
className="flex-col gap-0.5 h-auto px-2 py-2" className="flex-col cursor-pointer gap-0.5 h-auto px-2 py-2"
onClick={onAuthClick} onClick={onAuthClick}
> >
<ProfileIcon /> <ProfileIcon />

View File

@@ -140,7 +140,7 @@ export default function AuthDialog({ isOpen, onClose }: AuthDialogProps) {
<Button <Button
onClick={otpSent ? handleLogin : handleSendOtp} onClick={otpSent ? handleLogin : handleSendOtp}
className="w-full h-12 rounded-xl font-bold text-base bg-[#005bff] hover:bg-[#0041c4]" className="w-full cursor-pointer h-12 rounded-xl font-bold text-base bg-[#005bff] hover:bg-[#0041c4]"
size="lg" size="lg"
disabled={isLoginLoading || isVerifyLoading} disabled={isLoginLoading || isVerifyLoading}
> >

View File

@@ -92,9 +92,9 @@ export default function SearchBar({
<button <button
key={product.id} key={product.id}
onClick={() => handleProductClick(product.id)} onClick={() => handleProductClick(product.id)}
className="w-full flex items-center gap-3 p-3 hover:bg-gray-50 transition-colors border-b last:border-b-0" className="w-full cursor-pointer flex items-center gap-3 p-3 hover:bg-gray-50 transition-colors border-b last:border-b-0"
> >
<div className="relative w-16 h-16 flex-shrink-0"> <div className="relative w-16 h-16 shrink-0">
<Image <Image
src={product.thumbnail} src={product.thumbnail}
alt={product.name} alt={product.name}
@@ -157,7 +157,7 @@ export default function SearchBar({
</div> </div>
<Button <Button
size="icon" size="icon"
className="h-auto hover:bg-[#005bff] cursor-pointer bg-transparent flex items-center mr-1.5 text-white" className="h-auto hover:bg-[#005bff] cursor-pointer bg-transparent flex items-center mr-1.5 text-white"
> >
<SearchIcon /> <SearchIcon />
</Button> </Button>

View File

@@ -349,7 +349,7 @@ export default function CartItemCard({ item, onUpdate }: CartItemCardProps) {
size="sm" size="sm"
onClick={handleDelete} onClick={handleDelete}
disabled={isRemoving} disabled={isRemoving}
className="w-fit p-0 h-auto hover:bg-transparent hover:text-red-500" className="w-fit cursor-pointer p-0 h-auto hover:bg-transparent hover:text-red-500"
> >
<Trash2 className="h-5 w-5" /> <Trash2 className="h-5 w-5" />
</Button> </Button>
@@ -387,7 +387,7 @@ export default function CartItemCard({ item, onUpdate }: CartItemCardProps) {
variant="outline" variant="outline"
size="icon" size="icon"
onClick={handleQuantityDecrease} onClick={handleQuantityDecrease}
className={` cursor-pointerrounded-xl bg-blue-50 ${ className={` cursor-pointer rounded-xl bg-blue-50 ${
isSyncing ? "opacity-70" : "" isSyncing ? "opacity-70" : ""
}`} }`}
> >
@@ -449,7 +449,7 @@ export default function CartItemCard({ item, onUpdate }: CartItemCardProps) {
<div className="flex justify-center mt-4"> <div className="flex justify-center mt-4">
<Button <Button
onClick={() => setShowStockModal(false)} onClick={() => setShowStockModal(false)}
className="w-full rounded-xl" className="w-full rounded-lg cursor-pointer"
> >
{t("understood")} {t("understood")}
</Button> </Button>

View File

@@ -8,7 +8,7 @@ export default function EmptyCart() {
const router=useRouter(); const router=useRouter();
return ( return (
<div className="flex min-h-[60vh] items-center justify-center px-4"> <div className="flex min-h-[60vh] items-center justify-center px-4">
<div className="w-full max-w-md rounded-2xl bg-gradient-to-br from-blue-50 to-white p-8 text-center shadow-lg"> <div className="w-full max-w-md rounded-2xl bg-linear-to-br from-blue-50 to-white p-8 text-center shadow-lg">
<div className="mx-auto mb-6 flex h-20 w-20 items-center justify-center rounded-full bg-blue-100"> <div className="mx-auto mb-6 flex h-20 w-20 items-center justify-center rounded-full bg-blue-100">
<ShoppingCart className="h-10 w-10 text-blue-600" /> <ShoppingCart className="h-10 w-10 text-blue-600" />
</div> </div>
@@ -21,7 +21,7 @@ export default function EmptyCart() {
{t("cart_empty_message")} {t("cart_empty_message")}
</p> </p>
<Button onClick={()=>router.push("/")} className="w-full rounded-xl bg-blue-600 px-6 py-3 text-sm font-medium text-white transition hover:bg-blue-700 active:scale-95"> <Button onClick={()=>router.push("/")} className="w-full cursor-pointer rounded-xl bg-blue-600 px-6 py-3 text-sm font-medium text-white transition hover:bg-blue-700 active:scale-95">
{t("start_shopping")} {t("start_shopping")}
</Button> </Button>
</div> </div>

View File

@@ -265,7 +265,7 @@ export default function OrderSummary({
<Button <Button
onClick={onCompleteOrder} onClick={onCompleteOrder}
disabled={!isFormValid || isLoading} disabled={!isFormValid || isLoading}
className="w-full rounded-xl bg-[#005bff] hover:bg-[#004dcc] h-12 text-lg font-bold disabled:opacity-50" className="w-full rounded-lg cursor-pointer bg-[#005bff] hover:bg-[#004dcc] h-12 text-lg font-bold disabled:opacity-50"
size="lg" size="lg"
> >
{isLoading ? `${t("order")}...` : t("order")} {isLoading ? `${t("order")}...` : t("order")}

View File

@@ -14,30 +14,20 @@ interface CartResponse {
errorDetails?: string; errorDetails?: string;
} }
// DEBUG: Enable detailed logging const pendingUpdates = new Map<number, number>();
const DEBUG = true;
const log = (...args: any[]) => {
if (DEBUG) console.log('[useCart]', ...args);
};
// CRITICAL: Single source of truth for pending updates
const pendingUpdates = new Map<number, number>(); // productId -> quantity
let updateLock = false; let updateLock = false;
class CartEventEmitter { class CartEventEmitter {
private listeners: Set<() => void> = new Set(); private listeners: Set<() => void> = new Set();
subscribe(callback: () => void) { subscribe(callback: () => void) {
log('🔔 New subscriber added. Total:', this.listeners.size + 1);
this.listeners.add(callback); this.listeners.add(callback);
return () => { return () => {
log('🔕 Subscriber removed. Total:', this.listeners.size - 1);
this.listeners.delete(callback); this.listeners.delete(callback);
}; };
} }
emit() { emit() {
log('📢 Emitting cart event to', this.listeners.size, 'listeners');
this.listeners.forEach((cb) => cb()); this.listeners.forEach((cb) => cb());
} }
} }
@@ -76,24 +66,13 @@ function transformCartResponse(response: any): CartResponse {
export function useCart(options?: Partial<UseQueryOptions<CartResponse>>) { export function useCart(options?: Partial<UseQueryOptions<CartResponse>>) {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
log('🎣 useCart hook called with options:', options);
const query = useQuery({ const query = useQuery({
queryKey: ["cart"], queryKey: ["cart"],
queryFn: async () => { queryFn: async () => {
log('🌐 Fetching cart from API...');
const response = await apiClient.get("/carts"); const response = await apiClient.get("/carts");
const transformed = transformCartResponse(response.data); const transformed = transformCartResponse(response.data);
log('✅ Cart fetched:', {
itemCount: transformed.data.length,
items: transformed.data.map(item => ({
productId: item.product?.id,
quantity: item.product_quantity
}))
});
return transformed; return transformed;
}, },
// CRITICAL FIX: Merge options AFTER defaults
refetchOnMount: false, refetchOnMount: false,
refetchOnWindowFocus: false, refetchOnWindowFocus: false,
refetchOnReconnect: true, refetchOnReconnect: true,
@@ -101,27 +80,17 @@ export function useCart(options?: Partial<UseQueryOptions<CartResponse>>) {
gcTime: 1000 * 60 * 5, gcTime: 1000 * 60 * 5,
retry: 2, retry: 2,
retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 10000), retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 10000),
// User options OVERRIDE defaults
...options, ...options,
}); });
log('🔧 Query config after merge:', {
refetchOnMount: query.refetch !== undefined,
staleTime: query.isStale,
dataUpdatedAt: query.dataUpdatedAt
});
useEffect(() => { useEffect(() => {
log('🔗 Setting up cart events listener in useCart');
const unsubscribe = cartEvents.subscribe(() => { const unsubscribe = cartEvents.subscribe(() => {
log('📥 Cart event received in useCart, invalidating query');
queryClient.invalidateQueries({ queryClient.invalidateQueries({
queryKey: ["cart"], queryKey: ["cart"],
refetchType: "none", refetchType: "none",
}); });
}); });
return () => { return () => {
log('🔌 Cleaning up cart events listener in useCart');
unsubscribe(); unsubscribe();
}; };
}, [queryClient]); }, [queryClient]);
@@ -140,7 +109,6 @@ export function useAddToCart() {
productId: number; productId: number;
quantity?: number; quantity?: number;
}) => { }) => {
log(' AddToCart mutation:', { productId, quantity });
const params = new URLSearchParams({ const params = new URLSearchParams({
product_id: String(productId), product_id: String(productId),
product_quantity: String(quantity), product_quantity: String(quantity),
@@ -167,17 +135,14 @@ export function useAddToCart() {
return { message: "success", data: "Added to cart" }; return { message: "success", data: "Added to cart" };
}, },
onMutate: async ({ productId, quantity }) => { onMutate: async ({ productId, quantity }) => {
log('🔒 AddToCart onMutate - Waiting for lock...');
while (updateLock) { while (updateLock) {
await new Promise(resolve => setTimeout(resolve, 50)); await new Promise((resolve) => setTimeout(resolve, 50));
} }
updateLock = true; updateLock = true;
log('🔓 Lock acquired');
await queryClient.cancelQueries({ queryKey: ["cart"] }); await queryClient.cancelQueries({ queryKey: ["cart"] });
const previousCart = queryClient.getQueryData<CartResponse>(["cart"]); const previousCart = queryClient.getQueryData<CartResponse>(["cart"]);
log('📸 Previous cart state:', previousCart?.data.length, 'items');
queryClient.setQueryData<CartResponse>(["cart"], (old) => { queryClient.setQueryData<CartResponse>(["cart"], (old) => {
if (!old) return old; if (!old) return old;
@@ -224,10 +189,8 @@ export function useAddToCart() {
); );
if (finalItem) { if (finalItem) {
pendingUpdates.set(productId, finalItem.product_quantity); pendingUpdates.set(productId, finalItem.product_quantity);
log('💾 Pending update saved:', productId, '→', finalItem.product_quantity);
} }
log('🔄 Cart updated optimistically:', updated.data.length, 'items');
return updated; return updated;
}); });
@@ -237,7 +200,6 @@ export function useAddToCart() {
return { previousCart }; return { previousCart };
}, },
onError: (error, variables, context) => { onError: (error, variables, context) => {
log('❌ AddToCart error:', error);
if (context?.previousCart) { if (context?.previousCart) {
queryClient.setQueryData(["cart"], context.previousCart); queryClient.setQueryData(["cart"], context.previousCart);
pendingUpdates.delete(variables.productId); pendingUpdates.delete(variables.productId);
@@ -245,7 +207,6 @@ export function useAddToCart() {
} }
}, },
onSuccess: (data, variables) => { onSuccess: (data, variables) => {
log('✅ AddToCart success');
pendingUpdates.delete(variables.productId); pendingUpdates.delete(variables.productId);
queryClient.invalidateQueries({ queryClient.invalidateQueries({
queryKey: ["cart"], queryKey: ["cart"],
@@ -260,7 +221,6 @@ export function useRemoveFromCart() {
return useMutation({ return useMutation({
mutationFn: async (productId: number) => { mutationFn: async (productId: number) => {
log('🗑️ RemoveFromCart mutation:', productId);
const params = new URLSearchParams({ product_id: String(productId) }); const params = new URLSearchParams({ product_id: String(productId) });
const response = await apiClient.patch("/carts", params.toString(), { const response = await apiClient.patch("/carts", params.toString(), {
@@ -286,7 +246,7 @@ export function useRemoveFromCart() {
}, },
onMutate: async (productId) => { onMutate: async (productId) => {
while (updateLock) { while (updateLock) {
await new Promise(resolve => setTimeout(resolve, 50)); await new Promise((resolve) => setTimeout(resolve, 50));
} }
updateLock = true; updateLock = true;
@@ -317,7 +277,6 @@ export function useRemoveFromCart() {
); );
pendingUpdates.delete(productId); pendingUpdates.delete(productId);
log('🗑️ Item removed optimistically:', productId);
return updated; return updated;
}); });
@@ -327,14 +286,12 @@ export function useRemoveFromCart() {
return { previousCart }; return { previousCart };
}, },
onError: (error, variables, context) => { onError: (error, variables, context) => {
log('❌ RemoveFromCart error:', error);
if (context?.previousCart) { if (context?.previousCart) {
queryClient.setQueryData(["cart"], context.previousCart); queryClient.setQueryData(["cart"], context.previousCart);
cartEvents.emit(); cartEvents.emit();
} }
}, },
onSuccess: () => { onSuccess: () => {
log('✅ RemoveFromCart success');
queryClient.invalidateQueries({ queryClient.invalidateQueries({
queryKey: ["cart"], queryKey: ["cart"],
refetchType: "active", refetchType: "active",
@@ -348,7 +305,6 @@ export function useCleanCart() {
return useMutation({ return useMutation({
mutationFn: async () => { mutationFn: async () => {
log('🧹 CleanCart mutation');
const response = await apiClient.delete("/carts", { const response = await apiClient.delete("/carts", {
headers: { headers: {
"Content-Type": "application/x-www-form-urlencoded", "Content-Type": "application/x-www-form-urlencoded",
@@ -372,7 +328,7 @@ export function useCleanCart() {
}, },
onMutate: async () => { onMutate: async () => {
while (updateLock) { while (updateLock) {
await new Promise(resolve => setTimeout(resolve, 50)); await new Promise((resolve) => setTimeout(resolve, 50));
} }
updateLock = true; updateLock = true;
@@ -414,7 +370,6 @@ export function useUpdateCartItemQuantity() {
productId: number; productId: number;
quantity: number; quantity: number;
}) => { }) => {
log('🔄 UpdateQuantity mutation:', { productId, quantity });
const params = new URLSearchParams({ const params = new URLSearchParams({
product_id: String(productId), product_id: String(productId),
product_quantity: String(quantity), product_quantity: String(quantity),
@@ -442,17 +397,14 @@ export function useUpdateCartItemQuantity() {
return { message: "success", data: "Updated cart" }; return { message: "success", data: "Updated cart" };
}, },
onMutate: async ({ productId, quantity }) => { onMutate: async ({ productId, quantity }) => {
log('🔒 UpdateQuantity onMutate - Waiting for lock...');
while (updateLock) { while (updateLock) {
await new Promise(resolve => setTimeout(resolve, 50)); await new Promise((resolve) => setTimeout(resolve, 50));
} }
updateLock = true; updateLock = true;
log('🔓 Lock acquired');
await queryClient.cancelQueries({ queryKey: ["cart"] }); await queryClient.cancelQueries({ queryKey: ["cart"] });
const previousCart = queryClient.getQueryData<CartResponse>(["cart"]); const previousCart = queryClient.getQueryData<CartResponse>(["cart"]);
log('📸 Previous cart state:', previousCart?.data.length, 'items');
queryClient.setQueryData<CartResponse>(["cart"], (old) => { queryClient.setQueryData<CartResponse>(["cart"], (old) => {
if (!old) return old; if (!old) return old;
@@ -478,9 +430,7 @@ export function useUpdateCartItemQuantity() {
); );
pendingUpdates.set(productId, quantity); pendingUpdates.set(productId, quantity);
log('💾 Pending update saved:', productId, '→', quantity);
log('🔄 Cart updated optimistically:', updated.data.length, 'items');
return updated; return updated;
}); });
@@ -490,7 +440,6 @@ export function useUpdateCartItemQuantity() {
return { previousCart }; return { previousCart };
}, },
onError: (error, variables, context) => { onError: (error, variables, context) => {
log('❌ UpdateQuantity error:', error);
if (context?.previousCart) { if (context?.previousCart) {
queryClient.setQueryData(["cart"], context.previousCart); queryClient.setQueryData(["cart"], context.previousCart);
pendingUpdates.delete(variables.productId); pendingUpdates.delete(variables.productId);
@@ -499,7 +448,6 @@ export function useUpdateCartItemQuantity() {
throw error; throw error;
}, },
onSuccess: (data, variables) => { onSuccess: (data, variables) => {
log('✅ UpdateQuantity success');
pendingUpdates.delete(variables.productId); pendingUpdates.delete(variables.productId);
queryClient.invalidateQueries({ queryClient.invalidateQueries({
queryKey: ["cart"], queryKey: ["cart"],
@@ -553,4 +501,4 @@ export function useCartCount() {
0 0
) || 0 ) || 0
); );
} }

View File

@@ -108,7 +108,7 @@ export default function CategoryFilters({
}} }}
/> />
<Button variant="outline" className="w-full rounded-xl" onClick={onReset}> <Button variant="outline" className="w-full rounded-lg cursor-pointer" onClick={onReset}>
{translations.reset} {translations.reset}
</Button> </Button>
</div> </div>

View File

@@ -28,7 +28,7 @@ export default function CategoryFiltersSheet({
<Sheet open={isOpen} onOpenChange={onOpenChange}> <Sheet open={isOpen} onOpenChange={onOpenChange}>
<SheetTrigger asChild> <SheetTrigger asChild>
<Button <Button
className="bg-[#005bff] hover:bg-[#0041c4] sm:hidden fixed bottom-20 right-4 rounded-xl font-bold gap-2 z-10 shadow-lg" className="bg-[#005bff] hover:bg-[#0041c4] sm:hidden fixed bottom-20 right-4 rounded-lg cursor-pointer font-bold gap-2 z-10 shadow-lg"
size="lg" size="lg"
> >
{filterLabel} {filterLabel}
@@ -40,7 +40,7 @@ export default function CategoryFiltersSheet({
<SheetTitle>{filterLabel}</SheetTitle> <SheetTitle>{filterLabel}</SheetTitle>
<button <button
onClick={() => onOpenChange(false)} onClick={() => onOpenChange(false)}
className="absolute top-4 right-4 rounded-md ring-offset-background transition-opacity hover:opacity-100" className="absolute top-4 right-4 rounded-md cursor-pointer ring-offset-background transition-opacity hover:opacity-100"
> >
<X className="h-4 w-4" /> <X className="h-4 w-4" />
<span className="sr-only">{closeLabel}</span> <span className="sr-only">{closeLabel}</span>

View File

@@ -106,22 +106,19 @@ export default function CategoryPageClient({
} }
}, [selectedCategory?.id]); }, [selectedCategory?.id]);
// Update products list - BU KISIM ÖNEMLİ! // Update products list
useEffect(() => { useEffect(() => {
if (productsData?.data) { if (productsData?.data) {
setAllProducts((prev) => { setAllProducts((prev) => {
// İlk sayfa ise direkt replace et
if (currentPage === 1) { if (currentPage === 1) {
return productsData.data; return productsData.data;
} }
// Sonraki sayfalar için deduplicate et
const existingIds = new Set(prev.map((p) => p.id)); const existingIds = new Set(prev.map((p) => p.id));
const newProducts = productsData.data.filter( const newProducts = productsData.data.filter(
(p: Product) => !existingIds.has(p.id) (p: Product) => !existingIds.has(p.id)
); );
// Eğer yeni ürün yoksa, return prev (gereksiz re-render önlenir)
if (newProducts.length === 0) { if (newProducts.length === 0) {
return prev; return prev;
} }
@@ -129,16 +126,13 @@ export default function CategoryPageClient({
return [...prev, ...newProducts]; return [...prev, ...newProducts];
}); });
} }
}, [productsData?.data, currentPage]); // productsData yerine productsData.data }, [productsData?.data, currentPage]);
// hasMore hesaplama - BU KISIM DA ÖNEMLİ!
const hasMore = useMemo(() => { const hasMore = useMemo(() => {
if (!productsData?.pagination) return false; if (!productsData?.pagination) return false;
// pagination.next_page_url varsa devam et
if (productsData.pagination.next_page_url) return true; if (productsData.pagination.next_page_url) return true;
// Alternatif olarak: current_page < last_page kontrolü
if ( if (
productsData.pagination.current_page && productsData.pagination.current_page &&
productsData.pagination.last_page productsData.pagination.last_page
@@ -148,7 +142,6 @@ export default function CategoryPageClient({
); );
} }
// Alternatif 2: hasMorePages flag'i varsa
if (productsData.pagination.hasMorePages !== undefined) { if (productsData.pagination.hasMorePages !== undefined) {
return productsData.pagination.hasMorePages; return productsData.pagination.hasMorePages;
} }
@@ -158,7 +151,6 @@ export default function CategoryPageClient({
const loadMoreData = useCallback(() => { const loadMoreData = useCallback(() => {
if (!hasMore || isFetching) return; if (!hasMore || isFetching) return;
console.log("Loading page:", currentPage + 1); // Debug için
setCurrentPage((prev) => prev + 1); setCurrentPage((prev) => prev + 1);
}, [hasMore, isFetching, currentPage]); }, [hasMore, isFetching, currentPage]);

View File

@@ -6,7 +6,7 @@ interface CategoryProductsGridProps {
products: Product[]; products: Product[];
hasMore: boolean; hasMore: boolean;
onLoadMore: () => void; onLoadMore: () => void;
isFetching?: boolean; // Yeni prop - loading durumu için isFetching?: boolean;
translations: { translations: {
loading: string; loading: string;
no_results: string; no_results: string;
@@ -46,7 +46,6 @@ export default function CategoryProductsGrid({
endMessage={ endMessage={
products.length > 0 && !hasMore ? ( products.length > 0 && !hasMore ? (
<div className="text-center py-4 text-gray-500 text-sm"> <div className="text-center py-4 text-gray-500 text-sm">
{/* Opsiyonel: "Tüm ürünler yüklendi" mesajı */}
</div> </div>
) : null ) : null
} }
@@ -68,7 +67,6 @@ export default function CategoryProductsGrid({
))} ))}
</div> </div>
{/* İlk yükleme için skeleton göster */}
{isFetching && products.length === 0 && ( {isFetching && products.length === 0 && (
<div className="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-4 gap-3 mt-3"> <div className="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-4 gap-3 mt-3">
{Array.from({ length: 6 }).map((_, i) => ( {Array.from({ length: 6 }).map((_, i) => (

View File

@@ -107,7 +107,7 @@ export default function CollectionFilters({
}} }}
/> />
<Button variant="outline" className="w-full rounded-xl" onClick={onReset}> <Button variant="outline" className="w-full rounded-lg cursor-pointer" onClick={onReset}>
{translations.reset} {translations.reset}
</Button> </Button>
</div> </div>

View File

@@ -28,7 +28,7 @@ export default function CollectionFiltersSheet({
<Sheet open={isOpen} onOpenChange={onOpenChange}> <Sheet open={isOpen} onOpenChange={onOpenChange}>
<SheetTrigger asChild> <SheetTrigger asChild>
<Button <Button
className="bg-[#005bff] hover:bg-[#0041c4] sm:hidden fixed bottom-20 right-4 rounded-xl font-bold gap-2 z-10 shadow-lg" className="bg-[#005bff] hover:bg-[#0041c4] sm:hidden fixed bottom-20 right-4 rounded-lg cursor-pointer font-bold gap-2 z-10 shadow-lg"
size="lg" size="lg"
> >
{filterLabel} {filterLabel}
@@ -40,7 +40,7 @@ export default function CollectionFiltersSheet({
<SheetTitle>{filterLabel}</SheetTitle> <SheetTitle>{filterLabel}</SheetTitle>
<button <button
onClick={() => onOpenChange(false)} onClick={() => onOpenChange(false)}
className="absolute top-4 right-4 rounded-md ring-offset-background transition-opacity hover:opacity-100" className="absolute top-4 right-4 rounded-md cursor-pointer ring-offset-background transition-opacity hover:opacity-100"
> >
<X className="h-4 w-4" /> <X className="h-4 w-4" />
<span className="sr-only">{closeLabel}</span> <span className="sr-only">{closeLabel}</span>

View File

@@ -8,7 +8,7 @@ export default function EmptyFavorites() {
const router=useRouter(); const router=useRouter();
return ( return (
<div className="flex min-h-[60vh] items-center justify-center px-4"> <div className="flex min-h-[60vh] items-center justify-center px-4">
<div className="w-full max-w-md rounded-2xl bg-gradient-to-br from-blue-50 to-white p-8 text-center shadow-lg"> <div className="w-full max-w-md rounded-2xl bg-linear-to-br from-blue-50 to-white p-8 text-center shadow-lg">
<div className="mx-auto mb-6 flex h-20 w-20 items-center justify-center rounded-full bg-blue-100"> <div className="mx-auto mb-6 flex h-20 w-20 items-center justify-center rounded-full bg-blue-100">
<Heart className="h-10 w-10 text-blue-600" /> <Heart className="h-10 w-10 text-blue-600" />
</div> </div>
@@ -21,7 +21,7 @@ export default function EmptyFavorites() {
{t("favorites_empty_message")} {t("favorites_empty_message")}
</p> </p>
<Button onClick={()=>router.push("/")} className="w-full rounded-xl bg-blue-600 px-6 py-3 text-sm font-medium text-white transition hover:bg-blue-700 active:scale-95"> <Button onClick={()=>router.push("/")} className="w-full rounded-lg cursor-pointer bg-blue-600 px-6 py-3 text-sm font-medium text-white transition hover:bg-blue-700 active:scale-95">
{t("start_shopping")} {t("start_shopping")}
</Button> </Button>
</div> </div>

View File

@@ -29,7 +29,6 @@ export default function HomePage() {
isError: collectionsError, isError: collectionsError,
} = useCollections(); } = useCollections();
// Prefetch favorites
useFavorites(); useFavorites();
const loadMore = () => { const loadMore = () => {

View File

@@ -169,7 +169,7 @@ export default function ProductCard({
{ {
onSuccess: (data) => onSuccess: (data) =>
toast.success( toast.success(
data.wasAdded ? "Added to favorites" : "Removed from favorites" data.wasAdded ? t("added_to_favorites") : t("removed_from_favorites")
), ),
onError: () => toast.error("Error. Try again"), onError: () => toast.error("Error. Try again"),
} }
@@ -196,12 +196,12 @@ export default function ProductCard({
quantity: localQuantity, quantity: localQuantity,
}); });
await refetchCart(); await refetchCart();
toast.success("Added to cart", { toast.success(t("added_to_cart"), {
description: `${name} has been added to your cart`, description: `${name} ${t("added_to_cart_description")}`,
}); });
} catch (error) { } catch (error) {
console.error("Add to cart error:", error); console.error("Add to cart error:", error);
toast.error("Failed to add to cart"); toast.error(t("add_to_cart_failed"));
} finally { } finally {
setIsSyncing(false); setIsSyncing(false);
} }
@@ -303,7 +303,7 @@ export default function ProductCard({
<button <button
onClick={handleFavorite} onClick={handleFavorite}
disabled={isFavoriteToggling || isFavoriteLoading} disabled={isFavoriteToggling || isFavoriteLoading}
className="absolute top-3 right-3 z-10 rounded-full bg-white/80 p-2 hover:bg-white transition-all disabled:opacity-50" className="absolute top-3 cursor-pointer right-3 z-10 rounded-full bg-white/80 p-2 hover:bg-white transition-all disabled:opacity-50"
> >
{isFavoriteLoading ? ( {isFavoriteLoading ? (
<div className="w-5 h-5 border-2 border-gray-300 border-t-gray-600 rounded-full animate-spin" /> <div className="w-5 h-5 border-2 border-gray-300 border-t-gray-600 rounded-full animate-spin" />
@@ -323,7 +323,7 @@ export default function ProductCard({
key={idx} key={idx}
data-carousel-control="true" data-carousel-control="true"
onClick={(e) => handleNavClick(e, () => api?.scrollTo(idx))} onClick={(e) => handleNavClick(e, () => api?.scrollTo(idx))}
className={`h-1.5 rounded-full transition-all ${ className={`h-1.5 rounded-full cursor-pointer transition-all ${
idx === current ? "w-6 bg-white" : "w-1.5 bg-white/60" idx === current ? "w-6 bg-white" : "w-1.5 bg-white/60"
}`} }`}
/> />
@@ -372,7 +372,7 @@ export default function ProductCard({
<Button <Button
onClick={handleAddToCart} onClick={handleAddToCart}
disabled={isSyncing} disabled={isSyncing}
className="w-full rounded-lg gap-2 bg-[#005bff] hover:bg-[#0041c4]" className="w-full rounded-lg cursor-pointer gap-2 bg-[#005bff] hover:bg-[#0041c4]"
size="sm" size="sm"
> >
{isSyncing ? ( {isSyncing ? (
@@ -394,7 +394,7 @@ export default function ProductCard({
size="icon" size="icon"
onClick={(e) => handleQuantityChange(e, -1)} onClick={(e) => handleQuantityChange(e, -1)}
disabled={isSyncing || localQuantity <= 1} disabled={isSyncing || localQuantity <= 1}
className="rounded-lg h-9 w-9 shrink-0" className="rounded-lg cursor-pointer h-9 w-9 shrink-0"
> >
<Minus className="h-4 w-4" /> <Minus className="h-4 w-4" />
</Button> </Button>
@@ -409,7 +409,7 @@ export default function ProductCard({
size="icon" size="icon"
onClick={(e) => handleQuantityChange(e, 1)} onClick={(e) => handleQuantityChange(e, 1)}
disabled={isSyncing} disabled={isSyncing}
className="rounded-lg h-9 w-9 shrink-0" className="rounded-lg cursor-pointer h-9 w-9 shrink-0"
> >
<Plus className="h-4 w-4 text-[#005bff]" /> <Plus className="h-4 w-4 text-[#005bff]" />
</Button> </Button>
@@ -444,7 +444,7 @@ export default function ProductCard({
e.stopPropagation(); e.stopPropagation();
setShowStockModal(false); setShowStockModal(false);
}} }}
className="w-full rounded-lg" className="w-full rounded-lg cursor-pointer"
> >
{t("understood")} {t("understood")}
</Button> </Button>

View File

@@ -139,7 +139,6 @@ export function useCollectionProductsInfinite(
}, },
getNextPageParam: (lastPage) => { getNextPageParam: (lastPage) => {
if (lastPage.pagination?.next_page_url) { if (lastPage.pagination?.next_page_url) {
// Extract page number from URL or increment
const currentPage = lastPage.pagination.page || 1; const currentPage = lastPage.pagination.page || 1;
return currentPage + 1; return currentPage + 1;
} }

View File

@@ -8,7 +8,7 @@ export default function EmptyOrders() {
const router=useRouter(); const router=useRouter();
return ( return (
<div className="flex min-h-[60vh] items-center justify-center px-4"> <div className="flex min-h-[60vh] items-center justify-center px-4">
<div className="w-full max-w-md rounded-2xl bg-gradient-to-br from-blue-50 to-white p-8 text-center shadow-lg"> <div className="w-full max-w-md rounded-2xl bg-linear-to-br from-blue-50 to-white p-8 text-center shadow-lg">
<div className="mx-auto mb-6 flex h-20 w-20 items-center justify-center rounded-full bg-blue-100"> <div className="mx-auto mb-6 flex h-20 w-20 items-center justify-center rounded-full bg-blue-100">
<ShoppingCart className="h-10 w-10 text-blue-600" /> <ShoppingCart className="h-10 w-10 text-blue-600" />
</div> </div>
@@ -21,7 +21,7 @@ export default function EmptyOrders() {
{t("orders_empty_message")} {t("orders_empty_message")}
</p> </p>
<Button onClick={()=>router.push("/")} className="w-full rounded-xl bg-blue-600 px-6 py-3 text-sm font-medium text-white transition hover:bg-blue-700 active:scale-95"> <Button onClick={()=>router.push("/")} className="w-full rounded-lg cursor-pointer bg-blue-600 px-6 py-3 text-sm font-medium text-white transition hover:bg-blue-700 active:scale-95">
{t("start_shopping")} {t("start_shopping")}
</Button> </Button>
</div> </div>

View File

@@ -258,6 +258,7 @@ export default function OrdersPageClient({ locale }: OrdersPageClientProps) {
variant="outline" variant="outline"
onClick={() => setIsCancelDialogOpen(false)} onClick={() => setIsCancelDialogOpen(false)}
disabled={isCancellingOrder} disabled={isCancellingOrder}
className="cursor-pointer"
> >
{t("keep_order")} {t("keep_order")}
</Button> </Button>
@@ -265,6 +266,7 @@ export default function OrdersPageClient({ locale }: OrdersPageClientProps) {
variant="destructive" variant="destructive"
onClick={confirmCancelOrder} onClick={confirmCancelOrder}
disabled={isCancellingOrder} disabled={isCancellingOrder}
className="cursor-pointer"
> >
{isCancellingOrder ? t("cancelling") : t("cancel_order")} {isCancellingOrder ? t("cancelling") : t("cancel_order")}
</Button> </Button>
@@ -404,7 +406,7 @@ function CompactOrderCard({
key={index} key={index}
className="flex items-center gap-4 p-3 bg-gray-50 rounded-lg hover:bg-gray-100 transition-colors" className="flex items-center gap-4 p-3 bg-gray-50 rounded-lg hover:bg-gray-100 transition-colors"
> >
<div className="relative w-16 h-16 flex-shrink-0 rounded-md overflow-hidden bg-white border"> <div className="relative w-16 h-16 shrink-0 rounded-md overflow-hidden bg-white border">
<Image <Image
src={ src={
item.product.images_400x400 || item.product.thumbnail item.product.images_400x400 || item.product.thumbnail
@@ -454,7 +456,7 @@ function CompactOrderCard({
onCancel(order); onCancel(order);
}} }}
disabled={isCancelling} disabled={isCancelling}
className="w-full" className="w-full cursor-pointer"
> >
{t("cancel_order")} {t("cancel_order")}
</Button> </Button>

View File

@@ -1,6 +1,6 @@
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import { apiClient } from "@/lib/api"; import { apiClient } from "@/lib/api";
import type { Order, OrdersResponse, CreateOrderRequest } from "@/lib/types/api"; import type { Order, OrdersResponse } from "@/lib/types/api";
export function useOrders(options?: { page?: number; perPage?: number }) { export function useOrders(options?: { page?: number; perPage?: number }) {
return useQuery<Order[]>({ return useQuery<Order[]>({
@@ -13,7 +13,6 @@ export function useOrders(options?: { page?: number; perPage?: number }) {
}, },
}); });
// API response'dan data array'ini döndür
return response.data.data; return response.data.data;
}, },
staleTime: 1000 * 60 * 5, staleTime: 1000 * 60 * 5,

View File

@@ -44,7 +44,7 @@ export function ProductImageGallery({
); );
return ( return (
<div className="flex-1 max-w-2xl"> <div className="contents max-w-2xl">
<div className="relative"> <div className="relative">
<div className="relative aspect-square w-full rounded-2xl overflow-hidden bg-white"> <div className="relative aspect-square w-full rounded-2xl overflow-hidden bg-white">
{images.length > 0 ? ( {images.length > 0 ? (
@@ -68,7 +68,7 @@ export function ProductImageGallery({
<button <button
key={index} key={index}
onClick={() => handleImageSelect(index)} onClick={() => handleImageSelect(index)}
className={`relative w-16 h-16 flex-shrink-0 rounded overflow-hidden border-2 transition-all ${ className={`relative w-16 h-16 shrink-0 rounded cursor-pointer overflow-hidden border-2 transition-all ${
selectedImage === index selectedImage === index
? "border-primary ring-2 ring-primary/20" ? "border-primary ring-2 ring-primary/20"
: "border-gray-200 hover:border-gray-300" : "border-gray-200 hover:border-gray-300"

View File

@@ -30,22 +30,7 @@ export function ProductInfoCard({
reviewsCount, reviewsCount,
t, t,
}: ProductInfoCardProps) { }: ProductInfoCardProps) {
const renderStars = (rating: number) => {
return (
<div className="flex gap-1">
{[1, 2, 3, 4, 5].map((star) => (
<Star
key={star}
className={`h-5 w-5 transition-all ${
star <= rating
? "fill-yellow-400 text-yellow-400"
: "text-gray-300"
}`}
/>
))}
</div>
);
};
return ( return (
<div className="flex-1 space-y-6 bg-white"> <div className="flex-1 space-y-6 bg-white">

View File

@@ -1,281 +1,270 @@
"use client" "use client";
import { useState, useCallback, useMemo, useRef, useEffect } from "react" import { useState, useCallback, useMemo, useRef, useEffect } from "react";
import { Skeleton } from "@/components/ui/skeleton" import { Skeleton } from "@/components/ui/skeleton";
import { useProductsBySlug, useRelatedProducts, useSubmitReview } from "@/features/products/hooks/useProducts" import {
useProductsBySlug,
useRelatedProducts,
useSubmitReview,
} from "@/features/products/hooks/useProducts";
import { import {
useAddToCart, useAddToCart,
useUpdateCartItemQuantity, useUpdateCartItemQuantity,
useRemoveFromCart, useRemoveFromCart,
useCart, useCart,
cartEvents, cartEvents,
} from "@/features/cart/hooks/useCart" } from "@/features/cart/hooks/useCart";
import { useTranslations } from "next-intl" import { useTranslations } from "next-intl";
import { toast } from "sonner" import { toast } from "sonner";
import { ProductImageGallery } from "./ProductImageGallery" import { ProductImageGallery } from "./ProductImageGallery";
import { ProductInfoCard } from "./ProductInfoCard" import { ProductInfoCard } from "./ProductInfoCard";
import { ProductPurchaseCard } from "./ProductPurchaseCard" import { ProductPurchaseCard } from "./ProductPurchaseCard";
import { ProductReviewsSection } from "./ProductReviewsSection" import { ProductReviewsSection } from "./ProductReviewsSection";
import { RelatedProductsSection } from "./RelatedProductsSection" import { RelatedProductsSection } from "./RelatedProductsSection";
import { ReviewModal } from "./ReviewModal" import { ReviewModal } from "./ReviewModal";
import { StockLimitModal } from "./StockLimitModal" import { StockLimitModal } from "./StockLimitModal";
interface ProductDetailProps { interface ProductDetailProps {
slug: string slug: string;
} }
const PENDING_PRODUCT_UPDATES_KEY = "pendingProductUpdates" const PENDING_PRODUCT_UPDATES_KEY = "pendingProductUpdates";
interface PendingUpdate { interface PendingUpdate {
quantity: number quantity: number;
timestamp: number timestamp: number;
retryCount: number retryCount: number;
} }
const DEBUG = true // const DEBUG = true
const log = (...args: any[]) => { // const log = (...args: any[]) => {
if (DEBUG) console.log("[ProductPage]", ...args) // if (DEBUG) console.log("[ProductPage]", ...args)
} // }
export default function ProductPageContent({ slug }: ProductDetailProps) { export default function ProductPageContent({ slug }: ProductDetailProps) {
const [localQuantity, setLocalQuantity] = useState(1) const [localQuantity, setLocalQuantity] = useState(1);
const [isFavorite, setIsFavorite] = useState(false) const [isFavorite, setIsFavorite] = useState(false);
const [isSyncing, setIsSyncing] = useState(false) const [isSyncing, setIsSyncing] = useState(false);
const [syncError, setSyncError] = useState(false) const [syncError, setSyncError] = useState(false);
const [showStockModal, setShowStockModal] = useState(false) const [showStockModal, setShowStockModal] = useState(false);
const [showReviewModal, setShowReviewModal] = useState(false) const [showReviewModal, setShowReviewModal] = useState(false);
const [isInitialized, setIsInitialized] = useState(false) // 🔥 NEW: Track initialization const [isInitialized, setIsInitialized] = useState(false);
const t = useTranslations() const t = useTranslations();
const debounceTimerRef = useRef<NodeJS.Timeout | undefined>(undefined) const debounceTimerRef = useRef<NodeJS.Timeout | undefined>(undefined);
const isRequestInFlightRef = useRef(false) const isRequestInFlightRef = useRef(false);
const pendingQuantityRef = useRef<number | null>(null) const pendingQuantityRef = useRef<number | null>(null);
const retryCountRef = useRef(0) const retryCountRef = useRef(0);
const retryTimerRef = useRef<NodeJS.Timeout | undefined>(undefined) const retryTimerRef = useRef<NodeJS.Timeout | undefined>(undefined);
const syncToServerRef = useRef<((quantity: number) => void) | null>(null) const syncToServerRef = useRef<((quantity: number) => void) | null>(null);
const retrySyncRef = useRef<((quantity: number) => void) | null>(null) const retrySyncRef = useRef<((quantity: number) => void) | null>(null);
const shouldSyncFromCartRef = useRef(true) const shouldSyncFromCartRef = useRef(true);
const lastSyncedQuantityRef = useRef<number | null>(null) const lastSyncedQuantityRef = useRef<number | null>(null);
const { data: product, isLoading: productLoading, error, refetch: refetchProduct } = useProductsBySlug(slug) const {
data: product,
isLoading: productLoading,
error,
refetch: refetchProduct,
} = useProductsBySlug(slug);
// 🔥 FIX: Memoize cart options to prevent infinite re-subscriptions
const cartOptions = useMemo( const cartOptions = useMemo(
() => ({ () => ({
refetchOnMount: true, refetchOnMount: true,
refetchOnWindowFocus: true, refetchOnWindowFocus: true,
staleTime: 0, staleTime: 0,
}), }),
[], []
) );
const { data: cartData, refetch: refetchCart, isFetching: isCartFetching } = useCart(cartOptions) const {
data: cartData,
refetch: refetchCart,
isFetching: isCartFetching,
} = useCart(cartOptions);
const { data: relatedProducts } = useRelatedProducts(product?.id || 0, { const { data: relatedProducts } = useRelatedProducts(product?.id || 0, {
enabled: !!product?.id, enabled: !!product?.id,
}) });
const addToCartMutation = useAddToCart() const addToCartMutation = useAddToCart();
const updateCartMutation = useUpdateCartItemQuantity() const updateCartMutation = useUpdateCartItemQuantity();
const removeFromCartMutation = useRemoveFromCart() const removeFromCartMutation = useRemoveFromCart();
const submitReviewMutation = useSubmitReview() const submitReviewMutation = useSubmitReview();
const cartItem = useMemo(() => { const cartItem = useMemo(() => {
const item = cartData?.data?.find((item: any) => item.product?.id === product?.id) const item = cartData?.data?.find(
log("🎯 Cart Item Found:", { (item: any) => item.product?.id === product?.id
productId: product?.id, );
cartItem: item,
quantity: item?.product_quantity,
isInitialized,
})
return item
}, [cartData, product, isInitialized])
const isInCart = !!cartItem return item;
const availableStock = product?.stock || 0 }, [cartData, product, isInitialized]);
log("📊 State:", { const isInCart = !!cartItem;
isInCart, const availableStock = product?.stock || 0;
localQuantity,
cartItemQuantity: cartItem?.product_quantity,
availableStock,
isSyncing,
shouldSyncFromCart: shouldSyncFromCartRef.current,
isInitialized,
})
const imageUrls = useMemo( const imageUrls = useMemo(
() => product?.media?.map((m) => m.images_800x800 || m.images_720x720 || m.thumbnail) || [], () =>
[product], product?.media?.map(
) (m) => m.images_800x800 || m.images_720x720 || m.thumbnail
) || [],
[product]
);
const reviews = useMemo(() => product?.reviews_resources || [], [product]) const reviews = useMemo(() => product?.reviews_resources || [], [product]);
const averageRating = useMemo( const averageRating = useMemo(
() => (product?.reviews?.rating ? Number.parseFloat(product.reviews.rating) : 0), () =>
[product], product?.reviews?.rating ? Number.parseFloat(product.reviews.rating) : 0,
) [product]
);
// 🔥 FIX: Subscribe to cart events ONCE with stable dependencies
useEffect(() => { useEffect(() => {
log("🔔 Setting up cart event subscription")
const unsubscribe = cartEvents.subscribe(() => { const unsubscribe = cartEvents.subscribe(() => {
log("📢 Cart event received! Refetching...") refetchCart();
refetchCart() });
})
return () => { return () => {
log("🔕 Cleaning up cart event subscription") unsubscribe();
unsubscribe() };
} }, [refetchCart]);
}, [refetchCart])
// 🔥 CRITICAL FIX: Initialize localQuantity from cart ONCE on mount
useEffect(() => { useEffect(() => {
if (!product?.id || isInitialized) return if (!product?.id || isInitialized) return;
log("🚀 Initializing component with product:", product.id)
if (cartItem?.product_quantity) { if (cartItem?.product_quantity) {
const serverQuantity = cartItem.product_quantity const serverQuantity = cartItem.product_quantity;
log("✅ Initial cart quantity found:", serverQuantity) setLocalQuantity(serverQuantity);
setLocalQuantity(serverQuantity) lastSyncedQuantityRef.current = serverQuantity;
lastSyncedQuantityRef.current = serverQuantity
} }
setIsInitialized(true) setIsInitialized(true);
}, [product?.id, cartItem, isInitialized]) }, [product?.id, cartItem, isInitialized]);
useEffect(() => { useEffect(() => {
setLocalQuantity(cartItem?.product_quantity || 1) setLocalQuantity(cartItem?.product_quantity || 1);
}, [cartItem]) }, [cartItem]);
const savePendingUpdate = useCallback( const savePendingUpdate = useCallback(
(quantity: number) => { (quantity: number) => {
if (!product?.id) return if (!product?.id) return;
try { try {
const stored = sessionStorage.getItem(PENDING_PRODUCT_UPDATES_KEY) const stored = sessionStorage.getItem(PENDING_PRODUCT_UPDATES_KEY);
const pending: Record<number, PendingUpdate> = stored ? JSON.parse(stored) : {} const pending: Record<number, PendingUpdate> = stored
? JSON.parse(stored)
: {};
pending[product.id] = { pending[product.id] = {
quantity, quantity,
timestamp: Date.now(), timestamp: Date.now(),
retryCount: retryCountRef.current, retryCount: retryCountRef.current,
} };
sessionStorage.setItem(PENDING_PRODUCT_UPDATES_KEY, JSON.stringify(pending)) sessionStorage.setItem(
PENDING_PRODUCT_UPDATES_KEY,
JSON.stringify(pending)
);
} catch (error) { } catch (error) {
console.error("Failed to save pending update:", error) console.error("Failed to save pending update:", error);
} }
}, },
[product?.id], [product?.id]
) );
const clearPendingUpdate = useCallback(() => { const clearPendingUpdate = useCallback(() => {
if (!product?.id) return if (!product?.id) return;
try { try {
const stored = sessionStorage.getItem(PENDING_PRODUCT_UPDATES_KEY) const stored = sessionStorage.getItem(PENDING_PRODUCT_UPDATES_KEY);
if (stored) { if (stored) {
const pending: Record<number, PendingUpdate> = JSON.parse(stored) const pending: Record<number, PendingUpdate> = JSON.parse(stored);
delete pending[product.id] delete pending[product.id];
if (Object.keys(pending).length === 0) { if (Object.keys(pending).length === 0) {
sessionStorage.removeItem(PENDING_PRODUCT_UPDATES_KEY) sessionStorage.removeItem(PENDING_PRODUCT_UPDATES_KEY);
} else { } else {
sessionStorage.setItem(PENDING_PRODUCT_UPDATES_KEY, JSON.stringify(pending)) sessionStorage.setItem(
PENDING_PRODUCT_UPDATES_KEY,
JSON.stringify(pending)
);
} }
} }
} catch (error) { } catch (error) {
console.error("Failed to clear pending update:", error) console.error("Failed to clear pending update:", error);
} }
}, [product?.id]) }, [product?.id]);
const retrySync = useCallback( const retrySync = useCallback(
(quantity: number) => { (quantity: number) => {
const maxRetries = 4 const maxRetries = 4;
const retryCount = retryCountRef.current const retryCount = retryCountRef.current;
if (retryCount >= maxRetries) { if (retryCount >= maxRetries) {
setSyncError(true) setSyncError(true);
setIsSyncing(false) setIsSyncing(false);
shouldSyncFromCartRef.current = true shouldSyncFromCartRef.current = true;
toast.error(t("error"), { toast.error(t("error"), {
description: t("update_quantity_failed"), description: t("update_quantity_failed"),
}) });
return return;
} }
const delay = Math.min(1000 * Math.pow(2, retryCount), 16000) const delay = Math.min(1000 * Math.pow(2, retryCount), 16000);
retryCountRef.current++ retryCountRef.current++;
retryTimerRef.current = setTimeout(() => { retryTimerRef.current = setTimeout(() => {
syncToServerRef.current?.(quantity) syncToServerRef.current?.(quantity);
}, delay) }, delay);
}, },
[t], [t]
) );
retrySyncRef.current = retrySync retrySyncRef.current = retrySync;
const syncToServer = useCallback( const syncToServer = useCallback(
async (quantity: number) => { async (quantity: number) => {
if (!product?.id) return if (!product?.id) return;
log("🚀 syncToServer called:", {
productId: product.id,
quantity,
isRequestInFlight: isRequestInFlightRef.current,
isInCart,
})
if (isRequestInFlightRef.current) { if (isRequestInFlightRef.current) {
log("⏳ Request in flight, queuing:", quantity) pendingQuantityRef.current = quantity;
pendingQuantityRef.current = quantity return;
return
} }
isRequestInFlightRef.current = true isRequestInFlightRef.current = true;
setIsSyncing(true) setIsSyncing(true);
setSyncError(false) setSyncError(false);
try { try {
if (quantity === 0) { if (quantity === 0) {
log("🗑️ Removing from cart") await removeFromCartMutation.mutateAsync(product.id);
await removeFromCartMutation.mutateAsync(product.id) toast.success(t("removed_from_cart"));
toast.success(t("removed_from_cart"))
} else if (isInCart) { } else if (isInCart) {
log("🔄 Updating cart quantity")
await updateCartMutation.mutateAsync({ await updateCartMutation.mutateAsync({
productId: product.id, productId: product.id,
quantity: quantity, quantity: quantity,
}) });
} else { } else {
log(" Adding to cart")
await addToCartMutation.mutateAsync({ await addToCartMutation.mutateAsync({
productId: product.id, productId: product.id,
quantity: quantity, quantity: quantity,
}) });
} }
log("✅ Sync successful") await refetchCart();
await refetchCart() retryCountRef.current = 0;
retryCountRef.current = 0 clearPendingUpdate();
clearPendingUpdate()
if (pendingQuantityRef.current !== null) { if (pendingQuantityRef.current !== null) {
const nextQuantity = pendingQuantityRef.current const nextQuantity = pendingQuantityRef.current;
pendingQuantityRef.current = null pendingQuantityRef.current = null;
log("📤 Processing queued update:", nextQuantity) setTimeout(() => syncToServerRef.current?.(nextQuantity), 100);
setTimeout(() => syncToServerRef.current?.(nextQuantity), 100)
} }
} catch (error) { } catch (error) {
log("❌ Sync failed:", error) setLocalQuantity(cartItem?.product_quantity || 1);
setLocalQuantity(cartItem?.product_quantity || 1)
toast.error("Failed to update quantity", { toast.error("Failed to update quantity", {
description: "Please try again", description: "Please try again",
}) });
retrySyncRef.current?.(quantity) retrySyncRef.current?.(quantity);
} finally { } finally {
isRequestInFlightRef.current = false isRequestInFlightRef.current = false;
setIsSyncing(false) setIsSyncing(false);
} }
}, },
[ [
@@ -288,119 +277,116 @@ export default function ProductPageContent({ slug }: ProductDetailProps) {
refetchCart, refetchCart,
clearPendingUpdate, clearPendingUpdate,
t, t,
], ]
) );
syncToServerRef.current = syncToServer syncToServerRef.current = syncToServer;
useEffect(() => { useEffect(() => {
if (!isInCart || !product?.id) return if (!isInCart || !product?.id) return;
// If local matches server, nothing to sync
if (localQuantity === (cartItem?.product_quantity || 1)) { if (localQuantity === (cartItem?.product_quantity || 1)) {
return return;
} }
if (debounceTimerRef.current) { if (debounceTimerRef.current) {
clearTimeout(debounceTimerRef.current) clearTimeout(debounceTimerRef.current);
} }
debounceTimerRef.current = setTimeout(() => { debounceTimerRef.current = setTimeout(() => {
syncToServerRef.current?.(localQuantity) syncToServerRef.current?.(localQuantity);
}, 800) }, 800);
return () => { return () => {
if (debounceTimerRef.current) { if (debounceTimerRef.current) {
clearTimeout(debounceTimerRef.current) clearTimeout(debounceTimerRef.current);
} }
} };
}, [localQuantity, isInCart, product?.id, cartItem?.product_quantity]) }, [localQuantity, isInCart, product?.id, cartItem?.product_quantity]);
// Cleanup
useEffect(() => { useEffect(() => {
return () => { return () => {
if (debounceTimerRef.current) clearTimeout(debounceTimerRef.current) if (debounceTimerRef.current) clearTimeout(debounceTimerRef.current);
if (retryTimerRef.current) clearTimeout(retryTimerRef.current) if (retryTimerRef.current) clearTimeout(retryTimerRef.current);
} };
}, []) }, []);
const handleAddToCart = useCallback(async () => { const handleAddToCart = useCallback(async () => {
if (!product?.id) return if (!product?.id) return;
if (localQuantity > availableStock) { if (localQuantity > availableStock) {
setShowStockModal(true) setShowStockModal(true);
return return;
} }
setIsSyncing(true) setIsSyncing(true);
shouldSyncFromCartRef.current = false shouldSyncFromCartRef.current = false;
try { try {
await addToCartMutation.mutateAsync({ await addToCartMutation.mutateAsync({
productId: product.id, productId: product.id,
quantity: localQuantity, quantity: localQuantity,
}) });
lastSyncedQuantityRef.current = localQuantity lastSyncedQuantityRef.current = localQuantity;
setTimeout(() => { setTimeout(() => {
shouldSyncFromCartRef.current = true shouldSyncFromCartRef.current = true;
refetchCart() refetchCart();
}, 150) }, 150);
setIsSyncing(false) setIsSyncing(false);
toast.success(t("added_to_cart"), { toast.success(t("added_to_cart"), {
description: `${product.name} ${t("added_to_cart_description")}`, description: `${product.name} ${t("added_to_cart_description")}`,
}) });
} catch (error) { } catch (error) {
console.error("Add to cart error:", error) console.error("Add to cart error:", error);
setIsSyncing(false) setIsSyncing(false);
shouldSyncFromCartRef.current = true shouldSyncFromCartRef.current = true;
toast.error(t("error"), { toast.error(t("error"), {
description: t("add_to_cart_failed"), description: t("add_to_cart_failed"),
}) });
} }
}, [product, localQuantity, availableStock, addToCartMutation, refetchCart, t]) }, [
product,
localQuantity,
availableStock,
addToCartMutation,
refetchCart,
t,
]);
const handleQuantityIncrease = useCallback(() => { const handleQuantityIncrease = useCallback(() => {
log(" Quantity increase clicked:", {
current: localQuantity,
availableStock,
})
if (localQuantity >= availableStock) { if (localQuantity >= availableStock) {
log("⚠️ Stock limit reached") setShowStockModal(true);
setShowStockModal(true) return;
return
} }
setLocalQuantity((prev) => { setLocalQuantity((prev) => {
const newVal = prev + 1 const newVal = prev + 1;
log("📈 New local quantity:", newVal) return newVal;
return newVal });
}) }, [localQuantity, availableStock]);
}, [localQuantity, availableStock])
const handleQuantityDecrease = useCallback(() => { const handleQuantityDecrease = useCallback(() => {
log(" Quantity decrease clicked:", { current: localQuantity }) if (localQuantity <= 0) return;
if (localQuantity <= 0) return
setLocalQuantity((prev) => { setLocalQuantity((prev) => {
const newVal = prev - 1 const newVal = prev - 1;
log("📉 New local quantity:", newVal) return newVal;
return newVal });
}) }, [localQuantity]);
}, [localQuantity])
const handleToggleFavorite = useCallback(() => { const handleToggleFavorite = useCallback(() => {
setIsFavorite(!isFavorite) setIsFavorite(!isFavorite);
}, [isFavorite]) }, [isFavorite]);
const handleSubmitReview = useCallback( const handleSubmitReview = useCallback(
async (rating: number, text: string) => { async (rating: number, text: string) => {
if (!product?.id || rating === 0 || !text.trim()) { if (!product?.id || rating === 0 || !text.trim()) {
toast.error(t("error"), { toast.error(t("error"), {
description: "Please provide rating and review text", description: "Please provide rating and review text",
}) });
return return;
} }
try { try {
@@ -409,20 +395,20 @@ export default function ProductPageContent({ slug }: ProductDetailProps) {
rating: rating, rating: rating,
title: text, title: text,
source: "site", source: "site",
}) });
await refetchProduct() await refetchProduct();
toast.success("Review submitted successfully!") toast.success("Review submitted successfully!");
setShowReviewModal(false) setShowReviewModal(false);
} catch (error) { } catch (error) {
toast.error(t("error"), { toast.error(t("error"), {
description: "Failed to submit review", description: "Failed to submit review",
}) });
} }
}, },
[product?.id, submitReviewMutation, refetchProduct, t], [product?.id, submitReviewMutation, refetchProduct, t]
) );
const loadingSkeleton = useMemo( const loadingSkeleton = useMemo(
() => ( () => (
@@ -443,25 +429,33 @@ export default function ProductPageContent({ slug }: ProductDetailProps) {
</div> </div>
</div> </div>
), ),
[], []
) );
if (productLoading) return loadingSkeleton if (productLoading) return loadingSkeleton;
if (error || !product) { if (error || !product) {
return ( return (
<div className=" mx-auto px-4 py-8 text-center"> <div className=" mx-auto px-4 py-8 text-center">
<h2 className="text-2xl font-bold text-red-600">{t("product_not_found")}</h2> <h2 className="text-2xl font-bold text-red-600">
<p className="text-gray-500 mt-2">{t("product_not_found_description")}</p> {t("product_not_found")}
</h2>
<p className="text-gray-500 mt-2">
{t("product_not_found_description")}
</p>
</div> </div>
) );
} }
return ( return (
<> <>
<div className="px-2 md:px-4 lg:px-6 rounded-lg mb-18 space-y-8 max-w-[1504px] mx-auto"> <div className="px-2 md:px-4 lg:px-6 rounded-lg mb-18 space-y-8 max-w-[1504px] mx-auto">
<div className="flex flex-col lg:flex-row gap-8 rounded-b-lg bg-white p-4"> <div className="flex flex-col lg:flex-row gap-8 rounded-b-lg bg-white p-4">
<ProductImageGallery images={imageUrls} productName={product.name} noImageText={t("no_image")} /> <ProductImageGallery
images={imageUrls}
productName={product.name}
noImageText={t("no_image")}
/>
<ProductInfoCard <ProductInfoCard
brandName={product.brand?.name} brandName={product.brand?.name}
@@ -519,5 +513,5 @@ export default function ProductPageContent({ slug }: ProductDetailProps) {
isSubmitting={submitReviewMutation.isPending} isSubmitting={submitReviewMutation.isPending}
/> />
</> </>
) );
} }

View File

@@ -62,7 +62,7 @@ export function ProductPurchaseCard({
<Link href="/cart"> <Link href="/cart">
<Button <Button
size="lg" size="lg"
className="w-full rounded-lg text-lg font-bold bg-green-600 hover:bg-green-700 mb-4" className="w-full rounded-lg cursor-pointer text-lg font-bold bg-green-600 hover:bg-green-700 mb-4"
> >
<ShoppingCart className="mr-2 h-5 w-5" /> <ShoppingCart className="mr-2 h-5 w-5" />
{t("go_to_cart")} {t("go_to_cart")}
@@ -75,7 +75,7 @@ export function ProductPurchaseCard({
size="icon" size="icon"
onClick={onQuantityDecrease} onClick={onQuantityDecrease}
disabled={isSyncing} disabled={isSyncing}
className={`rounded-lg h-12 w-12 ${ className={`rounded-lg cursor-pointer h-12 w-12 ${
isSyncing ? "opacity-70" : "" isSyncing ? "opacity-70" : ""
}`} }`}
> >
@@ -95,7 +95,7 @@ export function ProductPurchaseCard({
size="icon" size="icon"
onClick={onQuantityIncrease} onClick={onQuantityIncrease}
disabled={isSyncing} disabled={isSyncing}
className={`rounded-lg h-12 w-12 ${ className={`rounded-lg cursor-pointer h-12 w-12 ${
isSyncing ? "opacity-70" : "" isSyncing ? "opacity-70" : ""
}`} }`}
> >
@@ -127,7 +127,7 @@ export function ProductPurchaseCard({
size="lg" size="lg"
onClick={onAddToCart} onClick={onAddToCart}
disabled={isSyncing || productStock === 0} disabled={isSyncing || productStock === 0}
className="w-full rounded-lg text-lg font-bold bg-[#005bff] hover:bg-[#0041c4] cursor-pointer" className="w-full rounded-lg text-lg font-bold bg-[#005bff] hover:bg-[#0041c4] cursor-pointer"
> >
{isSyncing ? ( {isSyncing ? (
<> <>
@@ -158,7 +158,7 @@ export function ProductPurchaseCard({
<h4 className="text-lg font-bold">{channelName}</h4> <h4 className="text-lg font-bold">{channelName}</h4>
</div> </div>
</div> </div>
<Button variant="outline" size="lg" className="w-full rounded-lg"> <Button variant="outline" size="lg" className="w-full cursor-pointer rounded-lg">
{t("write_to_store")} {t("write_to_store")}
</Button> </Button>
</Card> </Card>

View File

@@ -56,7 +56,7 @@ export function ProductReviewsSection({
</span> */} </span> */}
</div> </div>
</div> </div>
<Button onClick={onWriteReview} className="rounded-lg bg-[#005bff] hover:bg-[#0041c4]"> <Button onClick={onWriteReview} className="rounded-lg cursor-pointer bg-[#005bff] hover:bg-[#0041c4]">
<Send className="mr-2 h-4 w-4" /> <Send className="mr-2 h-4 w-4" />
{t("write_review")} {t("write_review")}
</Button> </Button>

View File

@@ -38,7 +38,6 @@ export function RelatedProductsSection({
<h2 className="text-2xl font-bold mb-6">{t("related_products")}</h2> <h2 className="text-2xl font-bold mb-6">{t("related_products")}</h2>
<div className="grid grid-cols-2 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4"> <div className="grid grid-cols-2 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4">
{products.slice(0, 4).map((product) => { {products.slice(0, 4).map((product) => {
// Extract image URLs from media
const images = const images =
product.media?.map( product.media?.map(
(m) => (m) =>

View File

@@ -96,14 +96,14 @@ export function ReviewModal({
<Button <Button
variant="outline" variant="outline"
onClick={handleClose} onClick={handleClose}
className="flex-1 rounded-lg" className="flex-1 rounded-lg cursor-pointer"
> >
{t("cancel")} {t("cancel")}
</Button> </Button>
<Button <Button
onClick={handleSubmit} onClick={handleSubmit}
disabled={rating === 0 || !text.trim() || isSubmitting} disabled={rating === 0 || !text.trim() || isSubmitting}
className="flex-1 rounded-lg bg-[#005bff] hover:bg-[#0041c4]" className="flex-1 rounded-lg cursor-pointer bg-[#005bff] hover:bg-[#0041c4]"
> >
{isSubmitting ? ( {isSubmitting ? (
<> <>

View File

@@ -45,7 +45,7 @@ export function StockLimitModal({
<div className="flex justify-center mt-4"> <div className="flex justify-center mt-4">
<Button <Button
onClick={() => onOpenChange(false)} onClick={() => onOpenChange(false)}
className="w-full rounded-lg" className="w-full rounded-lg cursor-pointer"
> >
{t("understood")} {t("understood")}
</Button> </Button>

View File

@@ -207,7 +207,7 @@ export function useSubmitReview() {
}, },
(variables) => [ (variables) => [
reviewKeys.byProduct(variables.productId), reviewKeys.byProduct(variables.productId),
productKeys.bySlug(""), // Invalidates all slug queries productKeys.bySlug(""),
reviewKeys.all, reviewKeys.all,
] ]
); );

View File

@@ -38,7 +38,6 @@ export default function ClientProfilePage(props: ProfilePageProps) {
useEffect(() => { useEffect(() => {
if (user && !isEditing) { if (user && !isEditing) {
console.log("[Profile] User data loaded:", user);
setFormData({ setFormData({
name: user.first_name || "", name: user.first_name || "",
last_name: user.last_name || "", last_name: user.last_name || "",
@@ -90,7 +89,6 @@ export default function ClientProfilePage(props: ProfilePageProps) {
address: formData.address.trim(), address: formData.address.trim(),
}; };
console.log("[Profile] Saving data:", apiData);
try { try {
await updateProfile.mutateAsync(apiData); await updateProfile.mutateAsync(apiData);
@@ -160,7 +158,7 @@ export default function ClientProfilePage(props: ProfilePageProps) {
</p> </p>
<Button <Button
onClick={() => window.location.reload()} onClick={() => window.location.reload()}
className="w-full sm:w-auto" className="w-full sm:w-auto cursor-pointer"
> >
{t("try_again")} {t("try_again")}
</Button> </Button>
@@ -186,7 +184,7 @@ export default function ClientProfilePage(props: ProfilePageProps) {
: t("view_your_information")} : t("view_your_information")}
</p> </p>
</div> </div>
<div className="flex-shrink-0 w-12 h-12 sm:w-14 sm:h-14 bg-blue-600 rounded-full flex items-center justify-center shadow-sm"> <div className="shrink-0 w-12 h-12 sm:w-14 sm:h-14 bg-blue-600 rounded-full flex items-center justify-center shadow-sm">
<User className="h-6 w-6 sm:h-7 sm:w-7 text-white" /> <User className="h-6 w-6 sm:h-7 sm:w-7 text-white" />
</div> </div>
</div> </div>
@@ -209,7 +207,7 @@ export default function ClientProfilePage(props: ProfilePageProps) {
onClick={handleEdit} onClick={handleEdit}
variant="outline" variant="outline"
size="sm" size="sm"
className="self-start sm:self-center border-gray-300 hover:bg-gray-50 text-gray-700 h-9" className="self-start sm:self-center cursor-pointer border-gray-300 hover:bg-gray-50 text-gray-700 h-9"
> >
<Edit2 className="h-3.5 w-3.5 sm:h-4 sm:w-4 mr-1.5 sm:mr-2" /> <Edit2 className="h-3.5 w-3.5 sm:h-4 sm:w-4 mr-1.5 sm:mr-2" />
<span className="text-sm">{t("edit")}</span> <span className="text-sm">{t("edit")}</span>
@@ -329,7 +327,7 @@ export default function ClientProfilePage(props: ProfilePageProps) {
<Button <Button
onClick={handleSave} onClick={handleSave}
disabled={updateProfile.isPending} disabled={updateProfile.isPending}
className="w-full sm:flex-1 bg-blue-600 hover:bg-blue-700 h-10 sm:h-11 text-sm sm:text-base font-medium shadow-sm" className="w-full sm:flex-1 cursor-pointer bg-blue-600 hover:bg-blue-700 h-10 sm:h-11 text-sm sm:text-base font-medium shadow-sm"
> >
<Save className="h-4 w-4 mr-2" /> <Save className="h-4 w-4 mr-2" />
{updateProfile.isPending {updateProfile.isPending
@@ -340,7 +338,7 @@ export default function ClientProfilePage(props: ProfilePageProps) {
onClick={handleCancel} onClick={handleCancel}
variant="outline" variant="outline"
disabled={updateProfile.isPending} disabled={updateProfile.isPending}
className="w-full sm:flex-1 h-10 sm:h-11 text-sm sm:text-base font-medium border-gray-300 hover:bg-gray-50" className="w-full sm:flex-1 cursor-pointer h-10 sm:h-11 text-sm sm:text-base font-medium border-gray-300 hover:bg-gray-50"
> >
<X className="h-4 w-4 mr-2" /> <X className="h-4 w-4 mr-2" />
{t("cancel")} {t("cancel")}
@@ -358,7 +356,7 @@ export default function ClientProfilePage(props: ProfilePageProps) {
onClick={handleLogout} onClick={handleLogout}
variant="destructive" variant="destructive"
size="lg" size="lg"
className="w-full sm:w-auto sm:min-w-[280px] flex items-center justify-center gap-2 h-11 text-sm sm:text-base font-medium shadow-sm" className="w-full cursor-pointer sm:w-auto sm:min-w-[280px] flex items-center justify-center gap-2 h-11 text-sm sm:text-base font-medium shadow-sm"
> >
<LogOut className="h-4 w-4 sm:h-5 sm:w-5" /> <LogOut className="h-4 w-4 sm:h-5 sm:w-5" />
{t("common.logout")} {t("common.logout")}

View File

@@ -1,6 +1,6 @@
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import { apiClient } from "@/lib/api"; import { apiClient } from "@/lib/api";
import { userStore } from "../userStore"; // import { userStore } from "../userStore";
import type { ProfileResponse, UpdateProfileRequest, UpdateProfileResponse } from "@/lib/types/api"; import type { ProfileResponse, UpdateProfileRequest, UpdateProfileResponse } from "@/lib/types/api";
export const useUserProfile = () => { export const useUserProfile = () => {
@@ -11,7 +11,7 @@ export const useUserProfile = () => {
const userData = response.data.data; const userData = response.data.data;
// Store'a kaydet // Store'a kaydet
userStore.setUser(userData); // userStore.setUser(userData);
return userData; return userData;
}, },
@@ -29,7 +29,7 @@ export const useUpdateProfile = () => {
return response.data.data; return response.data.data;
}, },
onSuccess: (data) => { onSuccess: (data) => {
userStore.setUser(data); // userStore.setUser(data);
queryClient.setQueryData(["user-profile"], data); queryClient.setQueryData(["user-profile"], data);
queryClient.invalidateQueries({ queryKey: ["user-profile"] }); queryClient.invalidateQueries({ queryKey: ["user-profile"] });
}, },

View File

@@ -1,30 +0,0 @@
import type { UserProfile } from "@/lib/types/api";
// In-memory store (session-based, no persistence)
class UserStore {
private user: UserProfile | null = null;
setUser(user: UserProfile | null) {
this.user = user;
}
getUser(): UserProfile | null {
return this.user;
}
clearUser() {
this.user = null;
}
getOrderData(): { customer_name: string; customer_phone: string, customer_last_name: string } | null {
if (!this.user) return null;
return {
customer_name: this.user.first_name,
customer_last_name: this.user.last_name,
customer_phone: this.user.phone_number,
};
}
}
export const userStore = new UserStore();

View File

@@ -102,6 +102,7 @@
"empty_favorites": "У вас пока нет избранных товаров", "empty_favorites": "У вас пока нет избранных товаров",
"removed_from_favorites": "Товар удален из избранного", "removed_from_favorites": "Товар удален из избранного",
"added_to_cart": "Товар добавлен в корзину", "added_to_cart": "Товар добавлен в корзину",
"removed_from_cart": "Товар удален из корзины",
"error": "Произошла ошибка", "error": "Произошла ошибка",
"out_of_stock": "Нет в наличии", "out_of_stock": "Нет в наличии",
"personal_info": "Личная информация", "personal_info": "Личная информация",
@@ -181,6 +182,10 @@
"orders_empty": "У вас пока нет заказов", "orders_empty": "У вас пока нет заказов",
"orders_empty_message": "Начните делать заказы", "orders_empty_message": "Начните делать заказы",
"product": "Продукт", "product": "Продукт",
"collection_not_found": "Коллекция не найдена" "collection_not_found": "Коллекция не найдена",
"added_to_favorites": "Товар добавлен в избранное",
"submit_success": "Отзыв отправлен",
"submit_error": "Произошла ошибка"
} }

View File

@@ -78,7 +78,8 @@
"no": "Ýok", "no": "Ýok",
"yes": "Hawa", "yes": "Hawa",
"cart_empty": "Siziň söwda sebediňiz boş", "cart_empty": "Siziň söwda sebediňiz boş",
"add_to_cart": "Söwda sebedine goşmak", "add_to_cart": "Söwda sebedine üstünlikli goşuldy",
"go_to_cart": "Sebede geçmek", "go_to_cart": "Sebede geçmek",
"products": "Azyk harytlary", "products": "Azyk harytlary",
"become_seller": "Satyjy bolmak", "become_seller": "Satyjy bolmak",
@@ -102,6 +103,7 @@
"empty_favorites": "Siziň saýlanan harytlaryňyz ýok", "empty_favorites": "Siziň saýlanan harytlaryňyz ýok",
"removed_from_favorites": "Haryt saýlanlardan aýryldy", "removed_from_favorites": "Haryt saýlanlardan aýryldy",
"added_to_cart": "Haryt sebede goşuldy", "added_to_cart": "Haryt sebede goşuldy",
"removed_from_cart": "Haryt sebetden aýryldy",
"error": "Ýalňyşlyk ýüze çykdy", "error": "Ýalňyşlyk ýüze çykdy",
"out_of_stock": "Haryt ýok", "out_of_stock": "Haryt ýok",
"personal_info": "Şahsy maglumat", "personal_info": "Şahsy maglumat",
@@ -133,7 +135,7 @@
"product_description": "Haryt barada düşündiriş", "product_description": "Haryt barada düşündiriş",
"adding": "Goşulýar...", "adding": "Goşulýar...",
"added_to_cart_description": "sebede goşuldy", "added_to_cart_description": "sebede goşuldy",
"add_to_cart_failed": "Haryt sebede goşup bolmady", "add_to_cart_failed": "Haryt sebede goşulmady",
"cart_updated": "Sebet täzelendi", "cart_updated": "Sebet täzelendi",
"update_quantity_failed": "Mukdar täzelenip bolmady", "update_quantity_failed": "Mukdar täzelenip bolmady",
"logging_out": "Çykylýar...", "logging_out": "Çykylýar...",
@@ -181,5 +183,10 @@
"orders_empty": "Siziň sargytlaryňyz ýok", "orders_empty": "Siziň sargytlaryňyz ýok",
"orders_empty_message": "Sargyt etmäge başlaň!", "orders_empty_message": "Sargyt etmäge başlaň!",
"product": "haryt", "product": "haryt",
"collection_not_found": "Kolleksiýa tapylmady" "collection_not_found": "Kolleksiýa tapylmady",
"added_to_favorites": "Haryt saýlananlara goşuldy",
"submit_success": "Üstünlikli ugradyldy",
"submit_error": "Ýalňyşlyk ýüze çykdy"
} }

View File

@@ -235,7 +235,6 @@ export function useLogout() {
} }
}, },
onError: () => { onError: () => {
// Always clear local state on logout
TokenStorage.clearTokens(); TokenStorage.clearTokens();
queryClient.clear(); queryClient.clear();

View File

@@ -28,12 +28,11 @@ class TokenStorage {
static setAuthToken(token: string): void { static setAuthToken(token: string): void {
if (!this.isClient) return; if (!this.isClient) return;
localStorage.setItem(AUTH_TOKEN_KEY, token); localStorage.setItem(AUTH_TOKEN_KEY, token);
localStorage.removeItem(GUEST_TOKEN_KEY); // Auth token replaces guest token localStorage.removeItem(GUEST_TOKEN_KEY);
} }
static setGuestToken(token: string): void { static setGuestToken(token: string): void {
if (!this.isClient) return; if (!this.isClient) return;
// Only set guest token if no auth token exists
if (!this.getAuthToken()) { if (!this.getAuthToken()) {
localStorage.setItem(GUEST_TOKEN_KEY, token); localStorage.setItem(GUEST_TOKEN_KEY, token);
} }

View File

@@ -12,6 +12,20 @@ export interface ProductMedia {
} }
export interface Carousel {
title: string
image: string
url?: string | null
}
export interface Review {
id: number;
rating: number;
title: string;
created_at: string;
}
export type DeliveryType = "SELECTED_DELIVERY" | "PICK_UP"; export type DeliveryType = "SELECTED_DELIVERY" | "PICK_UP";
export interface PaymentType { export interface PaymentType {