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

View File

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

View File

@@ -12,10 +12,10 @@ import {
SelectTrigger, SelectTrigger,
SelectValue, SelectValue,
} from "@/components/ui/select"; } from "@/components/ui/select";
import { Input } from "@/components/ui/input";
import DeliveryTypeSelector from "./DeliveryTypeSelector"; import DeliveryTypeSelector from "./DeliveryTypeSelector";
import { useTranslations } from "next-intl"; import { useTranslations } from "next-intl";
import type { DeliveryType, PaymentType, Province } from "@/lib/types/api"; import type { DeliveryType, PaymentType, Province } from "@/lib/types/api";
import { Input } from "@/components/ui/input";
interface OrderBillingItem { interface OrderBillingItem {
title: string; title: string;
@@ -44,7 +44,9 @@ interface OrderSummaryProps {
availableRegions: string[]; availableRegions: string[];
paymentTypes: PaymentType[]; paymentTypes: PaymentType[];
phone: string; phone: string;
name: string;
onPhoneChange: (phone: string) => void; onPhoneChange: (phone: string) => void;
onNameChange: (name: string) => void;
onPaymentTypeChange: (type: PaymentType) => void; onPaymentTypeChange: (type: PaymentType) => void;
onDeliveryTypeChange: (type: DeliveryType) => void; onDeliveryTypeChange: (type: DeliveryType) => void;
onRegionChange: (regionCode: string) => void; onRegionChange: (regionCode: string) => void;
@@ -64,7 +66,10 @@ export default function OrderSummary({
regionGroups, regionGroups,
availableRegions, availableRegions,
paymentTypes, paymentTypes,
phone, onPhoneChange, phone,
name,
onPhoneChange,
onNameChange,
onPaymentTypeChange, onPaymentTypeChange,
onDeliveryTypeChange, onDeliveryTypeChange,
onRegionChange, onRegionChange,
@@ -78,10 +83,44 @@ export default function OrderSummary({
const provincesForSelectedRegion = selectedRegion const provincesForSelectedRegion = selectedRegion
? regionGroups[selectedRegion] || [] ? regionGroups[selectedRegion] || []
: []; : [];
const isFormValid = selectedRegion && selectedProvince && paymentType && phone; const isFormValid =
selectedRegion && selectedProvince && paymentType && phone && name;
return ( return (
<Card className="w-full md:w-[380px] p-6 rounded-xl h-fit sticky top-20"> <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 */} {/* Payment Type */}
<div className="mb-6"> <div className="mb-6">
<h3 className="text-lg font-semibold mb-3">{t("payment_type")}</h3> <h3 className="text-lg font-semibold mb-3">{t("payment_type")}</h3>
@@ -171,17 +210,6 @@ export default function OrderSummary({
</div> </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 */} {/* Note */}
<div className="mb-6"> <div className="mb-6">
<Label className="text-lg font-semibold mb-3 block">{t("note")}</Label> <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 { useState, useCallback, useMemo, useRef, useEffect } from "react";
import Image from "next/image"; import Image from "next/image";
import Link from "next/link"; 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 { Button } from "@/components/ui/button";
import { Card } from "@/components/ui/card"; import { Card } from "@/components/ui/card";
import { Separator } from "@/components/ui/separator"; import { Separator } from "@/components/ui/separator";
@@ -17,7 +25,12 @@ import {
DialogTitle, DialogTitle,
} from "@/components/ui/dialog"; } from "@/components/ui/dialog";
import { useProductsBySlug } from "@/features/products/hooks/useProducts"; 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 { useTranslations } from "next-intl";
import { toast } from "sonner"; import { toast } from "sonner";
@@ -25,7 +38,7 @@ 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;
@@ -40,7 +53,7 @@ export default function ProductPageContent({ slug }: ProductDetailProps) {
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 t = useTranslations(); const t = useTranslations();
const debounceTimerRef = useRef<NodeJS.Timeout | undefined>(undefined); const debounceTimerRef = useRef<NodeJS.Timeout | undefined>(undefined);
@@ -52,22 +65,29 @@ export default function ProductPageContent({ slug }: ProductDetailProps) {
const retrySyncRef = useRef<((quantity: number) => void) | null>(null); const retrySyncRef = useRef<((quantity: number) => void) | null>(null);
const autoplayTimerRef = useRef<NodeJS.Timeout | undefined>(undefined); 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 { data: cartData, refetch: refetchCart } = useCart();
const addToCartMutation = useAddToCart(); const addToCartMutation = useAddToCart();
const updateCartMutation = useUpdateCartItemQuantity(); const updateCartMutation = useUpdateCartItemQuantity();
const removeFromCartMutation = useRemoveFromCart(); const removeFromCartMutation = useRemoveFromCart();
const cartItem = useMemo(() => const cartItem = useMemo(
cartData?.data?.find((item: any) => item.product?.id === product?.id), () => cartData?.data?.find((item: any) => item.product?.id === product?.id),
[cartData, product] [cartData, product]
); );
const isInCart = !!cartItem; const isInCart = !!cartItem;
const availableStock = product?.stock || 0; const availableStock = product?.stock || 0;
const imageUrls = useMemo(() => const imageUrls = useMemo(
product?.media?.map(m => m.images_800x800 || m.images_720x720 || m.thumbnail) || [], () =>
product?.media?.map(
(m) => m.images_800x800 || m.images_720x720 || m.thumbnail
) || [],
[product] [product]
); );
@@ -77,7 +97,7 @@ export default function ProductPageContent({ slug }: ProductDetailProps) {
const startAutoplay = () => { const startAutoplay = () => {
autoplayTimerRef.current = setInterval(() => { autoplayTimerRef.current = setInterval(() => {
setSelectedImage(prev => (prev + 1) % imageUrls.length); setSelectedImage((prev) => (prev + 1) % imageUrls.length);
}, 3000); }, 3000);
}; };
@@ -91,20 +111,23 @@ export default function ProductPageContent({ slug }: ProductDetailProps) {
}, [imageUrls.length]); }, [imageUrls.length]);
// Reset autoplay timer when user manually selects image // Reset autoplay timer when user manually selects image
const handleImageSelect = useCallback((index: number) => { const handleImageSelect = useCallback(
setSelectedImage(index); (index: number) => {
setSelectedImage(index);
// Reset autoplay timer
if (autoplayTimerRef.current) { // Reset autoplay timer
clearInterval(autoplayTimerRef.current); if (autoplayTimerRef.current) {
} clearInterval(autoplayTimerRef.current);
}
if (imageUrls.length > 1) {
autoplayTimerRef.current = setInterval(() => { if (imageUrls.length > 1) {
setSelectedImage(prev => (prev + 1) % imageUrls.length); autoplayTimerRef.current = setInterval(() => {
}, 3000); setSelectedImage((prev) => (prev + 1) % imageUrls.length);
} }, 3000);
}, [imageUrls.length]); }
},
[imageUrls.length]
);
useEffect(() => { useEffect(() => {
if (cartItem?.product_quantity) { if (cartItem?.product_quantity) {
@@ -112,121 +135,148 @@ export default function ProductPageContent({ slug }: ProductDetailProps) {
} }
}, [cartItem]); }, [cartItem]);
const savePendingUpdate = useCallback((quantity: number) => { const savePendingUpdate = useCallback(
if (!product?.id) return; (quantity: number) => {
if (!product?.id) return;
try {
const stored = sessionStorage.getItem(PENDING_PRODUCT_UPDATES_KEY); try {
const pending: Record<number, PendingUpdate> = stored ? JSON.parse(stored) : {}; const stored = sessionStorage.getItem(PENDING_PRODUCT_UPDATES_KEY);
const pending: Record<number, PendingUpdate> = stored
pending[product.id] = { ? JSON.parse(stored)
quantity, : {};
timestamp: Date.now(),
retryCount: retryCountRef.current pending[product.id] = {
}; quantity,
timestamp: Date.now(),
sessionStorage.setItem(PENDING_PRODUCT_UPDATES_KEY, JSON.stringify(pending)); retryCount: retryCountRef.current,
} catch (error) { };
console.error('Failed to save pending update:', error);
} sessionStorage.setItem(
}, [product?.id]); PENDING_PRODUCT_UPDATES_KEY,
JSON.stringify(pending)
);
} catch (error) {
console.error("Failed to save pending update:", error);
}
},
[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((quantity: number) => { const retrySync = useCallback(
const maxRetries = 4; (quantity: number) => {
const retryCount = retryCountRef.current; const maxRetries = 4;
const retryCount = retryCountRef.current;
if (retryCount >= maxRetries) { if (retryCount >= maxRetries) {
setSyncError(true); setSyncError(true);
setIsSyncing(false); setIsSyncing(false);
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(async (quantity: number) => { const syncToServer = useCallback(
if (!product?.id) return; async (quantity: number) => {
if (!product?.id) return;
if (isRequestInFlightRef.current) { if (isRequestInFlightRef.current) {
pendingQuantityRef.current = quantity; pendingQuantityRef.current = quantity;
return; 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,
});
} }
isRequestInFlightRef.current = false; isRequestInFlightRef.current = true;
setIsSyncing(false); setIsSyncing(true);
retryCountRef.current = 0; setSyncError(false);
clearPendingUpdate();
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) { isRequestInFlightRef.current = false;
const nextQuantity = pendingQuantityRef.current; setIsSyncing(false);
pendingQuantityRef.current = null; retryCountRef.current = 0;
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(); clearPendingUpdate();
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);
} }
},
retrySyncRef.current?.(quantity); [
} product?.id,
}, [product?.id, isInCart, updateCartMutation, addToCartMutation, removeFromCartMutation, cartItem, clearPendingUpdate, refetchCart, t]); isInCart,
updateCartMutation,
addToCartMutation,
removeFromCartMutation,
cartItem,
clearPendingUpdate,
refetchCart,
t,
]
);
syncToServerRef.current = syncToServer; syncToServerRef.current = syncToServer;
@@ -239,17 +289,23 @@ export default function ProductPageContent({ slug }: ProductDetailProps) {
if (stored) { if (stored) {
const pending: Record<number, PendingUpdate> = JSON.parse(stored); const pending: Record<number, PendingUpdate> = JSON.parse(stored);
const productPending = pending[product.id]; const productPending = pending[product.id];
if (productPending && productPending.quantity !== (cartItem?.product_quantity || 1)) { if (
productPending &&
productPending.quantity !== (cartItem?.product_quantity || 1)
) {
setLocalQuantity(productPending.quantity); setLocalQuantity(productPending.quantity);
pendingQuantityRef.current = productPending.quantity; pendingQuantityRef.current = productPending.quantity;
retryCountRef.current = productPending.retryCount; retryCountRef.current = productPending.retryCount;
setTimeout(() => syncToServerRef.current?.(productPending.quantity), 500); setTimeout(
() => syncToServerRef.current?.(productPending.quantity),
500
);
} }
} }
} catch (error) { } catch (error) {
console.error('Failed to load pending updates:', error); console.error("Failed to load pending updates:", error);
} }
}; };
@@ -298,11 +354,11 @@ export default function ProductPageContent({ slug }: ProductDetailProps) {
productId: product.id, productId: product.id,
quantity: localQuantity, quantity: localQuantity,
}); });
await refetchCart(); await refetchCart();
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")}`,
}); });
@@ -320,39 +376,42 @@ export default function ProductPageContent({ slug }: ProductDetailProps) {
setShowStockModal(true); setShowStockModal(true);
return; return;
} }
setLocalQuantity(prev => prev + 1); setLocalQuantity((prev) => prev + 1);
}, [localQuantity, availableStock]); }, [localQuantity, availableStock]);
const handleQuantityDecrease = useCallback(() => { const handleQuantityDecrease = useCallback(() => {
// Allow decreasing to 0 to remove from cart // Allow decreasing to 0 to remove from cart
if (localQuantity <= 0) return; if (localQuantity <= 0) return;
setLocalQuantity(prev => prev - 1); setLocalQuantity((prev) => prev - 1);
}, [localQuantity]); }, [localQuantity]);
const handleToggleFavorite = useCallback(() => { const handleToggleFavorite = useCallback(() => {
setIsFavorite(!isFavorite); setIsFavorite(!isFavorite);
}, [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="container mx-auto px-4 py-8">
<div className="flex-1 max-w-2xl"> <div className="flex flex-col lg:flex-row gap-8">
<Skeleton className="aspect-square w-full rounded-2xl" /> <div className="flex-1 max-w-2xl">
<div className="mt-4 flex gap-2"> <Skeleton className="aspect-square w-full rounded-2xl" />
{[1, 2, 3].map((i) => ( <div className="mt-4 flex gap-2">
<Skeleton key={i} className="w-16 h-16 rounded" /> {[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> </div>
<div className="flex-1 space-y-6">
<Skeleton className="h-10 w-64" />
<Skeleton className="h-20 w-full" />
</div>
</div> </div>
</div> ),
), []); []
);
if (productLoading) { if (productLoading) {
return loadingSkeleton; return loadingSkeleton;
@@ -361,19 +420,23 @@ export default function ProductPageContent({ slug }: ProductDetailProps) {
if (error || !product) { if (error || !product) {
return ( return (
<div className="container mx-auto px-4 py-8 text-center"> <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> <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="container mx-auto px-4 py-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"> <div className="flex flex-col lg:flex-row gap-8 bg-white p-4">
<div className="flex-1 max-w-2xl"> <div className="flex-1 max-w-2xl">
<div className="relative"> <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 ? ( {imageUrls.length > 0 ? (
<Image <Image
src={imageUrls[selectedImage]} src={imageUrls[selectedImage]}
@@ -396,8 +459,8 @@ export default function ProductPageContent({ slug }: ProductDetailProps) {
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 flex-shrink-0 rounded 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"
}`} }`}
> >
@@ -414,58 +477,62 @@ export default function ProductPageContent({ slug }: ProductDetailProps) {
</div> </div>
</div> </div>
<div className="flex-1 space-y-6"> <div className="flex-1 space-y-6 bg-white">
<div> <div>
<h1 className="text-3xl font-bold mb-2">{product.name}</h1> <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> </div>
<Card className="p-4 rounded-xl border-gray-200"> <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"> <div className="space-y-3">
{product.brand?.name && ( {product.brand?.name && (
<> <>
<div className="flex justify-between items-center py-2"> <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> <span className="font-medium">{product.brand.name}</span>
</div> </div>
<Separator /> <Separator />
</> </>
)} )}
{product.stock !== undefined && ( {product.stock !== undefined && (
<> <>
<div className="flex justify-between items-center py-2"> <div className="flex justify-between items-center py-2">
<span className="text-gray-500">{t("stock")}</span> <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'}`}> <span
{product.stock === 0 ? t("out_of_stock") : product.stock <= 5 ? `${t("only_left", { count: product.stock })}` : product.stock} 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> </span>
</div> </div>
<Separator /> <Separator />
</> </>
)} )}
{product.barcode && ( {product.barcode && (
<> <>
<div className="flex justify-between items-center py-2"> <div className="flex justify-between items-center py-2">
<span className="text-gray-500">{t("barcode")}</span> <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> </div>
<Separator /> <Separator />
</> </>
)} )}
{product.colour && ( {product.colour && (
<> <>
<div className="flex justify-between items-center py-2"> <div className="flex justify-between items-center py-2">
@@ -475,20 +542,23 @@ export default function ProductPageContent({ slug }: ProductDetailProps) {
<Separator /> <Separator />
</> </>
)} )}
{product.properties && product.properties.length > 0 && ( {product.properties && product.properties.length > 0 && (
<> <>
{product.properties.map((prop, idx) => ( {product.properties.map(
prop.value && ( (prop, idx) =>
<div key={idx}> prop.value && (
<div className="flex justify-between items-center py-2"> <div key={idx}>
<span className="text-gray-500">{prop.name}</span> <div className="flex justify-between items-center py-2">
<span className="font-medium">{prop.value}</span> <span className="text-gray-500">{prop.name}</span>
<span className="font-medium">{prop.value}</span>
</div>
{idx < product.properties.length - 1 && (
<Separator />
)}
</div> </div>
{idx < product.properties.length - 1 && <Separator />} )
</div> )}
)
))}
</> </>
)} )}
</div> </div>
@@ -496,8 +566,10 @@ export default function ProductPageContent({ slug }: ProductDetailProps) {
{product.description && ( {product.description && (
<Card className="p-4 rounded-xl border-gray-200"> <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">
<div {t("product_description")}
</h3>
<div
className="text-gray-700 leading-relaxed prose prose-sm max-w-none" className="text-gray-700 leading-relaxed prose prose-sm max-w-none"
dangerouslySetInnerHTML={{ __html: product.description }} dangerouslySetInnerHTML={{ __html: product.description }}
/> />
@@ -513,21 +585,22 @@ export default function ProductPageContent({ slug }: ProductDetailProps) {
<span className="text-3xl font-bold text-primary"> <span className="text-3xl font-bold text-primary">
{product.price_amount} TMT {product.price_amount} TMT
</span> </span>
{product.old_price_amount && parseFloat(product.old_price_amount) > 0 && ( {product.old_price_amount &&
<span className="text-lg text-gray-400 line-through"> parseFloat(product.old_price_amount) > 0 && (
{product.old_price_amount} TMT <span className="text-lg text-gray-400 line-through">
</span> {product.old_price_amount} TMT
)} </span>
)}
</div> </div>
</div> </div>
<div className="space-y-3"> <div className="space-y-2">
{isInCart ? ( {isInCart ? (
<> <>
<Link href="/cart"> <Link href="/cart">
<Button <Button
size="lg" 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" /> <ShoppingCart className="mr-2 h-5 w-5" />
{t("go_to_cart")} {t("go_to_cart")}
@@ -540,17 +613,20 @@ export default function ProductPageContent({ slug }: ProductDetailProps) {
size="icon" size="icon"
onClick={handleQuantityDecrease} onClick={handleQuantityDecrease}
disabled={isSyncing} 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" /> <Minus className="h-5 w-5" />
</Button> </Button>
<div className="flex-1 text-center font-semibold text-xl border rounded-xl h-12 flex items-center justify-center relative"> <div className="flex-1 text-center font-semibold text-xl border rounded-xl h-12 flex items-center justify-center relative">
{localQuantity} {localQuantity}
{isSyncing && (
<Loader2 className="h-4 w-4 animate-spin absolute -top-1 -right-1 text-blue-500" />
)}
{syncError && ( {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> </div>
<Button <Button
@@ -558,12 +634,35 @@ export default function ProductPageContent({ slug }: ProductDetailProps) {
size="icon" size="icon"
onClick={handleQuantityIncrease} onClick={handleQuantityIncrease}
disabled={localQuantity >= availableStock || isSyncing} disabled={localQuantity >= availableStock || isSyncing}
className={`rounded-xl h-12 w-12 ${isSyncing ? 'opacity-70' : ''} ${ className={`rounded-lg h-12 w-12 ${
localQuantity >= availableStock ? 'opacity-50 cursor-not-allowed' : '' isSyncing ? "opacity-70" : ""
} ${
localQuantity >= availableStock
? "opacity-50 cursor-not-allowed"
: ""
}`} }`}
> >
<Plus className="h-5 w-5" /> <Plus className="h-5 w-5" />
</Button> </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> </div>
</> </>
) : ( ) : (
@@ -571,7 +670,7 @@ export default function ProductPageContent({ slug }: ProductDetailProps) {
size="lg" size="lg"
onClick={handleAddToCart} onClick={handleAddToCart}
disabled={isSyncing || product.stock === 0} 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 ? ( {isSyncing ? (
<> <>
@@ -581,28 +680,13 @@ export default function ProductPageContent({ slug }: ProductDetailProps) {
) : ( ) : (
<> <>
<ShoppingCart className="mr-2 h-5 w-5" /> <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>
)} )}
<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> </div>
</Card> </Card>
@@ -616,13 +700,15 @@ export default function ProductPageContent({ slug }: ProductDetailProps) {
</Avatar> </Avatar>
<div> <div>
<p className="text-sm text-gray-500">{t("store")}</p> <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>
</div> </div>
<Button <Button
variant="outline" variant="outline"
size="lg" size="lg"
className="w-full rounded-xl" className="w-full rounded-lg"
> >
{t("write_to_store")} {t("write_to_store")}
</Button> </Button>
@@ -644,16 +730,16 @@ export default function ProductPageContent({ slug }: ProductDetailProps) {
{t("stock_limit_title")} {t("stock_limit_title")}
</DialogTitle> </DialogTitle>
<DialogDescription className="text-center text-base pt-2"> <DialogDescription className="text-center text-base pt-2">
{t("stock_limit_message", { {t("stock_limit_message", {
product: product.name, product: product.name,
stock: availableStock stock: availableStock,
})} })}
</DialogDescription> </DialogDescription>
</DialogHeader> </DialogHeader>
<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"
> >
{t("understood")} {t("understood")}
</Button> </Button>
@@ -662,4 +748,4 @@ export default function ProductPageContent({ slug }: ProductDetailProps) {
</Dialog> </Dialog>
</> </>
); );
} }

View File

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

View File

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