fixed some ui styles

This commit is contained in:
Jelaletdin12
2025-12-10 17:02:17 +05:00
parent 14f9bd400e
commit 5085c0cffd
6 changed files with 379 additions and 260 deletions

View File

@@ -23,7 +23,8 @@ export default function CartPage() {
const [selectedRegion, setSelectedRegion] = useState<string>("");
const [selectedProvince, setSelectedProvince] = useState<number | null>(null);
const [note, setNote] = useState<string>("");
const [phone, setPhone] = useState<string>("");
const [phone, setPhone] = useState<string>("");
const [name, setName] = useState<string>("");
const router = useRouter();
const t = useTranslations();
@@ -36,9 +37,15 @@ export default function CartPage() {
useEffect(() => {
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_phone) setPhone(orderData.customer_phone);
}
}, []);
// Memoize region groups to prevent unnecessary recalculations
const regionGroups = useMemo(() => {
return provinces.reduce((acc, province) => {
if (!acc[province.region]) {
@@ -54,7 +61,6 @@ export default function CartPage() {
[regionGroups]
);
// Memoize items grouped by seller
const itemsBySeller = useMemo(() => {
return cartItems.reduce((acc, item) => {
const sellerId = item.product.channel?.[0]?.id || 0;
@@ -71,7 +77,6 @@ export default function CartPage() {
}, {} as Record<number, { seller: { id: number; name: string }; items: typeof cartItems }>);
}, [cartItems]);
// Memoize total amount
const totalAmount = useMemo(() => {
return cartItems.reduce((sum, item) => {
const price = parseFloat(item.product.price_amount || "0");
@@ -85,7 +90,7 @@ export default function CartPage() {
};
const handleCompleteOrder = () => {
if (!selectedRegion || !selectedProvince || !paymentType) {
if (!selectedRegion || !selectedProvince || !paymentType || !phone || !name) {
console.warn("Missing required fields for order");
return;
}
@@ -104,7 +109,7 @@ export default function CartPage() {
createOrder(
{
customer_name: orderData.customer_name,
customer_name: name,
customer_phone: phone,
customer_address: selectedProvinceData.name,
shipping_method: deliveryType === "PICK_UP" ? "pickup" : "standart",
@@ -141,8 +146,8 @@ export default function CartPage() {
}
return (
<div className="container mx-auto px-4 py-8 min-h-screen">
<h1 className="text-3xl font-bold mb-6">{t("cart")}</h1>
<div className="container mx-auto px-6">
<h1 className="text-3xl font-bold mb-6 pt-3">{t("cart")}</h1>
<div className="flex flex-col md:flex-row gap-6">
<div className="flex-1">
@@ -220,6 +225,10 @@ export default function CartPage() {
regionGroups={regionGroups}
availableRegions={availableRegions}
paymentTypes={paymentTypes}
phone={phone}
name={name}
onPhoneChange={setPhone}
onNameChange={setName}
onPaymentTypeChange={setPaymentType}
onDeliveryTypeChange={handleDeliveryTypeChange}
onRegionChange={setSelectedRegion}
@@ -227,8 +236,6 @@ export default function CartPage() {
onNoteChange={setNote}
onCompleteOrder={handleCompleteOrder}
isLoading={isCreatingOrder}
phone={phone}
onPhoneChange={setPhone}
/>
</div>
</div>

View File

@@ -71,7 +71,7 @@ export default function FavoritesPage() {
price_color="#0059ff"
height={360}
width={250}
button={true}
button={false}
stock={product.stock}
/>
);

View File

@@ -12,10 +12,10 @@ import {
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Input } from "@/components/ui/input";
import DeliveryTypeSelector from "./DeliveryTypeSelector";
import { useTranslations } from "next-intl";
import type { DeliveryType, PaymentType, Province } from "@/lib/types/api";
import { Input } from "@/components/ui/input";
interface OrderBillingItem {
title: string;
@@ -44,7 +44,9 @@ interface OrderSummaryProps {
availableRegions: string[];
paymentTypes: PaymentType[];
phone: string;
name: string;
onPhoneChange: (phone: string) => void;
onNameChange: (name: string) => void;
onPaymentTypeChange: (type: PaymentType) => void;
onDeliveryTypeChange: (type: DeliveryType) => void;
onRegionChange: (regionCode: string) => void;
@@ -64,7 +66,10 @@ export default function OrderSummary({
regionGroups,
availableRegions,
paymentTypes,
phone, onPhoneChange,
phone,
name,
onPhoneChange,
onNameChange,
onPaymentTypeChange,
onDeliveryTypeChange,
onRegionChange,
@@ -78,10 +83,44 @@ export default function OrderSummary({
const provincesForSelectedRegion = selectedRegion
? regionGroups[selectedRegion] || []
: [];
const isFormValid = selectedRegion && selectedProvince && paymentType && phone;
const isFormValid =
selectedRegion && selectedProvince && paymentType && phone && name;
return (
<Card className="w-full md:w-[380px] p-6 rounded-xl h-fit sticky top-20">
{/* Customer Information */}
<div className="mb-6">
<h3 className="text-lg font-semibold mb-3">
{t("customer_information")}
</h3>
<div className="space-y-4">
<div>
<Label className="text-sm font-medium mb-2 block">
{t("name")}
</Label>
<Input
type="text"
value={name}
onChange={(e) => onNameChange(e.target.value)}
placeholder={t("name")}
className="rounded-xl"
/>
</div>
<div>
<Label className="text-sm font-medium mb-2 block">
{t("phone")}
</Label>
<Input
type="tel"
value={phone}
onChange={(e) => onPhoneChange(e.target.value)}
placeholder={t("phone")}
className="rounded-xl"
/>
</div>
</div>
</div>
{/* Payment Type */}
<div className="mb-6">
<h3 className="text-lg font-semibold mb-3">{t("payment_type")}</h3>
@@ -171,17 +210,6 @@ export default function OrderSummary({
</div>
)}
{/* Phone Number */}
<div className="mb-6">
<Label className="text-lg font-semibold mb-3 block">{t("phone")}</Label>
<Input
type="tel"
value={phone}
onChange={(e) => onPhoneChange(e.target.value)}
placeholder={t("phone")}
className="rounded-xl"
/>
</div>
{/* Note */}
<div className="mb-6">
<Label className="text-lg font-semibold mb-3 block">{t("note")}</Label>

View File

@@ -3,7 +3,15 @@
import { useState, useCallback, useMemo, useRef, useEffect } from "react";
import Image from "next/image";
import Link from "next/link";
import { Minus, Plus, Heart, ShoppingCart, Store, Loader2, AlertTriangle } from "lucide-react";
import {
Minus,
Plus,
Heart,
ShoppingCart,
Store,
Loader2,
AlertTriangle,
} from "lucide-react";
import { Button } from "@/components/ui/button";
import { Card } from "@/components/ui/card";
import { Separator } from "@/components/ui/separator";
@@ -17,7 +25,12 @@ import {
DialogTitle,
} from "@/components/ui/dialog";
import { useProductsBySlug } from "@/features/products/hooks/useProducts";
import { useAddToCart, useUpdateCartItemQuantity, useRemoveFromCart, useCart } from "@/features/cart/hooks/useCart";
import {
useAddToCart,
useUpdateCartItemQuantity,
useRemoveFromCart,
useCart,
} from "@/features/cart/hooks/useCart";
import { useTranslations } from "next-intl";
import { toast } from "sonner";
@@ -25,7 +38,7 @@ interface ProductDetailProps {
slug: string;
}
const PENDING_PRODUCT_UPDATES_KEY = 'pendingProductUpdates';
const PENDING_PRODUCT_UPDATES_KEY = "pendingProductUpdates";
interface PendingUpdate {
quantity: number;
@@ -52,22 +65,29 @@ export default function ProductPageContent({ slug }: ProductDetailProps) {
const retrySyncRef = useRef<((quantity: number) => void) | null>(null);
const autoplayTimerRef = useRef<NodeJS.Timeout | undefined>(undefined);
const { data: product, isLoading: productLoading, error } = useProductsBySlug(slug);
const {
data: product,
isLoading: productLoading,
error,
} = useProductsBySlug(slug);
const { data: cartData, refetch: refetchCart } = useCart();
const addToCartMutation = useAddToCart();
const updateCartMutation = useUpdateCartItemQuantity();
const removeFromCartMutation = useRemoveFromCart();
const cartItem = useMemo(() =>
cartData?.data?.find((item: any) => item.product?.id === product?.id),
const cartItem = useMemo(
() => cartData?.data?.find((item: any) => item.product?.id === product?.id),
[cartData, product]
);
const isInCart = !!cartItem;
const availableStock = product?.stock || 0;
const imageUrls = useMemo(() =>
product?.media?.map(m => m.images_800x800 || m.images_720x720 || m.thumbnail) || [],
const imageUrls = useMemo(
() =>
product?.media?.map(
(m) => m.images_800x800 || m.images_720x720 || m.thumbnail
) || [],
[product]
);
@@ -77,7 +97,7 @@ export default function ProductPageContent({ slug }: ProductDetailProps) {
const startAutoplay = () => {
autoplayTimerRef.current = setInterval(() => {
setSelectedImage(prev => (prev + 1) % imageUrls.length);
setSelectedImage((prev) => (prev + 1) % imageUrls.length);
}, 3000);
};
@@ -91,20 +111,23 @@ export default function ProductPageContent({ slug }: ProductDetailProps) {
}, [imageUrls.length]);
// Reset autoplay timer when user manually selects image
const handleImageSelect = useCallback((index: number) => {
setSelectedImage(index);
const handleImageSelect = useCallback(
(index: number) => {
setSelectedImage(index);
// Reset autoplay timer
if (autoplayTimerRef.current) {
clearInterval(autoplayTimerRef.current);
}
// Reset autoplay timer
if (autoplayTimerRef.current) {
clearInterval(autoplayTimerRef.current);
}
if (imageUrls.length > 1) {
autoplayTimerRef.current = setInterval(() => {
setSelectedImage(prev => (prev + 1) % imageUrls.length);
}, 3000);
}
}, [imageUrls.length]);
if (imageUrls.length > 1) {
autoplayTimerRef.current = setInterval(() => {
setSelectedImage((prev) => (prev + 1) % imageUrls.length);
}, 3000);
}
},
[imageUrls.length]
);
useEffect(() => {
if (cartItem?.product_quantity) {
@@ -112,24 +135,32 @@ export default function ProductPageContent({ slug }: ProductDetailProps) {
}
}, [cartItem]);
const savePendingUpdate = useCallback((quantity: number) => {
if (!product?.id) return;
const savePendingUpdate = useCallback(
(quantity: number) => {
if (!product?.id) return;
try {
const stored = sessionStorage.getItem(PENDING_PRODUCT_UPDATES_KEY);
const pending: Record<number, PendingUpdate> = stored ? JSON.parse(stored) : {};
try {
const stored = sessionStorage.getItem(PENDING_PRODUCT_UPDATES_KEY);
const pending: Record<number, PendingUpdate> = stored
? JSON.parse(stored)
: {};
pending[product.id] = {
quantity,
timestamp: Date.now(),
retryCount: retryCountRef.current
};
pending[product.id] = {
quantity,
timestamp: Date.now(),
retryCount: retryCountRef.current,
};
sessionStorage.setItem(PENDING_PRODUCT_UPDATES_KEY, JSON.stringify(pending));
} catch (error) {
console.error('Failed to save pending update:', error);
}
}, [product?.id]);
sessionStorage.setItem(
PENDING_PRODUCT_UPDATES_KEY,
JSON.stringify(pending)
);
} catch (error) {
console.error("Failed to save pending update:", error);
}
},
[product?.id]
);
const clearPendingUpdate = useCallback(() => {
if (!product?.id) return;
@@ -143,90 +174,109 @@ export default function ProductPageContent({ slug }: ProductDetailProps) {
if (Object.keys(pending).length === 0) {
sessionStorage.removeItem(PENDING_PRODUCT_UPDATES_KEY);
} else {
sessionStorage.setItem(PENDING_PRODUCT_UPDATES_KEY, JSON.stringify(pending));
sessionStorage.setItem(
PENDING_PRODUCT_UPDATES_KEY,
JSON.stringify(pending)
);
}
}
} catch (error) {
console.error('Failed to clear pending update:', error);
console.error("Failed to clear pending update:", error);
}
}, [product?.id]);
const retrySync = useCallback((quantity: number) => {
const maxRetries = 4;
const retryCount = retryCountRef.current;
const retrySync = useCallback(
(quantity: number) => {
const maxRetries = 4;
const retryCount = retryCountRef.current;
if (retryCount >= maxRetries) {
setSyncError(true);
setIsSyncing(false);
toast.error(t("error"), {
description: t("update_quantity_failed"),
});
return;
}
if (retryCount >= maxRetries) {
setSyncError(true);
setIsSyncing(false);
toast.error(t("error"), {
description: t("update_quantity_failed"),
});
return;
}
const delay = Math.min(1000 * Math.pow(2, retryCount), 16000);
retryCountRef.current++;
const delay = Math.min(1000 * Math.pow(2, retryCount), 16000);
retryCountRef.current++;
retryTimerRef.current = setTimeout(() => {
syncToServerRef.current?.(quantity);
}, delay);
}, [t]);
retryTimerRef.current = setTimeout(() => {
syncToServerRef.current?.(quantity);
}, delay);
},
[t]
);
retrySyncRef.current = retrySync;
const syncToServer = useCallback(async (quantity: number) => {
if (!product?.id) return;
const syncToServer = useCallback(
async (quantity: number) => {
if (!product?.id) return;
if (isRequestInFlightRef.current) {
pendingQuantityRef.current = quantity;
return;
}
isRequestInFlightRef.current = true;
setIsSyncing(true);
setSyncError(false);
try {
// If quantity is 0, remove from cart
if (quantity === 0) {
await removeFromCartMutation.mutateAsync(product.id);
toast.success(t("removed_from_cart"));
} else if (isInCart) {
await updateCartMutation.mutateAsync({
productId: product.id,
quantity: quantity,
});
} else {
await addToCartMutation.mutateAsync({
productId: product.id,
quantity: quantity,
});
if (isRequestInFlightRef.current) {
pendingQuantityRef.current = quantity;
return;
}
isRequestInFlightRef.current = false;
setIsSyncing(false);
retryCountRef.current = 0;
clearPendingUpdate();
isRequestInFlightRef.current = true;
setIsSyncing(true);
setSyncError(false);
await refetchCart();
try {
// If quantity is 0, remove from cart
if (quantity === 0) {
await removeFromCartMutation.mutateAsync(product.id);
toast.success(t("removed_from_cart"));
} else if (isInCart) {
await updateCartMutation.mutateAsync({
productId: product.id,
quantity: quantity,
});
} else {
await addToCartMutation.mutateAsync({
productId: product.id,
quantity: quantity,
});
}
if (pendingQuantityRef.current !== null) {
const nextQuantity = pendingQuantityRef.current;
pendingQuantityRef.current = null;
setTimeout(() => syncToServerRef.current?.(nextQuantity), 100);
}
} catch (error) {
console.error('Sync failed:', error);
isRequestInFlightRef.current = false;
if (retryCountRef.current >= 3) {
setLocalQuantity(cartItem?.product_quantity || 1);
isRequestInFlightRef.current = false;
setIsSyncing(false);
retryCountRef.current = 0;
clearPendingUpdate();
}
retrySyncRef.current?.(quantity);
}
}, [product?.id, isInCart, updateCartMutation, addToCartMutation, removeFromCartMutation, cartItem, clearPendingUpdate, refetchCart, t]);
await refetchCart();
if (pendingQuantityRef.current !== null) {
const nextQuantity = pendingQuantityRef.current;
pendingQuantityRef.current = null;
setTimeout(() => syncToServerRef.current?.(nextQuantity), 100);
}
} catch (error) {
console.error("Sync failed:", error);
isRequestInFlightRef.current = false;
if (retryCountRef.current >= 3) {
setLocalQuantity(cartItem?.product_quantity || 1);
clearPendingUpdate();
}
retrySyncRef.current?.(quantity);
}
},
[
product?.id,
isInCart,
updateCartMutation,
addToCartMutation,
removeFromCartMutation,
cartItem,
clearPendingUpdate,
refetchCart,
t,
]
);
syncToServerRef.current = syncToServer;
@@ -240,16 +290,22 @@ export default function ProductPageContent({ slug }: ProductDetailProps) {
const pending: Record<number, PendingUpdate> = JSON.parse(stored);
const productPending = pending[product.id];
if (productPending && productPending.quantity !== (cartItem?.product_quantity || 1)) {
if (
productPending &&
productPending.quantity !== (cartItem?.product_quantity || 1)
) {
setLocalQuantity(productPending.quantity);
pendingQuantityRef.current = productPending.quantity;
retryCountRef.current = productPending.retryCount;
setTimeout(() => syncToServerRef.current?.(productPending.quantity), 500);
setTimeout(
() => syncToServerRef.current?.(productPending.quantity),
500
);
}
}
} catch (error) {
console.error('Failed to load pending updates:', error);
console.error("Failed to load pending updates:", error);
}
};
@@ -321,38 +377,41 @@ export default function ProductPageContent({ slug }: ProductDetailProps) {
return;
}
setLocalQuantity(prev => prev + 1);
setLocalQuantity((prev) => prev + 1);
}, [localQuantity, availableStock]);
const handleQuantityDecrease = useCallback(() => {
// Allow decreasing to 0 to remove from cart
if (localQuantity <= 0) return;
setLocalQuantity(prev => prev - 1);
setLocalQuantity((prev) => prev - 1);
}, [localQuantity]);
const handleToggleFavorite = useCallback(() => {
setIsFavorite(!isFavorite);
}, [isFavorite]);
const loadingSkeleton = useMemo(() => (
<div className="container mx-auto px-4 py-8">
<div className="flex flex-col lg:flex-row gap-8">
<div className="flex-1 max-w-2xl">
<Skeleton className="aspect-square w-full rounded-2xl" />
<div className="mt-4 flex gap-2">
{[1, 2, 3].map((i) => (
<Skeleton key={i} className="w-16 h-16 rounded" />
))}
const loadingSkeleton = useMemo(
() => (
<div className="container mx-auto px-4 py-8">
<div className="flex flex-col lg:flex-row gap-8">
<div className="flex-1 max-w-2xl">
<Skeleton className="aspect-square w-full rounded-2xl" />
<div className="mt-4 flex gap-2">
{[1, 2, 3].map((i) => (
<Skeleton key={i} className="w-16 h-16 rounded" />
))}
</div>
</div>
<div className="flex-1 space-y-6">
<Skeleton className="h-10 w-64" />
<Skeleton className="h-20 w-full" />
</div>
</div>
<div className="flex-1 space-y-6">
<Skeleton className="h-10 w-64" />
<Skeleton className="h-20 w-full" />
</div>
</div>
</div>
), []);
),
[]
);
if (productLoading) {
return loadingSkeleton;
@@ -361,19 +420,23 @@ export default function ProductPageContent({ slug }: ProductDetailProps) {
if (error || !product) {
return (
<div className="container mx-auto px-4 py-8 text-center">
<h2 className="text-2xl font-bold text-red-600">{t("product_not_found")}</h2>
<p className="text-gray-500 mt-2">{t("product_not_found_description")}</p>
<h2 className="text-2xl font-bold text-red-600">
{t("product_not_found")}
</h2>
<p className="text-gray-500 mt-2">
{t("product_not_found_description")}
</p>
</div>
);
}
return (
<>
<div className="container mx-auto px-4 py-8">
<div className="flex flex-col lg:flex-row gap-8">
<div className="px-2 md:px-4 lg:px-6 rounded-lg pb-12 space-y-8 max-w-[1504px] mx-auto">
<div className="flex flex-col lg:flex-row gap-8 bg-white p-4">
<div className="flex-1 max-w-2xl">
<div className="relative">
<div className="relative aspect-square w-full rounded-2xl overflow-hidden bg-gray-50">
<div className="relative aspect-square w-full rounded-2xl overflow-hidden bg-white">
{imageUrls.length > 0 ? (
<Image
src={imageUrls[selectedImage]}
@@ -414,30 +477,20 @@ export default function ProductPageContent({ slug }: ProductDetailProps) {
</div>
</div>
<div className="flex-1 space-y-6">
<div className="flex-1 space-y-6 bg-white">
<div>
<h1 className="text-3xl font-bold mb-2">{product.name}</h1>
{product.categories && product.categories.length > 0 && (
<div className="flex gap-2 flex-wrap mt-2">
{product.categories.map((cat, idx) => (
<span
key={idx}
className="text-sm px-3 py-1 bg-gray-100 rounded-full text-gray-600"
>
{cat.name}
</span>
))}
</div>
)}
</div>
<Card className="p-4 rounded-xl border-gray-200">
<h3 className="text-xl font-semibold mb-4">{t("about_product")}</h3>
<h3 className="text-xl font-semibold mb-4">
{t("about_product")}
</h3>
<div className="space-y-3">
{product.brand?.name && (
<>
<div className="flex justify-between items-center py-2">
<span className="text-gray-500">{t("brand")}</span>
<span className="text-gray-500">{t("brands")}</span>
<span className="font-medium">{product.brand.name}</span>
</div>
<Separator />
@@ -448,8 +501,20 @@ export default function ProductPageContent({ slug }: ProductDetailProps) {
<>
<div className="flex justify-between items-center py-2">
<span className="text-gray-500">{t("stock")}</span>
<span className={`font-medium ${product.stock === 0 ? 'text-red-500' : product.stock <= 5 ? 'text-orange-600' : 'text-green-600'}`}>
{product.stock === 0 ? t("out_of_stock") : product.stock <= 5 ? `${t("only_left", { count: product.stock })}` : product.stock}
<span
className={`font-medium ${
product.stock === 0
? "text-red-500"
: product.stock <= 5
? "text-orange-600"
: "text-green-600"
}`}
>
{product.stock === 0
? t("out_of_stock")
: product.stock <= 5
? `${t("only_left", { count: product.stock })}`
: product.stock}
</span>
</div>
<Separator />
@@ -460,7 +525,9 @@ export default function ProductPageContent({ slug }: ProductDetailProps) {
<>
<div className="flex justify-between items-center py-2">
<span className="text-gray-500">{t("barcode")}</span>
<span className="font-mono text-sm">{product.barcode}</span>
<span className="font-mono text-sm">
{product.barcode}
</span>
</div>
<Separator />
</>
@@ -478,17 +545,20 @@ export default function ProductPageContent({ slug }: ProductDetailProps) {
{product.properties && product.properties.length > 0 && (
<>
{product.properties.map((prop, idx) => (
prop.value && (
<div key={idx}>
<div className="flex justify-between items-center py-2">
<span className="text-gray-500">{prop.name}</span>
<span className="font-medium">{prop.value}</span>
{product.properties.map(
(prop, idx) =>
prop.value && (
<div key={idx}>
<div className="flex justify-between items-center py-2">
<span className="text-gray-500">{prop.name}</span>
<span className="font-medium">{prop.value}</span>
</div>
{idx < product.properties.length - 1 && (
<Separator />
)}
</div>
{idx < product.properties.length - 1 && <Separator />}
</div>
)
))}
)
)}
</>
)}
</div>
@@ -496,7 +566,9 @@ export default function ProductPageContent({ slug }: ProductDetailProps) {
{product.description && (
<Card className="p-4 rounded-xl border-gray-200">
<h3 className="text-xl font-semibold mb-3">{t("product_description")}</h3>
<h3 className="text-xl font-semibold mb-3">
{t("product_description")}
</h3>
<div
className="text-gray-700 leading-relaxed prose prose-sm max-w-none"
dangerouslySetInnerHTML={{ __html: product.description }}
@@ -513,21 +585,22 @@ export default function ProductPageContent({ slug }: ProductDetailProps) {
<span className="text-3xl font-bold text-primary">
{product.price_amount} TMT
</span>
{product.old_price_amount && parseFloat(product.old_price_amount) > 0 && (
<span className="text-lg text-gray-400 line-through">
{product.old_price_amount} TMT
</span>
)}
{product.old_price_amount &&
parseFloat(product.old_price_amount) > 0 && (
<span className="text-lg text-gray-400 line-through">
{product.old_price_amount} TMT
</span>
)}
</div>
</div>
<div className="space-y-3">
<div className="space-y-2">
{isInCart ? (
<>
<Link href="/cart">
<Button
size="lg"
className="w-full rounded-xl text-lg font-bold bg-green-600 hover:bg-green-700"
className="w-full rounded-lg text-lg font-bold bg-green-600 hover:bg-green-700 mb-4"
>
<ShoppingCart className="mr-2 h-5 w-5" />
{t("go_to_cart")}
@@ -540,17 +613,20 @@ export default function ProductPageContent({ slug }: ProductDetailProps) {
size="icon"
onClick={handleQuantityDecrease}
disabled={isSyncing}
className={`rounded-xl h-12 w-12 ${isSyncing ? 'opacity-70' : ''}`}
className={`rounded-lg h-12 w-12 ${
isSyncing ? "opacity-70" : ""
}`}
>
<Minus className="h-5 w-5" />
</Button>
<div className="flex-1 text-center font-semibold text-xl border rounded-xl h-12 flex items-center justify-center relative">
{localQuantity}
{isSyncing && (
<Loader2 className="h-4 w-4 animate-spin absolute -top-1 -right-1 text-blue-500" />
)}
{syncError && (
<span className="absolute -top-1 -right-1 h-2 w-2 bg-red-500 rounded-full" title="Sync error" />
<span
className="absolute -top-1 -right-1 h-2 w-2 bg-red-500 rounded-full"
title="Sync error"
/>
)}
</div>
<Button
@@ -558,12 +634,35 @@ export default function ProductPageContent({ slug }: ProductDetailProps) {
size="icon"
onClick={handleQuantityIncrease}
disabled={localQuantity >= availableStock || isSyncing}
className={`rounded-xl h-12 w-12 ${isSyncing ? 'opacity-70' : ''} ${
localQuantity >= availableStock ? 'opacity-50 cursor-not-allowed' : ''
className={`rounded-lg h-12 w-12 ${
isSyncing ? "opacity-70" : ""
} ${
localQuantity >= availableStock
? "opacity-50 cursor-not-allowed"
: ""
}`}
>
<Plus className="h-5 w-5" />
</Button>
<Button
variant="outline"
size="icon"
onClick={handleToggleFavorite}
className={`rounded-lg h-12 w-12 transition-all border cursor-pointer ${
isFavorite
? "bg-[#F0F8FF] border-blue-300 hover:bg-blue-100"
: "hover:bg-gray-50"
}`}
>
<Heart
className={`h-6! w-6! transition-all ${
isFavorite
? "fill-[#005bff] text-[#005bff]"
: "text-[#005bff]"
}`}
/>
</Button>
</div>
</>
) : (
@@ -571,7 +670,7 @@ export default function ProductPageContent({ slug }: ProductDetailProps) {
size="lg"
onClick={handleAddToCart}
disabled={isSyncing || product.stock === 0}
className="w-full rounded-xl text-lg font-bold"
className="w-full rounded-lg text-lg font-bold bg-[#005bff] hover:bg-[#0041c4] cursor-pointer"
>
{isSyncing ? (
<>
@@ -581,28 +680,13 @@ export default function ProductPageContent({ slug }: ProductDetailProps) {
) : (
<>
<ShoppingCart className="mr-2 h-5 w-5" />
{product.stock === 0 ? t("out_of_stock") : t("add_to_cart")}
{product.stock === 0
? t("out_of_stock")
: t("add_to_cart")}
</>
)}
</Button>
)}
<Button
variant="outline"
size="lg"
onClick={handleToggleFavorite}
className={`w-full rounded-xl transition-all ${
isFavorite
? "bg-red-50 border-red-300 hover:bg-red-100"
: "hover:bg-gray-50"
}`}
>
<Heart
className={`h-6 w-6 transition-all ${
isFavorite ? "fill-red-500 text-red-500" : "text-gray-600"
}`}
/>
</Button>
</div>
</Card>
@@ -616,13 +700,15 @@ export default function ProductPageContent({ slug }: ProductDetailProps) {
</Avatar>
<div>
<p className="text-sm text-gray-500">{t("store")}</p>
<h4 className="text-lg font-bold">{product.channel[0].name}</h4>
<h4 className="text-lg font-bold">
{product.channel[0].name}
</h4>
</div>
</div>
<Button
variant="outline"
size="lg"
className="w-full rounded-xl"
className="w-full rounded-lg"
>
{t("write_to_store")}
</Button>
@@ -646,14 +732,14 @@ export default function ProductPageContent({ slug }: ProductDetailProps) {
<DialogDescription className="text-center text-base pt-2">
{t("stock_limit_message", {
product: product.name,
stock: availableStock
stock: availableStock,
})}
</DialogDescription>
</DialogHeader>
<div className="flex justify-center mt-4">
<Button
onClick={() => setShowStockModal(false)}
className="w-full rounded-xl"
className="w-full rounded-lg"
>
{t("understood")}
</Button>

View File

@@ -109,7 +109,6 @@
"my_orders": "Мои заказы",
"active_orders": "Активные заказы",
"completed_orders": "Завершенные заказы",
"cancel_order": "Отменить заказ",
"keep_order": "Оставить заказ",
"cancel_confirmation": "Вы уверены, что хотите отменить этот заказ?",
"cancelling": "Отмена...",
@@ -142,12 +141,12 @@
"login_success": "Вход выполнен успешно",
"error_occurred": "Произошла ошибка",
"wrong_code": "Неверный код",
"phone_format": "Формат: 99365123456",
"phone_format": "Формат: 65123456",
"sending": "Отправка...",
"verifying": "Проверка...",
"verify": "Подтвердить",
"only_left": "Sadece {count} adet kaldı",
"stock_limit_title": "Stok Limiti",
"stock_limit_message": "{product} ürününden sadece {stock} adet mevcut. Daha fazla ekleyemezsiniz.",
"understood": "Anladım"
"only_left": "Осталось {count} шт.",
"stock_limit_title": "Недостаточно на складе",
"stock_limit_message": "{product} закончился. Можно купить только {stock} шт.",
"understood": "Понятно"
}

View File

@@ -109,7 +109,6 @@
"my_orders": "Meniň sargytlarym",
"active_orders": "Işjeň sargytlar",
"completed_orders": "Tamamlanan sargytlar",
"cancel_order": "Sargydy ýatyrmak",
"keep_order": "Sargydy saklamak",
"cancel_confirmation": "Siz bu sargydy ýatyrmagy hakykatdanam isleýärsiňizmi?",
"cancelling": "Ýatyrylýar...",
@@ -142,12 +141,12 @@
"login_success": "Giriş üstünlikli boldy",
"error_occurred": "Ýalňyşlyk ýüze çykdy",
"wrong_code": "Kod nädogry",
"phone_format": "Format: 99365123456",
"phone_format": "Format: 65123456",
"sending": "Iberilýär...",
"verifying": "Barlanýar...",
"verify": "Tassyklamak",
"only_left": "Sadece {count} adet kaldı",
"stock_limit_title": "Stok Limiti",
"stock_limit_message": "{product} ürününden sadece {stock} adet mevcut. Daha fazla ekleyemezsiniz.",
"understood": "Anladım"
"only_left": "Diňe {count} sany galdy",
"stock_limit_title": "Çäkli sanda",
"stock_limit_message": "{product} harytdan diňe {stock} sany bar. Mundan köp sebediňize goşup bilmersiňiz.",
"understood": "Düşündim"
}