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

872
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -16,16 +16,20 @@
"@dnd-kit/sortable": "^10.0.0",
"@dnd-kit/utilities": "^3.2.2",
"@radix-ui/react-accordion": "^1.2.8",
"@radix-ui/react-avatar": "^1.1.7",
"@radix-ui/react-aspect-ratio": "^1.1.8",
"@radix-ui/react-avatar": "^1.1.11",
"@radix-ui/react-checkbox": "^1.2.3",
"@radix-ui/react-dialog": "^1.1.11",
"@radix-ui/react-dropdown-menu": "^2.1.12",
"@radix-ui/react-label": "^2.1.4",
"@radix-ui/react-label": "^2.1.8",
"@radix-ui/react-navigation-menu": "^1.2.10",
"@radix-ui/react-popover": "^1.1.15",
"@radix-ui/react-progress": "^1.1.8",
"@radix-ui/react-select": "^2.2.2",
"@radix-ui/react-separator": "^1.1.4",
"@radix-ui/react-slot": "^1.2.0",
"@radix-ui/react-tabs": "^1.1.9",
"@radix-ui/react-slot": "^1.2.4",
"@radix-ui/react-switch": "^1.2.6",
"@radix-ui/react-tabs": "^1.1.13",
"@radix-ui/react-toggle": "^1.1.6",
"@radix-ui/react-toggle-group": "^1.1.7",
"@radix-ui/react-tooltip": "^1.2.4",
@@ -36,6 +40,7 @@
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"dayjs": "^1.11.13",
"embla-carousel-react": "^8.6.0",
"lucide-react": "^0.503.0",
"next": "^15.5.4",
"next-intl": "^4.3.9",
@@ -44,9 +49,11 @@
"react-dom": "^19.1.1",
"recharts": "^2.15.3",
"sonner": "^2.0.3",
"swiper": "^12.0.3",
"tailwind-merge": "^3.2.0",
"vaul": "^1.1.2",
"zod": "^4.1.11"
"zod": "^4.1.11",
"zustand": "^5.0.9"
},
"devDependencies": {
"@eslint/eslintrc": "^3",
@@ -71,4 +78,4 @@
"eslint src --fix"
]
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 728 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 71 KiB

BIN
public/apple-icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 94 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 110 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 97 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

BIN
public/contact/Telegram.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 87 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 767 KiB

BIN
public/flags/ru.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

BIN
public/flags/uz.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

BIN
public/icon-dark-32x32.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 585 B

BIN
public/icon-light-32x32.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 566 B

26
public/icon.svg Normal file
View File

@@ -0,0 +1,26 @@
<svg width="180" height="180" viewBox="0 0 180 180" fill="none" xmlns="http://www.w3.org/2000/svg">
<style>
@media (prefers-color-scheme: light) {
.background { fill: black; }
.foreground { fill: white; }
}
@media (prefers-color-scheme: dark) {
.background { fill: white; }
.foreground { fill: black; }
}
</style>
<g clip-path="url(#clip0_7960_43945)">
<rect class="background" width="180" height="180" rx="37" />
<g style="transform: scale(95%); transform-origin: center">
<path class="foreground"
d="M101.141 53H136.632C151.023 53 162.689 64.6662 162.689 79.0573V112.904H148.112V79.0573C148.112 78.7105 148.098 78.3662 148.072 78.0251L112.581 112.898C112.701 112.902 112.821 112.904 112.941 112.904H148.112V126.672H112.941C98.5504 126.672 86.5638 114.891 86.5638 100.5V66.7434H101.141V100.5C101.141 101.15 101.191 101.792 101.289 102.422L137.56 66.7816C137.255 66.7563 136.945 66.7434 136.632 66.7434H101.141V53Z" />
<path class="foreground"
d="M65.2926 124.136L14 66.7372H34.6355L64.7495 100.436V66.7372H80.1365V118.47C80.1365 126.278 70.4953 129.958 65.2926 124.136Z" />
</g>
</g>
<defs>
<clipPath id="clip0_7960_43945">
<rect width="180" height="180" fill="white" />
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 89 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 203 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 97 KiB

BIN
public/multifruit-juice.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 67 KiB

BIN
public/nescafe-gold-jar.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 71 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

BIN
public/nike-air-max-270.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 973 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

BIN
public/pepsi-bottle.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

