fixed some ui styles
This commit is contained in:
@@ -24,6 +24,7 @@ export default function CartPage() {
|
||||
const [selectedProvince, setSelectedProvince] = useState<number | null>(null);
|
||||
const [note, setNote] = 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>
|
||||
|
||||
@@ -71,7 +71,7 @@ export default function FavoritesPage() {
|
||||
price_color="#0059ff"
|
||||
height={360}
|
||||
width={250}
|
||||
button={true}
|
||||
button={false}
|
||||
stock={product.stock}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,7 +111,8 @@ export default function ProductPageContent({ slug }: ProductDetailProps) {
|
||||
}, [imageUrls.length]);
|
||||
|
||||
// Reset autoplay timer when user manually selects image
|
||||
const handleImageSelect = useCallback((index: number) => {
|
||||
const handleImageSelect = useCallback(
|
||||
(index: number) => {
|
||||
setSelectedImage(index);
|
||||
|
||||
// Reset autoplay timer
|
||||
@@ -101,10 +122,12 @@ export default function ProductPageContent({ slug }: ProductDetailProps) {
|
||||
|
||||
if (imageUrls.length > 1) {
|
||||
autoplayTimerRef.current = setInterval(() => {
|
||||
setSelectedImage(prev => (prev + 1) % imageUrls.length);
|
||||
setSelectedImage((prev) => (prev + 1) % imageUrls.length);
|
||||
}, 3000);
|
||||
}
|
||||
}, [imageUrls.length]);
|
||||
},
|
||||
[imageUrls.length]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (cartItem?.product_quantity) {
|
||||
@@ -112,24 +135,32 @@ export default function ProductPageContent({ slug }: ProductDetailProps) {
|
||||
}
|
||||
}, [cartItem]);
|
||||
|
||||
const savePendingUpdate = useCallback((quantity: number) => {
|
||||
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) : {};
|
||||
const pending: Record<number, PendingUpdate> = stored
|
||||
? JSON.parse(stored)
|
||||
: {};
|
||||
|
||||
pending[product.id] = {
|
||||
quantity,
|
||||
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) {
|
||||
console.error('Failed to save pending update:', error);
|
||||
console.error("Failed to save pending update:", error);
|
||||
}
|
||||
}, [product?.id]);
|
||||
},
|
||||
[product?.id]
|
||||
);
|
||||
|
||||
const clearPendingUpdate = useCallback(() => {
|
||||
if (!product?.id) return;
|
||||
@@ -143,15 +174,19 @@ 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 retrySync = useCallback(
|
||||
(quantity: number) => {
|
||||
const maxRetries = 4;
|
||||
const retryCount = retryCountRef.current;
|
||||
|
||||
@@ -170,11 +205,14 @@ export default function ProductPageContent({ slug }: ProductDetailProps) {
|
||||
retryTimerRef.current = setTimeout(() => {
|
||||
syncToServerRef.current?.(quantity);
|
||||
}, delay);
|
||||
}, [t]);
|
||||
},
|
||||
[t]
|
||||
);
|
||||
|
||||
retrySyncRef.current = retrySync;
|
||||
|
||||
const syncToServer = useCallback(async (quantity: number) => {
|
||||
const syncToServer = useCallback(
|
||||
async (quantity: number) => {
|
||||
if (!product?.id) return;
|
||||
|
||||
if (isRequestInFlightRef.current) {
|
||||
@@ -216,7 +254,7 @@ export default function ProductPageContent({ slug }: ProductDetailProps) {
|
||||
setTimeout(() => syncToServerRef.current?.(nextQuantity), 100);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Sync failed:', error);
|
||||
console.error("Sync failed:", error);
|
||||
isRequestInFlightRef.current = false;
|
||||
|
||||
if (retryCountRef.current >= 3) {
|
||||
@@ -226,7 +264,19 @@ export default function ProductPageContent({ slug }: ProductDetailProps) {
|
||||
|
||||
retrySyncRef.current?.(quantity);
|
||||
}
|
||||
}, [product?.id, isInCart, updateCartMutation, addToCartMutation, removeFromCartMutation, cartItem, clearPendingUpdate, refetchCart, t]);
|
||||
},
|
||||
[
|
||||
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,21 +377,22 @@ 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(() => (
|
||||
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">
|
||||
@@ -352,7 +409,9 @@ export default function ProductPageContent({ slug }: ProductDetailProps) {
|
||||
</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) => (
|
||||
{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 />}
|
||||
{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,7 +585,8 @@ 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 && (
|
||||
{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>
|
||||
@@ -521,13 +594,13 @@ export default function ProductPageContent({ slug }: ProductDetailProps) {
|
||||
</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>
|
||||
|
||||
@@ -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": "Понятно"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
Reference in New Issue
Block a user