first commit

This commit is contained in:
Samandar Turgunboyev
2025-12-15 18:41:13 +05:00
parent a5b46a9f26
commit 6bf86f39c6
109 changed files with 7007 additions and 295 deletions

View File

@@ -0,0 +1,11 @@
import Login from '@/features/auth/ui/Login';
const page = () => {
return (
<div>
<Login />
</div>
);
};
export default page;

View File

@@ -0,0 +1,11 @@
import OrderPage from '@/features/cart/ui/OrderPage';
const page = () => {
return (
<div>
<OrderPage />
</div>
);
};
export default page;

View File

@@ -0,0 +1,11 @@
import CartPage from '@/features/cart/ui/CartPage';
const page = () => {
return (
<div>
<CartPage />
</div>
);
};
export default page;

View File

@@ -0,0 +1,11 @@
import Product from '@/features/category/ui/Product';
const page = () => {
return (
<div>
<Product />
</div>
);
};
export default page;

View File

@@ -0,0 +1,11 @@
import SubCategory from '@/features/category/ui/SubCategory';
const page = () => {
return (
<div>
<SubCategory />
</div>
);
};
export default page;

View File

@@ -0,0 +1,11 @@
import Category from '@/features/category/ui/Category';
const page = () => {
return (
<div>
<Category />
</div>
);
};
export default page;

View File

@@ -0,0 +1,11 @@
import Favourite from '@/features/favourite/ui/Favourite';
const page = () => {
return (
<div>
<Favourite />
</div>
);
};
export default page;

View File

@@ -0,0 +1,27 @@
'use client';
import { usePathname } from '@/shared/config/i18n/navigation';
import Footer from '@/widgets/footer/ui';
import Navbar from '@/widgets/navbar/ui';
const HIDE_FOOTER_ROUTES = ['/auth', '/checkout'];
export default function LayoutShell({
children,
}: {
children: React.ReactNode;
}) {
const pathname = usePathname();
const hideFooter = HIDE_FOOTER_ROUTES.some((route) =>
pathname.startsWith(route),
);
return (
<>
<Navbar />
{children}
{!hideFooter && <Footer />}
</>
);
}

View File