BIN
public/placeholder-logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 568 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="215" height="48" fill="none"><path fill="#000" d="M57.588 9.6h6L73.828 38h-5.2l-2.36-6.88h-11.36L52.548 38h-5.2l10.24-28.4Zm7.16 17.16-4.16-12.16-4.16 12.16h8.32Zm23.694-2.24c-.186-1.307-.706-2.32-1.56-3.04-.853-.72-1.866-1.08-3.04-1.08-1.68 0-2.986.613-3.92 1.84-.906 1.227-1.36 2.947-1.36 5.16s.454 3.933 1.36 5.16c.934 1.227 2.24 1.84 3.92 1.84 1.254 0 2.307-.373 3.16-1.12.854-.773 1.387-1.867 1.6-3.28l5.12.24c-.186 1.68-.733 3.147-1.64 4.4-.906 1.227-2.08 2.173-3.52 2.84-1.413.667-2.986 1-4.72 1-2.08 0-3.906-.453-5.48-1.36-1.546-.907-2.76-2.2-3.64-3.88-.853-1.68-1.28-3.627-1.28-5.84 0-2.24.427-4.187 1.28-5.84.88-1.68 2.094-2.973 3.64-3.88 1.574-.907 3.4-1.36 5.48-1.36 1.68 0 3.227.32 4.64.96 1.414.64 2.56 1.56 3.44 2.76.907 1.2 1.454 2.6 1.64 4.2l-5.12.28Zm11.486-7.72.12 3.4c.534-1.227 1.307-2.173 2.32-2.84 1.04-.693 2.267-1.04 3.68-1.04 1.494 0 2.76.387 3.8 1.16 1.067.747 1.827 1.813 2.28 3.2.507-1.44 1.294-2.52 2.36-3.24 1.094-.747 2.414-1.12 3.96-1.12 1.414 0 2.64.307 3.68.92s1.84 1.52 2.4 2.72c.56 1.2.84 2.667.84 4.4V38h-4.96V25.92c0-1.813-.293-3.187-.88-4.12-.56-.96-1.413-1.44-2.56-1.44-.906 0-1.68.213-2.32.64-.64.427-1.133 1.053-1.48 1.88-.32.827-.48 1.84-.48 3.04V38h-4.56V25.92c0-1.2-.133-2.213-.4-3.04-.24-.827-.626-1.453-1.16-1.88-.506-.427-1.133-.64-1.88-.64-.906 0-1.68.227-2.32.68-.64.427-1.133 1.053-1.48 1.88-.32.827-.48 1.827-.48 3V38h-4.96V16.8h4.48Zm26.723 10.6c0-2.24.427-4.187 1.28-5.84.854-1.68 2.067-2.973 3.64-3.88 1.574-.907 3.4-1.36 5.48-1.36 1.84 0 3.494.413 4.96 1.24 1.467.827 2.64 2.08 3.52 3.76.88 1.653 1.347 3.693 1.4 6.12v1.32h-15.08c.107 1.813.614 3.227 1.52 4.24.907.987 2.134 1.48 3.68 1.48.987 0 1.88-.253 2.68-.76a4.803 4.803 0 0 0 1.84-2.2l5.08.36c-.64 2.027-1.84 3.64-3.6 4.84-1.733 1.173-3.733 1.76-6 1.76-2.08 0-3.906-.453-5.48-1.36-1.573-.907-2.786-2.2-3.64-3.88-.853-1.68-1.28-3.627-1.28-5.84Zm15.16-2.04c-.213-1.733-.76-3.013-1.64-3.84-.853-.827-1.893-1.24-3.12-1.24-1.44 0-2.6.453-3.48 1.36-.88.88-1.44 2.12-1.68 3.72h9.92ZM163.139 9.6V38h-5.04V9.6h5.04Zm8.322 7.2.24 5.88-.64-.36c.32-2.053 1.094-3.56 2.32-4.52 1.254-.987 2.787-1.48 4.6-1.48 2.32 0 4.107.733 5.36 2.2 1.254 1.44 1.88 3.387 1.88 5.84V38h-4.96V25.92c0-1.253-.12-2.28-.36-3.08-.24-.8-.64-1.413-1.2-1.84-.533-.427-1.253-.64-2.16-.64-1.44 0-2.573.48-3.4 1.44-.8.933-1.2 2.307-1.2 4.12V38h-4.96V16.8h4.48Zm30.003 7.72c-.186-1.307-.706-2.32-1.56-3.04-.853-.72-1.866-1.08-3.04-1.08-1.68 0-2.986.613-3.92 1.84-.906 1.227-1.36 2.947-1.36 5.16s.454 3.933 1.36 5.16c.934 1.227 2.24 1.84 3.92 1.84 1.254 0 2.307-.373 3.16-1.12.854-.773 1.387-1.867 1.6-3.28l5.12.24c-.186 1.68-.733 3.147-1.64 4.4-.906 1.227-2.08 2.173-3.52 2.84-1.413.667-2.986 1-4.72 1-2.08 0-3.906-.453-5.48-1.36-1.546-.907-2.76-2.2-3.64-3.88-.853-1.68-1.28-3.627-1.28-5.84 0-2.24.427-4.187 1.28-5.84.88-1.68 2.094-2.973 3.64-3.88 1.574-.907 3.4-1.36 5.48-1.36 1.68 0 3.227.32 4.64.96 1.414.64 2.56 1.56 3.44 2.76.907 1.2 1.454 2.6 1.64 4.2l-5.12.28Zm11.443 8.16V38h-5.6v-5.32h5.6Z"/><path fill="#171717" fill-rule="evenodd" d="m7.839 40.783 16.03-28.054L20 6 0 40.783h7.839Zm8.214 0H40L27.99 19.894l-4.02 7.032 3.976 6.914H20.02l-3.967 6.943Z" clip-rule="evenodd"/></svg>

