first commit
This commit is contained in:
321
src/features/auth/ui/Login.tsx
Normal file
321
src/features/auth/ui/Login.tsx
Normal file
@@ -0,0 +1,321 @@
|
||||
'use client';
|
||||
|
||||
import { useRouter } from '@/shared/config/i18n/navigation';
|
||||
import formatPhone from '@/shared/lib/formatPhone';
|
||||
import { Input } from '@/shared/ui/input';
|
||||
import { ArrowRight, Check, Lock, Phone } from 'lucide-react';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
|
||||
type Step = 'phone' | 'otp';
|
||||
|
||||
const Login = () => {
|
||||
const [step, setStep] = useState<Step>('phone');
|
||||
const [phoneNumber, setPhoneNumber] = useState<string>('+998');
|
||||
const [otp, setOtp] = useState<string[]>(['', '', '', '', '', '']);
|
||||
const [isLoading, setIsLoading] = useState<boolean>(false);
|
||||
const [error, setError] = useState<string>('');
|
||||
const [countdown, setCountdown] = useState<number>(60);
|
||||
const [canResend, setCanResend] = useState<boolean>(false);
|
||||
const router = useRouter();
|
||||
|
||||
const otpInputs = useRef<Array<HTMLInputElement | null>>([]);
|
||||
|
||||
/* Countdown */
|
||||
useEffect(() => {
|
||||
if (step === 'otp' && countdown > 0) {
|
||||
const timer = setTimeout(() => setCountdown((c) => c - 1), 1000);
|
||||
return () => clearTimeout(timer);
|
||||
}
|
||||
|
||||
if (countdown === 0) {
|
||||
setCanResend(true);
|
||||
}
|
||||
}, [countdown, step]);
|
||||
|
||||
/* Phone submit */
|
||||
const handlePhoneSubmit = (): void => {
|
||||
setError('');
|
||||
|
||||
if (phoneNumber.length < 9) {
|
||||
setError("Telefon raqamni to'liq kiriting");
|
||||
return;
|
||||
}
|
||||
|
||||
setIsLoading(true);
|
||||
|
||||
setTimeout(() => {
|
||||
setIsLoading(false);
|
||||
setStep('otp');
|
||||
setCountdown(60);
|
||||
setCanResend(false);
|
||||
}, 1500);
|
||||
};
|
||||
|
||||
/* OTP change */
|
||||
const handleOtpChange = (index: number, value: string): void => {
|
||||
if (value && !/^\d$/.test(value)) return;
|
||||
|
||||
const newOtp = [...otp];
|
||||
newOtp[index] = value;
|
||||
setOtp(newOtp);
|
||||
|
||||
if (value && index < 5) {
|
||||
otpInputs.current[index + 1]?.focus();
|
||||
}
|
||||
|
||||
if (newOtp.every((d) => d !== '') && index === 5) {
|
||||
handleOtpSubmit(newOtp);
|
||||
}
|
||||
};
|
||||
|
||||
/* OTP keydown */
|
||||
const handleOtpKeyDown = (
|
||||
index: number,
|
||||
e: React.KeyboardEvent<HTMLInputElement>,
|
||||
): void => {
|
||||
if (e.key === 'Backspace' && !otp[index] && index > 0) {
|
||||
otpInputs.current[index - 1]?.focus();
|
||||
}
|
||||
};
|
||||
|
||||
/* OTP paste */
|
||||
const handleOtpPaste = (e: React.ClipboardEvent<HTMLDivElement>): void => {
|
||||
e.preventDefault();
|
||||
const pasted = e.clipboardData.getData('text').slice(0, 6);
|
||||
|
||||
if (!/^\d+$/.test(pasted)) return;
|
||||
|
||||
const newOtp = pasted.split('');
|
||||
setOtp([...newOtp, ...Array(6 - newOtp.length).fill('')]);
|
||||
|
||||
const lastIndex = Math.min(newOtp.length - 1, 5);
|
||||
otpInputs.current[lastIndex]?.focus();
|
||||
|
||||
if (pasted.length === 6) {
|
||||
setTimeout(() => handleOtpSubmit(newOtp), 100);
|
||||
}
|
||||
};
|
||||
|
||||
const handleOtpSubmit = (otpArray: string[] = otp): void => {
|
||||
setError('');
|
||||
const otpCode = otpArray.join('');
|
||||
|
||||
if (otpCode.length < 6) {
|
||||
setError("Kodni to'liq kiriting");
|
||||
return;
|
||||
}
|
||||
|
||||
setIsLoading(true);
|
||||
|
||||
setTimeout(() => {
|
||||
setIsLoading(false);
|
||||
|
||||
if (otpCode === '123456') {
|
||||
localStorage.setItem('user', 'true');
|
||||
router.push('/');
|
||||
} else {
|
||||
setError("Noto'g'ri kod. Qayta urinib ko'ring.");
|
||||
setOtp(['', '', '', '', '', '']);
|
||||
otpInputs.current[0]?.focus();
|
||||
}
|
||||
}, 1500);
|
||||
};
|
||||
|
||||
/* Resend */
|
||||
const handleResendOtp = (): void => {
|
||||
if (!canResend) return;
|
||||
|
||||
setIsLoading(true);
|
||||
|
||||
setTimeout(() => {
|
||||
setIsLoading(false);
|
||||
setCountdown(60);
|
||||
setCanResend(false);
|
||||
setOtp(['', '', '', '', '', '']);
|
||||
otpInputs.current[0]?.focus();
|
||||
alert('Yangi kod yuborildi!');
|
||||
}, 1000);
|
||||
};
|
||||
|
||||
const handleChangeNumber = (): void => {
|
||||
setStep('phone');
|
||||
setPhoneNumber('');
|
||||
setOtp(['', '', '', '', '', '']);
|
||||
setError('');
|
||||
setCountdown(60);
|
||||
setCanResend(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="custom-container flex justify-center items-center h-[85vh]">
|
||||
<div className="bg-white rounded-2xl shadow-2xl w-full max-w-md overflow-hidden">
|
||||
{/* Header */}
|
||||
<div className="bg-gradient-to-r from-blue-600 to-indigo-600 p-8 text-white text-center">
|
||||
<div className="w-20 h-20 bg-white bg-opacity-20 rounded-full flex items-center justify-center mx-auto mb-4">
|
||||
{step === 'phone' ? (
|
||||
<Phone className="w-10 h-10" />
|
||||
) : (
|
||||
<Lock className="w-10 h-10" />
|
||||
)}
|
||||
</div>
|
||||
<h1 className="text-2xl font-bold mb-2">
|
||||
{step === 'phone' ? 'Xush kelibsiz!' : 'Kodni tasdiqlang'}
|
||||
</h1>
|
||||
<p className="text-blue-100">
|
||||
{step === 'phone'
|
||||
? 'Telefon raqamingizni kiriting'
|
||||
: `${phoneNumber} raqamiga yuborilgan kodni kiriting`}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Form */}
|
||||
<div className="p-8">
|
||||
{step === 'phone' ? (
|
||||
// Phone Number Step
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Telefon raqam
|
||||
</label>
|
||||
<div className="relative">
|
||||
<div className="absolute inset-y-0 left-0 pl-4 flex items-center pointer-events-none">
|
||||
<Phone className="w-5 h-5 text-gray-400" />
|
||||
</div>
|
||||
<Input
|
||||
type="tel"
|
||||
value={formatPhone(phoneNumber)}
|
||||
onChange={(e) => {
|
||||
const value = e.target.value.replace(/\D/g, '');
|
||||
setPhoneNumber(value);
|
||||
setError('');
|
||||
}}
|
||||
placeholder="+998 90 123-45-67"
|
||||
maxLength={17}
|
||||
className="w-full pl-12 pr-4 py-4 h-12 border-2 border-gray-300 rounded-xl focus:outline-none focus:border-blue-500 transition text-lg"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{error && <p className="text-red-500 text-sm mt-2">{error}</p>}
|
||||
|
||||
<button
|
||||
onClick={handlePhoneSubmit}
|
||||
disabled={isLoading || phoneNumber.length < 9}
|
||||
className="w-full mt-6 bg-gradient-to-r from-blue-600 to-indigo-600 text-white py-4 rounded-xl font-semibold hover:from-blue-700 hover:to-indigo-700 transition disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center gap-2"
|
||||
>
|
||||
{isLoading ? (
|
||||
<>
|
||||
<div className="w-5 h-5 border-2 border-white border-t-transparent rounded-full animate-spin" />
|
||||
Yuborilmoqda...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
Kodni olish
|
||||
<ArrowRight className="w-5 h-5" />
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
|
||||
<p className="text-center text-sm text-gray-500 mt-6">
|
||||
Davom etish orqali siz bizning{' '}
|
||||
<a href="#" className="text-blue-600 hover:underline">
|
||||
Foydalanish shartlari
|
||||
</a>{' '}
|
||||
va{' '}
|
||||
<a href="#" className="text-blue-600 hover:underline">
|
||||
Maxfiylik siyosati
|
||||
</a>
|
||||
ga rozilik bildirasiz
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
// OTP Step
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-4 text-center">
|
||||
6 raqamli kodni kiriting
|
||||
</label>
|
||||
|
||||
<div
|
||||
className="flex gap-2 justify-center mb-6"
|
||||
onPaste={handleOtpPaste}
|
||||
>
|
||||
{otp.map((digit, index) => (
|
||||
<Input
|
||||
key={index}
|
||||
ref={(el) => {
|
||||
otpInputs.current[index] = el;
|
||||
}}
|
||||
type="text"
|
||||
inputMode="numeric"
|
||||
maxLength={1}
|
||||
value={digit}
|
||||
onChange={(e) => handleOtpChange(index, e.target.value)}
|
||||
onKeyDown={(e) => handleOtpKeyDown(index, e)}
|
||||
className="w-12 h-14 text-center text-2xl font-bold border-2 border-gray-300 rounded-lg focus:outline-none focus:border-blue-500 transition"
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div className="bg-red-50 border border-red-200 text-red-600 px-4 py-3 rounded-lg mb-4 text-sm text-center">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<button
|
||||
onClick={() => handleOtpSubmit()}
|
||||
disabled={isLoading || otp.some((digit) => digit === '')}
|
||||
className="w-full bg-gradient-to-r from-blue-600 to-indigo-600 text-white py-4 rounded-xl font-semibold hover:from-blue-700 hover:to-indigo-700 transition disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center gap-2"
|
||||
>
|
||||
{isLoading ? (
|
||||
<>
|
||||
<div className="w-5 h-5 border-2 border-white border-t-transparent rounded-full animate-spin" />
|
||||
Tekshirilmoqda...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
Tasdiqlash
|
||||
<Check className="w-5 h-5" />
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
|
||||
{/* Resend OTP */}
|
||||
<div className="mt-6 text-center">
|
||||
{canResend ? (
|
||||
<button
|
||||
onClick={handleResendOtp}
|
||||
disabled={isLoading}
|
||||
className="text-blue-600 hover:text-blue-700 font-semibold hover:underline"
|
||||
>
|
||||
Kodni qayta yuborish
|
||||
</button>
|
||||
) : (
|
||||
<p className="text-gray-500 text-sm">
|
||||
Kodni qayta yuborish ({countdown}s)
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Change Number */}
|
||||
<button
|
||||
onClick={handleChangeNumber}
|
||||
className="w-full mt-4 text-gray-600 hover:text-gray-800 font-medium"
|
||||
>
|
||||
{"Raqamni o'zgartirish"}
|
||||
</button>
|
||||
|
||||
{/* Demo info */}
|
||||
<div className="mt-6 bg-blue-50 border border-blue-200 rounded-lg p-4">
|
||||
<p className="text-sm text-blue-800 text-center">
|
||||
<strong>Demo uchun:</strong>
|
||||
{`Kod sifatida "123456" kiriting`}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Login;
|
||||
318
src/features/cart/ui/CartPage.tsx
Normal file
318
src/features/cart/ui/CartPage.tsx
Normal file
@@ -0,0 +1,318 @@
|
||||
'use client';
|
||||
|
||||
import { useRouter } from '@/shared/config/i18n/navigation';
|
||||
import { Button } from '@/shared/ui/button';
|
||||
import { Input } from '@/shared/ui/input';
|
||||
import {
|
||||
ArrowLeft,
|
||||
CreditCard,
|
||||
Minus,
|
||||
Plus,
|
||||
ShoppingBag,
|
||||
Trash,
|
||||
Truck,
|
||||
} from 'lucide-react';
|
||||
import Image from 'next/image';
|
||||
import { useState } from 'react';
|
||||
|
||||
interface CartItem {
|
||||
id: number;
|
||||
name: string;
|
||||
price: number;
|
||||
oldPrice: number;
|
||||
image: string;
|
||||
quantity: number | string;
|
||||
inStock: boolean;
|
||||
}
|
||||
|
||||
const CartPage = () => {
|
||||
const router = useRouter();
|
||||
const [cartItems, setCartItems] = useState<CartItem[]>([
|
||||
{
|
||||
id: 5,
|
||||
name: 'Coca-Cola 1.5L',
|
||||
price: 12000,
|
||||
oldPrice: 14000,
|
||||
image: '/classic-coca-cola.png',
|
||||
quantity: 2,
|
||||
inStock: true,
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
name: 'Pepsi 2L',
|
||||
price: 11000,
|
||||
oldPrice: 13000,
|
||||
image: '/pepsi-bottle.jpg',
|
||||
quantity: 1,
|
||||
inStock: true,
|
||||
},
|
||||
{
|
||||
id: 8,
|
||||
name: 'Sprite 1.5L',
|
||||
price: 10000,
|
||||
oldPrice: 12000,
|
||||
image: '/clear-soda-bottle.png',
|
||||
quantity: 3,
|
||||
inStock: true,
|
||||
},
|
||||
]);
|
||||
|
||||
const subtotal = cartItems.reduce(
|
||||
(sum, item) => sum + item.price * Number(item.quantity),
|
||||
0,
|
||||
);
|
||||
const discount = cartItems.reduce((sum, item) => {
|
||||
if (item.oldPrice) {
|
||||
return sum + (item.oldPrice - item.price) * Number(item.quantity);
|
||||
}
|
||||
return sum;
|
||||
}, 0);
|
||||
const deliveryFee = subtotal > 50000 ? 0 : 15000;
|
||||
const total = subtotal - discount + deliveryFee;
|
||||
|
||||
const handleQuantityChange = (id: number, type: 'increase' | 'decrease') => {
|
||||
setCartItems((prev) =>
|
||||
prev.map((item) => {
|
||||
if (item.id === id) {
|
||||
if (type === 'increase')
|
||||
return { ...item, quantity: Number(item.quantity) + 1 };
|
||||
if (type === 'decrease' && Number(item.quantity) > 1)
|
||||
return { ...item, quantity: Number(item.quantity) - 1 };
|
||||
}
|
||||
return item;
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
// Remove item from cart
|
||||
const handleRemoveItem = (id: number) => {
|
||||
setCartItems((prev) => prev.filter((item) => item.id !== id));
|
||||
};
|
||||
|
||||
const handleCheckout = () => {
|
||||
router.push('/cart/order');
|
||||
};
|
||||
|
||||
if (cartItems.length === 0) {
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50 flex items-center justify-center">
|
||||
<div className="text-center">
|
||||
<ShoppingBag className="w-24 h-24 text-gray-300 mx-auto mb-4" />
|
||||
<h2 className="text-2xl font-bold text-gray-800 mb-2">
|
||||
{"Savatingiz bo'sh"}
|
||||
</h2>
|
||||
<p className="text-gray-600 mb-6">
|
||||
{"Mahsulotlar qo'shish uchun katalogga o'ting"}
|
||||
</p>
|
||||
<button
|
||||
onClick={() => router.push('/')}
|
||||
className="bg-blue-600 text-white px-6 py-3 rounded-lg hover:bg-blue-700 transition flex items-center gap-2 mx-auto"
|
||||
>
|
||||
<ArrowLeft className="w-5 h-5" /> Xarid qilishni boshlash
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50 py-8">
|
||||
<div className="max-w-7xl mx-auto px-4">
|
||||
{/* Header */}
|
||||
<div className="mb-6">
|
||||
<h1 className="text-3xl font-bold text-gray-800 mb-2">Savat</h1>
|
||||
<p className="text-gray-600">{cartItems.length} ta mahsulot</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||
{/* Cart Items */}
|
||||
<div className="lg:col-span-2">
|
||||
<div className="bg-white rounded-lg shadow-md overflow-hidden">
|
||||
{cartItems.map((item, index) => (
|
||||
<div
|
||||
key={item.id}
|
||||
className={`p-6 flex relative gap-4 ${index !== cartItems.length - 1 ? 'border-b' : ''}`}
|
||||
>
|
||||
{/* Product Image */}
|
||||
<Button
|
||||
variant={'destructive'}
|
||||
size={'icon'}
|
||||
onClick={() => handleRemoveItem(item.id)}
|
||||
className="absolute right-2 w-7 h-7 top-2 cursor-pointer"
|
||||
>
|
||||
<Trash className="size-4" />
|
||||
</Button>
|
||||
<div className="w-24 h-40 bg-gray-100 rounded-lg flex-shrink-0 overflow-hidden">
|
||||
<Image
|
||||
width={500}
|
||||
height={500}
|
||||
src={item.image}
|
||||
alt={item.name}
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Product Info */}
|
||||
<div className="flex-1">
|
||||
<h3 className="font-semibold text-lg mb-1">{item.name}</h3>
|
||||
<div className="flex items-center gap-2 mb-3 max-lg:flex-col max-lg:items-start max-lg:gap-1">
|
||||
<span className="text-blue-600 font-bold text-xl">
|
||||
{item.price.toLocaleString()} {"so'm"}
|
||||
</span>
|
||||
{item.oldPrice && (
|
||||
<span className="text-gray-400 line-through text-sm">
|
||||
{item.oldPrice.toLocaleString()} {"so'm"}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between max-lg:flex-col max-lg:items-start max-lg:gap-1">
|
||||
{/* Quantity Controls */}
|
||||
<div className="flex items-center border border-gray-300 rounded-lg">
|
||||
<button
|
||||
onClick={() =>
|
||||
handleQuantityChange(item.id, 'decrease')
|
||||
}
|
||||
className="p-2 cursor-pointer transition rounded-lg"
|
||||
disabled={Number(item.quantity) <= 1}
|
||||
>
|
||||
<Minus className="w-4 h-4" />
|
||||
</button>
|
||||
<Input
|
||||
type="text"
|
||||
min={1}
|
||||
value={item.quantity}
|
||||
onChange={(e) => {
|
||||
const value = e.target.value;
|
||||
// Bo'sh qiymatga ruxsat berish
|
||||
if (value === '') {
|
||||
setCartItems((prev) =>
|
||||
prev.map((cartItem) =>
|
||||
cartItem.id === item.id
|
||||
? { ...cartItem, quantity: '' }
|
||||
: cartItem,
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
const number = parseInt(value, 10);
|
||||
if (!isNaN(number) && number > 0) {
|
||||
setCartItems((prev) =>
|
||||
prev.map((cartItem) =>
|
||||
cartItem.id === item.id
|
||||
? { ...cartItem, quantity: number }
|
||||
: cartItem,
|
||||
),
|
||||
);
|
||||
}
|
||||
}}
|
||||
className="w-16 text-center outline-none ring-0 focus-visible:ring-0 border-none font-semibold"
|
||||
/>
|
||||
<button
|
||||
onClick={() =>
|
||||
handleQuantityChange(item.id, 'increase')
|
||||
}
|
||||
className="p-2 cursor-pointer transition rounded-lg"
|
||||
>
|
||||
<Plus className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Order Summary */}
|
||||
<div className="lg:col-span-1">
|
||||
<div className="bg-white rounded-lg shadow-md p-6 sticky top-4">
|
||||
<h3 className="text-xl font-bold mb-4">Buyurtma xulasasi</h3>
|
||||
|
||||
<div className="space-y-3 mb-4">
|
||||
<div className="flex justify-between text-gray-600">
|
||||
<span>Mahsulotlar narxi:</span>
|
||||
<span>
|
||||
{subtotal.toLocaleString()} {"so'm"}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{discount > 0 && (
|
||||
<div className="flex justify-between text-green-600">
|
||||
<span>Chegirma:</span>
|
||||
<span>
|
||||
-{discount.toLocaleString()} {"so'm"}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex justify-between text-gray-600">
|
||||
<span className="flex items-center gap-1">
|
||||
<Truck className="w-4 h-4" />
|
||||
Yetkazib berish:
|
||||
</span>
|
||||
<span>
|
||||
{deliveryFee === 0 ? (
|
||||
<span className="text-green-600 font-semibold">
|
||||
Bepul
|
||||
</span>
|
||||
) : (
|
||||
`${deliveryFee.toLocaleString()} so'm`
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{deliveryFee > 0 && (
|
||||
<p className="text-sm text-gray-500 bg-blue-50 p-2 rounded">
|
||||
{
|
||||
"50,000 so'mdan ortiq xarid qiling va yetkazib berishni bepul oling!"
|
||||
}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="border-t pt-4 mb-6">
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-lg font-semibold">Jami:</span>
|
||||
<span className="text-2xl font-bold text-blue-600">
|
||||
{total.toLocaleString()} {"so'm"}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={handleCheckout}
|
||||
className="w-full bg-blue-600 text-white py-4 rounded-lg font-semibold hover:bg-blue-700 transition flex items-center justify-center gap-2 mb-4"
|
||||
>
|
||||
<CreditCard className="w-5 h-5" />
|
||||
Buyurtmani rasmiylashtirish
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => router.push('/')}
|
||||
className="w-full border-2 border-gray-300 cursor-pointer text-gray-700 py-3 rounded-lg font-semibold hover:bg-gray-50 transition flex items-center justify-center gap-2"
|
||||
>
|
||||
<ArrowLeft className="w-5 h-5" />
|
||||
Xaridni davom ettirish
|
||||
</button>
|
||||
|
||||
{/* Additional Info */}
|
||||
<div className="mt-6 space-y-3 text-sm text-gray-600">
|
||||
<div className="flex items-start gap-2">
|
||||
<Truck className="w-5 h-5 text-blue-600 flex-shrink-0 mt-0.5" />
|
||||
<span>Tez yetkazib berish 1-2 kun ichida</span>
|
||||
</div>
|
||||
<div className="flex items-start gap-2">
|
||||
<CreditCard className="w-5 h-5 text-blue-600 flex-shrink-0 mt-0.5" />
|
||||
<span>{"Xavfsiz to'lov usullari"}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default CartPage;
|
||||
437
src/features/cart/ui/OrderPage.tsx
Normal file
437
src/features/cart/ui/OrderPage.tsx
Normal file
@@ -0,0 +1,437 @@
|
||||
'use client';
|
||||
|
||||
import formatPhone from '@/shared/lib/formatPhone';
|
||||
import { Input } from '@/shared/ui/input';
|
||||
import { Label } from '@/shared/ui/label';
|
||||
import { Textarea } from '@/shared/ui/textarea';
|
||||
import {
|
||||
Building2,
|
||||
CheckCircle2,
|
||||
Clock,
|
||||
CreditCard,
|
||||
MapPin,
|
||||
Package,
|
||||
Truck,
|
||||
User,
|
||||
Wallet,
|
||||
} from 'lucide-react';
|
||||
import Image from 'next/image';
|
||||
import { useState } from 'react';
|
||||
|
||||
const OrderPage = () => {
|
||||
const [formData, setFormData] = useState({
|
||||
fullName: '',
|
||||
phone: '+998',
|
||||
email: '',
|
||||
city: '',
|
||||
address: '',
|
||||
postalCode: '',
|
||||
notes: '',
|
||||
});
|
||||
|
||||
const [paymentMethod, setPaymentMethod] = useState('cash');
|
||||
const [deliveryMethod, setDeliveryMethod] = useState('standard');
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const [orderSuccess, setOrderSuccess] = useState(false);
|
||||
|
||||
// Cart items from previous page (in real app, this would come from context/store)
|
||||
const cartItems = [
|
||||
{
|
||||
id: 5,
|
||||
name: 'Coca-Cola 1.5L',
|
||||
price: 12000,
|
||||
quantity: 2,
|
||||
image: '/classic-coca-cola.png',
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
name: 'Pepsi 2L',
|
||||
price: 11000,
|
||||
quantity: 1,
|
||||
image: '/pepsi-bottle.jpg',
|
||||
},
|
||||
{
|
||||
id: 8,
|
||||
name: 'Sprite 1.5L',
|
||||
price: 10000,
|
||||
quantity: 3,
|
||||
image: '/clear-soda-bottle.png',
|
||||
},
|
||||
];
|
||||
|
||||
const subtotal = cartItems.reduce(
|
||||
(sum, item) => sum + item.price * item.quantity,
|
||||
0,
|
||||
);
|
||||
const deliveryFee =
|
||||
deliveryMethod === 'express' ? 25000 : subtotal > 50000 ? 0 : 15000;
|
||||
const total = subtotal + deliveryFee;
|
||||
|
||||
const handleInputChange = (
|
||||
e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>,
|
||||
) => {
|
||||
setFormData({
|
||||
...formData,
|
||||
[e.target.name]: e.target.value,
|
||||
});
|
||||
};
|
||||
|
||||
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
setIsSubmitting(true);
|
||||
|
||||
// Simulate API call
|
||||
setTimeout(() => {
|
||||
setIsSubmitting(false);
|
||||
setOrderSuccess(true);
|
||||
}, 2000);
|
||||
};
|
||||
|
||||
if (orderSuccess) {
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50 flex items-center justify-center p-4">
|
||||
<div className="bg-white rounded-lg shadow-lg p-8 max-w-md w-full text-center">
|
||||
<div className="w-20 h-20 bg-green-100 rounded-full flex items-center justify-center mx-auto mb-4">
|
||||
<CheckCircle2 className="w-12 h-12 text-green-600" />
|
||||
</div>
|
||||
<h2 className="text-2xl font-bold text-gray-800 mb-2">
|
||||
Buyurtma qabul qilindi!
|
||||
</h2>
|
||||
<p className="text-gray-600 mb-4">
|
||||
Buyurtma raqami:{' '}
|
||||
<span className="font-bold">
|
||||
#ORD-{Math.floor(Math.random() * 10000)}
|
||||
</span>
|
||||
</p>
|
||||
<p className="text-gray-500 mb-6">
|
||||
Buyurtmangiz muvaffaqiyatli qabul qilindi. Tez orada sizga aloqaga
|
||||
chiqamiz.
|
||||
</p>
|
||||
<div className="bg-blue-50 p-4 rounded-lg mb-6">
|
||||
<p className="text-sm text-gray-700">
|
||||
Buyurtma holati haqida SMS orqali xabardor qilinasiz
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => (window.location.href = '/')}
|
||||
className="w-full bg-blue-600 text-white py-3 rounded-lg font-semibold hover:bg-blue-700 transition"
|
||||
>
|
||||
Bosh sahifaga qaytish
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50 py-8">
|
||||
<div className="max-w-7xl mx-auto px-4">
|
||||
{/* Header */}
|
||||
<div className="mb-6">
|
||||
<h1 className="text-3xl font-bold text-gray-800 mb-2">
|
||||
Buyurtmani rasmiylashtirish
|
||||
</h1>
|
||||
<p className="text-gray-600">{"Ma'lumotlaringizni to'ldiring"}</p>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||
{/* Left Column - Forms */}
|
||||
<div className="lg:col-span-2 space-y-6">
|
||||
{/* Contact Information */}
|
||||
<div className="bg-white rounded-lg shadow-md p-6">
|
||||
<div className="flex items-center gap-2 mb-4">
|
||||
<User className="w-5 h-5 text-blue-600" />
|
||||
<h2 className="text-xl font-semibold">
|
||||
{"Shaxsiy ma'lumotlar"}
|
||||
</h2>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<Label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
{"To'liq ism"}
|
||||
</Label>
|
||||
<Input
|
||||
type="text"
|
||||
name="fullName"
|
||||
value={formData.fullName}
|
||||
onChange={handleInputChange}
|
||||
required
|
||||
className="w-full border h-12 border-gray-300 rounded-lg px-4 py-3 focus:outline-none focus:border-blue-500"
|
||||
placeholder="Ismingiz va familiyangiz"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Telefon raqam
|
||||
</Label>
|
||||
<Input
|
||||
type="tel"
|
||||
name="phone"
|
||||
value={formatPhone(formData.phone)}
|
||||
onChange={handleInputChange}
|
||||
required
|
||||
className="w-full h-12 border border-gray-300 rounded-lg px-4 py-3 focus:outline-none focus:border-blue-500"
|
||||
placeholder="+998 90 123 45 67"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Delivery Address */}
|
||||
<div className="bg-white rounded-lg shadow-md p-6">
|
||||
<div className="flex items-center gap-2 mb-4">
|
||||
<MapPin className="w-5 h-5 text-blue-600" />
|
||||
<h2 className="text-xl font-semibold">
|
||||
Yetkazib berish manzili
|
||||
</h2>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<Label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Shahar
|
||||
</Label>
|
||||
<Input
|
||||
type="text"
|
||||
name="city"
|
||||
value={formData.city}
|
||||
onChange={handleInputChange}
|
||||
required
|
||||
className="w-full border h-12 border-gray-300 rounded-lg px-4 py-3 focus:outline-none focus:border-blue-500"
|
||||
placeholder="Toshkent"
|
||||
/>
|
||||
</div>
|
||||
<div className="md:col-span-2">
|
||||
<Label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
{"To'liq manzil"}
|
||||
</Label>
|
||||
<Textarea
|
||||
name="address"
|
||||
value={formData.address}
|
||||
onChange={handleInputChange}
|
||||
required
|
||||
rows={3}
|
||||
className="w-full border min-h-32 max-h-44 border-gray-300 rounded-lg px-4 py-3 focus:outline-none focus:border-blue-500"
|
||||
placeholder="Ko'cha, uy raqami, xonadon..."
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-white rounded-lg shadow-md p-6">
|
||||
<div className="flex items-center gap-2 mb-4">
|
||||
<Truck className="w-5 h-5 text-blue-600" />
|
||||
<h2 className="text-xl font-semibold">
|
||||
Yetkazib berish usuli
|
||||
</h2>
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
<label className="flex items-center p-4 border-2 rounded-lg cursor-pointer transition hover:bg-gray-50">
|
||||
<Input
|
||||
type="radio"
|
||||
name="delivery"
|
||||
value="standard"
|
||||
checked={deliveryMethod === 'standard'}
|
||||
onChange={(e) => setDeliveryMethod(e.target.value)}
|
||||
className="w-4 h-4 text-blue-600"
|
||||
/>
|
||||
<div className="ml-4 flex-1">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<Clock className="w-5 h-5 text-gray-600" />
|
||||
<span className="font-semibold">
|
||||
Standart yetkazib berish
|
||||
</span>
|
||||
</div>
|
||||
<span className="font-bold text-blue-600">
|
||||
{subtotal > 50000 ? 'Bepul' : "15,000 so'm"}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-sm text-gray-500 mt-1">
|
||||
2-3 kun ichida
|
||||
</p>
|
||||
</div>
|
||||
</label>
|
||||
|
||||
<label className="flex items-center p-4 border-2 rounded-lg cursor-pointer transition hover:bg-gray-50">
|
||||
<Input
|
||||
type="radio"
|
||||
name="delivery"
|
||||
value="express"
|
||||
checked={deliveryMethod === 'express'}
|
||||
onChange={(e) => setDeliveryMethod(e.target.value)}
|
||||
className="w-4 h-4 text-blue-600"
|
||||
/>
|
||||
<div className="ml-4 flex-1">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<Package className="w-5 h-5 text-gray-600" />
|
||||
<span className="font-semibold">
|
||||
Tez yetkazib berish
|
||||
</span>
|
||||
</div>
|
||||
<span className="font-bold text-blue-600">
|
||||
{"25,000 so'm"}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-sm text-gray-500 mt-1">1 kun ichida</p>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-white rounded-lg shadow-md p-6">
|
||||
<div className="flex items-center gap-2 mb-4">
|
||||
<CreditCard className="w-5 h-5 text-blue-600" />
|
||||
<h2 className="text-xl font-semibold">{"To'lov usuli"}</h2>
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
<Label className="flex items-center p-4 border-2 rounded-lg cursor-pointer transition hover:bg-gray-50">
|
||||
<Input
|
||||
type="radio"
|
||||
name="payment"
|
||||
value="cash"
|
||||
checked={paymentMethod === 'cash'}
|
||||
onChange={(e) => setPaymentMethod(e.target.value)}
|
||||
className="w-4 h-4 text-blue-600"
|
||||
/>
|
||||
<div className="ml-4 flex items-center gap-3">
|
||||
<Wallet className="w-6 h-6 text-green-600" />
|
||||
<div>
|
||||
<span className="font-semibold">Naqd pul</span>
|
||||
<p className="text-sm text-gray-500">
|
||||
{"Yetkazib berishda to'lash"}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</Label>
|
||||
|
||||
<Label className="flex items-center p-4 border-2 rounded-lg cursor-pointer transition hover:bg-gray-50">
|
||||
<Input
|
||||
type="radio"
|
||||
name="payment"
|
||||
value="card"
|
||||
checked={paymentMethod === 'card'}
|
||||
onChange={(e) => setPaymentMethod(e.target.value)}
|
||||
className="w-4 h-4 text-blue-600"
|
||||
/>
|
||||
<div className="ml-4 flex items-center gap-3">
|
||||
<CreditCard className="w-6 h-6 text-blue-600" />
|
||||
<div>
|
||||
<span className="font-semibold">Plastik karta</span>
|
||||
<p className="text-sm text-gray-500">
|
||||
{"Online to'lov"}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</Label>
|
||||
|
||||
<Label className="flex items-center p-4 border-2 rounded-lg cursor-pointer transition hover:bg-gray-50">
|
||||
<Input
|
||||
type="radio"
|
||||
name="payment"
|
||||
value="terminal"
|
||||
checked={paymentMethod === 'terminal'}
|
||||
onChange={(e) => setPaymentMethod(e.target.value)}
|
||||
className="w-4 h-4 text-blue-600"
|
||||
/>
|
||||
<div className="ml-4 flex items-center gap-3">
|
||||
<Building2 className="w-6 h-6 text-purple-600" />
|
||||
<div>
|
||||
<span className="font-semibold">Terminal orqali</span>
|
||||
<p className="text-sm text-gray-500">
|
||||
Yetkazib berishda terminal
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</Label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Right Column - Order Summary */}
|
||||
<div className="lg:col-span-1">
|
||||
<div className="bg-white rounded-lg shadow-md p-6 sticky top-4">
|
||||
<h3 className="text-xl font-bold mb-4">Mahsulotlar</h3>
|
||||
|
||||
{/* Cart Items */}
|
||||
<div className="space-y-3 mb-4 max-h-60 overflow-y-auto">
|
||||
{cartItems.map((item) => (
|
||||
<div key={item.id} className="flex gap-3 pb-3 border-b">
|
||||
<Image
|
||||
width={500}
|
||||
height={500}
|
||||
src={item.image}
|
||||
alt={item.name}
|
||||
className="w-16 h-16 object-contain bg-gray-100 rounded"
|
||||
/>
|
||||
<div className="flex-1">
|
||||
<h4 className="font-medium text-sm">{item.name}</h4>
|
||||
<p className="text-sm text-gray-500">
|
||||
{item.quantity} x {item.price.toLocaleString()}{' '}
|
||||
{"so'm"}
|
||||
</p>
|
||||
<p className="font-semibold text-sm">
|
||||
{(item.price * item.quantity).toLocaleString()}{' '}
|
||||
{"so'm"}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Pricing */}
|
||||
<div className="space-y-2 mb-4 pt-4 border-t">
|
||||
<div className="flex justify-between text-gray-600">
|
||||
<span>Mahsulotlar:</span>
|
||||
<span>
|
||||
{subtotal.toLocaleString()} {"so'm"}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-between text-gray-600">
|
||||
<span>Yetkazib berish:</span>
|
||||
<span>
|
||||
{deliveryFee === 0 ? (
|
||||
<span className="text-green-600 font-semibold">
|
||||
Bepul
|
||||
</span>
|
||||
) : (
|
||||
`${deliveryFee.toLocaleString()} so'm`
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="border-t pt-4 mb-6">
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-lg font-semibold">Jami:</span>
|
||||
<span className="text-2xl font-bold text-blue-600">
|
||||
{total.toLocaleString()} {"so'm"}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
disabled={isSubmitting}
|
||||
className="w-full bg-blue-600 text-white py-4 rounded-lg font-semibold hover:bg-blue-700 transition disabled:bg-gray-400 disabled:cursor-not-allowed"
|
||||
>
|
||||
{isSubmitting ? (
|
||||
<span className="flex items-center justify-center gap-2">
|
||||
<div className="w-5 h-5 border-2 border-white border-t-transparent rounded-full animate-spin" />
|
||||
Yuborilmoqda...
|
||||
</span>
|
||||
) : (
|
||||
'Buyurtmani tasdiqlash'
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default OrderPage;
|
||||
1099
src/features/category/lib/data.ts
Normal file
1099
src/features/category/lib/data.ts
Normal file
File diff suppressed because it is too large
Load Diff
36
src/features/category/ui/Category.tsx
Normal file
36
src/features/category/ui/Category.tsx
Normal file
@@ -0,0 +1,36 @@
|
||||
'use client';
|
||||
import { useRouter } from '@/shared/config/i18n/navigation';
|
||||
import { categoryList, CategoryType } from '@/widgets/welcome/lib/data';
|
||||
import { ChevronRight } from 'lucide-react';
|
||||
|
||||
const Category = () => {
|
||||
const router = useRouter();
|
||||
const handleCategoryClick = (category: CategoryType) => {
|
||||
router.push(`/category/${category.name}`);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50 p-4">
|
||||
<div className="max-w-6xl mx-auto">
|
||||
<h1 className="text-2xl font-semibold text-gray-900 mb-6">
|
||||
Kategoriyalar
|
||||
</h1>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-3">
|
||||
{categoryList.map((category, index) => (
|
||||
<button
|
||||
key={index}
|
||||
onClick={() => handleCategoryClick(category)}
|
||||
className="bg-white border border-gray-200 rounded-lg p-4 flex items-center justify-between hover:border-gray-300 transition-colors"
|
||||
>
|
||||
<span className="text-gray-900 font-medium">{category.name}</span>
|
||||
<ChevronRight className="w-5 h-5 text-gray-400" />
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Category;
|
||||
76
src/features/category/ui/Product.tsx
Normal file
76
src/features/category/ui/Product.tsx
Normal file
@@ -0,0 +1,76 @@
|
||||
'use client';
|
||||
|
||||
import { useRouter } from '@/shared/config/i18n/navigation';
|
||||
import { ProductCard } from '@/widgets/categories/ui/product-card';
|
||||
import { ArrowLeft } from 'lucide-react';
|
||||
import { useParams } from 'next/navigation';
|
||||
import { useState } from 'react';
|
||||
import { subCategoriesData } from '../lib/data';
|
||||
|
||||
const Product = () => {
|
||||
const { subId } = useParams();
|
||||
const router = useRouter();
|
||||
|
||||
const decodedSubId = decodeURIComponent(subId as string);
|
||||
|
||||
const subCategory =
|
||||
subCategoriesData.find((cat) => cat.name === decodedSubId) ||
|
||||
subCategoriesData[0];
|
||||
const [products, setProducts] = useState(subCategory.products);
|
||||
const handleBack = () => {
|
||||
router.back();
|
||||
};
|
||||
|
||||
const handleRemove = (id: number) => {
|
||||
setProducts((prev) =>
|
||||
prev.map((product) =>
|
||||
product.id === id ? { ...product, liked: false } : product,
|
||||
),
|
||||
);
|
||||
};
|
||||
|
||||
const handleLiked = (id: number) => {
|
||||
setProducts((prev) =>
|
||||
prev.map((product) =>
|
||||
product.id === id ? { ...product, liked: true } : product,
|
||||
),
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="custom-container p-4 mb-5">
|
||||
<div>
|
||||
{/* Header */}
|
||||
<div className="mb-6">
|
||||
<button
|
||||
onClick={handleBack}
|
||||
className="flex items-center gap-2 text-gray-600 hover:text-gray-900 mb-4"
|
||||
>
|
||||
<ArrowLeft className="w-5 h-5" />
|
||||
<span>Orqaga</span>
|
||||
</button>
|
||||
|
||||
<h1 className="text-2xl font-semibold text-gray-900">
|
||||
{decodedSubId}
|
||||
</h1>
|
||||
<p className="text-gray-600 text-sm mt-1">
|
||||
{subCategory.products.length} ta mahsulot
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 lg:grid-cols-4 gap-3">
|
||||
{products.map((product) => (
|
||||
<ProductCard
|
||||
key={product.id}
|
||||
product={product}
|
||||
handleRemove={handleRemove}
|
||||
handleLiked={handleLiked}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Product;
|
||||
45
src/features/category/ui/SubCategory.tsx
Normal file
45
src/features/category/ui/SubCategory.tsx
Normal file
@@ -0,0 +1,45 @@
|
||||
'use client';
|
||||
|
||||
import { useRouter } from '@/shared/config/i18n/navigation';
|
||||
import { categoryList } from '@/widgets/welcome/lib/data';
|
||||
import { ChevronRight } from 'lucide-react';
|
||||
import { useParams } from 'next/navigation';
|
||||
|
||||
const SubCategory = () => {
|
||||
const { categoryId } = useParams();
|
||||
|
||||
const router = useRouter();
|
||||
const category =
|
||||
categoryList.find((cat) => cat.name === categoryId) || categoryList[0];
|
||||
|
||||
const handleSubCategoryClick = (subCategory: { name: string }) => {
|
||||
router.push(`/category/${categoryId}/${subCategory.name}`);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50 p-4">
|
||||
<div className="max-w-6xl mx-auto">
|
||||
<h1 className="text-2xl font-semibold text-gray-900 mb-6">
|
||||
{category.name}
|
||||
</h1>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-3">
|
||||
{category.subCategories.map((subCategory, index) => (
|
||||
<button
|
||||
key={index}
|
||||
onClick={() => handleSubCategoryClick(subCategory)}
|
||||
className="bg-white border border-gray-200 rounded-lg p-4 flex items-center justify-between hover:border-gray-300 transition-colors"
|
||||
>
|
||||
<span className="text-gray-900 font-medium">
|
||||
{subCategory.name}
|
||||
</span>
|
||||
<ChevronRight className="w-5 h-5 text-gray-400" />
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SubCategory;
|
||||
145
src/features/favourite/ui/Favourite.tsx
Normal file
145
src/features/favourite/ui/Favourite.tsx
Normal file
@@ -0,0 +1,145 @@
|
||||
'use client';
|
||||
|
||||
import { useRouter } from '@/shared/config/i18n/navigation';
|
||||
import { Button } from '@/shared/ui/button';
|
||||
import { ProductCard } from '@/widgets/categories/ui/product-card';
|
||||
import { Heart } from 'lucide-react';
|
||||
import { useState } from 'react';
|
||||
|
||||
// Fake data
|
||||
const LIKED_PRODUCTS = [
|
||||
{
|
||||
id: 1,
|
||||
name: 'Samsung Galaxy S23 Ultra 256GB, Phantom Black',
|
||||
price: 12500000,
|
||||
oldPrice: 15000000,
|
||||
image: 'https://images.unsplash.com/photo-1610945415295-d9bbf067e59c?w=400',
|
||||
rating: 4.8,
|
||||
reviews: 342,
|
||||
discount: 17,
|
||||
inStock: true,
|
||||
liked: true,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: 'Apple AirPods Pro 2-chi avlod (USB-C)',
|
||||
price: 2850000,
|
||||
oldPrice: 3200000,
|
||||
image: 'https://images.unsplash.com/photo-1606841837239-c5a1a4a07af7?w=400',
|
||||
rating: 4.9,
|
||||
reviews: 567,
|
||||
discount: 11,
|
||||
liked: true,
|
||||
inStock: true,
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: "Sony PlayStation 5 Slim 1TB + 2 ta o'yin",
|
||||
price: 7500000,
|
||||
oldPrice: 8500000,
|
||||
image: 'https://images.unsplash.com/photo-1606813907291-d86efa9b94db?w=400',
|
||||
rating: 4.7,
|
||||
reviews: 234,
|
||||
discount: 12,
|
||||
inStock: true,
|
||||
liked: true,
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
name: 'MacBook Air 13 M2 chip, 8GB RAM, 256GB SSD',
|
||||
price: 14200000,
|
||||
oldPrice: 16000000,
|
||||
image: 'https://images.unsplash.com/photo-1517336714731-489689fd1ca8?w=400',
|
||||
rating: 4.9,
|
||||
reviews: 891,
|
||||
liked: true,
|
||||
discount: 11,
|
||||
inStock: true,
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
name: 'Dyson V15 Detect Simsiz Changyutgich',
|
||||
price: 6800000,
|
||||
oldPrice: 7800000,
|
||||
image: 'https://images.unsplash.com/photo-1558317374-067fb5f30001?w=400',
|
||||
rating: 4.6,
|
||||
reviews: 178,
|
||||
discount: 13,
|
||||
liked: true,
|
||||
inStock: false,
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
name: 'Nike Air Max 270 React Erkaklar Krosovkasi',
|
||||
price: 1250000,
|
||||
oldPrice: 1650000,
|
||||
image: 'https://images.unsplash.com/photo-1542291026-7eec264c27ff?w=400',
|
||||
rating: 4.5,
|
||||
liked: true,
|
||||
reviews: 423,
|
||||
discount: 24,
|
||||
inStock: true,
|
||||
},
|
||||
];
|
||||
|
||||
export default function Favourite() {
|
||||
const [likedProducts, setLikedProducts] = useState(LIKED_PRODUCTS);
|
||||
const router = useRouter();
|
||||
|
||||
const handleRemove = (id: number) => {
|
||||
setLikedProducts((prev) => prev.filter((product) => product.id !== id));
|
||||
};
|
||||
|
||||
if (likedProducts.length === 0) {
|
||||
return (
|
||||
<div className="min-h-screen bg-slate-50 py-12">
|
||||
<div className="container mx-auto px-4">
|
||||
<div className="flex flex-col items-center justify-center py-20">
|
||||
<div className="w-32 h-32 bg-slate-100 rounded-full flex items-center justify-center mb-6">
|
||||
<Heart className="w-16 h-16 text-slate-300" />
|
||||
</div>
|
||||
<h2 className="text-2xl font-bold text-slate-800 mb-2">
|
||||
{"Sevimlilar bo'sh"}
|
||||
</h2>
|
||||
<p className="text-slate-500 text-center max-w-md mb-8">
|
||||
{`Hali hech qanday mahsulotni sevimlilarga qo'shmadingiz.
|
||||
Mahsulotlar ro'yxatiga o'ting va yoqqan mahsulotlaringizni
|
||||
saqlang.`}
|
||||
</p>
|
||||
<Button
|
||||
className="bg-blue-600 text-white px-8 py-3 rounded-xl hover:bg-blue-700"
|
||||
onClick={() => router.push('/')}
|
||||
>
|
||||
Xarid qilishni boshlash
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-slate-50 py-8">
|
||||
<div className="container mx-auto px-4">
|
||||
<div className="flex items-center justify-between mb-8">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold text-slate-800 mb-2">
|
||||
Sevimli mahsulotlar
|
||||
</h1>
|
||||
<p className="text-slate-500">{likedProducts.length} ta mahsulot</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
|
||||
{likedProducts.map((product) => (
|
||||
<ProductCard
|
||||
key={product.id}
|
||||
product={product}
|
||||
handleRemove={handleRemove}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
360
src/features/product/ui/Product.tsx
Normal file
360
src/features/product/ui/Product.tsx
Normal file
@@ -0,0 +1,360 @@
|
||||
'use client';
|
||||
|
||||
import { Carousel, CarouselContent, CarouselItem } from '@/shared/ui/carousel';
|
||||
import { ProductCard } from '@/widgets/categories/ui/product-card';
|
||||
import {
|
||||
Heart,
|
||||
Minus,
|
||||
Plus,
|
||||
RotateCcw,
|
||||
Shield,
|
||||
ShoppingCart,
|
||||
Star,
|
||||
Truck,
|
||||
} from 'lucide-react';
|
||||
import Image from 'next/image';
|
||||
import { useState } from 'react';
|
||||
|
||||
const ProductDetail = () => {
|
||||
const [quantity, setQuantity] = useState(1);
|
||||
const [selectedImage, setSelectedImage] = useState(0);
|
||||
const [liked, setLiked] = useState(false);
|
||||
|
||||
// Fake product data
|
||||
const product = {
|
||||
id: 5,
|
||||
name: 'Coca-Cola 1.5L',
|
||||
price: 12000,
|
||||
oldPrice: 14000,
|
||||
image: '/classic-coca-cola.png',
|
||||
rating: 4.8,
|
||||
reviews: 342,
|
||||
discount: 14,
|
||||
inStock: true,
|
||||
description:
|
||||
"Coca-Cola klassik ta'mi bilan ajoyib gazlangan ichimlik. 1.5 litrlik shisha butilkada. Sovuq holda iste'mol qilish tavsiya etiladi.",
|
||||
category: 'Ichimliklar',
|
||||
brand: 'Coca-Cola',
|
||||
volume: '1.5L',
|
||||
images: [
|
||||
'/classic-coca-cola.png',
|
||||
'/clear-soda-bottle.png',
|
||||
'/classic-coca-cola.png',
|
||||
'/classic-coca-cola.png',
|
||||
'/classic-coca-cola.png',
|
||||
'/classic-coca-cola.png',
|
||||
'/classic-coca-cola.png',
|
||||
'/classic-coca-cola.png',
|
||||
'/classic-coca-cola.png',
|
||||
'/classic-coca-cola.png',
|
||||
'/classic-coca-cola.png',
|
||||
],
|
||||
specifications: {
|
||||
Hajmi: '1.5 litr',
|
||||
'Qadoq turi': 'Plastik butilka',
|
||||
'Ishlab chiqaruvchi': 'Coca-Cola Company',
|
||||
'Saqlash muddati': '12 oy',
|
||||
'Energiya qiymati': '180 kJ / 43 kcal',
|
||||
},
|
||||
};
|
||||
|
||||
const [relatedProducts, setRelatedProducts] = useState([
|
||||
{
|
||||
id: 6,
|
||||
name: 'Pepsi 2L',
|
||||
price: 11000,
|
||||
reviews: 342,
|
||||
liked: false,
|
||||
inStock: true,
|
||||
oldPrice: 13000,
|
||||
image: '/pepsi-bottle.jpg',
|
||||
rating: 4.6,
|
||||
discount: 15,
|
||||
},
|
||||
{
|
||||
id: 8,
|
||||
name: 'Sprite 1.5L',
|
||||
price: 10000,
|
||||
inStock: true,
|
||||
oldPrice: 12000,
|
||||
image: '/clear-soda-bottle.png',
|
||||
rating: 4.5,
|
||||
reviews: 342,
|
||||
liked: false,
|
||||
discount: 17,
|
||||
},
|
||||
{
|
||||
id: 7,
|
||||
name: 'Fanta Orange 1L',
|
||||
price: 9000,
|
||||
oldPrice: 10000,
|
||||
inStock: true,
|
||||
image: '/fanta-orange-bottle.png',
|
||||
rating: 4.4,
|
||||
reviews: 342,
|
||||
liked: true,
|
||||
discount: 10,
|
||||
},
|
||||
]);
|
||||
|
||||
const handleQuantityChange = (type: string) => {
|
||||
if (type === 'increase') {
|
||||
setQuantity(quantity + 1);
|
||||
} else if (type === 'decrease' && quantity > 1) {
|
||||
setQuantity(quantity - 1);
|
||||
}
|
||||
};
|
||||
|
||||
const addToCart = () => {
|
||||
alert(`${quantity} ta ${product.name} savatchaga qo'shildi!`);
|
||||
};
|
||||
|
||||
const handleRemove = (id: number) => {
|
||||
setRelatedProducts((prev) =>
|
||||
prev.map((product) =>
|
||||
product.id === id ? { ...product, liked: false } : product,
|
||||
),
|
||||
);
|
||||
};
|
||||
|
||||
const handleLiked = (id: number) => {
|
||||
setRelatedProducts((prev) =>
|
||||
prev.map((product) =>
|
||||
product.id === id ? { ...product, liked: true } : product,
|
||||
),
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="custom-container pb-5">
|
||||
<div className="">
|
||||
<div className="bg-white rounded-lg shadow-md p-6 mb-8">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
|
||||
<div>
|
||||
<div className="relative bg-gray-100 rounded-lg overflow-hidden mb-4">
|
||||
<Image
|
||||
width={500}
|
||||
height={500}
|
||||
src={product.images[selectedImage]}
|
||||
alt={product.name}
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
{product.discount > 0 && (
|
||||
<div className="absolute top-4 left-4 bg-red-500 text-white px-3 py-1 rounded-full text-sm font-semibold">
|
||||
-{product.discount}%
|
||||
</div>
|
||||
)}
|
||||
{!product.inStock && (
|
||||
<div className="absolute inset-0 bg-black bg-opacity-50 flex items-center justify-center">
|
||||
<span className="text-white text-xl font-bold">
|
||||
Mavjud emas
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Carousel
|
||||
opts={{
|
||||
align: 'start',
|
||||
dragFree: true,
|
||||
}}
|
||||
className="w-full"
|
||||
>
|
||||
<CarouselContent className="-ml-2">
|
||||
{product.images.map((img, index) => (
|
||||
<CarouselItem
|
||||
key={index}
|
||||
className="pl-2 basis-1/3 sm:basis-1/4 md:basis-1/5 lg:basis-1/6"
|
||||
>
|
||||
<button
|
||||
onClick={() => setSelectedImage(index)}
|
||||
className={`aspect-square w-full rounded-lg border-2 overflow-hidden transition ${
|
||||
selectedImage === index
|
||||
? 'border-blue-500'
|
||||
: 'border-gray-200 hover:border-blue-300'
|
||||
}`}
|
||||
>
|
||||
<Image
|
||||
src={img}
|
||||
alt={`thumb-${index}`}
|
||||
width={150}
|
||||
height={150}
|
||||
className="w-full h-full object-contain"
|
||||
/>
|
||||
</button>
|
||||
</CarouselItem>
|
||||
))}
|
||||
</CarouselContent>
|
||||
</Carousel>
|
||||
</div>
|
||||
|
||||
{/* Product Info */}
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold text-gray-800 mb-2">
|
||||
{product.name}
|
||||
</h1>
|
||||
|
||||
{/* Rating */}
|
||||
<div className="flex items-center gap-2 mb-4">
|
||||
<div className="flex items-center">
|
||||
{[...Array(5)].map((_, i) => (
|
||||
<Star
|
||||
key={i}
|
||||
className={`w-5 h-5 ${
|
||||
i < Math.floor(product.rating)
|
||||
? 'fill-yellow-400 text-yellow-400'
|
||||
: 'text-gray-300'
|
||||
}`}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<span className="text-gray-600">{product.rating}</span>
|
||||
<span className="text-gray-400">({product.reviews} sharh)</span>
|
||||
</div>
|
||||
|
||||
{/* Price */}
|
||||
<div className="mb-6">
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="text-4xl font-bold text-blue-600">
|
||||
{product.price.toLocaleString()} {"so'm"}
|
||||
</span>
|
||||
{product.oldPrice && (
|
||||
<span className="text-xl text-gray-400 line-through">
|
||||
{product.oldPrice.toLocaleString()} {"so'm"}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Description */}
|
||||
<p className="text-gray-600 mb-6">{product.description}</p>
|
||||
|
||||
{/* Brand and Category */}
|
||||
<div className="grid grid-cols-2 gap-4 mb-6">
|
||||
<div>
|
||||
<span className="text-gray-500">Brand:</span>
|
||||
<p className="font-semibold">{product.brand}</p>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-gray-500">Kategoriya:</span>
|
||||
<p className="font-semibold">{product.category}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Quantity Selector */}
|
||||
<div className="mb-6">
|
||||
<label className="text-gray-700 font-medium mb-2 block">
|
||||
Miqdor:
|
||||
</label>
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="flex items-center border border-gray-300 rounded-lg">
|
||||
<button
|
||||
onClick={() => handleQuantityChange('decrease')}
|
||||
className="p-3 hover:bg-gray-100 transition"
|
||||
disabled={quantity <= 1}
|
||||
>
|
||||
<Minus className="w-5 h-5" />
|
||||
</button>
|
||||
<span className="px-6 font-semibold text-lg">
|
||||
{quantity}
|
||||
</span>
|
||||
<button
|
||||
onClick={() => handleQuantityChange('increase')}
|
||||
className="p-3 hover:bg-gray-100 transition"
|
||||
>
|
||||
<Plus className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
<span className="text-gray-600">
|
||||
Jami:{' '}
|
||||
<span className="font-bold text-lg">
|
||||
{(product.price * quantity).toLocaleString()} {"so'm"}
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Action Buttons */}
|
||||
<div className="flex gap-4 mb-6">
|
||||
<button
|
||||
onClick={addToCart}
|
||||
disabled={!product.inStock}
|
||||
className={`flex-1 py-4 rounded-lg font-semibold text-white flex items-center justify-center gap-2 transition ${
|
||||
product.inStock
|
||||
? 'bg-blue-600 hover:bg-blue-700'
|
||||
: 'bg-gray-400 cursor-not-allowed'
|
||||
}`}
|
||||
>
|
||||
<ShoppingCart className="w-5 h-5" />
|
||||
{"Savatchaga qo'shish"}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setLiked(!liked)}
|
||||
className={`p-4 rounded-lg border-2 transition ${
|
||||
liked
|
||||
? 'border-red-500 bg-red-50'
|
||||
: 'border-gray-300 hover:border-red-500'
|
||||
}`}
|
||||
>
|
||||
<Heart
|
||||
className={`w-6 h-6 ${liked ? 'fill-red-500 text-red-500' : 'text-gray-600'}`}
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Features */}
|
||||
<div className="grid grid-cols-3 gap-4 border-t pt-6">
|
||||
<div className="flex flex-col items-center text-center">
|
||||
<Truck className="w-8 h-8 text-blue-600 mb-2" />
|
||||
<span className="text-sm text-gray-600">
|
||||
Bepul yetkazib berish
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex flex-col items-center text-center">
|
||||
<Shield className="w-8 h-8 text-blue-600 mb-2" />
|
||||
<span className="text-sm text-gray-600">Kafolat</span>
|
||||
</div>
|
||||
<div className="flex flex-col items-center text-center">
|
||||
<RotateCcw className="w-8 h-8 text-blue-600 mb-2" />
|
||||
<span className="text-sm text-gray-600">
|
||||
14 kun qaytarish
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Specifications */}
|
||||
<div className="bg-white rounded-lg shadow-md p-6 mb-8">
|
||||
<h2 className="text-2xl font-bold mb-4">Xususiyatlari</h2>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
{Object.entries(product.specifications).map(([key, value]) => (
|
||||
<div key={key} className="flex justify-between border-b pb-2">
|
||||
<span className="text-gray-600">{key}:</span>
|
||||
<span className="font-semibold">{value}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Related Products */}
|
||||
<div className="bg-white rounded-lg shadow-md p-6">
|
||||
<h2 className="text-2xl font-bold mb-6">{"O'xshash mahsulotlar"}</h2>
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-6">
|
||||
{relatedProducts.map((item) => (
|
||||
<ProductCard
|
||||
key={item.id}
|
||||
product={item}
|
||||
handleRemove={handleRemove}
|
||||
handleLiked={handleLiked}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProductDetail;
|
||||
1069
src/features/profile/ui/Profile.tsx
Normal file
1069
src/features/profile/ui/Profile.tsx
Normal file
File diff suppressed because it is too large
Load Diff
156
src/features/search/ui/Search.tsx
Normal file
156
src/features/search/ui/Search.tsx
Normal file
@@ -0,0 +1,156 @@
|
||||
'use client';
|
||||
|
||||
import { Input } from '@/shared/ui/input';
|
||||
import {
|
||||
categories,
|
||||
Product,
|
||||
ProductDetail,
|
||||
} from '@/widgets/categories/lib/data';
|
||||
import { ProductCard } from '@/widgets/categories/ui/product-card';
|
||||
import { Search, X } from 'lucide-react';
|
||||
import { useRouter, useSearchParams } from 'next/navigation';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
|
||||
const SearchResult: React.FC = () => {
|
||||
const router = useRouter();
|
||||
const searchParams = useSearchParams();
|
||||
|
||||
const queryFromUrl = searchParams.get('q') ?? '';
|
||||
|
||||
const [query, setQuery] = useState(queryFromUrl);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [results, setResults] = useState<ProductDetail[]>([]);
|
||||
|
||||
const allProducts = useMemo<ProductDetail[]>(() => {
|
||||
return categories.flatMap((category: Product) =>
|
||||
category.products.map((product) => ({
|
||||
...product,
|
||||
categoryName: category.name,
|
||||
})),
|
||||
);
|
||||
}, []);
|
||||
|
||||
const recommendedProducts = useMemo<ProductDetail[]>(() => {
|
||||
return allProducts.filter((product) => product.rating >= 4.5).slice(0, 8);
|
||||
}, [allProducts]);
|
||||
|
||||
const handleSearch = (searchQuery: string) => {
|
||||
if (!searchQuery.trim()) return;
|
||||
|
||||
setLoading(true);
|
||||
router.push(`/search?q=${encodeURIComponent(searchQuery)}`);
|
||||
|
||||
setTimeout(() => {
|
||||
const filtered = allProducts.filter(
|
||||
(product) =>
|
||||
product.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
product.categoryName
|
||||
?.toLowerCase()
|
||||
.includes(searchQuery.toLowerCase()),
|
||||
);
|
||||
|
||||
setResults(filtered);
|
||||
setLoading(false);
|
||||
}, 300);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (queryFromUrl) {
|
||||
handleSearch(queryFromUrl);
|
||||
}
|
||||
}, [queryFromUrl]);
|
||||
|
||||
const clearSearch = () => {
|
||||
setQuery('');
|
||||
setResults([]);
|
||||
router.push('/search');
|
||||
};
|
||||
|
||||
const handleRemove = (id: number) => {
|
||||
setResults((prev) =>
|
||||
prev.map((product) =>
|
||||
product.id === id ? { ...product, liked: false } : product,
|
||||
),
|
||||
);
|
||||
};
|
||||
|
||||
const handleLiked = (id: number) => {
|
||||
setResults((prev) =>
|
||||
prev.map((product) =>
|
||||
product.id === id ? { ...product, liked: true } : product,
|
||||
),
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="custom-container justify-center items-center h-screen">
|
||||
<div className="lg:hidden">
|
||||
<div className="relative">
|
||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-5 h-5 text-slate-400" />
|
||||
|
||||
<Input
|
||||
value={query}
|
||||
placeholder="Mahsulot nomi"
|
||||
onChange={(e) => {
|
||||
setQuery(e.target.value);
|
||||
handleSearch(e.target.value);
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') handleSearch(query);
|
||||
}}
|
||||
className="w-full border rounded-lg pl-10 pr-10 h-12"
|
||||
/>
|
||||
|
||||
{query && (
|
||||
<button
|
||||
onClick={clearSearch}
|
||||
className="absolute right-7 top-1/2 -translate-y-1/2"
|
||||
>
|
||||
<X />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="px-4 py-8">
|
||||
{loading ? (
|
||||
<div className="text-center py-20">Yuklanmoqda...</div>
|
||||
) : query ? (
|
||||
results.length ? (
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 lg:grid-cols-4 gap-4">
|
||||
{results.map((product) => (
|
||||
<ProductCard
|
||||
key={product.id}
|
||||
product={product}
|
||||
handleRemove={handleRemove}
|
||||
handleLiked={handleLiked}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-center py-20">Natija topilmadi</div>
|
||||
)
|
||||
) : (
|
||||
<div className="lg:hidden">
|
||||
<h2 className="text-lg font-semibold mb-4">
|
||||
Tavsiya etilgan mahsulotlar
|
||||
</h2>
|
||||
|
||||
<div className="grid grid-cols-1 gap-4">
|
||||
{recommendedProducts.map((product) => (
|
||||
<ProductCard
|
||||
key={product.id}
|
||||
product={product}
|
||||
handleRemove={handleRemove}
|
||||
handleLiked={handleLiked}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SearchResult;
|
||||
Reference in New Issue
Block a user