@@ -1,17 +1,16 @@
import type { Metadata } from 'next';
import '../globals.css';
import { golosText } from '@/shared/config/fonts';
import { poppins } from '@/shared/config/fonts';
import { routing } from '@/shared/config/i18n/routing';
import QueryProvider from '@/shared/config/react-query/QueryProvider';
import { ThemeProvider } from '@/shared/config/theme-provider';
import { PRODUCT_INFO } from '@/shared/constants/data';
import type { Metadata } from 'next';
import { hasLocale, Locale, NextIntlClientProvider } from 'next-intl';
import { routing } from '@/shared/config/i18n/routing';
import { notFound } from 'next/navigation';
import Footer from '@/widgets/footer/ui';
import Navbar from '@/widgets/navbar/ui';
import { ReactNode } from 'react';
import { setRequestLocale } from 'next-intl/server';
import QueryProvider from '@/shared/config/react-query/QueryProvider';
import { notFound } from 'next/navigation';
import Script from 'next/script';
import { ReactNode } from 'react';
import '../globals.css';
import LayoutShell from './layout-shell';
export const metadata: Metadata = {
title: PRODUCT_INFO.name,
@@ -39,7 +38,7 @@ export default async function RootLayout({ children, params }: Props) {
return (
<html lang={locale} suppressHydrationWarning>
<body className={`${golosText.variable} antialiased`}>
<body className={`${poppins.className} antialiased`}>
<NextIntlClientProvider locale={locale}>
<ThemeProvider
attribute={'class'}
@@ -48,9 +47,7 @@ export default async function RootLayout({ children, params }: Props) {
disableTransitionOnChange
>
<QueryProvider>
<Navbar />
{children}
<Footer />
<LayoutShell>{children}</LayoutShell>
</QueryProvider>
</ThemeProvider>
</NextIntlClientProvider>

View File

@@ -1,13 +1,14 @@
import { getPosts } from '@/shared/config/api/testApi';
import Welcome from '@/widgets/welcome';
import { subCategoriesData } from '@/features/category/lib/data';
import { CategoryCarousel } from '@/widgets/categories/ui/category-carousel';
import Welcome from '@/widgets/welcome/ui';
export default async function Home() {
const res = await getPosts({ _limit: 1 });
console.log('SSR res', res.data);
return (
<div>
<Welcome />
{subCategoriesData.slice(0, 6).map((e) => (
<CategoryCarousel category={e} key={e.id} />
))}
</div>
);
}

View File

@@ -0,0 +1,11 @@
import ProductDetail from '@/features/product/ui/Product';
const page = () => {
return (
<div>
<ProductDetail />
</div>
);
};
export default page;

View File

@@ -0,0 +1,11 @@
import Profile from '@/features/profile/ui/Profile';
const page = () => {
return (
<div>
<Profile />
</div>
);
};
export default page;

View File

@@ -0,0 +1,11 @@
import SearchResult from '@/features/search/ui/Search';
const page = () => {
return (
<div>
<SearchResult />
</div>
);
};
export default page;

BIN
src/assets/banner.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 KiB

BIN
src/assets/water-bottle.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

View 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;

View 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;

View 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;

File diff suppressed because it is too large Load Diff

View 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;

View 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;

View 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;

View 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>
);
}

View 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;

File diff suppressed because it is too large Load Diff

View 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;

View File

@@ -11,14 +11,11 @@ const httpClient = axios.create({
httpClient.interceptors.request.use(
async (config) => {
console.log(`API REQUEST to ${config.url}`, config);
// Language configs
let language = LanguageRoutes.UZ;
try {
language = (await getLocale()) as LanguageRoutes;
} catch (e) {
console.log('error', e);
} catch {
language = getLocaleCS() || LanguageRoutes.UZ;
}

View File

@@ -1,9 +1,9 @@
import { Golos_Text } from 'next/font/google';
import { Poppins } from 'next/font/google';
const golosText = Golos_Text({
const poppins = Poppins({
weight: ['400', '500', '600', '700', '800'],
variable: '--font-golos-text',
subsets: ['latin', 'cyrillic'],
variable: '--font-poppins',
subsets: ['latin'],
});
export { golosText };
export { poppins };

View File

@@ -33,7 +33,7 @@ const useCloser = (
document.removeEventListener('mousedown', handleClickOutside);
document.removeEventListener('scroll', handleScroll);
};
}, [ref, closeFunction]);
}, [ref, closeFunction, scrollClose]);
};
export default useCloser;

View File

@@ -1,5 +1,5 @@
import { LanguageRoutes } from '../config/i18n/types';
import { getLocale } from 'next-intl/server';
import getLocaleCS from './getLocaleCS';
/**
* Format price. With label.
@@ -7,12 +7,12 @@ import { getLocale } from 'next-intl/server';
* @param withLabel Show label. Default false
* @returns string. Ex. X XXX XXX sum
*/
const formatPrice = async (amount: number | string, withLabel?: boolean) => {
const locale = (await getLocale()) as LanguageRoutes;
const formatPrice = (amount: number | string, withLabel?: boolean) => {
const locale = getLocaleCS() as LanguageRoutes;
const label = withLabel
? locale == LanguageRoutes.RU
? locale === LanguageRoutes.RU
? ' сум'
: locale == LanguageRoutes.KI
: locale === LanguageRoutes.KI
? ' сўм'
: ' som'
: '';
@@ -22,7 +22,7 @@ const formatPrice = async (amount: number | string, withLabel?: boolean) => {
const formattedDollars = dollars.replace(/\B(?=(\d{3})+(?!\d))/g, ' ');
if (String(amount).length == 0) {
if (String(amount).length === 0) {
return formattedDollars + '.' + cents + label;
} else {
return formattedDollars + label;

View File

@@ -0,0 +1,11 @@
'use client';
import * as AspectRatioPrimitive from '@radix-ui/react-aspect-ratio';
function AspectRatio({
...props
}: React.ComponentProps<typeof AspectRatioPrimitive.Root>) {
return <AspectRatioPrimitive.Root data-slot="aspect-ratio" {...props} />;
}
export { AspectRatio };

53
src/shared/ui/avatar.tsx Normal file
View File

@@ -0,0 +1,53 @@
'use client';
import * as React from 'react';
import * as AvatarPrimitive from '@radix-ui/react-avatar';
import { cn } from '@/shared/lib/utils';
function Avatar({
className,
...props
}: React.ComponentProps<typeof AvatarPrimitive.Root>) {
return (
<AvatarPrimitive.Root
data-slot="avatar"
className={cn(
'relative flex size-8 shrink-0 overflow-hidden rounded-full',
className,
)}
{...props}
/>
);
}
function AvatarImage({
className,
...props
}: React.ComponentProps<typeof AvatarPrimitive.Image>) {
return (
<AvatarPrimitive.Image
data-slot="avatar-image"
className={cn('aspect-square size-full', className)}
{...props}
/>
);
}
function AvatarFallback({
className,
...props
}: React.ComponentProps<typeof AvatarPrimitive.Fallback>) {
return (
<AvatarPrimitive.Fallback
data-slot="avatar-fallback"
className={cn(
'bg-muted flex size-full items-center justify-center rounded-full',
className,
)}
{...props}
/>
);
}
export { Avatar, AvatarImage, AvatarFallback };

46
src/shared/ui/badge.tsx Normal file
View File

@@ -0,0 +1,46 @@
import * as React from 'react';
import { Slot } from '@radix-ui/react-slot';
import { cva, type VariantProps } from 'class-variance-authority';
import { cn } from '@/shared/lib/utils';
const badgeVariants = cva(
'inline-flex items-center justify-center rounded-full border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden',
{
variants: {
variant: {
default:
'border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90',
secondary:
'border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90',
destructive:
'border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60',
outline:
'text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground',
},
},
defaultVariants: {
variant: 'default',
},
},
);
function Badge({
className,
variant,
asChild = false,
...props
}: React.ComponentProps<'span'> &
VariantProps<typeof badgeVariants> & { asChild?: boolean }) {
const Comp = asChild ? Slot : 'span';
return (
<Comp
data-slot="badge"
className={cn(badgeVariants({ variant }), className)}
{...props}
/>
);
}
export { Badge, badgeVariants };

92
src/shared/ui/card.tsx Normal file
View File

@@ -0,0 +1,92 @@
import * as React from 'react';
import { cn } from '@/shared/lib/utils';
function Card({ className, ...props }: React.ComponentProps<'div'>) {
return (
<div
data-slot="card"
className={cn(
'bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm',
className,
)}
{...props}
/>
);
}
function CardHeader({ className, ...props }: React.ComponentProps<'div'>) {
return (
<div
data-slot="card-header"
className={cn(
'@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-2 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6',
className,
)}
{...props}
/>
);
}
function CardTitle({ className, ...props }: React.ComponentProps<'div'>) {
return (
<div
data-slot="card-title"
className={cn('leading-none font-semibold', className)}
{...props}
/>
);
}
function CardDescription({ className, ...props }: React.ComponentProps<'div'>) {
return (
<div
data-slot="card-description"
className={cn('text-muted-foreground text-sm', className)}
{...props}
/>
);
}
function CardAction({ className, ...props }: React.ComponentProps<'div'>) {
return (
<div
data-slot="card-action"
className={cn(
'col-start-2 row-span-2 row-start-1 self-start justify-self-end',
className,
)}
{...props}
/>
);
}
function CardContent({ className, ...props }: React.ComponentProps<'div'>) {
return (
<div
data-slot="card-content"
className={cn('px-6', className)}
{...props}
/>
);
}
function CardFooter({ className, ...props }: React.ComponentProps<'div'>) {
return (
<div
data-slot="card-footer"
className={cn('flex items-center px-6 [.border-t]:pt-6', className)}
{...props}
/>
);
}
export {
Card,
CardHeader,
CardFooter,
CardTitle,
CardAction,
CardDescription,
CardContent,
};

241
src/shared/ui/carousel.tsx Normal file
View File

@@ -0,0 +1,241 @@
'use client';
import useEmblaCarousel, {
type UseEmblaCarouselType,
} from 'embla-carousel-react';
import { ChevronLeft, ChevronRight } from 'lucide-react';
import * as React from 'react';
import { cn } from '@/shared/lib/utils';
import { Button } from '@/shared/ui/button';
type CarouselApi = UseEmblaCarouselType[1];
type UseCarouselParameters = Parameters<typeof useEmblaCarousel>;
type CarouselOptions = UseCarouselParameters[0];
type CarouselPlugin = UseCarouselParameters[1];
export type CarouselProps = {
opts?: CarouselOptions;
plugins?: CarouselPlugin;
orientation?: 'horizontal' | 'vertical';
setApi?: (api: CarouselApi) => void;
};
type CarouselContextProps = {
carouselRef: ReturnType<typeof useEmblaCarousel>[0];
api: ReturnType<typeof useEmblaCarousel>[1];
scrollPrev: () => void;
scrollNext: () => void;
canScrollPrev: boolean;
canScrollNext: boolean;
} & CarouselProps;
const CarouselContext = React.createContext<CarouselContextProps | null>(null);
export function useCarousel() {
const context = React.useContext(CarouselContext);
if (!context) {
throw new Error('useCarousel must be used within a <Carousel />');
}
return context;
}
function Carousel({
orientation = 'horizontal',
opts,
setApi,
plugins,
className,
children,
...props
}: React.ComponentProps<'div'> & CarouselProps) {
const [carouselRef, api] = useEmblaCarousel(
{
...opts,
axis: orientation === 'horizontal' ? 'x' : 'y',
},
plugins,
);
const [canScrollPrev, setCanScrollPrev] = React.useState(false);
const [canScrollNext, setCanScrollNext] = React.useState(false);
const onSelect = React.useCallback((api: CarouselApi) => {
if (!api) return;
setCanScrollPrev(api.canScrollPrev());
setCanScrollNext(api.canScrollNext());
}, []);
const scrollPrev = React.useCallback(() => {
api?.scrollPrev();
}, [api]);
const scrollNext = React.useCallback(() => {
api?.scrollNext();
}, [api]);
const handleKeyDown = React.useCallback(
(event: React.KeyboardEvent<HTMLDivElement>) => {
if (event.key === 'ArrowLeft') {
event.preventDefault();
scrollPrev();
} else if (event.key === 'ArrowRight') {
event.preventDefault();
scrollNext();
}
},
[scrollPrev, scrollNext],
);
React.useEffect(() => {
if (!api || !setApi) return;
setApi(api);
}, [api, setApi]);
React.useEffect(() => {
if (!api) return;
onSelect(api);
api.on('reInit', onSelect);
api.on('select', onSelect);
return () => {
api?.off('select', onSelect);
};
}, [api, onSelect]);
return (
<CarouselContext.Provider
value={{
carouselRef,
api: api,
opts,
orientation:
orientation || (opts?.axis === 'y' ? 'vertical' : 'horizontal'),
scrollPrev,
scrollNext,
canScrollPrev,
canScrollNext,
}}
>
<div
onKeyDownCapture={handleKeyDown}
className={cn('relative', className)}
role="region"
aria-roledescription="carousel"
data-slot="carousel"
{...props}
>
{children}
</div>
</CarouselContext.Provider>
);
}
function CarouselContent({ className, ...props }: React.ComponentProps<'div'>) {
const { carouselRef, orientation } = useCarousel();
return (
<div
ref={carouselRef}
className="overflow-hidden"
data-slot="carousel-content"
>
<div
className={cn(
'flex',
orientation === 'horizontal' ? '-ml-4' : '-mt-4 flex-col',
className,
)}
{...props}
/>
</div>
);
}
function CarouselItem({ className, ...props }: React.ComponentProps<'div'>) {
const { orientation } = useCarousel();
return (
<div
role="group"
aria-roledescription="slide"
data-slot="carousel-item"
className={cn(
'min-w-0 shrink-0 grow-0 basis-full',
orientation === 'horizontal' ? 'pl-4' : 'pt-4',
className,
)}
{...props}
/>
);
}
function CarouselPrevious({
className,
variant = 'outline',
size = 'icon',
...props
}: React.ComponentProps<typeof Button>) {
const { orientation, scrollPrev, canScrollPrev } = useCarousel();
return (
<Button
data-slot="carousel-previous"
variant={variant}
size={size}
className={cn(
'absolute size-8 rounded-full',
orientation === 'horizontal'
? 'top-1/2 -right-12 -translate-y-1/2'
: '-bottom-12 left-1/2 -translate-x-1/2 rotate-90',
className,
)}
disabled={!canScrollPrev}
onClick={scrollPrev}
{...props}
>
<ChevronLeft className={cn(className, 'size-5')} />
<span className="sr-only">Previous slide</span>
</Button>
);
}
function CarouselNext({
className,
variant = 'outline',
size = 'icon',
...props
}: React.ComponentProps<typeof Button>) {
const { orientation, scrollNext, canScrollNext } = useCarousel();
return (
<Button
data-slot="carousel-next"
variant={variant}
size={size}
className={cn(
'absolute size-8 rounded-full',
orientation === 'horizontal'
? 'top-1/2 -right-12 -translate-y-1/2'
: '-bottom-12 left-1/2 -translate-x-1/2 rotate-90',
className,
)}
disabled={!canScrollNext}
onClick={scrollNext}
{...props}
>
<ChevronRight className={cn(className, 'size-5')} />
<span className="sr-only">Next slide</span>
</Button>
);
}
export {
Carousel,
CarouselContent,
CarouselItem,
CarouselNext,
CarouselPrevious,
type CarouselApi,
};

24
src/shared/ui/label.tsx Normal file
View File

@@ -0,0 +1,24 @@
'use client';
import * as React from 'react';
import * as LabelPrimitive from '@radix-ui/react-label';
import { cn } from '@/shared/lib/utils';
function Label({
className,
...props
}: React.ComponentProps<typeof LabelPrimitive.Root>) {
return (
<LabelPrimitive.Root
data-slot="label"
className={cn(
'flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50',
className,
)}
{...props}
/>
);
}
export { Label };

48
src/shared/ui/popover.tsx Normal file
View File

@@ -0,0 +1,48 @@
'use client';
import * as PopoverPrimitive from '@radix-ui/react-popover';
import * as React from 'react';
import { cn } from '@/shared/lib/utils';
function Popover({
...props
}: React.ComponentProps<typeof PopoverPrimitive.Root>) {
return <PopoverPrimitive.Root data-slot="popover" {...props} />;
}
function PopoverTrigger({
...props
}: React.ComponentProps<typeof PopoverPrimitive.Trigger>) {
return <PopoverPrimitive.Trigger data-slot="popover-trigger" {...props} />;
}
function PopoverContent({
className,
align = 'center',
sideOffset = 4,
...props
}: React.ComponentProps<typeof PopoverPrimitive.Content>) {
return (
<PopoverPrimitive.Portal>
<PopoverPrimitive.Content
data-slot="popover-content"
align={align}
sideOffset={sideOffset}
className={cn(
'bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-72 origin-(--radix-popover-content-transform-origin) rounded-md border p-4 shadow-md outline-hidden',
className,
)}
{...props}
/>
</PopoverPrimitive.Portal>
);
}
function PopoverAnchor({
...props
}: React.ComponentProps<typeof PopoverPrimitive.Anchor>) {
return <PopoverPrimitive.Anchor data-slot="popover-anchor" {...props} />;
}
export { Popover, PopoverAnchor, PopoverContent, PopoverTrigger };

View File

@@ -0,0 +1,31 @@
'use client';
import * as React from 'react';
import * as ProgressPrimitive from '@radix-ui/react-progress';
import { cn } from '@/shared/lib/utils';
function Progress({
className,
value,
...props
}: React.ComponentProps<typeof ProgressPrimitive.Root>) {
return (
<ProgressPrimitive.Root
data-slot="progress"
className={cn(
'bg-primary/20 relative h-2 w-full overflow-hidden rounded-full',
className,
)}
{...props}
>
<ProgressPrimitive.Indicator
data-slot="progress-indicator"
className="bg-primary h-full w-full flex-1 transition-all"
style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
/>
</ProgressPrimitive.Root>
);
}
export { Progress };

31
src/shared/ui/switch.tsx Normal file
View File

@@ -0,0 +1,31 @@
'use client';
import * as React from 'react';
import * as SwitchPrimitive from '@radix-ui/react-switch';
import { cn } from '@/shared/lib/utils';
function Switch({
className,
...props
}: React.ComponentProps<typeof SwitchPrimitive.Root>) {
return (
<SwitchPrimitive.Root
data-slot="switch"
className={cn(
'peer data-[state=checked]:bg-primary data-[state=unchecked]:bg-input focus-visible:border-ring focus-visible:ring-ring/50 dark:data-[state=unchecked]:bg-input/80 inline-flex h-[1.15rem] w-8 shrink-0 items-center rounded-full border border-transparent shadow-xs transition-all outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50',
className,
)}
{...props}
>
<SwitchPrimitive.Thumb
data-slot="switch-thumb"
className={cn(
'bg-background dark:data-[state=unchecked]:bg-foreground dark:data-[state=checked]:bg-primary-foreground pointer-events-none block size-4 rounded-full ring-0 transition-transform data-[state=checked]:translate-x-[calc(100%-2px)] data-[state=unchecked]:translate-x-0',
)}
/>
</SwitchPrimitive.Root>
);
}
export { Switch };

66
src/shared/ui/tabs.tsx Normal file
View File

@@ -0,0 +1,66 @@
'use client';
import * as React from 'react';
import * as TabsPrimitive from '@radix-ui/react-tabs';
import { cn } from '@/shared/lib/utils';
function Tabs({
className,
...props
}: React.ComponentProps<typeof TabsPrimitive.Root>) {
return (
<TabsPrimitive.Root
data-slot="tabs"
className={cn('flex flex-col gap-2', className)}
{...props}
/>
);
}
function TabsList({
className,
...props
}: React.ComponentProps<typeof TabsPrimitive.List>) {
return (
<TabsPrimitive.List
data-slot="tabs-list"
className={cn(
'bg-muted text-muted-foreground inline-flex h-9 w-fit items-center justify-center rounded-lg p-[3px]',
className,
)}
{...props}
/>
);
}
function TabsTrigger({
className,
...props
}: React.ComponentProps<typeof TabsPrimitive.Trigger>) {
return (
<TabsPrimitive.Trigger
data-slot="tabs-trigger"
className={cn(
"data-[state=active]:bg-background dark:data-[state=active]:text-foreground focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:outline-ring dark:data-[state=active]:border-input dark:data-[state=active]:bg-input/30 text-foreground dark:text-muted-foreground inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 rounded-md border border-transparent px-2 py-1 text-sm font-medium whitespace-nowrap transition-[color,box-shadow] focus-visible:ring-[3px] focus-visible:outline-1 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:shadow-sm [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className,
)}
{...props}
/>
);
}
function TabsContent({
className,
...props
}: React.ComponentProps<typeof TabsPrimitive.Content>) {
return (
<TabsPrimitive.Content
data-slot="tabs-content"
className={cn('flex-1 outline-none', className)}
{...props}
/>
);
}
export { Tabs, TabsList, TabsTrigger, TabsContent };

View File

@@ -0,0 +1,18 @@
import * as React from 'react';
import { cn } from '@/shared/lib/utils';
function Textarea({ className, ...props }: React.ComponentProps<'textarea'>) {
return (
<textarea
data-slot="textarea"
className={cn(
'border-input placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 flex field-sizing-content min-h-16 w-full rounded-md border bg-transparent px-3 py-2 text-base shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 md:text-sm',
className,
)}
{...props}
/>
);
}
export { Textarea };

View File

@@ -0,0 +1,148 @@
export const categories: Product[] = [
{
id: 1,
name: 'Elektronika',
products: [
{
id: 1,
name: 'Krasovka',
price: 125000,
oldPrice: 14000000,
image: '/adidas-ultraboost-running-shoes.jpg',
rating: 4.5,
reviews: 128,
discount: 10,
inStock: true,
liked: false,
},
{
id: 2,
name: 'Apple MacBook Pro 14"',
price: 25000000,
oldPrice: 27000000,
image: '/apple-ipad-pro-tablet.jpg',
rating: 5.0,
reviews: 89,
discount: 8,
liked: false,
inStock: true,
},
{
id: 3,
name: 'Apple MacBook Pro 14"',
price: 25000000,
oldPrice: 27000000,
image: '/apple-ipad-pro-tablet.jpg',
rating: 5.0,
reviews: 89,
discount: 8,
inStock: true,
liked: true,
},
{
id: 4,
name: 'Apple MacBook Pro 14"',
price: 25000000,
oldPrice: 27000000,
image: '/apple-ipad-pro-tablet.jpg',
rating: 5.0,
reviews: 89,
discount: 8,
inStock: true,
liked: false,
},
{
id: 5,
name: 'Sony WH-1000XM5',
price: 3500000,
oldPrice: 4000000,
image: '/sony-wh-1000xm5.png',
rating: 4.8,
reviews: 256,
discount: 12,
inStock: true,
liked: true,
},
],
},
{
id: 2,
name: 'Sport kiyimlari',
products: [
{
id: 6,
name: 'Nike Air Max 270',
price: 1800000,
oldPrice: 2000000,
image: '/nike-air-max-270.png',
rating: 4.3,
reviews: 342,
discount: 10,
inStock: false,
liked: false,
},
{
id: 7,
name: 'Adidas Ultraboost',
price: 2000000,
oldPrice: 2200000,
image: '/adidas-ultraboost-running-shoes.jpg',
rating: 4.6,
reviews: 215,
discount: 9,
liked: true,
inStock: true,
},
],
},
{
id: 3,
name: 'Uy jihozlari',
products: [
{
id: 8,
name: 'LG OLED TV 55"',
price: 18000000,
oldPrice: 20000000,
image: '/lg-oled-tv-55-inch.jpg',
rating: 4.7,
reviews: 76,
discount: 10,
inStock: true,
liked: false,
},
{
id: 9,
name: 'Canon EOS R6',
price: 22000000,
oldPrice: 24000000,
image: '/canon-eos-r6-camera.jpg',
rating: 4.9,
reviews: 54,
discount: 8,
inStock: true,
liked: false,
},
],
},
];
export interface Product {
id: number;
name: string;
products: ProductDetail[];
}
export interface ProductDetail {
id: number;
name: string;
categoryName?: string;
price: number;
oldPrice: number;
image: string;
rating: number;
reviews: number;
discount: number;
inStock: boolean;
liked: boolean;
}

View File

@@ -0,0 +1,74 @@
'use client';
import { SubCategory } from '@/features/category/lib/data';
import { useRouter } from '@/shared/config/i18n/navigation';
import {
Carousel,
CarouselContent,
CarouselItem,
CarouselNext,
CarouselPrevious,
} from '@/shared/ui/carousel';
import { ChevronRight } from 'lucide-react';
import { useState } from 'react';
import { ProductCard } from './product-card';
export function CategoryCarousel({ category }: { category: SubCategory }) {
const [products, setProducts] = useState(category.products);
const router = useRouter();
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 (
<section className="relative custom-container mt-8 justify-center items-center">
<div className="flex items-center justify-between mb-6 pb-3 border-b border-slate-200">
<div
className="flex items-center gap-2 group cursor-pointer"
onClick={() =>
router.push(`/category/${category.category}/${category.name}`)
}
>
<h2 className="text-2xl font-bold text-slate-800 group-hover:text-blue-600 transition-colors">
{category.name}
</h2>
<div className="p-1.5 bg-slate-100 rounded-full group-hover:bg-blue-100 transition-all">
<ChevronRight className="text-slate-600 group-hover:text-blue-600 group-hover:translate-x-0.5 transition-all" />
</div>
</div>
</div>
<Carousel className="w-full">
<CarouselContent className="flex">
{products.slice(0, 12).map((product) => (
<CarouselItem
key={product.id}
className="basis-1/1 md:basis-1/2 lg:basis-1/4 pb-2"
>
<ProductCard
product={product}
handleRemove={handleRemove}
handleLiked={handleLiked}
/>
</CarouselItem>
))}
</CarouselContent>
<CarouselNext className="-top-12 right-0 rounded-lg w-10 h-10 max-lg:hidden bg-blue-600 hover:bg-blue-600 cursor-pointer text-white border-0" />
<CarouselPrevious className="-top-12 right-12 rounded-lg w-10 h-10 max-lg:hidden bg-blue-600 hover:bg-blue-600 cursor-pointer text-white border-0" />
</Carousel>
</section>
);
}

View File

@@ -0,0 +1,166 @@
'use client';
import { useRouter } from '@/shared/config/i18n/navigation';
import formatPrice from '@/shared/lib/formatPrice';
import { Button } from '@/shared/ui/button';
import { Card, CardContent } from '@/shared/ui/card';
import { Input } from '@/shared/ui/input';
import { Heart, Minus, Plus, ShoppingCart, Star } from 'lucide-react';
import Image from 'next/image';
import { MouseEvent, useState } from 'react';
interface Product {
id: number;
name: string;
price: number;
oldPrice: number;
image: string;
rating: number;
reviews: number;
discount: number;
inStock: boolean;
liked: boolean;
}
export function ProductCard({
product,
handleRemove,
handleLiked,
}: {
product: Product;
handleRemove: (id: number) => void;
handleLiked?: (id: number) => void;
}) {
const [quantity, setQuantity] = useState<number | ''>(0);
const router = useRouter();
const increase = (e: MouseEvent<HTMLButtonElement>) => {
e.stopPropagation();
setQuantity((q) => (q === '' ? 1 : q + 1));
};
const decrease = (e: MouseEvent<HTMLButtonElement>) => {
setQuantity((q) => (q && q > 1 ? q - 1 : 0));
e.stopPropagation();
};
return (
<Card
onClick={() => router.push(`/product/${product.id}`)}
className="group relative p-0 overflow-hidden border border-slate-200 bg-white shadow-sm hover:shadow-xl transition-all duration-300 rounded-2xl hover:border-blue-400"
>
<CardContent className="!p-0">
<div className="relative overflow-hidden">
{product.discount > 0 && (
<div className="absolute top-3 left-3 z-10 bg-orange-500 text-white px-2.5 py-1 rounded-full text-sm font-bold">
-{product.discount}%
</div>
)}
<Button
onClick={(e) => {
e.stopPropagation();
if (product.liked) {
handleRemove(product.id);
} else if (handleLiked && !product.liked) {
handleLiked(product.id);
}
}}
className="absolute hover:bg-white cursor-pointer top-3 right-3 z-10 bg-white rounded-full p-2 shadow-md hover:scale-110 transition-all duration-300"
>
<Heart
className={`w-5 h-5 transition-colors ${product.liked ? 'fill-red-500 text-red-500' : 'text-slate-400 hover:text-red-400'}`}
/>
</Button>
<div className="relative h-96 bg-slate-50 overflow-hidden">
<Image
width={500}
height={500}
src={product.image || '/placeholder.svg'}
alt={product.name}
className="w-full h-full object-cover group-hover:scale-105 transition-transform duration-500"
/>
</div>
</div>
<div className="p-4 space-y-3">
<div className="flex items-center gap-2">
<div className="flex items-center gap-1 bg-orange-50 px-2 py-1 rounded-full">
<Star className="w-4 h-4 fill-orange-400 text-orange-400" />
<span className="text-sm font-semibold text-orange-600">
{product.rating}
</span>
</div>
<span className="text-slate-500 text-sm">
({product.reviews} ta sharh)
</span>
</div>
<h3 className="font-semibold text-base text-slate-800 line-clamp-2 min-h-[3rem] leading-snug hover:text-blue-600 transition-colors">
{product.name}
</h3>
<div className="space-y-1">
<div className="flex items-baseline gap-2">
<span className="text-2xl font-bold text-blue-600">
{formatPrice(product.price)}
</span>
</div>
{product.oldPrice && (
<div className="flex items-center gap-2">
<span className="text-sm text-slate-400 line-through">
{formatPrice(product.oldPrice)}
</span>
<span className="text-xs bg-orange-100 text-orange-600 px-2 py-0.5 rounded-full font-semibold">
Tejang!
</span>
</div>
)}
</div>
{quantity === 0 ? (
<Button
disabled={!product.inStock}
onClick={(e) => {
setQuantity(1);
e.stopPropagation();
}}
className="w-full h-11 rounded-xl bg-blue-600 text-white"
>
<ShoppingCart className="w-5 h-5 mr-2" />
Savatga qoshish
</Button>
) : (
<div className="flex items-center justify-between border border-blue-500 rounded-xl h-11 px-2">
<Button size="icon" variant="ghost" onClick={decrease}>
<Minus />
</Button>
<Input
type="text"
value={quantity}
onChange={(e) => {
const value = e.target.value;
if (/^\d*$/.test(value)) {
setQuantity(value === '' ? '' : Number(value));
}
}}
onBlur={() => {
if (quantity === '' || quantity < 1) {
setQuantity(1);
}
}}
className="w-full text-center outline-none ring-0 focus-visible:ring-0 border-none font-semibold"
/>
<Button size="icon" variant="ghost" onClick={increase}>
<Plus />
</Button>
</div>
)}
</div>
</CardContent>
</Card>
);
}

View File

@@ -1,6 +1,6 @@
const sections = [
{
title: 'Product',
title: 'Kategoriyalar',
links: [
{ name: 'Overview', href: '#' },
{ name: 'Pricing', href: '#' },

View File

@@ -1,82 +1,114 @@
import { PRODUCT_INFO } from '@/shared/constants/data';
import { InstagramIcon, YoutubeIcon } from 'lucide-react';
import { sections } from '../lib/data';
import { ModeToggle } from '@/shared/ui/theme-toggle';
import formatPhone from '@/shared/lib/formatPhone';
import { categoryList } from '@/widgets/welcome/lib/data';
import { Facebook, Instagram, Mail, Phone, Send, Twitter } from 'lucide-react';
import Image from 'next/image';
import { Fragment } from 'react';
const Footer = () => {
return (
<section className="py-32">
<div className="custom-container">
<div className="flex w-full flex-col items-center justify-between gap-10 text-center lg:flex-row lg:items-start lg:text-left">
<div className="flex w-full flex-col items-center justify-between gap-6 lg:items-start">
{/* Logo */}
<div className="flex items-center gap-2 lg:justify-start">
<a href="https://shadcnblocks.com">
<img
src={PRODUCT_INFO.logo}
<section className="max-lg:py-9 py-12 z-50 w-full bg-slate-50 border-t border-slate-200">
<div className="custom-container max-lg:hidden">
<div className="flex w-full gap-10 flex-col items-center justify-between text-center lg:flex-row lg:items-start lg:text-left">
<div className="flex w-fit flex-col items-center justify-between gap-4 lg:items-start mb-8 lg:mb-0">
<div className="flex items-center gap-3 lg:justify-start">
<div className="p-2 bg-blue-600 rounded-xl">
<Image
width={500}
height={500}
src={PRODUCT_INFO.logo || '/placeholder.svg'}
alt={PRODUCT_INFO.name}
title={PRODUCT_INFO.name}
className="h-8"
className="h-6 w-6 brightness-0 invert"
/>
</a>
<h2 className="text-xl font-semibold">{PRODUCT_INFO.name}</h2>
</div>
<p className="text-sm text-muted-foreground">
A collection of 100+ responsive HTML templates for your startup
business or side project.
</p>
<ul className="flex items-center space-x-6 text-muted-foreground">
<li className="font-medium hover:text-primary">
<a href="#">
<InstagramIcon className="size-6" />
</a>
</li>
<li className="font-medium hover:text-primary">
<a href="#">
<YoutubeIcon className="size-6" />
</a>
</li>
<li className="font-medium hover:text-primary">
<a href="#">
<InstagramIcon className="size-6" />
</a>
</li>
<li className="font-medium hover:text-primary">
<a href="#">
<InstagramIcon className="size-6" />
</a>
</li>
</ul>
<ModeToggle />
</div>
<div className="grid w-full grid-cols-3 gap-6 lg:gap-20">
{sections.map((section, sectionIdx) => (
<div key={sectionIdx}>
<h3 className="mb-6 font-bold">{section.title}</h3>
<ul className="space-y-4 text-sm text-muted-foreground">
{section.links.map((link, linkIdx) => (
<li
key={linkIdx}
className="font-medium hover:text-primary"
>
<a href={link.href}>{link.name}</a>
</li>
))}
</ul>
</div>
))}
<h2 className="text-xl font-bold text-slate-800">
{PRODUCT_INFO.name}
</h2>
</div>
<p className="text-slate-600 max-w-xs leading-relaxed text-sm">
Lorem ipsum dolor sit amet consectetur, adipisicing elit. Est,
totam?
</p>
</div>
<div className="grid w-full grid-cols-1 md:grid-cols-3 gap-8 lg:gap-16">
<div>
<h3 className="mb-4 font-bold text-base text-black">
Kategoriyalar
</h3>
<ul className="space-y-2 text-sm">
{categoryList.slice(0, 3).map((link) => (
<Fragment key={link.name}>
{link.subCategories.slice(0, 2).map((e, linkIdx) => (
<li
key={linkIdx}
className="text-muted-foreground hover:text-blue-600 transition-colors cursor-pointer"
>
<a href={`/category/${link.name}/${e.name}`}>
{e.name}
</a>
</li>
))}
</Fragment>
))}
</ul>
</div>
<div>
<h3 className="mb-4 font-bold text-base text-black">Sahifalar</h3>
<ul className="space-y-2 text-sm">
<li className="text-muted-foreground hover:text-blue-600 transition-colors cursor-pointer">
<a href={'#'}>Biz haqimizda</a>
</li>
<li className="text-muted-foreground hover:text-blue-600 transition-colors cursor-pointer">
<a href={'#'}>Mahfiylik siyosati</a>
</li>
<li className="text-muted-foreground hover:text-blue-600 transition-colors cursor-pointer">
<a href={'#'}>Savol va javoblar</a>
</li>
</ul>
</div>
<div>
<h3 className="mb-4 font-bold text-base text-black">Aloqa</h3>
<ul className="space-y-2 text-sm">
<li className="text-muted-foreground hover:text-blue-600 transition-colors cursor-pointer">
<a href={'#'} className="flex items-center gap-2">
<Send className="size-4" />
<p>Telegram</p>
</a>
</li>
<li className="text-muted-foreground hover:text-blue-600 transition-colors cursor-pointer">
<a href={'#'} className="flex items-center gap-2">
<Instagram className="size-4" />
<p>Instagram</p>
</a>
</li>
<li className="text-muted-foreground hover:text-blue-600 transition-colors cursor-pointer">
<a href={'#'} className="flex items-center gap-2">
<Facebook className="size-4" />
<p>Facebook</p>
</a>
</li>
<li className="text-muted-foreground hover:text-blue-600 transition-colors cursor-pointer">
<a href={'#'} className="flex items-center gap-2">
<Twitter className="size-4" />
<p>Twitter</p>
</a>
</li>
<li className="text-muted-foreground hover:text-blue-600 transition-colors cursor-pointer">
<a href={'#'} className="flex items-center gap-2">
<Mail className="size-4" />
<p>e-mail</p>
</a>
</li>
<li className="text-muted-foreground hover:text-blue-600 transition-colors cursor-pointer">
<a href={'#'} className="flex items-center gap-2">
<Phone className="size-4" />
<p>{formatPhone('+998901234567')}</p>
</a>
</li>
</ul>
</div>
</div>
</div>
<div className="mt-8 flex flex-col justify-between gap-4 border-t pt-8 text-center text-sm font-medium text-muted-foreground lg:flex-row lg:items-center lg:text-left">
<p>
© {new Date().getFullYear()} {PRODUCT_INFO.creator}. All rights
reserved.
</p>
<ul className="flex justify-center gap-4 lg:justify-start">
<li className="hover:text-primary">
<a href={PRODUCT_INFO.terms_of_use}>Terms and Conditions</a>
</li>
</ul>
</div>
</div>
</section>

View File

@@ -1,6 +1,6 @@
import { LanguageRoutes } from '@/shared/config/i18n/types';
import { Book, Sunset, Trees, Zap } from 'lucide-react';
import { MenuItem } from './model';
import { LanguageRoutes } from '@/shared/config/i18n/types';
const menu: MenuItem[] = [
{ title: 'Home', url: '#' },
@@ -80,14 +80,14 @@ const languages: { name: string; key: LanguageRoutes }[] = [
name: "O'zbekcha",
key: LanguageRoutes.UZ,
},
{
name: 'Ўзбекча',
key: LanguageRoutes.KI,
},
// {
// name: 'Ўзбекча',
// key: LanguageRoutes.KI,
// },
{
name: 'Русский',
key: LanguageRoutes.RU,
},
];
export { menu, languages };
export { languages, menu };

View File

@@ -0,0 +1,23 @@
import { CategoryType } from '@/widgets/welcome/lib/data';
import { create } from 'zustand';
type Category = {
active: CategoryType | null;
openToolbar: boolean;
};
type Actions = {
setActive: (active: CategoryType | null) => void;
setOpenToolbar: (openToolbar: boolean) => void;
setCloseToolbar: (openToolbar: boolean) => void;
};
const useCategoryActive = create<Category & Actions>((set) => ({
active: null,
openToolbar: false,
setActive: (active: CategoryType | null) => set(() => ({ active })),
setOpenToolbar: () => set(() => ({ openToolbar: true })),
setCloseToolbar: () => set(() => ({ openToolbar: false })),
}));
export default useCategoryActive;

View File

@@ -1,17 +1,16 @@
'use client';
import * as React from 'react';
import { GlobeIcon } from 'lucide-react';
import type { LanguageRoutes } from '@/shared/config/i18n/types';
import { Button } from '@/shared/ui/button';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from '@/shared/ui/dropdown-menu';
import { Button } from '@/shared/ui/button';
import { languages } from '../lib/data';
import Image from 'next/image';
import { useParams, usePathname, useRouter } from 'next/navigation';
import { LanguageRoutes } from '@/shared/config/i18n/types';
import { languages } from '../lib/data';
export function ChangeLang() {
const { locale } = useParams();
@@ -27,15 +26,45 @@ export function ChangeLang() {
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline">
<GlobeIcon />
<span>{languages.find((e) => e.key == locale)?.name}</span>
<DropdownMenuTrigger asChild className="h-7">
<Button
variant="ghost"
className="bg-white/20 hover:bg-white/30 backdrop-blur-sm rounded-sm px-3"
>
<div className="w-5 h-5 rounded-full overflow-hidden border border-white/50 mr-2">
{languages.find((e) => e.key === locale)?.key === 'uz' ? (
<Image
src="/flags/uz.png"
alt="uz"
width={20}
height={20}
className="object-cover"
/>
) : (
<Image
src="/flags/ru.png"
alt="ru"
width={20}
height={20}
className="object-cover"
/>
)}
</div>
<span className="text-white font-medium text-sm">
{languages.find((e) => e.key === locale)?.name}
</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuContent
align="end"
className="bg-white border border-slate-200 shadow-lg rounded-lg"
>
{languages.map((e, i) => (
<DropdownMenuItem key={i} onClick={() => changeLocale(e.key)}>
<DropdownMenuItem
key={i}
onClick={() => changeLocale(e.key)}
className="hover:bg-blue-50 cursor-pointer text-slate-700 hover:text-blue-700 px-3 py-2"
>
{e.name}
</DropdownMenuItem>
))}

View File

@@ -0,0 +1,78 @@
'use client';
import { usePathname } from '@/shared/config/i18n/navigation';
import { LanguageRoutes } from '@/shared/config/i18n/types';
import { Button } from '@/shared/ui/button';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from '@/shared/ui/dropdown-menu';
import Image from 'next/image';
import { useParams, useRouter } from 'next/navigation';
import { languages } from '../lib/data';
export const MobileLanguageSelector = () => {
const { locale } = useParams();
const pathname = usePathname();
const router = useRouter();
const changeLocale = (newLocale: LanguageRoutes) => {
const segments = pathname.split('/');
segments[1] = newLocale;
const newPath = segments.join('/');
router.push(newPath);
};
const currentLanguage = languages.find((e) => e.key === locale);
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="ghost"
size="icon"
className="h-10 w-10 rounded-lg hover:bg-slate-100"
>
<div className="w-6 h-6 rounded-full overflow-hidden border-2 border-slate-200">
<Image
src={
currentLanguage?.key === 'uz'
? '/flags/uz.png'
: '/flags/ru.png'
}
alt={currentLanguage?.key || 'language'}
width={24}
height={24}
className="object-cover w-full h-full"
/>
</div>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent
align="end"
className="bg-white border border-slate-200 shadow-lg rounded-lg min-w-[120px]"
>
{languages.map((lang) => (
<DropdownMenuItem
key={lang.key}
onClick={() => changeLocale(lang.key)}
className="hover:bg-blue-50 cursor-pointer text-slate-700 hover:text-blue-700 px-3 py-2 flex items-center gap-2"
>
<div className="w-5 h-5 rounded-full overflow-hidden border border-slate-200">
<Image
src={lang.key === 'uz' ? '/flags/uz.png' : '/flags/ru.png'}
alt={lang.key}
width={20}
height={20}
className="object-cover w-full h-full"
/>
</div>
<span className="font-medium">{lang.name}</span>
</DropdownMenuItem>
))}
</DropdownMenuContent>
</DropdownMenu>
);
};

View File

@@ -0,0 +1,72 @@
'use client';
import { cn } from '@/shared/lib/utils';
import { Button } from '@/shared/ui/button';
import { Heart, Home, LayoutGrid, ShoppingCart, User } from 'lucide-react';
import Link from 'next/link';
import { usePathname } from 'next/navigation';
import { useEffect, useState } from 'react';
const NavbarMobile = () => {
const pathname = usePathname();
const [profile, setProfile] = useState<string | null>(null);
useEffect(() => {
const user = localStorage.getItem('user');
setProfile(user);
}, []);
const navItems = [
{ label: 'Asosiy', icon: Home, href: '/' },
{ label: 'Katalog', icon: LayoutGrid, href: '/category' },
{ label: 'Sevimli', icon: Heart, href: '/favourite' },
{ label: 'Savatda', icon: ShoppingCart, href: '/cart' },
{
label: 'Profil',
icon: User,
href: profile === 'true' ? '/profile' : '/auth',
},
];
return (
<nav className="fixed bottom-0 left-0 right-0 z-50 lg:hidden bg-white border-t shadow-xl rounded-t-3xl">
<div className="grid grid-cols-5 h-14 px-2">
{navItems.map((item) => {
const isActive =
item.href === '/'
? pathname === '/' || pathname === `/${pathname.split('/')[1]}`
: pathname.includes(item.href);
return (
<Link key={item.href} href={item.href}>
<Button
variant="ghost"
className={cn(
'h-full w-full flex flex-col items-center justify-center gap-1 rounded-xl',
isActive && 'text-blue-500',
)}
>
<item.icon
className={cn(
'size-6 transition-colors',
isActive ? 'text-blue-500' : 'text-gray-500',
)}
/>
<span
className={cn(
'text-[10px] font-medium',
isActive ? 'text-blue-500' : 'text-gray-500',
)}
>
{item.label}
</span>
</Button>
</Link>
);
})}
</div>
</nav>
);
};
export default NavbarMobile;

View File

@@ -0,0 +1,66 @@
import formatPrice from '@/shared/lib/formatPrice';
import { categories, ProductDetail } from '@/widgets/categories/lib/data';
import { PackageOpen } from 'lucide-react';
import Image from 'next/image';
import { Fragment, useEffect, useState } from 'react';
type SearchResultProps = {
query: string;
};
export const SearchResult = ({ query }: SearchResultProps) => {
const [searchProduct, setSearchProduct] = useState<ProductDetail[]>([]);
useEffect(() => {
setSearchProduct(
categories.flatMap((cat) =>
cat.products.filter((pro) =>
pro.name.toLowerCase().includes(query.toLowerCase()),
),
),
);
}, [query]);
if (searchProduct.length === 0) {
return (
<div className="flex flex-col justify-center items-center min-h-[300px] max-h-[600px] gap-2">
<PackageOpen className="size-22 text-muted-foreground" />
<p className="text-lg text-muted-foreground text-center">
Hech narsa topilmadi
</p>
</div>
);
}
return (
<div className="space-y-3">
<p className="text-sm font-semibold text-foreground">
{query.length > 0 ? 'Qidiruv natijalari' : 'Tavsiya etiladi'}
</p>
{searchProduct.slice(0, 5).map((product, index) => (
<Fragment key={index}>
<div className="flex items-center gap-3 p-2 rounded-lg hover:bg-slate-100 cursor-pointer transition">
<Image
width={500}
height={500}
src={product.image}
alt={product.name}
className="w-10 h-10 rounded-md object-cover"
/>
<div className="flex-1">
<p className="text-sm font-medium text-slate-800">
{product.name}
</p>
<p className="text-xs text-slate-500">{product.rating}</p>
<p className="text-xs text-slate-600">
{formatPrice(product.price)}
</p>
</div>
</div>
</Fragment>
))}
</div>
);
};

View File

@@ -1,122 +1,324 @@
import { Accordion } from '@/shared/ui/accordion';
'use client';
import formatPhone from '@/shared/lib/formatPhone';
import { Button } from '@/shared/ui/button';
import { Input } from '@/shared/ui/input';
import { Popover, PopoverContent } from '@/shared/ui/popover';
import { categoryList } from '@/widgets/welcome/lib/data';
import { PopoverTrigger } from '@radix-ui/react-popover';
import {
NavigationMenu,
NavigationMenuList,
} from '@/shared/ui/navigation-menu';
import {
Sheet,
SheetContent,
SheetHeader,
SheetTitle,
SheetTrigger,
} from '@/shared/ui/sheet';
import { Menu } from 'lucide-react';
import { menu } from '../lib/data';
import { PRODUCT_INFO } from '@/shared/constants/data';
import RenderMenuItem from './RenderItem';
import RenderMobileMenuItem from './RenderMobileMenuItem';
ChevronRight,
Facebook,
Heart,
Instagram,
LayoutGrid,
Mail,
Phone,
Search,
Send,
ShoppingCart,
Twitter,
User,
XIcon,
} from 'lucide-react';
import Image from 'next/image';
import { useRouter, useSearchParams } from 'next/navigation';
import { useEffect, useState } from 'react';
import useCategoryActive from '../lib/openCategory';
import { ChangeLang } from './ChangeLang';
import Link from 'next/link';
import { MobileLanguageSelector } from './MobileLanguageSelector';
import NavbarMobile from './NavbarMobile';
import { SearchResult } from './SearchResult';
const Navbar = () => {
const auth = {
login: { title: 'Login', url: '#' },
signup: { title: 'Sign up', url: '#' },
};
const [isSticky, setIsSticky] = useState(false);
const [query, setQuery] = useState('');
const searchParams = useSearchParams();
const queryFromUrl = searchParams.get('q') ?? '';
useEffect(() => {
setQuery(queryFromUrl);
}, [queryFromUrl]);
const [searchOpen, setSearchOpen] = useState(false);
const { active, openToolbar, setActive, setOpenToolbar, setCloseToolbar } =
useCategoryActive();
useEffect(() => {
const handleScroll = () => {
setIsSticky(window.scrollY > 40);
};
handleScroll();
window.addEventListener('scroll', handleScroll);
return () => window.removeEventListener('scroll', handleScroll);
}, []);
const router = useRouter();
return (
<section className="py-4">
<div className="custom-container">
{/* Desktop Menu */}
<nav className="hidden justify-between lg:flex">
<div className="flex items-center gap-6">
{/* Logo */}
<Link href={'/'} className="flex items-center gap-2">
<img
src={PRODUCT_INFO.logo}
className="max-h-8"
alt={PRODUCT_INFO.name}
/>
<span className="text-lg font-semibold tracking-tighter">
{PRODUCT_INFO.name}
</span>
</Link>
<div className="flex items-center">
<NavigationMenu>
<NavigationMenuList>
{menu.map((item) => RenderMenuItem(item))}
</NavigationMenuList>
</NavigationMenu>
</div>
</div>
<div className="flex gap-2">
<ChangeLang />
<Button asChild variant="outline">
<Link href={auth.login.url}>{auth.login.title}</Link>
</Button>
<Button asChild>
<Link href={auth.signup.url}>{auth.signup.title}</Link>
</Button>
</div>
</nav>
{/* Mobile Menu */}
<div className="block lg:hidden">
<div className="flex items-center justify-between">
{/* Logo */}
<Link href={'/'} className="flex items-center gap-2">
<img
src={PRODUCT_INFO.logo}
className="max-h-8"
alt={PRODUCT_INFO.name}
/>
</Link>
<Sheet>
<div className="space-x-2">
<ChangeLang />
<SheetTrigger asChild>
<Button variant="outline" size="icon">
<Menu className="size-4" />
</Button>
</SheetTrigger>
</div>
<SheetContent className="overflow-y-auto">
<SheetHeader>
<SheetTitle>
<Link href={'/'} className="flex items-center gap-2">
<img
src={PRODUCT_INFO.logo}
className="max-h-8"
alt={PRODUCT_INFO.name}
/>
</Link>
</SheetTitle>
</SheetHeader>
<div className="flex flex-col gap-6 p-4">
<Accordion
type="single"
collapsible
className="flex w-full flex-col gap-4"
>
{menu.map((item) => RenderMobileMenuItem(item))}
</Accordion>
<div className="flex flex-col gap-3">
<Button asChild variant="outline">
<Link href={auth.login.url}>{auth.login.title}</Link>
</Button>
<Button asChild>
<Link href={auth.signup.url}>{auth.signup.title}</Link>
</Button>
</div>
</div>
</SheetContent>
</Sheet>
</div>
<>
<div className="w-full bg-blue-600 h-10 max-lg:hidden">
<div className="custom-container h-full flex justify-between items-center">
<ul className="text-sm flex items-center justify-center gap-4">
<li className="text-white transition-colors cursor-pointer">
<a href={'#'} className="flex items-center gap-2">
<Send className="size-4" />
</a>
</li>
<li className="text-white transition-colors cursor-pointer">
<a href={'#'} className="flex items-center gap-2">
<Instagram className="size-4" />
</a>
</li>
<li className="text-white transition-colors cursor-pointer">
<a href={'#'} className="flex items-center gap-2">
<Facebook className="size-4" />
</a>
</li>
<li className="text-white transition-colors cursor-pointer">
<a href={'#'} className="flex items-center gap-2">
<Twitter className="size-4" />
</a>
</li>
<li className="text-white transition-colors cursor-pointer">
<a href={'#'} className="flex items-center gap-2">
<Mail className="size-4" />
</a>
</li>
<li className="text-white transition-colors cursor-pointer">
<a href={'#'} className="flex items-center gap-2">
<Phone className="size-4" />
<p>{formatPhone('+998901234567')}</p>
</a>
</li>
</ul>
<ChangeLang />
</div>
</div>
</section>
<section
className={`py-4 shadow-sm z-50 w-full bg-white transition-all duration-300 mb-5 border-b border-slate-200
${isSticky ? 'fixed top-0 shadow-md' : 'relative'}
`}
>
<div className="custom-container h-6 flex justify-between items-center gap-3">
<div
className="p-2 flex gap-2 rounded-xl cursor-pointer"
onClick={() => router.push('/')}
>
<Image
src="/favicon.png"
alt="logo"
width={40}
height={20}
className="w-6 h-6"
/>
<h3>Fias</h3>
</div>
<div className="w-full flex justify-end lg:hidden gap-2">
<Button
variant={'ghost'}
size={'icon'}
onClick={() => router.push('/search')}
>
<Search className="size-5" />
</Button>
<MobileLanguageSelector />
</div>
<div className="flex-1 flex gap-3">
<Button
variant={'outline'}
className="h-10 max-lg:hidden cursor-pointer"
onClick={() => {
if (openToolbar) {
setCloseToolbar(false);
} else if (!openToolbar) {
setOpenToolbar(true);
}
}}
>
{openToolbar ? (
<XIcon className="text-foreground" />
) : (
<LayoutGrid className="size-4 text-foreground" />
)}
<p className="text-foreground">Kataloglar</p>
</Button>
<div className="relative w-full max-lg:hidden">
<Input
placeholder="Mahsulot nomi"
value={query}
onFocus={() => setSearchOpen(true)}
onBlur={() => setTimeout(() => setSearchOpen(false), 200)}
onKeyDown={(e) => {
if (e.key === 'Enter' && query.trim()) {
router.push(`/search?q=${encodeURIComponent(query)}`);
setSearchOpen(false);
}
}}
onChange={(e) => setQuery(e.target.value)}
className="border border-slate-200 focus:border-blue-400 focus:ring-blue-400 px-9 h-10"
/>
<Search className="absolute top-1/2 -translate-y-1/2 left-2 size-5 text-muted-foreground" />
{/* Search Results Dropdown */}
{searchOpen && (
<div className="absolute top-full left-0 right-0 mt-2 bg-white border border-slate-200 rounded-xl shadow-lg min-h-[300px] max-h-[600px] overflow-y-auto scrollbar-thin z-50">
<div className="p-4">
<SearchResult query={query} />
</div>
</div>
)}
</div>
</div>
<div className="flex gap-2">
<Button
variant={'ghost'}
className="h-10 max-lg:hidden cursor-pointer border border-slate-200"
onClick={() => router.push('/favourite')}
>
<Heart className="size-4 text-foreground" />
</Button>
<Button
variant={'ghost'}
onClick={() => router.push('/cart')}
className="h-10 max-lg:hidden cursor-pointer border border-slate-200"
>
<ShoppingCart className="size-4 text-foreground" />
</Button>
<Button
variant={'ghost'}
onClick={() => {
const user = localStorage.getItem('user');
if (user === 'true') {
router.push('/profile');
} else {
router.push('/auth');
}
}}
className="h-10 max-lg:hidden cursor-pointer border border-slate-200"
>
<User className="size-4 text-foreground" />
</Button>
</div>
</div>
<NavbarMobile />
<Popover open={openToolbar} modal={false}>
<PopoverTrigger className="!h-0 absolute top-12 w-screen">
<div />
</PopoverTrigger>
<PopoverContent
className="w-[100vw] !p-0 h-[100vh] rounded-b-xl border-t-2 border-blue-500"
onInteractOutside={() => setOpenToolbar(false)}
>
<div className="flex h-[90vh]">
<div className="border-r border-slate-200 w-[20%] py-3 flex flex-col gap-2 px-3 overflow-y-auto scrollbar-thin bg-slate-50">
{categoryList.map((e, index) => {
const isActive = active?.name === e.name;
return (
<Button
key={index}
variant="secondary"
onMouseEnter={() => setActive(e)}
onClick={() => setActive(e)}
className={`flex justify-between items-center cursor-pointer px-4 h-14 rounded-xl transition-all duration-300 ${
isActive
? 'bg-blue-100 border border-blue-400'
: 'hover:bg-slate-100 border border-transparent'
}`}
>
<div className="flex gap-3 items-center">
<div
className={`p-1.5 rounded-lg ${isActive ? 'bg-white' : 'bg-white'}`}
>
<Image
src={e.image || '/placeholder.svg'}
alt={e.name}
className="w-5 h-5 object-contain"
/>
</div>
<p
className={`text-sm font-medium ${isActive ? 'text-blue-700' : 'text-slate-700'}`}
>
{e.name}
</p>
</div>
<ChevronRight
className={`size-5 transition-transform duration-300 ${
isActive
? 'rotate-90 text-blue-600'
: 'text-slate-400'
}`}
/>
</Button>
);
})}
</div>
<div className="w-[80%] overflow-y-auto p-8 scrollbar-thin bg-white">
<h3 className="text-2xl font-bold mb-6 text-blue-600">
{active?.name}
</h3>
<div className="grid grid-cols-3 gap-4">
{active?.subCategories.map((sub, index) => (
<Button
key={index}
onClick={() => {
setCloseToolbar(false);
router.push(`/category/${active.name}/${sub.name}`);
}}
variant="outline"
className="justify-start h-12 cursor-pointer border border-slate-200 hover:border-blue-400 hover:bg-blue-50 transition-all text-slate-700 hover:text-blue-700 font-medium rounded-xl bg-transparent"
>
{sub.name}
</Button>
))}
</div>
</div>
</div>
</PopoverContent>
</Popover>
</section>
{isSticky && <div className="h-[72px]" />}
<style jsx global>{`
.scrollbar-thin::-webkit-scrollbar {
width: 6px;
height: 6px;
}
.scrollbar-thin::-webkit-scrollbar-track {
background: #f1f5f9;
border-radius: 100px;
}
.scrollbar-thin::-webkit-scrollbar-thumb {
background: #cbd5e1;
border-radius: 100px;
}
.scrollbar-thin::-webkit-scrollbar-thumb:hover {
background: #94a3b8;
}
.scrollbar-thin {
scrollbar-width: thin;
scrollbar-color: #cbd5e1 #f1f5f9;
}
`}</style>
</>
);
};

View File

@@ -1,32 +0,0 @@
'use client';
import { getPosts } from '@/shared/config/api/testApi';
import { useQuery } from '@tanstack/react-query';
import Link from 'next/link';
import React from 'react';
const Welcome = () => {
const { data } = useQuery({
queryKey: ['posts'],
queryFn: () => getPosts({ _limit: 1 }),
});
console.log('CSR posts', data);
return (
<div className="custom-container h-full bg-accent min-h-[400px] rounded-2xl flex items-center justify-center">
<Link
className="github-button"
href="https://github.com/fiasuz/create-fias"
data-color-scheme="no-preference: light; light: light; dark: dark;"
data-icon="octicon-star"
data-size="large"
aria-label="Star fiasuz/create-fias on GitHub"
>
Star on github
</Link>
</div>
);
};
export default Welcome;

View File

@@ -0,0 +1,76 @@
import Category from '@/assets/water-bottle.png';
import { StaticImageData } from 'next/image';
export const categoryList = [
{
name: 'Ichimliklar',
image: Category,
subCategories: [
{ name: 'Suvlar' },
{ name: 'Gazlangan ichimliklar' },
{ name: 'Sharbatlar' },
{ name: 'Energetik ichimliklar' },
{ name: 'Choy va qahva' },
],
},
{
name: 'Shirinliklar',
image: Category,
subCategories: [
{ name: 'Shokoladlar' },
{ name: 'Konfetlar' },
{ name: 'Pechene va vafli' },
{ name: 'Tortlar' },
{ name: 'Murabbo va asal' },
],
},
{
name: 'Moylar',
image: Category,
subCategories: [
{ name: 'Kungaboqar yogi' },
{ name: 'Zaytun yogi' },
{ name: 'Sariyog' },
{ name: 'Margarin' },
],
},
{
name: 'Non mahsulotlari',
image: Category,
subCategories: [
{ name: 'Oq non' },
{ name: 'Qora non' },
{ name: 'Bulochkalar' },
{ name: 'Lavash va pita' },
{ name: 'Kekslar' },
],
},
{
name: 'Gosht',
image: Category,
subCategories: [
{ name: 'Mol goshti' },
{ name: 'Qoy goshti' },
{ name: 'Tovuq goshti' },
{ name: 'Kurka goshti' },
{ name: 'Qiyma mahsulotlar' },
],
},
{
name: 'Boshqa',
image: Category,
subCategories: [
{ name: 'Ziravorlar' },
{ name: 'Konserva mahsulotlari' },
{ name: 'Soslar' },
],
},
];
export interface CategoryType {
name: string;
image: StaticImageData;
subCategories: {
name: string;
}[];
}

View File

@@ -0,0 +1,66 @@
'use client';
import Banner from '@/assets/banner.webp';
import { AspectRatio } from '@/shared/ui/aspect-ratio';
import useCategoryActive from '@/widgets/navbar/lib/openCategory';
import Image from 'next/image';
import 'swiper/css';
import { Swiper, SwiperSlide } from 'swiper/react';
import { categoryList } from '../lib/data';
const Welcome = () => {
const { setActive, setOpenToolbar } = useCategoryActive();
return (
<div className="custom-container">
{/* Banner */}
<AspectRatio ratio={16 / 6} className="!p-0">
<div className="relative w-full h-full">
<Image
src={Banner || '/placeholder.svg'}
alt="Banner"
fill
className="rounded-2xl object-cover shadow-lg border border-slate-200"
priority
/>
</div>
</AspectRatio>
{/* Category Slider */}
<div className="mx-auto mt-5 max-lg:hidden">
<Swiper
spaceBetween={4}
slidesPerView={4}
breakpoints={{
320: { slidesPerView: 1 },
640: { slidesPerView: 2 },
1024: { slidesPerView: 4 },
}}
>
{categoryList.map((item, index) => (
<SwiperSlide key={index} className="py-3 px-1">
<div
className="flex gap-1 items-center justify-center bg-gray-100/60 p-3 rounded-lg shadow-sm cursor-pointer space-x-3"
onClick={() => {
setOpenToolbar(true);
setActive(item);
}}
>
<Image
src={item.image}
alt={item.name}
className="w-7 h-7 object-contain"
/>
<p className="text-sm font-bold truncate line-clamp-2 leading-tight text-slate-700">
{item.name}
</p>
</div>
</SwiperSlide>
))}
</Swiper>
</div>
</div>
);
};
export default Welcome;