After

Width:  |  Height:  |  Size: 3.1 KiB

BIN
public/placeholder-user.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

BIN
public/placeholder.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

1
public/placeholder.svg Normal file
View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="1200" fill="none"><rect width="1200" height="1200" fill="#EAEAEA" rx="3"/><g opacity=".5"><g opacity=".5"><path fill="#FAFAFA" d="M600.709 736.5c-75.454 0-136.621-61.167-136.621-136.62 0-75.454 61.167-136.621 136.621-136.621 75.453 0 136.62 61.167 136.62 136.621 0 75.453-61.167 136.62-136.62 136.62Z"/><path stroke="#C9C9C9" stroke-width="2.418" d="M600.709 736.5c-75.454 0-136.621-61.167-136.621-136.62 0-75.454 61.167-136.621 136.621-136.621 75.453 0 136.62 61.167 136.62 136.621 0 75.453-61.167 136.62-136.62 136.62Z"/></g><path stroke="url(#a)" stroke-width="2.418" d="M0-1.209h553.581" transform="scale(1 -1) rotate(45 1163.11 91.165)"/><path stroke="url(#b)" stroke-width="2.418" d="M404.846 598.671h391.726"/><path stroke="url(#c)" stroke-width="2.418" d="M599.5 795.742V404.017"/><path stroke="url(#d)" stroke-width="2.418" d="m795.717 796.597-391.441-391.44"/><path fill="#fff" d="M600.709 656.704c-31.384 0-56.825-25.441-56.825-56.824 0-31.384 25.441-56.825 56.825-56.825 31.383 0 56.824 25.441 56.824 56.825 0 31.383-25.441 56.824-56.824 56.824Z"/><g clip-path="url(#e)"><path fill="#666" fill-rule="evenodd" d="M616.426 586.58h-31.434v16.176l3.553-3.554.531-.531h9.068l.074-.074 8.463-8.463h2.565l7.18 7.181V586.58Zm-15.715 14.654 3.698 3.699 1.283 1.282-2.565 2.565-1.282-1.283-5.2-5.199h-6.066l-5.514 5.514-.073.073v2.876a2.418 2.418 0 0 0 2.418 2.418h26.598a2.418 2.418 0 0 0 2.418-2.418v-8.317l-8.463-8.463-7.181 7.181-.071.072Zm-19.347 5.442v4.085a6.045 6.045 0 0 0 6.046 6.045h26.598a6.044 6.044 0 0 0 6.045-6.045v-7.108l1.356-1.355-1.282-1.283-.074-.073v-17.989h-38.689v23.43l-.146.146.146.147Z" clip-rule="evenodd"/></g><path stroke="#C9C9C9" stroke-width="2.418" d="M600.709 656.704c-31.384 0-56.825-25.441-56.825-56.824 0-31.384 25.441-56.825 56.825-56.825 31.383 0 56.824 25.441 56.824 56.825 0 31.383-25.441 56.824-56.824 56.824Z"/></g><defs><linearGradient id="a" x1="554.061" x2="-.48" y1=".083" y2=".087" gradientUnits="userSpaceOnUse"><stop stop-color="#C9C9C9" stop-opacity="0"/><stop offset=".208" stop-color="#C9C9C9"/><stop offset=".792" stop-color="#C9C9C9"/><stop offset="1" stop-color="#C9C9C9" stop-opacity="0"/></linearGradient><linearGradient id="b" x1="796.912" x2="404.507" y1="599.963" y2="599.965" gradientUnits="userSpaceOnUse"><stop stop-color="#C9C9C9" stop-opacity="0"/><stop offset=".208" stop-color="#C9C9C9"/><stop offset=".792" stop-color="#C9C9C9"/><stop offset="1" stop-color="#C9C9C9" stop-opacity="0"/></linearGradient><linearGradient id="c" x1="600.792" x2="600.794" y1="403.677" y2="796.082" gradientUnits="userSpaceOnUse"><stop stop-color="#C9C9C9" stop-opacity="0"/><stop offset=".208" stop-color="#C9C9C9"/><stop offset=".792" stop-color="#C9C9C9"/><stop offset="1" stop-color="#C9C9C9" stop-opacity="0"/></linearGradient><linearGradient id="d" x1="404.85" x2="796.972" y1="403.903" y2="796.02" gradientUnits="userSpaceOnUse"><stop stop-color="#C9C9C9" stop-opacity="0"/><stop offset=".208" stop-color="#C9C9C9"/><stop offset=".792" stop-color="#C9C9C9"/><stop offset="1" stop-color="#C9C9C9" stop-opacity="0"/></linearGradient><clipPath id="e"><path fill="#fff" d="M581.364 580.535h38.689v38.689h-38.689z"/></clipPath></defs></svg>

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 136 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 159 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

BIN
public/sony-wh-1000xm5.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 561 KiB

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

Some files were not shown because too many files have changed in this diff Show More