change
This commit is contained in:
8
src/app/500.tsx
Normal file
8
src/app/500.tsx
Normal file
@@ -0,0 +1,8 @@
|
||||
import React from 'react'
|
||||
|
||||
const Custom500 = () => {
|
||||
return (
|
||||
<div>500</div>
|
||||
)
|
||||
}
|
||||
export default Custom500
|
||||
26
src/app/Error.tsx
Normal file
26
src/app/Error.tsx
Normal file
@@ -0,0 +1,26 @@
|
||||
import React from 'react';
|
||||
|
||||
interface ErrorProps {
|
||||
statusCode?: number;
|
||||
}
|
||||
|
||||
const ErrorPage: React.FC<ErrorProps> = ({statusCode}) => {
|
||||
return (
|
||||
<div>
|
||||
<p>
|
||||
{statusCode
|
||||
? `An error ${statusCode} occurred on server`
|
||||
: 'An error occurred on client'}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
export async function getServerSideProps() {
|
||||
return {
|
||||
props: {statusCode: 500},
|
||||
};
|
||||
}
|
||||
|
||||
export default ErrorPage;
|
||||
21
src/app/[locale]/auth/login/page.tsx
Normal file
21
src/app/[locale]/auth/login/page.tsx
Normal file
@@ -0,0 +1,21 @@
|
||||
"use client"
|
||||
import React from 'react'
|
||||
import LoginSection from "@/features/auth/ui/login-section";
|
||||
import {useAuthStore} from "@/shared/store/authStore";
|
||||
|
||||
const Page = () => {
|
||||
const {user, isAuthenticated} = useAuthStore();
|
||||
|
||||
if(user || isAuthenticated) {
|
||||
window.location.href = '/profile';
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex min-h-svh w-full items-center justify-center p-6 md:p-10">
|
||||
<div className="w-full max-w-sm">
|
||||
<LoginSection/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default Page
|
||||
8
src/app/[locale]/auth/register/page.tsx
Normal file
8
src/app/[locale]/auth/register/page.tsx
Normal file
@@ -0,0 +1,8 @@
|
||||
import React from 'react'
|
||||
|
||||
const Page = () => {
|
||||
return (
|
||||
<div>Page</div>
|
||||
)
|
||||
}
|
||||
export default Page
|
||||
32
src/app/[locale]/brand/[brandId]/page.tsx
Normal file
32
src/app/[locale]/brand/[brandId]/page.tsx
Normal file
@@ -0,0 +1,32 @@
|
||||
import React from "react";
|
||||
import { getBrandProducts } from "@/shared/api/brandsSvc";
|
||||
import ProductsList from "@/features/brand-products/ui/products-list";
|
||||
import MyPagionation from "@/shared/ui/my-pagionation";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
type PageProps = {
|
||||
params: {
|
||||
brandId: number;
|
||||
};
|
||||
searchParams: { page?: string };
|
||||
};
|
||||
|
||||
const Page = async ({ params, searchParams }: Readonly<PageProps>) => {
|
||||
const { brandId } = await params;
|
||||
const { page } = await searchParams;
|
||||
const products = await getBrandProducts(brandId);
|
||||
|
||||
return (
|
||||
<div className={"section-wrapper"}>
|
||||
<div className={"section-wrapper"}>
|
||||
<ProductsList products={products.data} />
|
||||
</div>
|
||||
<MyPagionation
|
||||
currentPage={Number(page)}
|
||||
totalPages={products.pagination.total}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
export default Page;
|
||||
36
src/app/[locale]/category/[categoryId]/page.tsx
Normal file
36
src/app/[locale]/category/[categoryId]/page.tsx
Normal file
@@ -0,0 +1,36 @@
|
||||
import React from "react";
|
||||
import ProductsSection from "@/features/category-details/ui/products-section";
|
||||
import MyPagionation from "@/shared/ui/my-pagionation";
|
||||
import { getProducts } from "@/shared/api/productSvc";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
type PageProps = {
|
||||
params: {
|
||||
categoryId: number;
|
||||
};
|
||||
searchParams: { page?: string };
|
||||
};
|
||||
|
||||
const Page = async ({ params, searchParams }: Readonly<PageProps>) => {
|
||||
const { page } = await searchParams;
|
||||
|
||||
const { categoryId } = await params;
|
||||
const { data: products } = await getProducts({
|
||||
categoryId,
|
||||
currentPage: Number(page),
|
||||
});
|
||||
|
||||
return (
|
||||
<div className={"section-wrapper"}>
|
||||
<div className={"section-wrapper"}>
|
||||
<ProductsSection products={products.data} />
|
||||
</div>
|
||||
<MyPagionation
|
||||
currentPage={Number(page)}
|
||||
totalPages={products.pagination.total}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
export default Page;
|
||||
14
src/app/[locale]/category/page.tsx
Normal file
14
src/app/[locale]/category/page.tsx
Normal file
@@ -0,0 +1,14 @@
|
||||
import React from "react";
|
||||
import CategorySection from "../../../features/home/ui/category-section";
|
||||
import { getCategory } from "@/shared/api/productCategorySvc";
|
||||
|
||||
const Page = async () => {
|
||||
const { data: categoryData } = await getCategory();
|
||||
|
||||
return (
|
||||
<div className={"section-wrapper"}>
|
||||
<CategorySection categories={categoryData} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
export default Page;
|
||||
113
src/app/[locale]/layout.tsx
Normal file
113
src/app/[locale]/layout.tsx
Normal file
@@ -0,0 +1,113 @@
|
||||
import React from "react";
|
||||
import "../globals.css";
|
||||
import { notFound } from "next/navigation";
|
||||
import { golosText, routing } from "@/shared/config";
|
||||
import { Locale } from "@/shared/types/locale";
|
||||
import { LanguageRoutes } from "@/shared/config/i18n/types";
|
||||
import { Navbar } from "@/widgets";
|
||||
import Footer from "@/widgets/footer/footer";
|
||||
import { Metadata } from "next";
|
||||
import { PRODUCT_INFO } from "@/shared/constants";
|
||||
import { hasLocale, NextIntlClientProvider } from "next-intl";
|
||||
import { setRequestLocale } from "next-intl/server";
|
||||
import QueryProvider from "@/shared/providers/QueryProvider";
|
||||
import ProgressBar from "@/shared/ui/progressbar";
|
||||
import { Toaster } from "@/shared/ui/sonner";
|
||||
import Script from "next/script";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: PRODUCT_INFO.name,
|
||||
description: PRODUCT_INFO.description,
|
||||
icons: PRODUCT_INFO.favicon,
|
||||
keywords: [
|
||||
"get green",
|
||||
"green energy",
|
||||
"get green energy trade",
|
||||
"quyosh uskunalari",
|
||||
"солнечное оборудование",
|
||||
],
|
||||
metadataBase: new URL(PRODUCT_INFO.url),
|
||||
alternates: {
|
||||
canonical: PRODUCT_INFO.url,
|
||||
languages: {
|
||||
uz: `${PRODUCT_INFO.url}/${LanguageRoutes.UZ}`,
|
||||
ru: `${PRODUCT_INFO.url}/${LanguageRoutes.RU}`,
|
||||
},
|
||||
},
|
||||
applicationName: PRODUCT_INFO.name,
|
||||
authors: [{ name: PRODUCT_INFO.creator, url: "https://felix-its.uz/" }],
|
||||
category: "website",
|
||||
openGraph: {
|
||||
title: PRODUCT_INFO.name,
|
||||
url: PRODUCT_INFO.url,
|
||||
description: PRODUCT_INFO.description,
|
||||
type: "website",
|
||||
countryName: "O'zbekiston",
|
||||
siteName: PRODUCT_INFO.name,
|
||||
images: {
|
||||
url: "/og-banner.png",
|
||||
alt: "get green",
|
||||
width: 1200,
|
||||
height: 630,
|
||||
},
|
||||
alternateLocale: [LanguageRoutes.UZ, LanguageRoutes.RU],
|
||||
},
|
||||
};
|
||||
|
||||
type LayoutProps = {
|
||||
children: React.ReactNode;
|
||||
params: Promise<{ locale: Locale }>;
|
||||
};
|
||||
|
||||
export default async function LocaleLayout({ children, params }: LayoutProps) {
|
||||
const { locale } = await params;
|
||||
if (!hasLocale(routing.locales, locale)) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
// Enable static rendering
|
||||
setRequestLocale(locale);
|
||||
|
||||
return (
|
||||
<html lang={locale} suppressHydrationWarning>
|
||||
<head>
|
||||
{/* Google tag (gtag.js) */}
|
||||
<Script
|
||||
src="https://www.googletagmanager.com/gtag/js?id=AW-17219198796"
|
||||
strategy="afterInteractive"
|
||||
/>
|
||||
<Script id="gtag-init" strategy="afterInteractive">
|
||||
{`
|
||||
window.dataLayer = window.dataLayer || [];
|
||||
function gtag(){dataLayer.push(arguments);}
|
||||
gtag('js', new Date());
|
||||
gtag('config', 'AW-17219198796');
|
||||
`}
|
||||
</Script>
|
||||
{/* Google Ads Conversion Tracking */}
|
||||
<Script id="conversion-tracking" strategy="afterInteractive">
|
||||
{`
|
||||
gtag('event', 'conversion', {
|
||||
send_to: 'AW-17219198796/SQJ6CJuH8dwaEMy-4JJA',
|
||||
value: 1.0,
|
||||
currency: 'USD'
|
||||
});
|
||||
`}
|
||||
</Script>
|
||||
</head>
|
||||
<body
|
||||
className={`${golosText.variable} ${golosText.className} font-poppins antialiased`}
|
||||
>
|
||||
<NextIntlClientProvider locale={locale as LanguageRoutes}>
|
||||
<QueryProvider>
|
||||
<ProgressBar />
|
||||
<Navbar />
|
||||
{children}
|
||||
<Footer />
|
||||
<Toaster position="bottom-center" richColors />
|
||||
</QueryProvider>
|
||||
</NextIntlClientProvider>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
40
src/app/[locale]/page.tsx
Normal file
40
src/app/[locale]/page.tsx
Normal file
@@ -0,0 +1,40 @@
|
||||
import {
|
||||
ContactSection,
|
||||
DownloadAppSection,
|
||||
FreeRecommendationSection,
|
||||
HeroSection,
|
||||
PartnersSection,
|
||||
ProfitSection,
|
||||
} from "@/features/home/ui";
|
||||
import { getCompilation } from "@/shared/api/compilationsSvc";
|
||||
import ProductsSection from "@/features/category-details/ui/products-section";
|
||||
import SectionsSection from "@/features/home/ui/sections-section";
|
||||
import { getBrands } from "@/shared/api/brandsSvc";
|
||||
|
||||
const Page = async () => {
|
||||
const { data } = await getCompilation();
|
||||
const { data: partners } = await getBrands();
|
||||
|
||||
return (
|
||||
<div>
|
||||
<HeroSection />
|
||||
<SectionsSection />
|
||||
{data.map((section) => (
|
||||
<div
|
||||
key={section.id}
|
||||
className={"my-container section-wrapper px-4 md:px-0"}
|
||||
>
|
||||
<h1 className={"section-title"}>{section.title}</h1>
|
||||
<ProductsSection products={section.products} />
|
||||
</div>
|
||||
))}
|
||||
<FreeRecommendationSection />
|
||||
<ProfitSection />
|
||||
<PartnersSection partners={partners} />
|
||||
<ContactSection />
|
||||
<DownloadAppSection />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Page;
|
||||
13
src/app/[locale]/partners/page.tsx
Normal file
13
src/app/[locale]/partners/page.tsx
Normal file
@@ -0,0 +1,13 @@
|
||||
import React from "react";
|
||||
import PartnersSection from "@/features/partners/ui/partners-section/partners-section";
|
||||
import { getPartners } from "@/shared/api/partnersSvc";
|
||||
|
||||
const Page = async () => {
|
||||
const { data: partners } = await getPartners();
|
||||
return (
|
||||
<div className={"section-wrapper"}>
|
||||
<PartnersSection partners={partners} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
export default Page;
|
||||
32
src/app/[locale]/product/[productId]/page.tsx
Normal file
32
src/app/[locale]/product/[productId]/page.tsx
Normal file
@@ -0,0 +1,32 @@
|
||||
import React from 'react'
|
||||
import ProductDetailsSection from "@/features/product-details/ui/product-details-section";
|
||||
import {ContactSection, DownloadAppSection, FreeRecommendationSection, PartnersSection} from "@/features/home/ui";
|
||||
import {getProductById} from "@/shared/api/productSvc";
|
||||
import { getBrands } from '@/shared/api/brandsSvc';
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
type PageProps = {
|
||||
params: {
|
||||
productId: number
|
||||
},
|
||||
};
|
||||
|
||||
const Page = async ({params}: Readonly<PageProps>) => {
|
||||
const {productId} = await params;
|
||||
const {data: product} = await getProductById(productId)
|
||||
const { data: partners } = await getBrands();
|
||||
|
||||
return (
|
||||
<div className={"section-wrapper bg-white"}>
|
||||
<ProductDetailsSection product={product.data}/>
|
||||
<FreeRecommendationSection/>
|
||||
<div className="mt-24">
|
||||
<PartnersSection partners={partners}/>
|
||||
</div>
|
||||
<ContactSection/>
|
||||
<DownloadAppSection/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default Page
|
||||
9
src/app/[locale]/profile/applications/page.tsx
Normal file
9
src/app/[locale]/profile/applications/page.tsx
Normal file
@@ -0,0 +1,9 @@
|
||||
import React from 'react'
|
||||
import ApplicationsSections from "@/features/profile/ui/applications-section";
|
||||
|
||||
const Page = () => {
|
||||
return (
|
||||
<ApplicationsSections/>
|
||||
)
|
||||
}
|
||||
export default Page
|
||||
9
src/app/[locale]/profile/contact/page.tsx
Normal file
9
src/app/[locale]/profile/contact/page.tsx
Normal file
@@ -0,0 +1,9 @@
|
||||
import React from 'react'
|
||||
import ContactSection from "@/features/profile/ui/contact-section";
|
||||
|
||||
const Page = () => {
|
||||
return (
|
||||
<ContactSection/>
|
||||
)
|
||||
}
|
||||
export default Page
|
||||
20
src/app/[locale]/profile/layout.tsx
Normal file
20
src/app/[locale]/profile/layout.tsx
Normal file
@@ -0,0 +1,20 @@
|
||||
import React from 'react'
|
||||
import {ProfileSidebar} from "@/widgets/profile-sidebar/profile-sidebar";
|
||||
import {SidebarProvider} from "@/shared/ui/sidebar";
|
||||
import PrivateRoute from "@/shared/providers/PrivateRouteProvider";
|
||||
|
||||
const Layout = ({children}: Readonly<{ children: React.ReactNode }>) => {
|
||||
return (
|
||||
<PrivateRoute>
|
||||
<div className="my-12 min-h-screen">
|
||||
<div className="my-container section-wrapper">
|
||||
<SidebarProvider className={"gap-4 !min-h-auto"}>
|
||||
<ProfileSidebar/>
|
||||
{children}
|
||||
</SidebarProvider>
|
||||
</div>
|
||||
</div>
|
||||
</PrivateRoute>
|
||||
)
|
||||
}
|
||||
export default Layout
|
||||
10
src/app/[locale]/profile/orders/page.tsx
Normal file
10
src/app/[locale]/profile/orders/page.tsx
Normal file
@@ -0,0 +1,10 @@
|
||||
"use client"
|
||||
import React from 'react'
|
||||
import OrdersSection from "@/features/profile/ui/orders-section";
|
||||
|
||||
const Page = () => {
|
||||
return (
|
||||
<OrdersSection/>
|
||||
)
|
||||
}
|
||||
export default Page
|
||||
9
src/app/[locale]/profile/page.tsx
Normal file
9
src/app/[locale]/profile/page.tsx
Normal file
@@ -0,0 +1,9 @@
|
||||
import React from 'react'
|
||||
import InformationSection from "@/features/profile/ui/information-section";
|
||||
|
||||
const Page = () => {
|
||||
return (
|
||||
<InformationSection/>
|
||||
)
|
||||
}
|
||||
export default Page
|
||||
13
src/app/[locale]/profile/settings/page.tsx
Normal file
13
src/app/[locale]/profile/settings/page.tsx
Normal file
@@ -0,0 +1,13 @@
|
||||
import { useTranslations } from 'next-intl'
|
||||
import React from 'react'
|
||||
|
||||
const Page = () => {
|
||||
const t = useTranslations("")
|
||||
return (
|
||||
<div className={"bg-white rounded-xl w-full p-4"}>
|
||||
<h1 className={"text-md font-semibold"}>{t("Sozlamalar")}</h1>
|
||||
<span className={"text-sm font-semibold text-gray-500"}>{t("Ma'lumotlarni yangilash")}</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default Page
|
||||
9
src/app/[locale]/profile/terms/page.tsx
Normal file
9
src/app/[locale]/profile/terms/page.tsx
Normal file
@@ -0,0 +1,9 @@
|
||||
import React from 'react'
|
||||
import TermsSection from "@/features/profile/ui/terms-section";
|
||||
|
||||
const Page = () => {
|
||||
return (
|
||||
<TermsSection/>
|
||||
)
|
||||
}
|
||||
export default Page
|
||||
13
src/app/[locale]/services/page.tsx
Normal file
13
src/app/[locale]/services/page.tsx
Normal file
@@ -0,0 +1,13 @@
|
||||
import React from "react";
|
||||
import { getServices } from "@/shared/api/servicesSvc";
|
||||
import ServicesSection from "@/features/services/ui/services-section/services-section";
|
||||
|
||||
const Page = async () => {
|
||||
const { data: services } = await getServices();
|
||||
return (
|
||||
<div className={"section-wrapper"}>
|
||||
<ServicesSection services={services} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
export default Page;
|
||||
13
src/app/[locale]/useful/page.tsx
Normal file
13
src/app/[locale]/useful/page.tsx
Normal file
@@ -0,0 +1,13 @@
|
||||
import React from 'react'
|
||||
import {getUseful} from "@/shared/api/usefulSvc";
|
||||
import UsefulSection from "@/features/useful/ui/useful-section";
|
||||
|
||||
const Page = async() => {
|
||||
const {data: usefuls} = await getUseful()
|
||||
return (
|
||||
<div className={"section-wrapper"}>
|
||||
<UsefulSection usefuls={usefuls}/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default Page
|
||||
BIN
src/app/favicon.ico
Normal file
BIN
src/app/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.4 KiB |
129
src/app/globals.css
Normal file
129
src/app/globals.css
Normal file
@@ -0,0 +1,129 @@
|
||||
@import "tailwindcss";
|
||||
@import "tw-animate-css";
|
||||
@import "../shared/style/custom-utils.css";
|
||||
|
||||
@custom-variant dark (&:is(.dark *));
|
||||
|
||||
@theme inline {
|
||||
--color-background: var(--background);
|
||||
--color-foreground: var(--foreground);
|
||||
--font-sans: var(--font-golos-text);
|
||||
--color-sidebar-ring: var(--sidebar-ring);
|
||||
--color-sidebar-border: var(--sidebar-border);
|
||||
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
||||
--color-sidebar-accent: var(--sidebar-accent);
|
||||
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
||||
--color-sidebar-primary: var(--sidebar-primary);
|
||||
--color-sidebar-foreground: var(--sidebar-foreground);
|
||||
--color-sidebar: var(--sidebar);
|
||||
--color-chart-5: var(--chart-5);
|
||||
--color-chart-4: var(--chart-4);
|
||||
--color-chart-3: var(--chart-3);
|
||||
--color-chart-2: var(--chart-2);
|
||||
--color-chart-1: var(--chart-1);
|
||||
--color-ring: var(--ring);
|
||||
--color-input: var(--input);
|
||||
--color-border: var(--border);
|
||||
--color-destructive: var(--destructive);
|
||||
--color-accent-foreground: var(--accent-foreground);
|
||||
--color-accent: var(--accent);
|
||||
--color-muted-foreground: var(--muted-foreground);
|
||||
--color-muted: var(--muted);
|
||||
--color-secondary-foreground: var(--secondary-foreground);
|
||||
--color-secondary: var(--secondary);
|
||||
--color-primary-foreground: var(--primary-foreground);
|
||||
--color-primary: var(--primary);
|
||||
--color-popover-foreground: var(--popover-foreground);
|
||||
--color-popover: var(--popover);
|
||||
--color-card-foreground: var(--card-foreground);
|
||||
--color-card: var(--card);
|
||||
--radius-sm: calc(var(--radius) - 4px);
|
||||
--radius-md: calc(var(--radius) - 2px);
|
||||
--radius-lg: var(--radius);
|
||||
--radius-xl: calc(var(--radius) + 4px);
|
||||
}
|
||||
|
||||
/* Ranglarni ko'rish uchun https://oklch.com/ saytidan foydalaning */
|
||||
:root {
|
||||
--radius: 0.625rem;
|
||||
--background: oklch(0.98 0 0);
|
||||
--foreground: oklch(0.141 0.005 285.823);
|
||||
--card: oklch(1 0 0);
|
||||
--card-foreground: oklch(0.141 0.005 285.823);
|
||||
--popover: oklch(1 0 0);
|
||||
--popover-foreground: oklch(0.141 0.005 285.823);
|
||||
--primary: var(--chart-2);
|
||||
--primary-foreground: oklch(0.985 0 0);
|
||||
--secondary: oklch(0.967 0.001 286.375);
|
||||
--secondary-foreground: oklch(0.21 0.006 285.885);
|
||||
--muted: oklch(0.967 0.001 286.375);
|
||||
--muted-foreground: oklch(0.552 0.016 285.938);
|
||||
--accent: oklch(0.967 0.001 286.375);
|
||||
--accent-foreground: oklch(0.21 0.006 285.885);
|
||||
--destructive: oklch(0.577 0.245 27.325);
|
||||
--border: oklch(0.92 0.004 286.32);
|
||||
--input: oklch(0.92 0.004 286.32);
|
||||
--ring: oklch(0.705 0.015 286.067);
|
||||
--chart-1: oklch(0.646 0.222 41.116);
|
||||
--chart-2: oklch(0.6 0.118 184.704);
|
||||
--chart-3: oklch(0.398 0.07 227.392);
|
||||
--chart-4: oklch(0.828 0.189 84.429);
|
||||
--chart-5: oklch(0.769 0.188 70.08);
|
||||
--sidebar: oklch(0.985 0 0);
|
||||
--sidebar-foreground: oklch(0.141 0.005 285.823);
|
||||
--sidebar-primary: oklch(0.21 0.006 285.885);
|
||||
--sidebar-primary-foreground: oklch(0.985 0 0);
|
||||
--sidebar-accent: oklch(0.967 0.001 286.375);
|
||||
--sidebar-accent-foreground: oklch(0.21 0.006 285.885);
|
||||
--sidebar-border: oklch(0.92 0.004 286.32);
|
||||
--sidebar-ring: oklch(0.705 0.015 286.067);
|
||||
}
|
||||
|
||||
.dark {
|
||||
--background: oklch(0.141 0.005 285.823);
|
||||
--foreground: oklch(0.985 0 0);
|
||||
--card: oklch(0.21 0.006 285.885);
|
||||
--card-foreground: oklch(0.985 0 0);
|
||||
--popover: oklch(0.21 0.006 285.885);
|
||||
--popover-foreground: oklch(0.985 0 0);
|
||||
--primary: oklch(0.62 0.1532 154.89);
|
||||
--primary-foreground: oklch(0.21 0.006 285.885);
|
||||
--secondary: oklch(0.274 0.006 286.033);
|
||||
--secondary-foreground: oklch(0.985 0 0);
|
||||
--muted: oklch(0.274 0.006 286.033);
|
||||
--muted-foreground: oklch(0.705 0.015 286.067);
|
||||
--accent: oklch(0.274 0.006 286.033);
|
||||
--accent-foreground: oklch(0.985 0 0);
|
||||
--destructive: oklch(0.704 0.191 22.216);
|
||||
--border: oklch(1 0 0 / 10%);
|
||||
--input: oklch(1 0 0 / 15%);
|
||||
--ring: oklch(0.552 0.016 285.938);
|
||||
--chart-1: oklch(0.488 0.243 264.376);
|
||||
--chart-2: oklch(0.696 0.17 162.48);
|
||||
--chart-3: oklch(0.769 0.188 70.08);
|
||||
--chart-4: oklch(0.627 0.265 303.9);
|
||||
--chart-5: oklch(0.645 0.246 16.439);
|
||||
--sidebar: oklch(0.21 0.006 285.885);
|
||||
--sidebar-foreground: oklch(0.985 0 0);
|
||||
--sidebar-primary: oklch(0.488 0.243 264.376);
|
||||
--sidebar-primary-foreground: oklch(0.985 0 0);
|
||||
--sidebar-accent: oklch(0.274 0.006 286.033);
|
||||
--sidebar-accent-foreground: oklch(0.985 0 0);
|
||||
--sidebar-border: oklch(1 0 0 / 10%);
|
||||
--sidebar-ring: oklch(0.552 0.016 285.938);
|
||||
}
|
||||
|
||||
*{
|
||||
/*border: 1px solid red;*/
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
|
||||
@layer base {
|
||||
* {
|
||||
@apply border-border outline-ring/50;
|
||||
}
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
}
|
||||
}
|
||||
|
||||
11
src/app/layout.tsx
Normal file
11
src/app/layout.tsx
Normal file
@@ -0,0 +1,11 @@
|
||||
import { ReactNode } from 'react';
|
||||
|
||||
type Props = {
|
||||
children: ReactNode;
|
||||
};
|
||||
|
||||
// Since we have a `not-found.tsx` page on the root, a layout file
|
||||
// is required, even if it's just passing children through.
|
||||
export default function RootLayout({ children }: Props) {
|
||||
return children;
|
||||
}
|
||||
35
src/app/loading.tsx
Normal file
35
src/app/loading.tsx
Normal file
@@ -0,0 +1,35 @@
|
||||
"use client";
|
||||
|
||||
import React from "react";
|
||||
import { motion } from "motion/react";
|
||||
|
||||
const Loading = () => {
|
||||
return (
|
||||
<html>
|
||||
<body>
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
transition={{ duration: 1 }}
|
||||
className="h-screen w-full flex flex-col gap-4 justify-center items-center bg-primary"
|
||||
>
|
||||
<motion.div
|
||||
animate={{
|
||||
scale: [1, 1.2, 1],
|
||||
rotate: [0, 360, 0],
|
||||
}}
|
||||
transition={{
|
||||
duration: 2,
|
||||
repeat: Infinity,
|
||||
ease: "easeInOut",
|
||||
}}
|
||||
className="w-10 h-10 bg-white rounded-full"
|
||||
/>
|
||||
</motion.div>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
};
|
||||
|
||||
export default Loading;
|
||||
29
src/app/not-found.tsx
Normal file
29
src/app/not-found.tsx
Normal file
@@ -0,0 +1,29 @@
|
||||
import React from "react";
|
||||
import { Link } from "@/shared/config/i18n/navigation";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
const Custom404 = () => {
|
||||
const t = useTranslations("");
|
||||
return (
|
||||
<html>
|
||||
<body>
|
||||
<div className="flex items-center justify-center h-screen w-full text-center">
|
||||
<div>
|
||||
<h1 className={"text-4xl font-bold text-primary"}>404</h1>
|
||||
<p className={"my-5"}>
|
||||
{t("Sahifa topilmadi, Iltimos qayta urinib ko`ring")}
|
||||
</p>
|
||||
<Link
|
||||
href="/"
|
||||
className="mt-6 px-4 py-2 bg-primary text-white rounded-lg hover:bg-primary/90"
|
||||
>
|
||||
{t("Orga qaytish")}
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
};
|
||||
|
||||
export default Custom404;
|
||||
6
src/app/page.tsx
Normal file
6
src/app/page.tsx
Normal file
@@ -0,0 +1,6 @@
|
||||
import { redirect } from 'next/navigation';
|
||||
|
||||
// This page only renders when the app is built statically (output: 'export')
|
||||
export default function RootPage() {
|
||||
redirect('/uz');
|
||||
}
|
||||
172
src/features/auth/ui/login-section.tsx
Normal file
172
src/features/auth/ui/login-section.tsx
Normal file
@@ -0,0 +1,172 @@
|
||||
"use client";
|
||||
|
||||
import { cn } from "@/shared/lib/utils";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/shared/ui/card";
|
||||
import { Label } from "@radix-ui/react-label";
|
||||
import { Input } from "@/shared/ui/input";
|
||||
import { Button } from "@/shared/ui/button";
|
||||
import Image from "next/image";
|
||||
import { FormEvent, useState } from "react";
|
||||
import { InputOTP, InputOTPGroup, InputOTPSlot } from "@/shared/ui/input-otp";
|
||||
import { REGEXP_ONLY_DIGITS_AND_CHARS } from "input-otp";
|
||||
import { Checkbox } from "@/shared/ui/checkbox";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/shared/ui/dialog";
|
||||
import { sendPhoneNumber, verifyCode } from "@/shared/api";
|
||||
import { useRouter } from "@/shared/config/i18n/navigation";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
const LoginSection = ({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentPropsWithoutRef<"div">) => {
|
||||
const [step, setStep] = useState(1);
|
||||
const [phone, setPhone] = useState("");
|
||||
const [code, setCode] = useState("");
|
||||
const router = useRouter();
|
||||
const t = useTranslations("");
|
||||
|
||||
const handlePhoneSubmit = async (e: FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
await sendPhoneNumber(phone);
|
||||
setStep(2);
|
||||
};
|
||||
|
||||
const handleVerifySubmit = async (e: FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
await verifyCode(phone, parseInt(code));
|
||||
router.push("/profile");
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={cn("flex flex-col gap-6", className)} {...props}>
|
||||
{step === 1 ? (
|
||||
<Card className="shadow-none border-none">
|
||||
<CardHeader>
|
||||
<Image
|
||||
className="mx-auto mb-10"
|
||||
src="/getgreen.png"
|
||||
alt=""
|
||||
width={300}
|
||||
height={300}
|
||||
/>
|
||||
<CardTitle className="text-2xl">{t("Login")}</CardTitle>
|
||||
<CardDescription>
|
||||
{t("Hisobingizga kirish uchun telefon raqamingizni kiriting")}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<form onSubmit={handlePhoneSubmit}>
|
||||
<div className="flex flex-col gap-6">
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="phone">{t("Telefon")}</Label>
|
||||
<Input
|
||||
id="phone"
|
||||
type="tel"
|
||||
name="phone"
|
||||
value={phone}
|
||||
onChange={(e) => setPhone(e.target.value)}
|
||||
placeholder="+998 94 456 78 90"
|
||||
required
|
||||
/>
|
||||
<div className="flex items-center space-x-2 mt-2">
|
||||
<Checkbox id="terms" required />
|
||||
{t.rich("terms_of_use", {
|
||||
tag: (chunks) => (
|
||||
<label
|
||||
htmlFor="terms"
|
||||
className="text-sm font-medium leading-none"
|
||||
>
|
||||
<TermsOfUse /> {chunks}
|
||||
</label>
|
||||
),
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
<Button type="submit" className="w-full">
|
||||
{t("login")}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</CardContent>
|
||||
</Card>
|
||||
) : (
|
||||
<Card className="shadow-none border-none">
|
||||
<CardHeader>
|
||||
<Image
|
||||
className="mx-auto mb-10"
|
||||
src="/getgreen.png"
|
||||
alt=""
|
||||
width={300}
|
||||
height={300}
|
||||
/>
|
||||
<CardTitle className="text-2xl">OTP</CardTitle>
|
||||
<CardDescription>{t("Tasdiqlash kodini kiriting")}</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<form onSubmit={handleVerifySubmit}>
|
||||
<div className="flex flex-col gap-6">
|
||||
<div className="grid gap-2">
|
||||
<InputOTP
|
||||
maxLength={5}
|
||||
pattern={REGEXP_ONLY_DIGITS_AND_CHARS}
|
||||
value={code}
|
||||
onChange={setCode}
|
||||
>
|
||||
<InputOTPGroup>
|
||||
{[0, 1, 2, 3, 4].map((i) => (
|
||||
<InputOTPSlot key={i} index={i} />
|
||||
))}
|
||||
</InputOTPGroup>
|
||||
</InputOTP>
|
||||
</div>
|
||||
<Button type="submit" className="w-full">
|
||||
{t("Tasdiqlash")}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default LoginSection;
|
||||
|
||||
const TermsOfUse = () => {
|
||||
const t = useTranslations("")
|
||||
return (
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>
|
||||
<span className="border-b border-primary cursor-pointer">
|
||||
{t("Offer va shartlar")}
|
||||
</span>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="min-w-6/12">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{t("Offerta shartlari")}</DialogTitle>
|
||||
<DialogDescription>
|
||||
{t("Bu yerda offerta shartlarini o'qib chiqishingiz mumkin")}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="overflow-y-scroll h-[70vh]">
|
||||
{/* Replace with actual offer content */}
|
||||
Lorem ipsum dolor sit amet, consectetur...
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
46
src/features/brand-products/ui/products-list.tsx
Normal file
46
src/features/brand-products/ui/products-list.tsx
Normal file
@@ -0,0 +1,46 @@
|
||||
import React from "react";
|
||||
import ProductCard from "@/shared/ui/product-card";
|
||||
import { Product } from "@/shared/types/product";
|
||||
import { BrandProductsResultType } from "@/shared/types/brands";
|
||||
|
||||
interface ProductSectionProps {
|
||||
products: BrandProductsResultType[];
|
||||
}
|
||||
|
||||
const ProductsList = ({ products }: ProductSectionProps) => {
|
||||
return (
|
||||
<div className={"my-container"}>
|
||||
<section id={"invertor-section"} className={"my-container"}>
|
||||
<div className={"flex flex-col justify-center"}>
|
||||
<div>
|
||||
<div
|
||||
className={
|
||||
"grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 "
|
||||
}
|
||||
>
|
||||
{products.map((i) => {
|
||||
const product: Product = {
|
||||
id: i.id,
|
||||
name: i.name,
|
||||
price: i.price,
|
||||
price_usd: i.price,
|
||||
price_discount: i.price_discount,
|
||||
discount_percent: i.discount_percent,
|
||||
is_leader_of_sales: i.is_leader_of_sales,
|
||||
poster: i.poster,
|
||||
poster_thumb: i.poster_thumb,
|
||||
is_favorite: i.is_favorite,
|
||||
is_cart: i.is_cart,
|
||||
count: i.count,
|
||||
power: i.power,
|
||||
};
|
||||
return <ProductCard key={product.id} product={product} />;
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
export default ProductsList;
|
||||
30
src/features/category-details/ui/products-section.tsx
Normal file
30
src/features/category-details/ui/products-section.tsx
Normal file
@@ -0,0 +1,30 @@
|
||||
import React from 'react'
|
||||
import {CategoryCard} from "@/shared/ui/category-card";
|
||||
import {Button} from "@/shared/ui/button";
|
||||
import ProductCard from "@/shared/ui/product-card";
|
||||
import {Product} from "@/shared/types/product";
|
||||
|
||||
interface ProductSectionProps{
|
||||
products: Product[]
|
||||
}
|
||||
|
||||
const ProductsSection = ({products}: ProductSectionProps) => {
|
||||
return (
|
||||
<div className={"my-container"}>
|
||||
<section id={"invertor-section"} className={"my-container"}>
|
||||
<div className={"flex flex-col justify-center"}>
|
||||
<div>
|
||||
<div className={"grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 "}>
|
||||
{
|
||||
products.map(product=>(
|
||||
<ProductCard key={product.id} product={product}/>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default ProductsSection
|
||||
29
src/features/home/lib/data.ts
Normal file
29
src/features/home/lib/data.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import { List, MailIcon, MapPin, PhoneCall } from "lucide-react";
|
||||
import { CardProps } from "../models/types";
|
||||
import formatPhone from "@/shared/lib/formatPhone";
|
||||
import { PRODUCT_INFO } from "@/shared/constants";
|
||||
|
||||
const contactData: CardProps[] = [
|
||||
{
|
||||
icon: PhoneCall,
|
||||
title: "Telefon raqam",
|
||||
value: formatPhone(PRODUCT_INFO.contact.phone),
|
||||
},
|
||||
{
|
||||
icon: MailIcon,
|
||||
title: "Email",
|
||||
value: PRODUCT_INFO.contact.email,
|
||||
},
|
||||
{
|
||||
icon: MapPin,
|
||||
title: "Adress",
|
||||
value: "office",
|
||||
},
|
||||
{
|
||||
icon: List,
|
||||
title: "Ish vaqtlari",
|
||||
value: "9:00 - 18:00",
|
||||
},
|
||||
];
|
||||
|
||||
export { contactData };
|
||||
7
src/features/home/models/types.ts
Normal file
7
src/features/home/models/types.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { LucideProps } from "lucide-react";
|
||||
|
||||
export interface CardProps {
|
||||
icon: React.ComponentType<LucideProps>,
|
||||
title: string;
|
||||
value: string;
|
||||
}
|
||||
34
src/features/home/ui/aboutus-section.tsx
Normal file
34
src/features/home/ui/aboutus-section.tsx
Normal file
@@ -0,0 +1,34 @@
|
||||
import React from "react";
|
||||
import Image from "next/image";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
const AboutusSection = () => {
|
||||
const t = useTranslations("");
|
||||
return (
|
||||
<section id={"about-section"} className={"bg-slate-900 py-12"}>
|
||||
<div
|
||||
className={
|
||||
"max-w-7xl mx-auto grid grid-cols-1 md:grid-cols-2 justify-between items-center py-10 gap-12"
|
||||
}
|
||||
>
|
||||
<div className={"relative w-full"}>
|
||||
<Image
|
||||
src={"/images/aboutus.png"}
|
||||
alt={"About us image"}
|
||||
layout="responsive"
|
||||
width={500}
|
||||
height={500}
|
||||
/>
|
||||
</div>
|
||||
<div className={"text-white px-4"}>
|
||||
<h1 className="section-title">{t("Biz haqimizda")}</h1>
|
||||
<p className="section-subtitle">{t("about_us_subtitle")}</p>
|
||||
<br aria-hidden />
|
||||
<p className="section-subtitle">{t("about_us_desc")}</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default AboutusSection;
|
||||
81
src/features/home/ui/category-section.tsx
Normal file
81
src/features/home/ui/category-section.tsx
Normal file
@@ -0,0 +1,81 @@
|
||||
"use client"
|
||||
import {Accordion, AccordionContent, AccordionItem, AccordionTrigger} from "@/shared/ui/accordion";
|
||||
import {Link} from "@/shared/config/i18n/navigation";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
|
||||
interface Category {
|
||||
id: number;
|
||||
name: string;
|
||||
image: string;
|
||||
parent_id: number | null;
|
||||
parents: Category[];
|
||||
}
|
||||
|
||||
interface CategoriesProps {
|
||||
categories: Category[];
|
||||
}
|
||||
|
||||
const CategoriesSection: React.FC<CategoriesProps> = ({categories}) => {
|
||||
const t = useTranslations("")
|
||||
const renderCategory = (category: Category, level: number = 0) => {
|
||||
const itemValue = `category-${category.id}`;
|
||||
const hasChildren = category.parents && category.parents.length > 0;
|
||||
const hasImage =
|
||||
level === 0 && category.image && !category.image.includes("no_brend.png")
|
||||
? category.image
|
||||
: null;
|
||||
|
||||
return (
|
||||
<AccordionItem
|
||||
key={category.id}
|
||||
value={itemValue}
|
||||
className={`border-b ${level === 0 ? "border-gray-200" : "border-gray-100"}`}
|
||||
>
|
||||
<AccordionTrigger
|
||||
showArrowIcon={hasChildren}
|
||||
className={`flex items-center gap-4 p-4 hover:bg-gray-50 transition-colors ${
|
||||
level === 0 ? "text-lg font-semibold" : "text-base font-medium"
|
||||
}`}
|
||||
>
|
||||
<div className={"flex items-center justify-center gap-10"}>
|
||||
{hasImage && (
|
||||
<img
|
||||
src={category.image}
|
||||
alt={category.name}
|
||||
className="w-12 h-12 object-cover rounded-md"
|
||||
/>
|
||||
)}
|
||||
{hasChildren ? (
|
||||
<span>{category.name}</span>
|
||||
) : (
|
||||
<Link href={`/category/${category.id}`}>
|
||||
{category.name}
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
</AccordionTrigger>
|
||||
{hasChildren && (
|
||||
<AccordionContent className="pl-6">
|
||||
<Accordion type="multiple" className="w-full">
|
||||
{category.parents.map((child) => renderCategory(child, level + 1))}
|
||||
</Accordion>
|
||||
</AccordionContent>
|
||||
)}
|
||||
</AccordionItem>
|
||||
);
|
||||
};
|
||||
return (
|
||||
<div className="my-container section-wrapper mx-auto p-4">
|
||||
<h1 className="section-title text-center">{t("Kategoriyalar")}</h1>
|
||||
<Accordion
|
||||
type="multiple"
|
||||
className="w-full rounded-lg grid space-y-10"
|
||||
>
|
||||
{categories.map((category) => renderCategory(category))}
|
||||
</Accordion>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default CategoriesSection;
|
||||
43
src/features/home/ui/contact-section.tsx
Normal file
43
src/features/home/ui/contact-section.tsx
Normal file
@@ -0,0 +1,43 @@
|
||||
import React from "react";
|
||||
import { CardProps } from "../models/types";
|
||||
import { contactData } from "../lib/data";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
function ContactCard({ icon: Icon, title, value }: CardProps) {
|
||||
const t = useTranslations("");
|
||||
return (
|
||||
<div className={"bg-white rounded-4xl p-10 flex items-center gap-10"}>
|
||||
<Icon size={70} />
|
||||
<div className={"flex flex-col"}>
|
||||
<span className={"text-2xl font-bold"}>{t(title)}</span>
|
||||
<span>{t(value)}</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const ContactSection = () => {
|
||||
const t = useTranslations("");
|
||||
return (
|
||||
<section id={"contact-section.tsx"} className={"section-wrapper"}>
|
||||
<div className="bg-primary py-24 px-4">
|
||||
<div className={"my-container"}>
|
||||
<h1 className={"section-title text-center pb-10 text-white"}>
|
||||
{t("Kontaktlar")}
|
||||
</h1>
|
||||
<div className={"grid grid-cols-2 max-sm:grid-cols-1 gap-8"}>
|
||||
{contactData.map((e, i) => (
|
||||
<ContactCard
|
||||
icon={e.icon}
|
||||
title={e.title}
|
||||
value={e.value}
|
||||
key={i}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
export default ContactSection;
|
||||
75
src/features/home/ui/download-app-section.tsx
Normal file
75
src/features/home/ui/download-app-section.tsx
Normal file
@@ -0,0 +1,75 @@
|
||||
import React from "react";
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import { PRODUCT_INFO } from "@/shared/constants";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
const DownloadAppSection = () => {
|
||||
const t = useTranslations("")
|
||||
return (
|
||||
<section
|
||||
id={"download-app-section"}
|
||||
className={
|
||||
"bg-slate-900 my-container rounded-4xl max-md:rounded-3xl mb-20 relative overflow-hidden max-md:pt-12"
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="bg-radial from-blue-950 via-transparent to-transparent w-[700px] h-[700px] rounded-full absolute -bottom-60 -left-60"
|
||||
aria-hidden
|
||||
/>
|
||||
|
||||
<div
|
||||
className="bg-radial from-indigo-900 via-transparent to-transparent w-[700px] h-[700px] rounded-full absolute -top-80 -right-80"
|
||||
aria-hidden
|
||||
/>
|
||||
<div
|
||||
className={
|
||||
"max-w-7xl mx-auto grid grid-cols-1 md:grid-cols-2 justify-between items-center gap-12 relative z-20"
|
||||
}
|
||||
>
|
||||
<div className={"text-white px-4"}>
|
||||
<h1 className="section-title">{t("Ilovamizni yuklab oling")}</h1>
|
||||
<p className="section-subtitle">
|
||||
{t("download_our_app_desc")}
|
||||
</p>
|
||||
<br aria-hidden />
|
||||
<div className={"flex gap-4 mt-5"}>
|
||||
<Link href={PRODUCT_INFO.app.ios}>
|
||||
<Image
|
||||
src={"/images/app-store-light.svg"}
|
||||
alt={""}
|
||||
width={200}
|
||||
height={200}
|
||||
className="max-md:w-40"
|
||||
/>
|
||||
</Link>
|
||||
<Link href={PRODUCT_INFO.app.android}>
|
||||
<Image
|
||||
src={"/images/google-play-light.svg"}
|
||||
alt={""}
|
||||
width={200}
|
||||
height={200}
|
||||
className="max-md:w-40"
|
||||
/>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className={
|
||||
"relative flex justify-end max-md:justify-center pt-24 max-md:pt-20"
|
||||
}
|
||||
>
|
||||
<Image
|
||||
className={"-mb-96"}
|
||||
src={"/images/screenshot-home.png"}
|
||||
alt={"About us image"}
|
||||
width={500}
|
||||
height={500}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default DownloadAppSection;
|
||||
76
src/features/home/ui/free-recommendation-section.tsx
Normal file
76
src/features/home/ui/free-recommendation-section.tsx
Normal file
@@ -0,0 +1,76 @@
|
||||
"use client";
|
||||
|
||||
import React, { FormEvent, useState } from "react";
|
||||
import { Input } from "@/shared/ui/input";
|
||||
import { Button } from "@/shared/ui/button";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { contactInfoSubmit } from "@/shared/api/contactSvs";
|
||||
import formatPhone from "@/shared/lib/formatPhone";
|
||||
import { toast } from "sonner";
|
||||
|
||||
const FreeRecommendationSection = () => {
|
||||
const t = useTranslations("");
|
||||
const [phone, setPhone] = useState("+998 ");
|
||||
const [name, setName] = useState("");
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [isError, setIsError] = useState("")
|
||||
|
||||
const onContactSubmit = async (e: FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
if (phone.length === 17 || name.length > 2) {
|
||||
setIsLoading(true);
|
||||
setIsError("")
|
||||
try {
|
||||
await contactInfoSubmit(phone, name);
|
||||
setName("");
|
||||
setPhone("+998 ");
|
||||
toast.success("Muvaffaqiyatli yuborildi");
|
||||
setIsLoading(false);
|
||||
} catch (error) {
|
||||
setIsLoading(false);
|
||||
}
|
||||
} else {
|
||||
setIsError("Ma'lumotlarni to'ldiring")
|
||||
}
|
||||
};
|
||||
return (
|
||||
<section
|
||||
id={"free-recommendation-section"}
|
||||
className={
|
||||
"relative overflow-hidden section-wrapper bg-[url('/images/recommendation-bg.jpg')] bg-cover bg-no-repeat bg-center"
|
||||
}
|
||||
>
|
||||
<div className={"flex justify-center items-center p-16 max-sm:p-10"}>
|
||||
<div
|
||||
className={"bg-slate-950/35 w-full h-screen absolute top-0 left-0"}
|
||||
/>
|
||||
<div className="bg-white rounded-xl p-10 relative z-20 w-full max-w-[600px]">
|
||||
<h1 className="text-center text-2xl mb-2 font-bold pb-5">
|
||||
{t("Bepul maslahat uchun ma'lumotlaringizni kiriting")}
|
||||
</h1>
|
||||
<form
|
||||
action="w-full"
|
||||
className={"space-y-5"}
|
||||
onSubmit={onContactSubmit}
|
||||
>
|
||||
<Input
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
placeholder={t("Ismingiz")}
|
||||
/>
|
||||
<Input
|
||||
value={formatPhone(phone)}
|
||||
onChange={(e) => setPhone(formatPhone(e.target.value))}
|
||||
placeholder={t("Telefon raqamingiz")}
|
||||
/>
|
||||
<Button className={"w-full"} type="submit" disabled={isLoading}>
|
||||
{!isLoading ? t("Yuborish") : t("Yuborilmoqda")}
|
||||
</Button>
|
||||
</form>
|
||||
{isError && <p className="text-center text-red-500 mt-4">{t(isError)}</p>}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
export default FreeRecommendationSection;
|
||||
45
src/features/home/ui/hero-section.tsx
Normal file
45
src/features/home/ui/hero-section.tsx
Normal file
@@ -0,0 +1,45 @@
|
||||
"use client"
|
||||
|
||||
import React from "react";
|
||||
import Image from "next/image";
|
||||
import { Button } from "@/shared/ui/button";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { useRouter } from "next/navigation";
|
||||
|
||||
const HeroSection = () => {
|
||||
const t = useTranslations("")
|
||||
const router = useRouter()
|
||||
return (
|
||||
<section className={"relative overflow-hidden"}>
|
||||
<div
|
||||
className={
|
||||
"bg-[url('/images/hero-bg.jpg')] flex justify-between items-center bg-cover bg-no-repeat bg-center h-[calc(100vh-4rem)]"
|
||||
}
|
||||
>
|
||||
<div
|
||||
className={"bg-slate-950/75 w-full h-screen absolute top-0 left-0"}
|
||||
/>
|
||||
<div className="my-container relative z-20 flex justify-between items-center max-lg:flex-col pt-24 px-4">
|
||||
<div className={"w-1/2 max-lg:w-full"}>
|
||||
<h1 className="section-title text-white">
|
||||
{t("Quyosh uskunalarini ulgurji narxlarda sotib oling!")}
|
||||
</h1>
|
||||
<p className="section-subtitle text-white">
|
||||
{t("Quyosh elektr stansiyasi uchun hamma narsani bir joyda va eng yaxshi narxda sotib oling")}
|
||||
</p>
|
||||
<Button onClick={() => router.push("/category")} className={"px-10 mt-5 z-30"}>
|
||||
{t("Batafsil")}
|
||||
</Button>
|
||||
</div>
|
||||
<Image
|
||||
src={"/hero-solar-panel.png"}
|
||||
alt={""}
|
||||
width={900}
|
||||
height={900}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
export default HeroSection;
|
||||
8
src/features/home/ui/index.ts
Normal file
8
src/features/home/ui/index.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
export { default as HeroSection } from "@/features/home/ui/hero-section";
|
||||
export { default as AboutusSection } from "@/features/home/ui/aboutus-section";
|
||||
export { default as CategorySection } from "@/features/home/ui/category-section";
|
||||
export { default as FreeRecommendationSection } from "@/features/home/ui/free-recommendation-section";
|
||||
export { default as ProfitSection } from "@/features/home/ui/profit-section";
|
||||
export { default as PartnersSection } from "@/features/home/ui/partners-section";
|
||||
export { default as ContactSection } from "@/features/home/ui/contact-section";
|
||||
export { default as DownloadAppSection } from "@/features/home/ui/download-app-section";
|
||||
32
src/features/home/ui/invertor-section.tsx
Normal file
32
src/features/home/ui/invertor-section.tsx
Normal file
@@ -0,0 +1,32 @@
|
||||
import React from 'react'
|
||||
import {Button} from "@/shared/ui/button";
|
||||
import ProductCard from "@/shared/ui/product-card";
|
||||
import { useTranslations } from 'next-intl';
|
||||
|
||||
|
||||
const InvertorSection = () => {
|
||||
const t = useTranslations("")
|
||||
return (
|
||||
<section id={"invertor-section"} className={"my-container section-wrapper"}>
|
||||
<div className={"flex flex-col justify-center"}>
|
||||
<h1 className="section-title uppercase text-center pb-5">{t("Quyosh panellari")}</h1>
|
||||
<div>
|
||||
<div className={"grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4"}>
|
||||
<ProductCard/>
|
||||
<ProductCard/>
|
||||
<ProductCard/>
|
||||
<ProductCard/>
|
||||
<ProductCard/>
|
||||
<ProductCard/>
|
||||
<ProductCard/>
|
||||
<ProductCard/>
|
||||
</div>
|
||||
</div>
|
||||
<Button className={"mx-auto mt-10 px-16"}>
|
||||
{t("Hammasini ko'rish")}
|
||||
</Button>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
export default InvertorSection
|
||||
42
src/features/home/ui/partners-section.tsx
Normal file
42
src/features/home/ui/partners-section.tsx
Normal file
@@ -0,0 +1,42 @@
|
||||
import React from "react";
|
||||
import Image from "next/image";
|
||||
import { BrandsResult } from "@/shared/types/brands";
|
||||
import { useTranslations } from "next-intl";
|
||||
import Link from "next/link";
|
||||
|
||||
interface Props {
|
||||
partners: BrandsResult[];
|
||||
}
|
||||
|
||||
const PartnersSection = ({ partners }: Props) => {
|
||||
const t = useTranslations("")
|
||||
return (
|
||||
<section
|
||||
id={"partners-section"}
|
||||
className={"my-container section-wrapper bg-white rounded-4xl"}
|
||||
>
|
||||
<div className={"flex flex-col justify-center"}>
|
||||
<h1 className="section-title uppercase text-center pb-5">
|
||||
{t("Hamkorlarimiz")}
|
||||
</h1>
|
||||
<div>
|
||||
<div
|
||||
className={"grid grid-cols-2 md:grid-cols-4 lg:grid-cols-5 gap-4"}
|
||||
>
|
||||
{partners.map((e, i) => (
|
||||
<Link href={`/brand/${e.id}`} key={i} className={"w-full h-full flex items-center justify-center"}>
|
||||
<Image
|
||||
src={e.image}
|
||||
alt={"category"}
|
||||
width={150}
|
||||
height={150}
|
||||
/>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
export default PartnersSection;
|
||||
31
src/features/home/ui/profit-section.tsx
Normal file
31
src/features/home/ui/profit-section.tsx
Normal file
@@ -0,0 +1,31 @@
|
||||
import React from 'react'
|
||||
import Image from "next/image";
|
||||
import { useTranslations } from 'next-intl';
|
||||
|
||||
const ProfitSection = () => {
|
||||
const t = useTranslations("")
|
||||
return (
|
||||
<section id={"profit-section"} className={"py-12"}>
|
||||
<div className={"max-w-7xl mx-auto grid grid-cols-1 md:grid-cols-2 justify-between items-center py-10 gap-12"}>
|
||||
<div className={"relative w-full"}>
|
||||
<Image src={"/images/profit.png"} alt={"About us image"} layout="responsive" width={500} height={500}/>
|
||||
</div>
|
||||
<div className={"px-4"}>
|
||||
<p className="section-subtitle">
|
||||
{t("profit_1_desc")}
|
||||
</p>
|
||||
<br aria-hidden/>
|
||||
<p className="section-subtitle">
|
||||
{t("profit_2_desc")}
|
||||
</p>
|
||||
<br aria-hidden/>
|
||||
<p className="section-subtitle">
|
||||
{t("profit_3_desc")}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
export default ProfitSection;
|
||||
67
src/features/home/ui/sections-section.tsx
Normal file
67
src/features/home/ui/sections-section.tsx
Normal file
@@ -0,0 +1,67 @@
|
||||
import React from "react";
|
||||
import { Link } from "@/shared/config/i18n/navigation";
|
||||
import {
|
||||
BlocksIcon,
|
||||
BookMarkedIcon,
|
||||
BoxesIcon,
|
||||
HandshakeIcon,
|
||||
} from "lucide-react";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
const SectionsSection = () => {
|
||||
const t = useTranslations("");
|
||||
return (
|
||||
<div className={"my-container section-wrapper"}>
|
||||
<div className={"grid grid-cols-4 max-md:grid-cols-2 gap-6 max-sm:gap-4"}>
|
||||
<Link href={"/category"} className={"block w-full"}>
|
||||
<div
|
||||
className={"bg-white p-12 w-full flex flex-col items-center gap-2"}
|
||||
>
|
||||
<BlocksIcon strokeWidth={1} size={64} className={"text-primary"} />
|
||||
<h1 className={"text-xl font-bold text-center"}>{t("Katalog")}</h1>
|
||||
</div>
|
||||
</Link>
|
||||
|
||||
<Link href={"/services"} className={"block w-full"}>
|
||||
<div
|
||||
className={"bg-white p-12 w-full flex flex-col items-center gap-2"}
|
||||
>
|
||||
<BoxesIcon strokeWidth={1} size={64} className={"text-primary"} />
|
||||
<h1 className={"text-xl font-bold text-center"}>
|
||||
{t("Xizmatlar")}
|
||||
</h1>
|
||||
</div>
|
||||
</Link>
|
||||
|
||||
<Link href={"/partners"} className={"block w-full"}>
|
||||
<div
|
||||
className={"bg-white p-12 w-full flex flex-col items-center gap-2"}
|
||||
>
|
||||
<HandshakeIcon
|
||||
strokeWidth={1}
|
||||
size={64}
|
||||
className={"text-primary"}
|
||||
/>
|
||||
<h1 className={"text-xl font-bold text-center"}>
|
||||
{t("Hamkorlik")}
|
||||
</h1>
|
||||
</div>
|
||||
</Link>
|
||||
|
||||
<Link href={"/useful"} className={"block w-full"}>
|
||||
<div
|
||||
className={"bg-white p-12 w-full flex flex-col items-center gap-2"}
|
||||
>
|
||||
<BookMarkedIcon
|
||||
strokeWidth={1}
|
||||
size={64}
|
||||
className={"text-primary"}
|
||||
/>
|
||||
<h1 className={"text-xl font-bold text-center"}>{t("Foydali")}</h1>
|
||||
</div>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
export default SectionsSection;
|
||||
74
src/features/partners/ui/partners-section/partner-modal.tsx
Normal file
74
src/features/partners/ui/partners-section/partner-modal.tsx
Normal file
@@ -0,0 +1,74 @@
|
||||
"use client"
|
||||
import {Partners} from "@/shared/types/partners";
|
||||
import {Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle} from "@/shared/ui/dialog";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectGroup,
|
||||
SelectItem,
|
||||
SelectLabel,
|
||||
SelectTrigger,
|
||||
SelectValue
|
||||
} from "@/shared/ui/select";
|
||||
import {Input} from "@/shared/ui/input";
|
||||
import {Textarea} from "@/shared/ui/textarea";
|
||||
import React from "react";
|
||||
import {Button} from "@/shared/ui/button";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
interface PartnerModalProps {
|
||||
selectedPartner: Partners | null;
|
||||
setSelectedPartner: (partner: Partners | null) => void;
|
||||
}
|
||||
|
||||
const PartnerModal = ({selectedPartner, setSelectedPartner}: PartnerModalProps) => {
|
||||
const t = useTranslations("")
|
||||
return (
|
||||
<Dialog open={!!selectedPartner} onOpenChange={
|
||||
(open) => {
|
||||
if (!open) {
|
||||
setSelectedPartner(null)
|
||||
}
|
||||
}
|
||||
}>
|
||||
<DialogContent className={"min-w-4/12"}>
|
||||
<DialogHeader>
|
||||
<DialogTitle>{selectedPartner?.name}</DialogTitle>
|
||||
<DialogDescription className={"space-y-5 mt-5"}>
|
||||
<Select>
|
||||
<SelectTrigger className={"w-full"}>
|
||||
<SelectValue placeholder={t("Viloyat")}/>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
<SelectLabel>{t("Viloyat")}</SelectLabel>
|
||||
<SelectItem value="pineapple">Pineapple</SelectItem>
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Select>
|
||||
<SelectTrigger className={"w-full"}>
|
||||
<SelectValue placeholder={t("Tuman")}/>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
<SelectLabel>{t("Tuman")}</SelectLabel>
|
||||
<SelectItem value="pineapple">Pineapple</SelectItem>
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
<Input placeholder={t("Telefon raqamingiz")}/>
|
||||
<Input placeholder={"full_name"}/>
|
||||
<Textarea placeholder="Type your message here."/>
|
||||
<div className={"text-end"}>
|
||||
<Button size={"lg"}>{t("Ariza yuborish")}</Button>
|
||||
</div>
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
|
||||
export default PartnerModal
|
||||
@@ -0,0 +1,52 @@
|
||||
"use client";
|
||||
import React, { useState } from "react";
|
||||
import { Partners } from "@/shared/types/partners";
|
||||
import Image from "next/image";
|
||||
import PartnerModal from "@/features/partners/ui/partners-section/partner-modal";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
interface PartnersSectionProps {
|
||||
partners: Partners[];
|
||||
}
|
||||
|
||||
const PartnersSection = ({ partners }: PartnersSectionProps) => {
|
||||
const t = useTranslations("")
|
||||
const [selectedPartner, setSelectedPartner] = useState<Partners | null>(null);
|
||||
return (
|
||||
<div className={"my-container section-wrapper min-h-[70vh]"}>
|
||||
<h1 className={"section-title text-center"}>{t("Hamkorlik")}</h1>
|
||||
<div
|
||||
className={
|
||||
"grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 mx-4 md:mx-0 gap-6 mt-12"
|
||||
}
|
||||
>
|
||||
{partners.map((partner) => (
|
||||
<div
|
||||
key={partner.id}
|
||||
onClick={() => {
|
||||
setSelectedPartner(partner);
|
||||
}}
|
||||
className={
|
||||
"cursor-pointer border hover:border-primary transition-all duration-400 bg-white p-12 w-full flex flex-col justify-center items-center gap-2 h-full"
|
||||
}
|
||||
>
|
||||
<Image
|
||||
src={partner.image}
|
||||
alt={partner.name}
|
||||
width={100}
|
||||
height={100}
|
||||
/>
|
||||
<h1 className={"text-xl font-bold text-center"}>{partner.name}</h1>
|
||||
</div>
|
||||
))}
|
||||
{selectedPartner && (
|
||||
<PartnerModal
|
||||
selectedPartner={selectedPartner!}
|
||||
setSelectedPartner={setSelectedPartner}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
export default PartnersSection;
|
||||
47
src/features/product-details/ui/buyForm.tsx
Normal file
47
src/features/product-details/ui/buyForm.tsx
Normal file
@@ -0,0 +1,47 @@
|
||||
import { Product } from "@/shared/types/product";
|
||||
import { Button } from "@/shared/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogTrigger,
|
||||
} from "@/shared/ui/dialog";
|
||||
import { Tabs, TabsList, TabsTrigger } from "@/shared/ui/tabs";
|
||||
import React, { useState } from "react";
|
||||
import PhysicalTab from "./physicalTab";
|
||||
import LegalTab from "./legalTab";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
interface Props {
|
||||
product: Product;
|
||||
}
|
||||
|
||||
const BuyForm = ({ product }: Props) => {
|
||||
const t = useTranslations("")
|
||||
const [isDialogOpen, setIsDialogOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button className="mt-8 px-16" onClick={() => setIsDialogOpen(true)}>
|
||||
{t("Sotib olish")}
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="sm:max-w-6/12 max-h-[90vh] overflow-y-auto">
|
||||
<Tabs defaultValue="physical" className="w-full mt-4">
|
||||
<TabsList className="grid w-full grid-cols-2">
|
||||
<TabsTrigger value="physical">{t("Jismoniy shaxs")}</TabsTrigger>
|
||||
<TabsTrigger value="legal">{t("Yuridik shaxs")}</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
{/* Physical */}
|
||||
<PhysicalTab product={product} setIsDialogOpen={setIsDialogOpen} />
|
||||
|
||||
{/* Legal */}
|
||||
<LegalTab product={product} setIsDialogOpen={setIsDialogOpen} />
|
||||
</Tabs>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default BuyForm;
|
||||
272
src/features/product-details/ui/legalTab.tsx
Normal file
272
src/features/product-details/ui/legalTab.tsx
Normal file
@@ -0,0 +1,272 @@
|
||||
import { getRegions } from "@/shared/api/regionSvc";
|
||||
import { createUserOrder } from "@/shared/api/userOrdersSvc";
|
||||
import formatPhone from "@/shared/lib/formatPhone";
|
||||
import { Product } from "@/shared/types/product";
|
||||
import { Button } from "@/shared/ui/button";
|
||||
import { DialogFooter } from "@/shared/ui/dialog";
|
||||
import { Input } from "@/shared/ui/input";
|
||||
import { Label } from "@/shared/ui/label";
|
||||
import { RadioGroup, RadioGroupItem } from "@/shared/ui/radio-group";
|
||||
import { TabsContent } from "@/shared/ui/tabs";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { Form, Formik } from "formik";
|
||||
import { Loader2 } from "lucide-react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import React, { useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
|
||||
interface Props {
|
||||
product: Product;
|
||||
setIsDialogOpen: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
}
|
||||
|
||||
const LegalTab = ({ product, setIsDialogOpen }: Props) => {
|
||||
const t = useTranslations("")
|
||||
const { data: regions } = useQuery({
|
||||
queryKey: ["getRegions"],
|
||||
queryFn: getRegions,
|
||||
});
|
||||
|
||||
const [isOrderCreating, setIsOrderCreating] = useState(false);
|
||||
const [corpDistricts, setCorpDistricts] = useState<
|
||||
{ id: number; name: string }[] | null
|
||||
>(null);
|
||||
|
||||
const handleCorpRegionChange = (regionId: number) => {
|
||||
const selectedRegion = regions?.data?.find(
|
||||
(region) => region.id === regionId
|
||||
);
|
||||
setCorpDistricts(selectedRegion?.cities || []);
|
||||
};
|
||||
|
||||
return (
|
||||
<Formik
|
||||
initialValues={{
|
||||
phone: "+998 ",
|
||||
director_full_name: "",
|
||||
company_name: "",
|
||||
inn: "",
|
||||
bank_name: "",
|
||||
mfo: "",
|
||||
oked: "",
|
||||
payment_account: "",
|
||||
address: "",
|
||||
home: "",
|
||||
landmark: "",
|
||||
city_id: "",
|
||||
branch_id: 1,
|
||||
with_installation: true,
|
||||
delivery_type: "delivery",
|
||||
payment_type: "bank",
|
||||
with_didox: true,
|
||||
products: [{ id: product.id, count: 1 }],
|
||||
}}
|
||||
onSubmit={async (values, helpers) => {
|
||||
setIsOrderCreating(true);
|
||||
let payload = {
|
||||
branch_id: 1,
|
||||
type: "ready_solutions",
|
||||
delivery_type: values.delivery_type,
|
||||
client_type: "legal",
|
||||
client_information: {
|
||||
director_full_name: values.director_full_name,
|
||||
company_name: values.company_name,
|
||||
inn: values.inn,
|
||||
bank_name: values.bank_name,
|
||||
mfo: values.mfo,
|
||||
oked: values.oked,
|
||||
payment_account: values.payment_account,
|
||||
address: values.address,
|
||||
email: "",
|
||||
phone: Number(values.phone.replace(/\D/g, "")),
|
||||
},
|
||||
address: {
|
||||
city_id: Number(values.city_id),
|
||||
address: values.address,
|
||||
home: values.home,
|
||||
landmark: values.landmark,
|
||||
},
|
||||
with_installation: values.with_installation,
|
||||
payment_type: values.payment_type,
|
||||
with_didox: values.with_didox,
|
||||
products: values.products,
|
||||
};
|
||||
|
||||
try {
|
||||
await createUserOrder(payload);
|
||||
toast.success(t("Buyurtma muvaffaqiyatli yaratildi!"), {
|
||||
description: t("Siz bilan tez orada bog'lanamiz"),
|
||||
});
|
||||
setIsDialogOpen(false);
|
||||
setIsOrderCreating(false);
|
||||
} catch (e: any) {
|
||||
toast.error(t("Buyurtma yaratishda xatolik!"), {
|
||||
description:
|
||||
t("Ma'lumotlar to'liq kiritilganligiga ishonch hosil qiling yoki qaytadan urinib ko'ring"),
|
||||
});
|
||||
setIsOrderCreating(false);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{(formikProps) => (
|
||||
<Form>
|
||||
{/* Legal */}
|
||||
<TabsContent value="legal" className="space-y-4 mt-4">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<Input
|
||||
placeholder={t("Kompaniya nomi")}
|
||||
onChange={formikProps.handleChange("company_name")}
|
||||
/>
|
||||
<Input
|
||||
placeholder={t("Direktor")}
|
||||
onChange={formikProps.handleChange("director_full_name")}
|
||||
/>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<Input
|
||||
placeholder={t("Yuridik manzil")}
|
||||
onChange={formikProps.handleChange("address")}
|
||||
/>
|
||||
<Input
|
||||
placeholder={t("Telefon raqam")}
|
||||
onChange={(e) =>
|
||||
formikProps.setFieldValue(
|
||||
"phone",
|
||||
formatPhone(e.target.value)
|
||||
)
|
||||
}
|
||||
value={formikProps.values.phone}
|
||||
/>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<Input
|
||||
placeholder="INN"
|
||||
onChange={(e) =>
|
||||
formikProps.setFieldValue(
|
||||
"inn",
|
||||
e.target.value.replace(/\D/g, "")
|
||||
)
|
||||
}
|
||||
value={formikProps.values.inn}
|
||||
/>
|
||||
<Input
|
||||
placeholder={t("Bank nomi")}
|
||||
onChange={formikProps.handleChange("bank_name")}
|
||||
/>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<Input
|
||||
placeholder="MFO"
|
||||
onChange={formikProps.handleChange("mfo")}
|
||||
/>
|
||||
<Input
|
||||
placeholder="OKED"
|
||||
onChange={formikProps.handleChange("oked")}
|
||||
/>
|
||||
</div>
|
||||
<Input placeholder={t("Hisob raqam")} onChange={formikProps.handleChange("payment_account")}/>
|
||||
<Label className={"mb-2"}>{t("Yetkazib berish")}</Label>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<select
|
||||
onChange={(e) => handleCorpRegionChange(Number(e.target.value))}
|
||||
className="w-full p-2 rounded-md border"
|
||||
>
|
||||
<option value="">{t("Viloyatni tanlang")}</option>
|
||||
{regions?.data.map((region) => (
|
||||
<option key={region.id} value={region.id}>
|
||||
{region.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
|
||||
<select
|
||||
onChange={formikProps.handleChange("city_id")}
|
||||
className="w-full p-2 rounded-md border"
|
||||
disabled={!corpDistricts}
|
||||
>
|
||||
<option value="">{t("Tuman/shahar")}</option>
|
||||
{corpDistricts?.map((district) => (
|
||||
<option key={district.id} value={district.id}>
|
||||
{district.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<Input
|
||||
placeholder={t("Manzil")}
|
||||
onChange={formikProps.handleChange("address")}
|
||||
/>
|
||||
<Input
|
||||
placeholder={t("Uy raqami")}
|
||||
onChange={formikProps.handleChange("home")}
|
||||
/>
|
||||
<Input
|
||||
placeholder={t("Mo'ljal")}
|
||||
onChange={formikProps.handleChange("landmark")}
|
||||
/>
|
||||
<div>
|
||||
<Label className="mb-2 block">{t("O‘rnatish xizmati kerakmi?")}</Label>
|
||||
<RadioGroup
|
||||
onValueChange={(value) =>
|
||||
formikProps.setFieldValue(
|
||||
"with_installation",
|
||||
value === "yes"
|
||||
)
|
||||
}
|
||||
defaultValue="yes"
|
||||
className="flex gap-4"
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<RadioGroupItem value="yes" id="c-install-yes" />
|
||||
<Label htmlFor="c-install-yes">{t("Ha")}</Label>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<RadioGroupItem value="no" id="c-install-no" />
|
||||
<Label htmlFor="c-install-no">{t("Yo‘q")}</Label>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label className="mb-2 block">{t("Yetkazib berish kerakmi")}</Label>
|
||||
<RadioGroup
|
||||
onValueChange={(value) =>
|
||||
formikProps.setFieldValue("delivery_type", value)
|
||||
}
|
||||
defaultValue="delivery"
|
||||
className="flex gap-4"
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<RadioGroupItem value="delivery" id="c-install-yes" />
|
||||
<Label htmlFor="c-install-yes">{t("Ha")}</Label>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<RadioGroupItem value="pickup" id="c-install-no" />
|
||||
<Label htmlFor="c-install-no">{t("Yo‘q o'zim olib ketaman")}</Label>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
</div>
|
||||
|
||||
<DialogFooter className="mt-4">
|
||||
<Button
|
||||
size="lg"
|
||||
type="submit"
|
||||
className="w-full"
|
||||
disabled={isOrderCreating}
|
||||
>
|
||||
{isOrderCreating ? (
|
||||
<Loader2 className="animate-spin" />
|
||||
) : (
|
||||
t("Yuborish")
|
||||
)}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
|
||||
</TabsContent>
|
||||
</Form>
|
||||
)}
|
||||
</Formik>
|
||||
);
|
||||
};
|
||||
|
||||
export default LegalTab;
|
||||
247
src/features/product-details/ui/physicalTab.tsx
Normal file
247
src/features/product-details/ui/physicalTab.tsx
Normal file
@@ -0,0 +1,247 @@
|
||||
import { getRegions } from "@/shared/api/regionSvc";
|
||||
import { createUserOrder } from "@/shared/api/userOrdersSvc";
|
||||
import formatPhone from "@/shared/lib/formatPhone";
|
||||
import { Product } from "@/shared/types/product";
|
||||
import { Button } from "@/shared/ui/button";
|
||||
import { DialogFooter } from "@/shared/ui/dialog";
|
||||
import { Input } from "@/shared/ui/input";
|
||||
import { Label } from "@/shared/ui/label";
|
||||
import { RadioGroup, RadioGroupItem } from "@/shared/ui/radio-group";
|
||||
import { TabsContent } from "@/shared/ui/tabs";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { Form, Formik } from "formik";
|
||||
import { Loader2 } from "lucide-react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import React, { useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
|
||||
interface Props {
|
||||
product: Product;
|
||||
setIsDialogOpen: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
}
|
||||
|
||||
const PhysicalTab = ({ product, setIsDialogOpen }: Props) => {
|
||||
const t = useTranslations("")
|
||||
const { data: regions } = useQuery({
|
||||
queryKey: ["getRegions"],
|
||||
queryFn: getRegions,
|
||||
});
|
||||
|
||||
const [isOrderCreating, setIsOrderCreating] = useState(false);
|
||||
const [districts, setDistricts] = useState<
|
||||
{ id: number; name: string }[] | null
|
||||
>(null);
|
||||
const handleRegionChange = (regionId: number) => {
|
||||
const selectedRegion = regions?.data?.find(
|
||||
(region) => region.id === regionId
|
||||
);
|
||||
setDistricts(selectedRegion?.cities || []);
|
||||
};
|
||||
|
||||
return (
|
||||
<Formik
|
||||
initialValues={{
|
||||
full_name: "",
|
||||
phone: "+998 ",
|
||||
jshir: "",
|
||||
series: "",
|
||||
address: "",
|
||||
home: "",
|
||||
landmark: "",
|
||||
city_id: "",
|
||||
branch_id: 1,
|
||||
with_installation: true,
|
||||
delivery_type: "delivery",
|
||||
payment_type: "bank",
|
||||
with_didox: true,
|
||||
products: [{ id: product.id, count: 1 }],
|
||||
}}
|
||||
onSubmit={async (values, helpers) => {
|
||||
setIsOrderCreating(true);
|
||||
let payload = {
|
||||
branch_id: 1,
|
||||
series: 1,
|
||||
type: "ready_solutions",
|
||||
delivery_type: values.delivery_type,
|
||||
client_type: "physical",
|
||||
client_information: {
|
||||
full_name: values.full_name,
|
||||
jshir: values.jshir,
|
||||
series: values.series,
|
||||
phone: Number(values.phone.replace(/\D/g, "")),
|
||||
},
|
||||
address: {
|
||||
city_id: Number(values.city_id),
|
||||
address: values.address,
|
||||
home: values.home,
|
||||
landmark: values.landmark,
|
||||
},
|
||||
with_installation: values.with_installation,
|
||||
payment_type: values.payment_type,
|
||||
with_didox: values.with_didox,
|
||||
products: values.products,
|
||||
};
|
||||
|
||||
try {
|
||||
await createUserOrder(payload);
|
||||
toast.success(t("Buyurtma muvaffaqiyatli yaratildi!"), {
|
||||
description: t("Siz bilan tez orada bog'lanamiz"),
|
||||
});
|
||||
setIsDialogOpen(false);
|
||||
setIsOrderCreating(false);
|
||||
} catch (e: any) {
|
||||
toast.error(t("Buyurtma yaratishda xatolik!"), {
|
||||
description:
|
||||
t("Ma'lumotlar to'liq kiritilganligiga ishonch hosil qiling yoki qaytadan urinib ko'ring"),
|
||||
});
|
||||
setIsOrderCreating(false);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{(formikProps) => (
|
||||
<Form>
|
||||
{/* Physical */}
|
||||
<TabsContent value="physical" className="space-y-4 mt-4">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<Input
|
||||
placeholder={t("full_name")}
|
||||
onChange={formikProps.handleChange("full_name")}
|
||||
/>
|
||||
{formikProps.errors.full_name && (
|
||||
<p className="text-red-500 text-sm">
|
||||
{formikProps.errors.full_name}
|
||||
</p>
|
||||
)}
|
||||
<Input
|
||||
placeholder={t("Telefon raqam")}
|
||||
onChange={(e) =>
|
||||
formikProps.setFieldValue(
|
||||
"phone",
|
||||
formatPhone(e.target.value)
|
||||
)
|
||||
}
|
||||
value={formikProps.values.phone}
|
||||
/>
|
||||
</div>
|
||||
<div className={"grid grid-cols-2 gap-4"}>
|
||||
<Input
|
||||
placeholder="JShShIR"
|
||||
value={formikProps.values.jshir}
|
||||
onChange={(e) =>
|
||||
formikProps.setFieldValue(
|
||||
"jshir",
|
||||
e.target.value.replace(/\D/g, "")
|
||||
)
|
||||
}
|
||||
/>
|
||||
<Input
|
||||
placeholder={t("Passport seriya va raqami")}
|
||||
onChange={formikProps.handleChange("series")}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Label className={"mb-2"}>{t("Yetkazib berish")}</Label>
|
||||
<div className={"grid grid-cols-2 gap-4"}>
|
||||
<select
|
||||
onChange={(e) => handleRegionChange(Number(e.target.value))}
|
||||
className="w-full p-2 rounded-md border"
|
||||
>
|
||||
<option value="">{t("Viloyatni tanlang")}</option>
|
||||
{regions?.data?.map((region) => (
|
||||
<option key={region.id} value={region.id}>
|
||||
{region.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
|
||||
<select
|
||||
onChange={formikProps.handleChange("city_id")}
|
||||
className="w-full p-2 rounded-md border"
|
||||
disabled={!districts}
|
||||
>
|
||||
<option value="">{t("Tuman/shahar")}</option>
|
||||
{districts?.map((district) => (
|
||||
<option key={district.id} value={district.id}>
|
||||
{district.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<Input
|
||||
placeholder={t("Manzil")}
|
||||
onChange={formikProps.handleChange("address")}
|
||||
/>
|
||||
<Input
|
||||
placeholder={t("Uy raqami")}
|
||||
onChange={formikProps.handleChange("home")}
|
||||
/>
|
||||
<Input
|
||||
placeholder={t("Mo'ljal")}
|
||||
onChange={formikProps.handleChange("landmark")}
|
||||
/>
|
||||
|
||||
<div>
|
||||
<Label className="mb-2 block">{t("O‘rnatish xizmati kerakmi?")}</Label>
|
||||
<RadioGroup
|
||||
onValueChange={(value) =>
|
||||
formikProps.setFieldValue(
|
||||
"with_installation",
|
||||
value === "yes"
|
||||
)
|
||||
}
|
||||
defaultValue="yes"
|
||||
className="flex gap-4"
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<RadioGroupItem value="yes" id="install-yes" />
|
||||
<Label htmlFor="install-yes">{t("Ha")}</Label>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<RadioGroupItem value="no" id="install-no" />
|
||||
<Label htmlFor="install-no">{t("Yo‘q")}</Label>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
</div>
|
||||
<div>
|
||||
<Label className="mb-2 block">{t("Yetkazib berish kerakmi?")}</Label>
|
||||
<RadioGroup
|
||||
onValueChange={(value) =>
|
||||
formikProps.setFieldValue("delivery_type", value)
|
||||
}
|
||||
defaultValue="yes"
|
||||
className="flex gap-4"
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<RadioGroupItem value="delivery" id="c-install-yes" />
|
||||
<Label htmlFor="c-install-yes">{t("Ha")}</Label>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<RadioGroupItem value="pickup" id="c-install-no" />
|
||||
<Label htmlFor="c-install-no">{t("Yo‘q o'zim olib ketaman")}</Label>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
</div>
|
||||
|
||||
<DialogFooter className="mt-4">
|
||||
<Button
|
||||
size="lg"
|
||||
type="submit"
|
||||
className="w-full"
|
||||
disabled={isOrderCreating}
|
||||
>
|
||||
{isOrderCreating ? (
|
||||
<Loader2 className="animate-spin" />
|
||||
) : (
|
||||
t("Yuborish")
|
||||
)}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
|
||||
</TabsContent>
|
||||
</Form>
|
||||
)}
|
||||
</Formik>
|
||||
);
|
||||
};
|
||||
|
||||
export default PhysicalTab;
|
||||
82
src/features/product-details/ui/product-details-section.tsx
Normal file
82
src/features/product-details/ui/product-details-section.tsx
Normal file
@@ -0,0 +1,82 @@
|
||||
"use client";
|
||||
|
||||
import React from "react";
|
||||
import Image from "next/image";
|
||||
import { Button } from "@/shared/ui/button";
|
||||
import { Product } from "@/shared/types/product";
|
||||
import formatNumberWithSpaces from "@/shared/lib/formatNumberWithSpace";
|
||||
import { useAuthStore } from "@/shared/store/authStore";
|
||||
import { Link } from "@/shared/config/i18n/navigation";
|
||||
import BuyForm from "./buyForm";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
interface ProductDetailsSectionProps {
|
||||
product: Product;
|
||||
}
|
||||
|
||||
const ProductDetailsSection = ({ product }: ProductDetailsSectionProps) => {
|
||||
const { isAuthenticated } = useAuthStore();
|
||||
const t = useTranslations("")
|
||||
|
||||
return (
|
||||
<div className="my-container px-4">
|
||||
<section className="my-container">
|
||||
<div className="flex flex-col justify-center">
|
||||
<div className="p-24 grid grid-cols-2 max-md:grid-cols-1 items-center max-md:p-0">
|
||||
<div className="flex items-center justify-center">
|
||||
<Image
|
||||
className="w-10/12 max-md:w-full rounded-4xl"
|
||||
src={product.poster}
|
||||
alt={product.name}
|
||||
width={200}
|
||||
height={200}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="font-bold text-4xl leading-relaxed">
|
||||
{product.name}
|
||||
</h2>
|
||||
<div
|
||||
className="my-5"
|
||||
dangerouslySetInnerHTML={{ __html: product.short_description! }}
|
||||
/>
|
||||
<div className="gap-5">
|
||||
<span className="text-3xl font-bold">
|
||||
{formatNumberWithSpaces(product.price)} {t("so'm")}
|
||||
<span className="text-base font-light"> {t("QQS bilan")}</span>
|
||||
</span>
|
||||
<span className="block">
|
||||
{formatNumberWithSpaces(product.price_usd)} y.e.
|
||||
</span>
|
||||
{product.discount_percent > 0 && (
|
||||
<>
|
||||
<span className="text-xl font-bold text-red-500 line-through">
|
||||
{formatNumberWithSpaces(product.price_discount)} {t("so'm")}
|
||||
</span>
|
||||
<span className="text-xl font-bold text-red-500">
|
||||
{product.discount_percent}% {t("chegirma")}
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{isAuthenticated ? (
|
||||
<BuyForm product={product} />
|
||||
) : (
|
||||
<Button className="mt-8 px-16" asChild>
|
||||
<Link href={"/auth/login"}>{t("Sotib olish")}</Link>
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="text-base flex flex-col justify-center items-center mb-24 mt-12"
|
||||
dangerouslySetInnerHTML={{ __html: product.description! }}
|
||||
/>
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
export default ProductDetailsSection;
|
||||
30
src/features/product-details/ui/related-products-section.tsx
Normal file
30
src/features/product-details/ui/related-products-section.tsx
Normal file
@@ -0,0 +1,30 @@
|
||||
import React from 'react'
|
||||
import ProductCard from "@/shared/ui/product-card";
|
||||
import {Product} from "@/shared/types/product";
|
||||
import { useTranslations } from 'next-intl';
|
||||
|
||||
interface RelatedProductsSectionProps {
|
||||
products: Product[]
|
||||
}
|
||||
|
||||
const RelatedProductsSection = ({products}: RelatedProductsSectionProps) => {
|
||||
const t = useTranslations("")
|
||||
return (
|
||||
<div className={"bg-background section-wrapper"}>
|
||||
<div className={"my-container"}>
|
||||
<h1 className="section-title uppercase text-center pb-5">{t("Boshqa mahsulotlar")}</h1>
|
||||
<div className={"mb-24 grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4"}>
|
||||
{
|
||||
products.map((product) => (
|
||||
<ProductCard
|
||||
key={product.id}
|
||||
product={product}
|
||||
/>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default RelatedProductsSection
|
||||
125
src/features/profile/ui/applications-section.tsx
Normal file
125
src/features/profile/ui/applications-section.tsx
Normal file
@@ -0,0 +1,125 @@
|
||||
"use client"
|
||||
import React, {useState} from 'react'
|
||||
import {Card, CardContent, CardDescription, CardTitle} from "@/shared/ui/card";
|
||||
import {Separator} from "@/shared/ui/separator";
|
||||
import {Badge} from "@/shared/ui/badge";
|
||||
import {Accordion, AccordionContent, AccordionItem, AccordionTrigger} from "@/shared/ui/accordion";
|
||||
import {useQuery} from "@tanstack/react-query";
|
||||
import {getUserRequests, getUserRequestsById} from "@/shared/api/userRequestsSvc";
|
||||
import Loader from "@/shared/ui/loader";
|
||||
import { useTranslations } from 'next-intl';
|
||||
|
||||
const ApplicationsSections = () => {
|
||||
const t = useTranslations("")
|
||||
const [selectedRequest, setSelectedRequest] = useState<number>(0)
|
||||
const {data: requests, isLoading: requestsIsLoading} = useQuery({
|
||||
queryKey: ["getUserRequests"],
|
||||
queryFn: getUserRequests,
|
||||
})
|
||||
|
||||
const {data: requestDetails} = useQuery({
|
||||
queryKey: ["getUserRequestsById", selectedRequest],
|
||||
queryFn: () => getUserRequestsById(selectedRequest),
|
||||
enabled: !!selectedRequest,
|
||||
})
|
||||
return (
|
||||
<div className={"profile-section-wrapper"}>
|
||||
<h1 className={"profile-section-title"}>{t("Mening arizalarim")}</h1>
|
||||
<span className={"profile-section-subtitle"}>
|
||||
{t("Sizning arizalaringiz va ularning holati haqida ma'lumotlar")}
|
||||
</span>
|
||||
<div className={"mt-4 space-y-4"}>
|
||||
{
|
||||
requestsIsLoading ? <Loader height={"h-[30vh]"}/> :requests?.data?.map(request => (
|
||||
<Accordion onClick={
|
||||
() => setSelectedRequest(request.id)
|
||||
} type="single" className={"border px-5 rounded-xl"} collapsible>
|
||||
<AccordionItem value="item-1">
|
||||
<AccordionTrigger className={" cursor-pointer"}>
|
||||
<div className={"w-full"}>
|
||||
<div className={"flex justify-between items-center"}>
|
||||
<div>
|
||||
<CardTitle className={"text-lg"}>{t("Ariza")} #{request.id}</CardTitle>
|
||||
<CardDescription>{t("Yaratilish vaqti")}: {request.created_at}</CardDescription>
|
||||
</div>
|
||||
<Badge style={{ backgroundColor: request.status.bg_color, color: request.status.font_color }}>
|
||||
{request.status.translation}
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
</AccordionTrigger>
|
||||
<AccordionContent>
|
||||
<Card key={request.id} className={"shadow-none border-none p-0 mt-5 rounded-none"}>
|
||||
<CardContent className={"p-0"}>
|
||||
<CardTitle>{t("Ariza tafsilotlari")}</CardTitle>
|
||||
<div className={"mt-5 space-y-3"}>
|
||||
<div className={"flex justify-between items-center"}>
|
||||
<CardDescription>{t("Ariza turi")}</CardDescription>
|
||||
<CardDescription>{requestDetails?.data.service.name}</CardDescription>
|
||||
</div>
|
||||
<div className={"flex justify-between items-center"}>
|
||||
<CardDescription>{t("Ariza raqami")}</CardDescription>
|
||||
<CardDescription>#{requestDetails?.data.id}</CardDescription>
|
||||
</div>
|
||||
<div className={"flex justify-between items-center"}>
|
||||
<CardDescription>{t("Ariza holati")}</CardDescription>
|
||||
<Badge style={{ backgroundColor: request.status.bg_color, color: request.status.font_color }}>
|
||||
{request.status.translation}
|
||||
</Badge>
|
||||
</div>
|
||||
<div className={"flex justify-between items-center"}>
|
||||
<CardDescription>{t("Yaratilish vaqti")}</CardDescription>
|
||||
<CardDescription>{
|
||||
request.created_at
|
||||
}</CardDescription>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
<Separator/>
|
||||
<CardContent className={"p-0"}>
|
||||
<CardTitle>{t("Xizmat tafsilotlari")}</CardTitle>
|
||||
<div className={"mt-5 space-y-3"}>
|
||||
<div className={"flex justify-between items-center"}>
|
||||
<CardDescription>{t("Quvvat")}</CardDescription>
|
||||
<CardDescription>
|
||||
{requestDetails?.data.power.name}
|
||||
</CardDescription>
|
||||
</div>
|
||||
<div className={"flex justify-between items-center"}>
|
||||
<CardDescription>{t("Manzil")}</CardDescription>
|
||||
<CardDescription>
|
||||
{requestDetails?.data.city.name}, {" "}
|
||||
{requestDetails?.data.city.region.name}
|
||||
</CardDescription>
|
||||
</div>
|
||||
<div className={"flex justify-between items-center"}>
|
||||
<CardDescription>{t("full_name")}</CardDescription>
|
||||
<CardDescription>{
|
||||
requestDetails?.data.full_name
|
||||
}</CardDescription>
|
||||
</div>
|
||||
<div className={"flex justify-between items-center"}>
|
||||
<CardDescription>{t("Telefon raqami")}</CardDescription>
|
||||
<CardDescription>{
|
||||
requestDetails?.data.phone
|
||||
}</CardDescription>
|
||||
</div>
|
||||
<div className={"flex justify-between items-center"}>
|
||||
<CardDescription>{t("Izoh")}</CardDescription>
|
||||
<CardDescription>
|
||||
{requestDetails?.data.comment}
|
||||
</CardDescription>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default ApplicationsSections
|
||||
58
src/features/profile/ui/contact-section.tsx
Normal file
58
src/features/profile/ui/contact-section.tsx
Normal file
@@ -0,0 +1,58 @@
|
||||
"use client";
|
||||
import React from "react";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { getFeedback } from "@/shared/api/feedbackSvc";
|
||||
import Link from "next/link";
|
||||
import Loader from "@/shared/ui/loader";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
const ContactSection = () => {
|
||||
const t = useTranslations("");
|
||||
const { data: feedback, isLoading: feedbackIsLoading } = useQuery({
|
||||
queryKey: ["getFeedback"],
|
||||
queryFn: getFeedback,
|
||||
});
|
||||
return (
|
||||
<div className={"profile-section-wrapper"}>
|
||||
<h1 className={"profile-section-title"}>{t("Bog'lanish")}</h1>
|
||||
<span className={"profile-section-subtitle"}>
|
||||
{t(
|
||||
"Biz bilan bog'lanish uchun quyidagi usullardan foydalanishingiz mumkin"
|
||||
)}
|
||||
</span>
|
||||
<div>
|
||||
{feedbackIsLoading ? (
|
||||
<Loader height={"h-[30vh]"} />
|
||||
) : (
|
||||
<div className={"grid grid-cols-2 items-center justify-between my-4"}>
|
||||
<div className={"flex flex-col"}>
|
||||
<span className={"text-lg font-bold"}>{t("Call Center")}</span>
|
||||
<Link
|
||||
className={"w-fit text-primary"}
|
||||
href={`tel:${feedback?.data.phone}`}
|
||||
>
|
||||
{feedback?.data.phone}
|
||||
</Link>
|
||||
</div>
|
||||
<div className={"flex flex-col"}>
|
||||
<span className={"text-lg font-bold"}>Telegram</span>
|
||||
<Link
|
||||
className={"w-fit text-primary"}
|
||||
href={feedback?.data.telegram_support || ""}
|
||||
>
|
||||
{feedback?.data.telegram_support}
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{/*<div className={"space-x-4"}>*/}
|
||||
{/* <Button className={""} size={"lg"}><InstagramIcon/> @Instagram</Button>*/}
|
||||
{/* <Button className={""} size={"lg"}><YoutubeIcon/> @Instagram</Button>*/}
|
||||
{/* <Button className={""} size={"lg"}><InstagramIcon/> @Instagram</Button>*/}
|
||||
{/* <Button className={""} size={"lg"}><YoutubeIcon/> @Instagram</Button>*/}
|
||||
{/*</div>*/}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
export default ContactSection;
|
||||
98
src/features/profile/ui/information-section.tsx
Normal file
98
src/features/profile/ui/information-section.tsx
Normal file
@@ -0,0 +1,98 @@
|
||||
"use client";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { Input } from "@/shared/ui/input";
|
||||
import { Button } from "@/shared/ui/button";
|
||||
import { useQuery, useMutation } from "@tanstack/react-query";
|
||||
import { getUserMe, updateUserMe } from "@/shared/api/userMeSvc";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
const InformationSection = () => {
|
||||
const t = useTranslations("");
|
||||
const { data: user } = useQuery({
|
||||
queryKey: ["getUserMe"],
|
||||
queryFn: getUserMe,
|
||||
});
|
||||
|
||||
const [formData, setFormData] = useState({
|
||||
first_name: "",
|
||||
last_name: "",
|
||||
middle_name: "",
|
||||
phone: "",
|
||||
gender: true,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (user?.data) {
|
||||
setFormData({
|
||||
first_name: user.data.first_name || "",
|
||||
last_name: user.data.last_name || "",
|
||||
middle_name: user.data.middle_name || "",
|
||||
phone: user.data.phone || "",
|
||||
gender: true,
|
||||
});
|
||||
}
|
||||
}, [user]);
|
||||
|
||||
const mutation = useMutation({
|
||||
mutationFn: updateUserMe,
|
||||
onError: () => {
|
||||
alert(t("Xatolik yuz berdi"));
|
||||
},
|
||||
});
|
||||
|
||||
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const { name, value } = e.target;
|
||||
setFormData((prev) => ({ ...prev, [name]: value }));
|
||||
};
|
||||
|
||||
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
mutation.mutate(formData);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={"profile-section-wrapper"}>
|
||||
<h1 className={"profile-section-title"}>{t("Profil ma'lumotlari")}</h1>
|
||||
<span className={"profile-section-subtitle"}>
|
||||
{t("Sizning profil ma'lumotlaringiz va ularni o'zgartirish")}
|
||||
</span>
|
||||
<form onSubmit={handleSubmit} className={"space-y-5 mt-5 text-end"}>
|
||||
<div className={"grid grid-cols-2 gap-5"}>
|
||||
<Input
|
||||
name="first_name"
|
||||
placeholder={t("Ismingiz")}
|
||||
value={formData.first_name}
|
||||
onChange={handleChange}
|
||||
required
|
||||
/>
|
||||
<Input
|
||||
name="last_name"
|
||||
placeholder={t("Familiyangiz")}
|
||||
value={formData.last_name}
|
||||
onChange={handleChange}
|
||||
required
|
||||
/>
|
||||
<Input
|
||||
name="middle_name"
|
||||
placeholder={t("Sharif")}
|
||||
value={formData.middle_name}
|
||||
onChange={handleChange}
|
||||
required
|
||||
/>
|
||||
<Input
|
||||
name="phone"
|
||||
placeholder={t("Telefon raqam")}
|
||||
value={formData.phone}
|
||||
onChange={handleChange}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<Button size={"lg"} type="submit" disabled={mutation.isPending}>
|
||||
{mutation.isPending ? t("Saqlanmoqda") : t("Saqlash")}
|
||||
</Button>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default InformationSection;
|
||||
154
src/features/profile/ui/orders-section.tsx
Normal file
154
src/features/profile/ui/orders-section.tsx
Normal file
@@ -0,0 +1,154 @@
|
||||
import React, {useState} from 'react'
|
||||
import {Card, CardContent, CardDescription, CardFooter, CardTitle} from "@/shared/ui/card";
|
||||
import {Separator} from "@/shared/ui/separator";
|
||||
import {Badge} from "@/shared/ui/badge";
|
||||
import {Button} from "@/shared/ui/button";
|
||||
import {Accordion, AccordionContent, AccordionItem, AccordionTrigger} from "@/shared/ui/accordion";
|
||||
import {useQuery} from "@tanstack/react-query";
|
||||
import {getUserOrders, getUserOrdersById} from "@/shared/api/userOrdersSvc";
|
||||
import formatNumberWithSpaces from "@/shared/lib/formatNumberWithSpace";
|
||||
import Link from "next/link";
|
||||
import Loader from "@/shared/ui/loader";
|
||||
import { useTranslations } from 'next-intl';
|
||||
|
||||
const OrdersSection = () => {
|
||||
const t = useTranslations("")
|
||||
const [selectedOrder, setSelectedOrder] = useState<number>(0)
|
||||
const {data: orders, isLoading: ordersIsLoading} = useQuery({
|
||||
queryKey: ["getUserOrders"],
|
||||
queryFn: getUserOrders,
|
||||
})
|
||||
|
||||
const {data: orderDetails} = useQuery({
|
||||
queryKey: ["getUserOrdersById", selectedOrder],
|
||||
queryFn: () => getUserOrdersById(selectedOrder),
|
||||
enabled: !!selectedOrder,
|
||||
})
|
||||
|
||||
return (
|
||||
<div className={"profile-section-wrapper"}>
|
||||
<h1 className={"profile-section-title"}>{t("Mening buyurtmalarim")}</h1>
|
||||
<span className={"profile-section-subtitle"}>
|
||||
{t("Sizning buyurtmalaringiz va ularning holati haqida ma'lumotlar")}
|
||||
</span>
|
||||
<div className={"mt-4 space-y-4"}>
|
||||
{ordersIsLoading ? <Loader height={"h-[30vh]"}/>:
|
||||
orders?.data?.map(order => (
|
||||
<Accordion key={order.id} onClick={()=>setSelectedOrder(order.id)} type="single" className={"border px-5 rounded-xl"} collapsible>
|
||||
<AccordionItem value="item-1">
|
||||
<AccordionTrigger className={"cursor-pointer"}>
|
||||
<div className={"w-full"}>
|
||||
<div className={"flex justify-between items-center mb-5"}>
|
||||
<div>
|
||||
<CardTitle className={"text-lg"}>{t("Buyurtma")} #{order.id}</CardTitle>
|
||||
<CardDescription>{t("Yaratilish vaqti")}: {order.created_at}</CardDescription>
|
||||
</div>
|
||||
<div className={"text-end"}>
|
||||
<CardDescription className={"text-lg"}>{formatNumberWithSpaces(order.total_amount!)} so'm</CardDescription>
|
||||
<Badge style={{ backgroundColor: order.status.bg_color, color: order.status.font_color }}>
|
||||
{order.status.translation}
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</AccordionTrigger>
|
||||
<AccordionContent>
|
||||
<Card key={order.id} className={"shadow-none border-none p-0 mt-5 rounded-none"}>
|
||||
<CardContent className={"p-0"}>
|
||||
<CardTitle>{t("Buyurtma tafsilotlari")}</CardTitle>
|
||||
<div className={"mt-5 space-y-3"}>
|
||||
<div className={"flex justify-between items-center"}>
|
||||
<CardDescription>{t("Buyurtma raqami")}</CardDescription>
|
||||
<CardDescription>#{orderDetails?.data.id}</CardDescription>
|
||||
</div>
|
||||
<div className={"flex justify-between items-center"}>
|
||||
<CardDescription>{t("Buyurtma holati")}</CardDescription>
|
||||
<Badge
|
||||
style={{ backgroundColor: orderDetails?.data.status.bg_color, color: orderDetails?.data.status.font_color }}
|
||||
>{orderDetails?.data.status.translation}</Badge>
|
||||
</div>
|
||||
<div className={"flex justify-between items-center"}>
|
||||
<CardDescription>{t("To'lov holati")}</CardDescription>
|
||||
<Badge
|
||||
style={{ backgroundColor: orderDetails?.data.payment_status.bg_color, color: orderDetails?.data.payment_status.font_color }}
|
||||
>{orderDetails?.data.payment_status.translation}</Badge>
|
||||
</div>
|
||||
<div className={"flex justify-between items-center"}>
|
||||
<CardDescription>{t("Yaratilish vaqti")}</CardDescription>
|
||||
<CardDescription>
|
||||
{orderDetails?.data.created_at}
|
||||
</CardDescription>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
<Separator/>
|
||||
<CardContent className={"p-0"}>
|
||||
<CardTitle>{t("Xaridlar ro'yxati")}</CardTitle>
|
||||
<div className={"mt-5 space-y-3"}>
|
||||
{
|
||||
orderDetails?.data.products.map(product => (
|
||||
<div key={product.id} className={"flex justify-between w-full items-center"}>
|
||||
<CardDescription>{product.name}</CardDescription>
|
||||
<CardDescription>{product.count} x {formatNumberWithSpaces(product.price || product.total_price)} {t("so'm")}</CardDescription>
|
||||
</div>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
</CardContent>
|
||||
<Separator/>
|
||||
<CardContent className={"p-0"}>
|
||||
<CardTitle>{t("Mijoz ma'lumotlari")}</CardTitle>
|
||||
<div className={"mt-5 space-y-3"}>
|
||||
<div className={"flex justify-between items-center"}>
|
||||
<CardDescription>{t("Mijoz turi")}</CardDescription>
|
||||
<CardDescription>{orderDetails?.data.client_type}</CardDescription>
|
||||
</div>
|
||||
<div className={"flex justify-between items-center"}>
|
||||
<CardDescription>{t("Mijoz turi")}</CardDescription>
|
||||
<CardDescription>{orderDetails?.data.client_information.full_name}</CardDescription>
|
||||
</div>
|
||||
<div className={"flex justify-between items-center"}>
|
||||
<CardDescription>{t("Mijoz telefon")}</CardDescription>
|
||||
<CardDescription>{orderDetails?.data.client_information.phone}</CardDescription>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
<Separator/>
|
||||
<CardContent className={"p-0"}>
|
||||
<CardTitle>{t("Yetkazib berish")}</CardTitle>
|
||||
<div className={"mt-5 space-y-3"}>
|
||||
<div className={"flex justify-between items-center"}>
|
||||
<CardDescription>{t("Yetkazib berish turi")}</CardDescription>
|
||||
<CardDescription>{orderDetails?.data.delivery_type}</CardDescription>
|
||||
</div>
|
||||
<div className={"flex justify-between items-center"}>
|
||||
<CardDescription>{t("Yetkazib berish manzili")}</CardDescription>
|
||||
<CardDescription>{orderDetails?.data.address.city.region.name}, {orderDetails?.data.address.city.name}</CardDescription>
|
||||
</div>
|
||||
<div className={"flex justify-between items-center"}>
|
||||
<CardDescription>{t("Yetkazib berish narxi")}</CardDescription>
|
||||
<CardDescription>{formatNumberWithSpaces(orderDetails?.data.price_delivery!)} so'm</CardDescription>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
<Separator/>
|
||||
<CardFooter className={"flex justify-between p-0 pb-2"}>
|
||||
<span/>
|
||||
<Button size={"lg"} asChild>
|
||||
<Link href={orderDetails?.data.pay_url! || ""} target={"_blank"}>
|
||||
{t("To'lash")}
|
||||
</Link>
|
||||
</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
|
||||
))
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default OrdersSection
|
||||
26
src/features/profile/ui/terms-section.tsx
Normal file
26
src/features/profile/ui/terms-section.tsx
Normal file
@@ -0,0 +1,26 @@
|
||||
"use client"
|
||||
import React from 'react'
|
||||
import {useQuery} from "@tanstack/react-query";
|
||||
import {getPolicy} from "@/shared/api/policySvc";
|
||||
import Loader from "@/shared/ui/loader";
|
||||
import { useTranslations } from 'next-intl';
|
||||
|
||||
const TermsSection = () => {
|
||||
const t = useTranslations("")
|
||||
const {data: policy, isLoading: policyIsLoading} = useQuery({
|
||||
queryKey: ["getPolicy"],
|
||||
queryFn: getPolicy
|
||||
})
|
||||
return (
|
||||
<div className={"profile-section-wrapper"}>
|
||||
<div className={"flex justify-between items-center mb-4"}>
|
||||
<div>
|
||||
<h1 className={"profile-section-title"}>{policy?.data.name}</h1>
|
||||
<span className={"profile-section-subtitle"}>{t("Offerta shartlari")}</span>
|
||||
</div>
|
||||
</div>
|
||||
{policyIsLoading ? <Loader height={"h-[30vh]"}/> : <div dangerouslySetInnerHTML={{__html: policy?.data?.body!}}/>}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default TermsSection
|
||||
74
src/features/services/ui/services-section/service-modal.tsx
Normal file
74
src/features/services/ui/services-section/service-modal.tsx
Normal file
@@ -0,0 +1,74 @@
|
||||
"use client"
|
||||
import {Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle} from "@/shared/ui/dialog";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectGroup,
|
||||
SelectItem,
|
||||
SelectLabel,
|
||||
SelectTrigger,
|
||||
SelectValue
|
||||
} from "@/shared/ui/select";
|
||||
import {Input} from "@/shared/ui/input";
|
||||
import {Textarea} from "@/shared/ui/textarea";
|
||||
import React from "react";
|
||||
import {Button} from "@/shared/ui/button";
|
||||
import {Service} from "@/shared/types/services";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
interface ServiceModalProps {
|
||||
selectedService: Service | null;
|
||||
setSelectedService: (service: Service | null) => void;
|
||||
}
|
||||
|
||||
const ServiceModal = ({selectedService, setSelectedService}: ServiceModalProps) => {
|
||||
const t = useTranslations("")
|
||||
return (
|
||||
<Dialog open={!!selectedService} onOpenChange={
|
||||
(open) => {
|
||||
if (!open) {
|
||||
setSelectedService(null)
|
||||
}
|
||||
}
|
||||
}>
|
||||
<DialogContent className={"min-w-4/12"}>
|
||||
<DialogHeader>
|
||||
<DialogTitle>{selectedService?.name}</DialogTitle>
|
||||
<DialogDescription className={"space-y-5 mt-5"}>
|
||||
<Select>
|
||||
<SelectTrigger className={"w-full"}>
|
||||
<SelectValue placeholder={t("Viloyat")}/>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
<SelectLabel>{t("Viloyat")}</SelectLabel>
|
||||
<SelectItem value="pineapple">Pineapple</SelectItem>
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Select>
|
||||
<SelectTrigger className={"w-full"}>
|
||||
<SelectValue placeholder={t("Tuman")}/>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
<SelectLabel>{t("Tuman")}</SelectLabel>
|
||||
<SelectItem value="pineapple">Pineapple</SelectItem>
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
<Input placeholder={t("Telefon raqamingiz")}/>
|
||||
<Input placeholder={t("full_name")}/>
|
||||
<Textarea placeholder="Type your message here."/>
|
||||
<div className={"text-end"}>
|
||||
<Button size={"lg"}>{t("Ariza yuborish")}</Button>
|
||||
</div>
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
|
||||
export default ServiceModal
|
||||
@@ -0,0 +1,47 @@
|
||||
"use client";
|
||||
import React, { useState } from "react";
|
||||
import { Service } from "@/shared/types/services";
|
||||
import Image from "next/image";
|
||||
import ServiceModal from "@/features/services/ui/services-section/service-modal";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
interface ServiceSectionProps {
|
||||
services: Service[];
|
||||
}
|
||||
|
||||
const ServicesSection = ({ services }: Readonly<ServiceSectionProps>) => {
|
||||
const t = useTranslations("");
|
||||
const [selectedService, setSelectedService] = useState<Service | null>(null);
|
||||
return (
|
||||
<div className={"my-container section-wrapper"}>
|
||||
<h1 className={"section-title text-center"}>{t("Xizmatlar")}</h1>
|
||||
<div className={"grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 mx-4 md:mx-0 gap-6 mt-12"}>
|
||||
{services.map((service) => (
|
||||
<div
|
||||
className={
|
||||
"cursor-pointer border hover:border-primary transition-all duration-400 bg-white p-12 w-full flex flex-col justify-center items-center gap-2 h-full"
|
||||
}
|
||||
key={service.id}
|
||||
onClick={() => {
|
||||
setSelectedService(service);
|
||||
}}
|
||||
>
|
||||
<div className={"w-full flex flex-col items-center gap-2"}>
|
||||
<Image src={service.image} alt={""} width={100} height={100} />
|
||||
<h1 className={"text-xl font-bold text-center"}>
|
||||
{service.name}
|
||||
</h1>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
{selectedService && (
|
||||
<ServiceModal
|
||||
selectedService={selectedService}
|
||||
setSelectedService={setSelectedService}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
export default ServicesSection;
|
||||
124
src/features/useful/ui/useful-section.tsx
Normal file
124
src/features/useful/ui/useful-section.tsx
Normal file
@@ -0,0 +1,124 @@
|
||||
"use client"
|
||||
|
||||
import {getUsefulById} from "@/shared/api/usefulSvc";
|
||||
import {UsefulItem} from "@/shared/types/useful";
|
||||
import {useState} from "react";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger
|
||||
} from "@/shared/ui/dialog";
|
||||
import {Accordion, AccordionContent, AccordionItem, AccordionTrigger} from "@/shared/ui/accordion";
|
||||
import {Button} from "@/shared/ui/button";
|
||||
import Image from "next/image";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
interface UsefulItemData {
|
||||
id: number
|
||||
name: string
|
||||
description: string
|
||||
video_url?: string
|
||||
link_url?: string
|
||||
file_url?: string
|
||||
}
|
||||
|
||||
interface UsefulSectionProps {
|
||||
usefuls: UsefulItem[]
|
||||
}
|
||||
|
||||
const UsefulSection = ({ usefuls }: UsefulSectionProps) => {
|
||||
const [items, setItems] = useState<UsefulItemData[]>([])
|
||||
const [openId, setOpenId] = useState<number | null>(null)
|
||||
const t = useTranslations("")
|
||||
|
||||
const fetchItems = async (id: number) => {
|
||||
try {
|
||||
const { data } = await getUsefulById(id);
|
||||
setItems(data);
|
||||
} catch (error) {
|
||||
console.error("Error fetching items", error);
|
||||
}
|
||||
}
|
||||
|
||||
const handleDialogOpen = (isOpen: boolean, id: number) => {
|
||||
if (isOpen && id !== openId) {
|
||||
setOpenId(id);
|
||||
fetchItems(id);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={"my-container section-wrapper"}>
|
||||
<h1 className={"section-title text-center"}>{t("Foydali")}</h1>
|
||||
<div className={"grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mt-12 px-4 md:px-0"}>
|
||||
{usefuls.map((useful) => (
|
||||
<div key={useful.id} className={"cursor-pointer border hover:border-primary transition-all duration-400 bg-white p-12 w-full flex flex-col justify-center items-center gap-2 h-full"}>
|
||||
<Dialog onOpenChange={(isOpen) => handleDialogOpen(isOpen, useful.id)}>
|
||||
<DialogTrigger asChild>
|
||||
<div className={"p-12 w-full flex flex-col items-center gap-2"}>
|
||||
<Image src={useful.image} alt={useful.name} width={100} height={100} />
|
||||
<h1 className={"text-xl font-bold text-center"}>{useful.name}</h1>
|
||||
</div>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="sm:max-w-6/12">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{useful.name}</DialogTitle>
|
||||
<DialogDescription>
|
||||
{t("Foydali ma'lumotlar ro'yxati")}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<Accordion type="single" collapsible className="w-full">
|
||||
{items.map((item) => (
|
||||
<AccordionItem value={`item-${item.id}`} key={item.id}>
|
||||
<AccordionTrigger>{item.name}</AccordionTrigger>
|
||||
<AccordionContent>
|
||||
<div className={`${item.video_url && "grid grid-cols-2"}`}>
|
||||
<p className="mb-2">{item.description}</p>
|
||||
{item.video_url && (
|
||||
<iframe
|
||||
width="100%"
|
||||
height="315"
|
||||
src={item.video_url.replace("watch?v=", "embed/")}
|
||||
title={item.name}
|
||||
className={"rounded-xl"}
|
||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
||||
allowFullScreen
|
||||
></iframe>
|
||||
)}
|
||||
</div>
|
||||
{item.file_url && (
|
||||
<a href={item.file_url} target="_blank" rel="noopener noreferrer">
|
||||
<Button size="sm">{t("Download PDF")}</Button>
|
||||
</a>
|
||||
)}
|
||||
|
||||
{item.link_url && (
|
||||
<a href={item.link_url} target="_blank" rel="noopener noreferrer">
|
||||
<Button size="sm">{t("Download PDF")}</Button>
|
||||
</a>
|
||||
)}
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
))}
|
||||
</Accordion>
|
||||
|
||||
<DialogFooter>
|
||||
<Button size={"lg"} type="button" onClick={() => setOpenId(null)}>
|
||||
{t("Yopish")}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default UsefulSection;
|
||||
11
src/middleware.ts
Normal file
11
src/middleware.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import createMiddleware from "next-intl/middleware";
|
||||
import { routing } from "./shared/config/i18n/routing";
|
||||
|
||||
export default createMiddleware(routing);
|
||||
|
||||
export const config = {
|
||||
// Match all pathnames except for
|
||||
// - … if they start with `/apiClient`, `/trpc`, `/_next` or `/_vercel`
|
||||
// - … the ones containing a dot (e.g. `favicon.ico`)
|
||||
matcher: "/((?!apiClient|trpc|_next|_vercel|.*\\..*).*)",
|
||||
};
|
||||
53
src/shared/api/apiClient.ts
Normal file
53
src/shared/api/apiClient.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
import axios, { AxiosResponse } from "axios";
|
||||
import { API_URL } from "@/shared/constants/apiEndpoints";
|
||||
import { getLocale } from "next-intl/server";
|
||||
import { getCurrentLocale } from "@/shared/lib/getCurrentLocale";
|
||||
import { useAuthStore } from "@/shared/store/authStore";
|
||||
|
||||
const apiClient = axios.create({
|
||||
baseURL: API_URL || "https://api.quyoshli.uz/api",
|
||||
});
|
||||
|
||||
apiClient.interceptors.request.use(async (config) => {
|
||||
console.log("API request", config);
|
||||
const token = useAuthStore.getState().user?.access_token;
|
||||
if (token) {
|
||||
config.headers.Authorization = `Bearer ${token}`;
|
||||
}
|
||||
let language = "uz";
|
||||
try {
|
||||
language = await getLocale();
|
||||
} catch (e) {
|
||||
language = getCurrentLocale() || "uz";
|
||||
}
|
||||
|
||||
config.headers["Accept-Language"] = language;
|
||||
return config;
|
||||
});
|
||||
|
||||
apiClient.interceptors.response.use(
|
||||
(response) => response,
|
||||
(error) => {
|
||||
console.error("API error:", error);
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
|
||||
export const GET = <T>(
|
||||
url: string,
|
||||
params?: object
|
||||
): Promise<AxiosResponse<T>> => apiClient.get(url, { params });
|
||||
export const POST = <T>(url: string, data: object): Promise<AxiosResponse<T>> =>
|
||||
apiClient.post(url, data);
|
||||
export const PUT = <T>(url: string, data: object): Promise<AxiosResponse<T>> =>
|
||||
apiClient.put(url, data);
|
||||
export const PATCH = <T>(
|
||||
url: string,
|
||||
data: object
|
||||
): Promise<AxiosResponse<T>> => apiClient.patch(url, data);
|
||||
export const DELETE = <T>(
|
||||
url: string,
|
||||
data: object
|
||||
): Promise<AxiosResponse<T>> => apiClient.delete(url, data);
|
||||
|
||||
export default apiClient;
|
||||
14
src/shared/api/authSvc.ts
Normal file
14
src/shared/api/authSvc.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import {POST} from "@/shared/api/apiClient";
|
||||
import {useAuthStore} from "@/shared/store/authStore";
|
||||
import {OAUTH, OAUTH_VERIFY} from "@/shared/constants";
|
||||
|
||||
export const sendPhoneNumber = async (phone: string) => {
|
||||
await POST(OAUTH, { phone });
|
||||
};
|
||||
|
||||
export const verifyCode = async (phone: string, verify_code: number): Promise<any> => {
|
||||
const response = await POST(OAUTH_VERIFY, { phone, verify_code });
|
||||
const {data} = response?.data;
|
||||
useAuthStore.getState().login(data);
|
||||
return data;
|
||||
};
|
||||
13
src/shared/api/brandsSvc.ts
Normal file
13
src/shared/api/brandsSvc.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import {BRANDS, PRODUCTS} from "@/shared/constants/apiEndpoints";
|
||||
import {GET} from "@/shared/api/apiClient";
|
||||
import { BrandProductsType, Brands } from "../types/brands";
|
||||
|
||||
export const getBrands = async (): Promise<Brands> => {
|
||||
const res = await GET<Brands>(BRANDS);
|
||||
return res.data;
|
||||
}
|
||||
|
||||
export const getBrandProducts = async (brandId: number): Promise<BrandProductsType> => {
|
||||
const res = await GET<BrandProductsType>(`${brandId}${PRODUCTS}`);
|
||||
return res.data;
|
||||
}
|
||||
8
src/shared/api/compilationsSvc.ts
Normal file
8
src/shared/api/compilationsSvc.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import {GET} from "@/shared/api/apiClient";
|
||||
import {COMPILATIONS} from "@/shared/constants/apiEndpoints";
|
||||
import {GetCompilationResponse} from "@/shared/types/compilations";
|
||||
|
||||
export const getCompilation = async () => {
|
||||
const res = await GET<GetCompilationResponse>(COMPILATIONS)
|
||||
return res.data;
|
||||
}
|
||||
11
src/shared/api/contactSvs.ts
Normal file
11
src/shared/api/contactSvs.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { AxiosResponse } from "axios";
|
||||
import { SUPPORT } from "../constants";
|
||||
import { POST } from "./apiClient";
|
||||
|
||||
export const contactInfoSubmit = async (
|
||||
phone: string,
|
||||
name: string
|
||||
): Promise<AxiosResponse> => {
|
||||
const response = await POST(SUPPORT, { phone, name });
|
||||
return response;
|
||||
};
|
||||
12
src/shared/api/feedbackSvc.ts
Normal file
12
src/shared/api/feedbackSvc.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import {GET} from "@/shared/api/apiClient";
|
||||
import {FEEDBACK} from "@/shared/constants";
|
||||
|
||||
export const getFeedback = async ()=>{
|
||||
const {data}= await GET<{
|
||||
data: {
|
||||
"phone": string
|
||||
"telegram_support": string
|
||||
}
|
||||
}>(FEEDBACK)
|
||||
return data
|
||||
}
|
||||
5
src/shared/api/index.ts
Normal file
5
src/shared/api/index.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import apiClient from "@/shared/api/apiClient";
|
||||
export * from "./authSvc"
|
||||
export {
|
||||
apiClient
|
||||
}
|
||||
13
src/shared/api/partnersSvc.ts
Normal file
13
src/shared/api/partnersSvc.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import {PARTNERS} from "@/shared/constants/apiEndpoints";
|
||||
import {GET} from "@/shared/api/apiClient";
|
||||
import {GetPartnersResponse} from "@/shared/types/partners";
|
||||
|
||||
export const getPartners = async (): Promise<GetPartnersResponse> => {
|
||||
const res = await GET<GetPartnersResponse>(PARTNERS);
|
||||
return res.data;
|
||||
}
|
||||
|
||||
export const getPartnerById = async (id: number): Promise<GetPartnersResponse> => {
|
||||
const res = await GET<GetPartnersResponse>(`${PARTNERS}/${id}`);
|
||||
return res.data;
|
||||
}
|
||||
10
src/shared/api/policySvc.ts
Normal file
10
src/shared/api/policySvc.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import {GET} from "@/shared/api/apiClient";
|
||||
import {PAGE_POLICY} from "@/shared/constants";
|
||||
|
||||
export const getPolicy = async ()=>{
|
||||
const {data} = await GET<{data: {
|
||||
name: string
|
||||
body: string
|
||||
}}>(PAGE_POLICY)
|
||||
return data
|
||||
}
|
||||
8
src/shared/api/productCategorySvc.ts
Normal file
8
src/shared/api/productCategorySvc.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import {ProductCategory} from "../types/productCategory"
|
||||
import {CATEGORIES} from "@/shared/constants/apiEndpoints";
|
||||
import {GET} from "@/shared/api/apiClient";
|
||||
|
||||
export const getCategory = async (): Promise<{data:ProductCategory[]}>=>{
|
||||
const res = await GET<{data:ProductCategory[]}>(CATEGORIES);
|
||||
return res.data;
|
||||
}
|
||||
23
src/shared/api/productSvc.ts
Normal file
23
src/shared/api/productSvc.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import {CATEGORIES, PRODUCTS} from "@/shared/constants/apiEndpoints";
|
||||
import {GetProductsResponse} from "@/shared/types/product";
|
||||
import {GET} from "@/shared/api/apiClient";
|
||||
import { BrandProductsType } from "../types/brands";
|
||||
|
||||
interface GetProductsProps {
|
||||
categoryId: number
|
||||
currentPage?: number
|
||||
}
|
||||
|
||||
export const getProducts = async ({categoryId, currentPage}: GetProductsProps): Promise<{
|
||||
data: GetProductsResponse
|
||||
}> => {
|
||||
const res = await GET<GetProductsResponse>(`${CATEGORIES}/${categoryId}${PRODUCTS}`, {
|
||||
...(currentPage !== undefined && { page: currentPage })
|
||||
});
|
||||
return res;
|
||||
}
|
||||
|
||||
export const getProductById = async (productId: number): Promise<any> => {
|
||||
const res = await GET(`${PRODUCTS}/${productId}`);
|
||||
return res;
|
||||
}
|
||||
8
src/shared/api/regionSvc.ts
Normal file
8
src/shared/api/regionSvc.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import {GET} from "@/shared/api/apiClient";
|
||||
import {REGIONS} from "@/shared/constants";
|
||||
import {GetRegionsResponse} from "@/shared/types/region";
|
||||
|
||||
export const getRegions = async ()=>{
|
||||
const {data} = await GET<GetRegionsResponse>(REGIONS)
|
||||
return data
|
||||
}
|
||||
13
src/shared/api/servicesSvc.ts
Normal file
13
src/shared/api/servicesSvc.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import {SERVICES} from "@/shared/constants/apiEndpoints";
|
||||
import {GET} from "@/shared/api/apiClient";
|
||||
import {GetServiceByIdResponse, GetServicesResponse} from "@/shared/types/services";
|
||||
|
||||
export const getServices = async (): Promise<GetServicesResponse> => {
|
||||
const {data} = await GET<GetServicesResponse>(`${SERVICES}`);
|
||||
return data
|
||||
}
|
||||
|
||||
export const getServiceById = async (id: number): Promise<GetServiceByIdResponse> => {
|
||||
const {data} = await GET<GetServiceByIdResponse>(`${SERVICES}/${id}`);
|
||||
return data
|
||||
}
|
||||
18
src/shared/api/usefulSvc.ts
Normal file
18
src/shared/api/usefulSvc.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import {GET} from "@/shared/api/apiClient";
|
||||
import {USEFUL_INFORMATION} from "@/shared/constants/apiEndpoints";
|
||||
import {GetUsefulResponse} from "@/shared/types/useful";
|
||||
|
||||
export const getUseful = async (): Promise<GetUsefulResponse> => {
|
||||
const {data} = await GET<GetUsefulResponse>(USEFUL_INFORMATION)
|
||||
return data
|
||||
}
|
||||
|
||||
export const getUsefulById = async (id: number): Promise<any> => {
|
||||
const {data} = await GET(`${USEFUL_INFORMATION}/${id}`)
|
||||
return data
|
||||
}
|
||||
|
||||
export const getUsefulItems = async (useful_id: number, items_id:number): Promise<any> => {
|
||||
const {data} = await GET(`${USEFUL_INFORMATION}/${useful_id}/items/${items_id}`)
|
||||
return data
|
||||
}
|
||||
17
src/shared/api/userMeSvc.ts
Normal file
17
src/shared/api/userMeSvc.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import {GET, POST, PUT} from "@/shared/api/apiClient";
|
||||
import {USER_ME} from "@/shared/constants";
|
||||
import {GetUserMeResponse} from "@/shared/types/user";
|
||||
|
||||
export const getUserMe = async () => {
|
||||
const {data} = await GET<GetUserMeResponse>(USER_ME)
|
||||
return data
|
||||
}
|
||||
export const updateUserMe = async (userData: {
|
||||
first_name?: string
|
||||
last_name?: string
|
||||
middle_name?: string
|
||||
phone?: string
|
||||
}) => {
|
||||
const {data} = await PUT(USER_ME, userData)
|
||||
return data
|
||||
}
|
||||
18
src/shared/api/userOrdersSvc.ts
Normal file
18
src/shared/api/userOrdersSvc.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import {GET, POST} from "@/shared/api/apiClient";
|
||||
import {CHECKOUT, USER_ORDERS} from "@/shared/constants";
|
||||
import {GetUserOrderByIdResponse, GetUserOrdersResponse} from "@/shared/types/userOrders";
|
||||
|
||||
export const getUserOrders = async ():Promise<GetUserOrdersResponse>=>{
|
||||
const {data} = await GET<GetUserOrdersResponse>(USER_ORDERS)
|
||||
return data
|
||||
}
|
||||
|
||||
export const getUserOrdersById = async (id: number):Promise<GetUserOrderByIdResponse>=>{
|
||||
const {data} = await GET<GetUserOrderByIdResponse>(`${USER_ORDERS}/${id}`)
|
||||
return data
|
||||
}
|
||||
|
||||
export const createUserOrder = async (data: any)=>{
|
||||
const res = await POST(CHECKOUT, data)
|
||||
return res
|
||||
}
|
||||
13
src/shared/api/userRequestsSvc.ts
Normal file
13
src/shared/api/userRequestsSvc.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import {GET} from "@/shared/api/apiClient";
|
||||
import {USER_ORDERS, USER_REQUESTS} from "@/shared/constants";
|
||||
import {GetUserRequestByIdResponse, GetUserRequestsResponse} from "@/shared/types/userRequests";
|
||||
|
||||
export const getUserRequests = async ():Promise<GetUserRequestsResponse>=>{
|
||||
const {data} = await GET<GetUserRequestsResponse>(USER_REQUESTS)
|
||||
return data
|
||||
}
|
||||
|
||||
export const getUserRequestsById = async (id: number):Promise<GetUserRequestByIdResponse>=>{
|
||||
const {data} = await GET<GetUserRequestByIdResponse>(`${USER_REQUESTS}/${id}`)
|
||||
return data
|
||||
}
|
||||
9
src/shared/config/fonts.ts
Normal file
9
src/shared/config/fonts.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import {Golos_Text} from "next/font/google";
|
||||
|
||||
const golosText = Golos_Text({
|
||||
weight: ["400", "500", "600", "700", "800"],
|
||||
variable: "--font-golos-text",
|
||||
subsets: ["latin"],
|
||||
});
|
||||
|
||||
export { golosText };
|
||||
130
src/shared/config/i18n/messages/ru.json
Normal file
130
src/shared/config/i18n/messages/ru.json
Normal file
@@ -0,0 +1,130 @@
|
||||
{
|
||||
"Sozlamalar": "Настройки",
|
||||
"Ma'lumotlarni yangilash": "Обновление данных",
|
||||
"Sahifa topilmadi, Iltimos qayta urinib ko`ring": "Страница не найдена, попробуйте еще раз",
|
||||
"Orga qaytish": "Возвращаться",
|
||||
"Login": "Авторизоваться",
|
||||
"Hisobingizga kirish uchun telefon raqamingizni kiriting": "Введите свой номер телефона, чтобы получить доступ к вашей учетной записи",
|
||||
"Telefon": "Телефон",
|
||||
"terms_of_use": "Я ознакомился с <tag>{tag}</tag>",
|
||||
"Tasdiqlash kodini kiriting": "Введите код подтверждения",
|
||||
"Tasdiqlash": "Подтвердить",
|
||||
"Offer va shartlar": "Предложение и условия",
|
||||
"Offerta shartlari": "Офферта термины",
|
||||
"Bu yerda offerta shartlarini o'qib chiqishingiz mumkin": "Вы можете прочитать здесь термины Offerta.",
|
||||
"Telefon raqam": "Номер телефона",
|
||||
"Email": "Электронная почта",
|
||||
"Adress": "Адрес",
|
||||
"Ish vaqtlari": "Рабочее время",
|
||||
"Biz haqimizda": "О нас",
|
||||
"about_us_subtitle": "Наш интернет-магазин предлагает полный ассортимент оборудования и своевременный доступ ко всем новым моделям. Здесь вы найдете все необходимое для себя и своего дома, принимая во внимание последние обновления и скидки. Таким образом, вы всегда получите самый новый продукт по самым оптимальным ценам.",
|
||||
"about_us_desc": "Наши каталоги постоянно пополняются новыми брендами и их продуктами, и вы всегда будете знать о сетевых поджелудочных зачинках, солнечных батареях и недавних разработках во всех спектрах услуг, связанных с энергией.",
|
||||
"Kategoriyalar": "Категории",
|
||||
"Kontaktlar": "Контакты",
|
||||
"Ilovamizni yuklab oling": "Загрузите наше приложение",
|
||||
"download_our_app_desc": "Наш интернет - магазин предоставляет вам полный ассортимент продукции для солнечного оборудования и уникальный доступ к соответствующей модели. Здесь вы найдете все необходимое для себя и своего дома, принимая во внимание последние обновления и скидки. Таким образом, вы всегда получите самый новый продукт по самым оптимальным ценам.",
|
||||
"Bepul maslahat uchun ro'yxatdan o'ting": "Зарегистрируйтесь на бесплатный совет",
|
||||
"Ro'yxatdan o'tish": "Зарегистрироваться",
|
||||
"Ismingiz": "Ваше имя",
|
||||
"Telefon raqamingiz": "Ваш номер телефона",
|
||||
"Quyosh uskunalarini ulgurji narxlarda sotib oling!": "Купите солнечное оборудование по оптовым ценам!",
|
||||
"Quyosh elektr stansiyasi uchun hamma narsani bir joyda va eng yaxshi narxda sotib oling": "Купите всё для солнечной электростанции у нас – по лучшим ценам.",
|
||||
"Batafsil": "Подробно",
|
||||
"Quyosh panellari": "Солнечные панели",
|
||||
"Hammasini ko'rish": "Просмотреть все",
|
||||
"Hamkorlarimiz": "Наши партнеры",
|
||||
"profit_1_desc": "Возьмите прибыль от энергии, производимой каждый день солнечными батареями. Вы можете подключиться к «зелёному» тарифу и продавать излишки солнечной энергии в сеть. Обычно оборудовано солнечными батареями, сетевым инвертором, установкой для солнечных батарей, защитой и переключением оборудования.",
|
||||
"profit_2_desc": "Всегда будет свет - когда вы являетесь владельцем автономной или резервной солнечной электростанции. Даже если отключена внешняя сеть, вы продолжаете обеспечивать электроэнергией свой дом. Такие станции при необходимости можно дополнительно оснастить аккумуляторами.",
|
||||
"profit_3_desc": "Вы можете купить все необходимые компоненты для солнечной электростанции в нашем интернет -магазине. Мы устанавливаем оборудование, которое предлагаем. Мы продаём только такое оборудование, которое приносит пользователю реальную экономию.",
|
||||
"Katalog": "Каталог",
|
||||
"Xizmatlar": "Услуги",
|
||||
"Hamkorlik": "Сотрудничество",
|
||||
"Foydali": "Полезный",
|
||||
"Viloyat": "Область",
|
||||
"Tuman": "Район",
|
||||
"Ariza yuborish": "Отправить заявление",
|
||||
"Sotib olish": "Покупка",
|
||||
"Jismoniy shaxs": "Индивидуальный",
|
||||
"Yuridik shaxs": "Юридическое лицо",
|
||||
"Buyurtma muvaffaqiyatli yaratildi!": "Порядок был успешно создан!",
|
||||
"Siz bilan tez orada bog'lanamiz": "Мы скоро свяжемся с вами.",
|
||||
"Buyurtma yaratishda xatolik!": "Ошибка в создании порядка!",
|
||||
"Ma'lumotlar to'liq kiritilganligiga ishonch hosil qiling yoki qaytadan urinib ko'ring": "Убедитесь, что данные введены и попробуйте еще раз.",
|
||||
"Kompaniya nomi": "Название компании",
|
||||
"Direktor": "Директор Ф.И.О.",
|
||||
"Yuridik manzil": "Юридический адрес",
|
||||
"Bank nomi": "Банк название",
|
||||
"Hisob raqam": "Номер счета",
|
||||
"Yetkazib berish": "Доставка",
|
||||
"Viloyatni tanlang": "Выберите регион",
|
||||
"Tuman/shahar": "Район / Город",
|
||||
"Manzil": "Адрес",
|
||||
"Uy raqami": "Домашний номер",
|
||||
"Mo'ljal": "Цель",
|
||||
"O‘rnatish xizmati kerakmi?": "Нужна служба установки?",
|
||||
"Ha": "Да",
|
||||
"Yo‘q": "Нет",
|
||||
"Yetkazib berish kerakmi": "Вам нужно доставить",
|
||||
"Yo‘q o'zim olib ketaman": "Нет, я возьму себя",
|
||||
"Yuborish": "Отправка",
|
||||
"full_name": "Ф.И.О.",
|
||||
"Passport seriya va raqami": "Серия паспорта и номер",
|
||||
"Yetkazib berish kerakmi?": "Стоит ли вам доставить?",
|
||||
"so'm": "Сум",
|
||||
"QQS bilan": "С НДС",
|
||||
"chegirma": "скидка",
|
||||
"Boshqa mahsulotlar": "Другие продукты",
|
||||
"Mening arizalarim": "Мои приложения",
|
||||
"Sizning arizalaringiz va ularning holati haqida ma'lumotlar": "Информация о ваших приложениях и их статусе",
|
||||
"Ariza": "Приложение",
|
||||
"Yaratilish vaqti": "Время создать",
|
||||
"Ariza tafsilotlari": "Детали приложения",
|
||||
"Ariza turi": "Тип приложения",
|
||||
"Ariza raqami": "Номер приложения",
|
||||
"Ariza holati": "Статус приложения",
|
||||
"Xizmat tafsilotlari": "Служба детали",
|
||||
"Quvvat": "Власть",
|
||||
"Telefon raqami": "Номер телефона",
|
||||
"Izoh": "Комментарий",
|
||||
"Bog'lanish": "Связаться с нами",
|
||||
"Biz bilan bog'lanish uchun quyidagi usullardan foydalanishingiz mumkin": "Вы можете использовать следующие способы связаться с нами",
|
||||
"Call Center": "Колл-центр",
|
||||
"Xatolik yuz berdi": "Произошла ошибка",
|
||||
"Profil ma'lumotlari": "Информация о профиле",
|
||||
"Sizning profil ma'lumotlaringiz va ularni o'zgartirish": "Информация о вашем профиле и их изменение",
|
||||
"Familiyangiz": "Ваша фамилия",
|
||||
"Sharif": "Шариф",
|
||||
"Saqlanmoqda": "Сохранение...",
|
||||
"Saqlash": "Хранилище",
|
||||
"Mening buyurtmalarim": "Мои заказы",
|
||||
"Sizning buyurtmalaringiz va ularning holati haqida ma'lumotlar": "Информация о ваших заказах и их статусе",
|
||||
"Buyurtma": "Заказ",
|
||||
"Buyurtma tafsilotlari": "Подробная информация о заказе",
|
||||
"Buyurtma raqami": "Номер заказа",
|
||||
"Buyurtma holati": "Статус заказа",
|
||||
"To'lov holati": "Статус оплаты",
|
||||
"Xaridlar ro'yxati": "Платный список",
|
||||
"Mijoz ma'lumotlari": "Информация о клиенте",
|
||||
"Mijoz turi": "Тип клиента",
|
||||
"Mijoz telefon": "Номер телефона клиента",
|
||||
"Yetkazib berish turi": "Тип доставки",
|
||||
"Yetkazib berish manzili": "Адрес доставки",
|
||||
"Yetkazib berish narxi": "Стоимость доставки",
|
||||
"To'lash": "Платить",
|
||||
"Foydali ma'lumotlar ro'yxati": "Полезный список информации",
|
||||
"Download PDF": "Загрузите файл",
|
||||
"Yopish": "Закрытие",
|
||||
"Bo'limlar": "Разделы",
|
||||
"Buyurtmalarim": "Мои заказы",
|
||||
"Arizalarim": "Мои приложения",
|
||||
"Offerta va foydalanish shartlari": "Офферта и Условия использования",
|
||||
"GET-GREEN ENERGY TRADE - Barcha huquqlar himoyalangan": "© GETGREEN ENERGY TRADE. Все права защищены.",
|
||||
"Asosiy": "Главная",
|
||||
"Market": "Магазин",
|
||||
"Kirish": "Войти",
|
||||
"Bepul maslahat uchun ma'lumotlaringizni kiriting": "Введите свои данные, чтобы получить бесплатную консультацию",
|
||||
"Yuborilmoqda": "Отправить",
|
||||
"Ma'lumotlarni to'ldiring": "Заполните данные",
|
||||
"Narx": "Цена",
|
||||
"office": "Юнусабадский район, улица Ифтихор, 1, Ташкент, Узбекистан"
|
||||
}
|
||||
130
src/shared/config/i18n/messages/uz.json
Normal file
130
src/shared/config/i18n/messages/uz.json
Normal file
@@ -0,0 +1,130 @@
|
||||
{
|
||||
"Sozlamalar": "Sozlamalar",
|
||||
"Ma'lumotlarni yangilash": "Ma'lumotlarni yangilash",
|
||||
"Sahifa topilmadi, Iltimos qayta urinib ko`ring": "Sahifa topilmadi, Iltimos qayta urinib ko`ring",
|
||||
"Orga qaytish": "Orga qaytish",
|
||||
"Login": "Login",
|
||||
"Hisobingizga kirish uchun telefon raqamingizni kiriting": "Hisobingizga kirish uchun telefon raqamingizni kiriting",
|
||||
"Telefon": "Telefon",
|
||||
"terms_of_use": "<tag>{tag}</tag> bilan tanishdim",
|
||||
"Tasdiqlash kodini kiriting": "Tasdiqlash kodini kiriting",
|
||||
"Tasdiqlash": "Tasdiqlash",
|
||||
"Offer va shartlar": "Offer va shartlar",
|
||||
"Offerta shartlari": "Offerta shartlari",
|
||||
"Bu yerda offerta shartlarini o'qib chiqishingiz mumkin": "Bu yerda offerta shartlarini o'qib chiqishingiz mumkin.",
|
||||
"Telefon raqam": "Telefon raqam",
|
||||
"Email": "Email",
|
||||
"Adress": "Adress",
|
||||
"Ish vaqtlari": "Ish vaqtlari",
|
||||
"Biz haqimizda": "Biz haqimizda",
|
||||
"about_us_subtitle": "Bizning onlayn-do'konimiz sizga quyosh uskunalari uchun mahsulotlarning to'liq assortimenti va mos modelni sotib olishning noyob imkoniyatini beradi. Bu yerda siz so'nggi yangilanishlar va chegirmalarni hisobga olgan holda o'zingiz va uyingiz uchun kerak bo'lgan hamma narsani topasiz. Shunday qilib, siz har doim eng yangi mahsulotni eng maqbul narxlarda olasiz.",
|
||||
"about_us_desc": "Bizning kataloglarimiz doimiy ravishda yangi brendlar va ularning mahsulotlari bilan to'ldirilib boriladi, biz bilan birga bo'lib, siz doimo tarmoq invertorlari, quyosh panellari va muqobil energiya bilan bog'liq xizmatlarning barcha spektri sohasidagi so'nggi ishlanmalardan xabardor bo'lasiz.",
|
||||
"Kategoriyalar": "Kategoriyalar",
|
||||
"Kontaktlar": "Kontaktlar",
|
||||
"Ilovamizni yuklab oling": "Ilovamizni yuklab oling",
|
||||
"download_our_app_desc": "Bizning onlayn-do'konimiz sizga quyosh uskunalari uchun mahsulotlarning to'liq assortimenti va mos modelni sotib olishning noyob imkoniyatini beradi. Bu yerda siz so'nggi yangilanishlar va chegirmalarni hisobga olgan holda o'zingiz va uyingiz uchun kerak bo'lgan hamma narsani topasiz. Shunday qilib, siz har doim eng yangi mahsulotni eng maqbul narxlarda olasiz.",
|
||||
"Bepul maslahat uchun ro'yxatdan o'ting": "Bepul maslahat uchun ro'yxatdan o'ting",
|
||||
"Ro'yxatdan o'tish": "Ro'yxatdan o'tish",
|
||||
"Ismingiz": "Ismingiz",
|
||||
"Telefon raqamingiz": "Telefon raqamingiz",
|
||||
"Quyosh uskunalarini ulgurji narxlarda sotib oling!": "Quyosh uskunalarini ulgurji narxlarda sotib oling!",
|
||||
"Quyosh elektr stansiyasi uchun hamma narsani bir joyda va eng yaxshi narxda sotib oling": "Quyosh elektr stansiyasi uchun hamma narsani bir joyda va eng yaxshi narxda sotib oling.",
|
||||
"Batafsil": "Batafsil",
|
||||
"Quyosh panellari": "Quyosh panellari",
|
||||
"Hammasini ko'rish": "Hammasini ko'rish",
|
||||
"Hamkorlarimiz": "Hamkorlarimiz",
|
||||
"profit_1_desc": "Quyosh panellari tomonidan har kuni ishlab chiqariladigan energiyadan foyda oling. Bu 'yashil tarif' bo'yicha tarmoq quyosh elektr stansiyasi tomonidan ta'minlanadi. Odatda quyosh panellari, tarmoq inverteri, quyosh panellari uchun o'rnatish to'plami, himoya va kommutatsiya uskunalari bilan jihozlangan.",
|
||||
"profit_2_desc": "Har doim yorug'lik bo'ladi - avtonom yoki zaxira quyosh elektr stantsiyasining egasi bo'lganingizda. Tarmoq o'chirilgan bo'lsa ham o'zingizni va uyingizni elektr energiyasi bilan ta'minlaysiz. Bunday quyosh stantsiyalari qo'shimcha ravishda qayta zaryadlanuvchi batareyalar bilan jihozlangan.",
|
||||
"profit_3_desc": "Quyosh elektr stansiyasi uchun barcha kerakli komponentlarni bizning onlayn do'konimizda xarid qilishingiz mumkin. Biz o'zimiz taklif qilayotgan uskunani o'rnatamiz. Va biz faqat foydalanuvchiga haqiqiy foyda keltiradigan narsalarni sotamiz.",
|
||||
"Katalog": "Katalog",
|
||||
"Xizmatlar": "Xizmatlar",
|
||||
"Hamkorlik": "Hamkorlik",
|
||||
"Foydali": "Foydali",
|
||||
"Viloyat": "Viloyat",
|
||||
"Tuman": "Tuman",
|
||||
"Ariza yuborish": "Ariza yuborish",
|
||||
"Sotib olish": "Sotib olish",
|
||||
"Jismoniy shaxs": "Jismoniy shaxs",
|
||||
"Yuridik shaxs": "Yuridik shaxs",
|
||||
"Buyurtma muvaffaqiyatli yaratildi!": "Buyurtma muvaffaqiyatli yaratildi!",
|
||||
"Siz bilan tez orada bog'lanamiz": "Siz bilan tez orada bog'lanamiz.",
|
||||
"Buyurtma yaratishda xatolik!": "Buyurtma yaratishda xatolik!",
|
||||
"Ma'lumotlar to'liq kiritilganligiga ishonch hosil qiling yoki qaytadan urinib ko'ring": "Ma'lumotlar to'liq kiritilganligiga ishonch hosil qiling yoki qaytadan urinib ko'ring.",
|
||||
"Kompaniya nomi": "Kompaniya nomi",
|
||||
"Direktor": "Direktor F.I.Sh.",
|
||||
"Yuridik manzil": "Yuridik manzil",
|
||||
"Bank nomi": "Bank nomi",
|
||||
"Hisob raqam": "Hisob raqam",
|
||||
"Yetkazib berish": "Yetkazib berish",
|
||||
"Viloyatni tanlang": "Viloyatni tanlang",
|
||||
"Tuman/shahar": "Tuman/shahar",
|
||||
"Manzil": "Manzil",
|
||||
"Uy raqami": "Uy raqami",
|
||||
"Mo'ljal": "Mo'ljal",
|
||||
"O‘rnatish xizmati kerakmi?": "O‘rnatish xizmati kerakmi?",
|
||||
"Ha": "Ha",
|
||||
"Yo‘q": "Yo‘q",
|
||||
"Yetkazib berish kerakmi": "Yetkazib berish kerakmi",
|
||||
"Yo‘q o'zim olib ketaman": "Yo‘q o'zim olib ketaman",
|
||||
"Yuborish": "Yuborish",
|
||||
"full_name": "F.I.Sh.",
|
||||
"Passport seriya va raqami": "Passport seriya va raqami",
|
||||
"Yetkazib berish kerakmi?": "Yetkazib berish kerakmi?",
|
||||
"so'm": "so'm",
|
||||
"QQS bilan": "QQS bilan",
|
||||
"chegirma": "chegirma",
|
||||
"Boshqa mahsulotlar": "Boshqa mahsulotlar",
|
||||
"Mening arizalarim": "Mening arizalarim",
|
||||
"Sizning arizalaringiz va ularning holati haqida ma'lumotlar": "Sizning arizalaringiz va ularning holati haqida ma'lumotlar",
|
||||
"Ariza": "Ariza",
|
||||
"Yaratilish vaqti": "Yaratilish vaqti",
|
||||
"Ariza tafsilotlari": "Ariza tafsilotlari",
|
||||
"Ariza turi": "Ariza turi",
|
||||
"Ariza raqami": "Ariza raqami",
|
||||
"Ariza holati": "Ariza holati",
|
||||
"Xizmat tafsilotlari": "Xizmat tafsilotlari",
|
||||
"Quvvat": "Quvvat",
|
||||
"Telefon raqami": "Telefon raqami",
|
||||
"Izoh": "Izoh",
|
||||
"Bog'lanish": "Biz bilan bog'lanish",
|
||||
"Biz bilan bog'lanish uchun quyidagi usullardan foydalanishingiz mumkin": "Biz bilan bog'lanish uchun quyidagi usullardan foydalanishingiz mumkin",
|
||||
"Call Center": "Call Center",
|
||||
"Xatolik yuz berdi": "Xatolik yuz berdi",
|
||||
"Profil ma'lumotlari": "Profil ma'lumotlari",
|
||||
"Sizning profil ma'lumotlaringiz va ularni o'zgartirish": "Sizning profil ma'lumotlaringiz va ularni o'zgartirish",
|
||||
"Familiyangiz": "Familiyangiz",
|
||||
"Sharif": "Sharif",
|
||||
"Saqlanmoqda": "Saqlanmoqda...",
|
||||
"Saqlash": "Saqlash",
|
||||
"Mening buyurtmalarim": "Mening buyurtmalarim",
|
||||
"Sizning buyurtmalaringiz va ularning holati haqida ma'lumotlar": "Sizning buyurtmalaringiz va ularning holati haqida ma'lumotlar",
|
||||
"Buyurtma": "Buyurtma",
|
||||
"Buyurtma tafsilotlari": "Buyurtma tafsilotlari",
|
||||
"Buyurtma raqami": "Buyurtma raqami",
|
||||
"Buyurtma holati": "Buyurtma holati",
|
||||
"To'lov holati": "To'lov holati",
|
||||
"Xaridlar ro'yxati": "Xaridlar ro'yxati",
|
||||
"Mijoz ma'lumotlari": "Mijoz ma'lumotlari",
|
||||
"Mijoz turi": "Mijoz turi",
|
||||
"Mijoz telefon": "Mijoz telefon raqami",
|
||||
"Yetkazib berish turi": "Yetkazib berish turi",
|
||||
"Yetkazib berish manzili": "Yetkazib berish manzili",
|
||||
"Yetkazib berish narxi": "Yetkazib berish narxi",
|
||||
"To'lash": "To'lash",
|
||||
"Foydali ma'lumotlar ro'yxati": "Foydali ma'lumotlar ro'yxati",
|
||||
"Download PDF": "Faylni yuklab olish",
|
||||
"Yopish": "Yopish",
|
||||
"Bo'limlar": "Bo'limlar",
|
||||
"Buyurtmalarim": "Buyurtmalarim",
|
||||
"Arizalarim": "Arizalarim",
|
||||
"Offerta va foydalanish shartlari": "Offerta va foydalanish shartlari",
|
||||
"GET-GREEN ENERGY TRADE - Barcha huquqlar himoyalangan": "GET-GREEN ENERGY TRADE - Barcha huquqlar himoyalangan",
|
||||
"Asosiy": "Asosiy",
|
||||
"Market": "Market",
|
||||
"Kirish": "Kirish",
|
||||
"Bepul maslahat uchun ma'lumotlaringizni kiriting": "Bepul maslahat uchun ma'lumotlaringizni kiriting",
|
||||
"Yuborilmoqda": "Yuborilmoqda...",
|
||||
"Ma'lumotlarni to'ldiring": "Ma'lumotlarni to'ldiring",
|
||||
"Narx": "Narx",
|
||||
"office": "Yunusobod tumani, Iftixor ko‘chasi, 1-uy, Toshkent, O‘zbekiston"
|
||||
}
|
||||
7
src/shared/config/i18n/navigation.ts
Normal file
7
src/shared/config/i18n/navigation.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { createNavigation } from "next-intl/navigation";
|
||||
import { routing } from "./routing";
|
||||
|
||||
// Lightweight wrappers around Next.js' navigation
|
||||
// APIs that consider the routing configuration
|
||||
export const { Link, redirect, usePathname, useRouter, getPathname } =
|
||||
createNavigation(routing);
|
||||
16
src/shared/config/i18n/request.ts
Normal file
16
src/shared/config/i18n/request.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { getRequestConfig } from "next-intl/server";
|
||||
import { hasLocale } from "next-intl";
|
||||
import { routing } from "./routing";
|
||||
|
||||
export default getRequestConfig(async ({ requestLocale }) => {
|
||||
// Typically corresponds to the `[locale]` segment
|
||||
const requested = await requestLocale;
|
||||
const locale = hasLocale(routing.locales, requested)
|
||||
? requested
|
||||
: routing.defaultLocale;
|
||||
|
||||
return {
|
||||
locale,
|
||||
messages: (await import(`./messages/${locale}.json`)).default,
|
||||
};
|
||||
});
|
||||
11
src/shared/config/i18n/routing.ts
Normal file
11
src/shared/config/i18n/routing.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { defineRouting } from "next-intl/routing";
|
||||
import { LanguageRoutes } from "./types";
|
||||
|
||||
export const routing = defineRouting({
|
||||
// A list of all locales that are supported
|
||||
locales: [LanguageRoutes.UZ, LanguageRoutes.RU],
|
||||
|
||||
// Used when no locale matches
|
||||
defaultLocale: LanguageRoutes.UZ,
|
||||
localeDetection: false,
|
||||
});
|
||||
4
src/shared/config/i18n/types.ts
Normal file
4
src/shared/config/i18n/types.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export enum LanguageRoutes {
|
||||
UZ = "uz", // o'zbekcha
|
||||
RU = "ru", // ruscha
|
||||
}
|
||||
7
src/shared/config/index.ts
Normal file
7
src/shared/config/index.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import {golosText} from "@/shared/config/fonts";
|
||||
import {routing} from "@/shared/config/i18n/routing";
|
||||
|
||||
export {
|
||||
golosText,
|
||||
routing
|
||||
}
|
||||
11
src/shared/config/theme-provider.tsx
Normal file
11
src/shared/config/theme-provider.tsx
Normal file
@@ -0,0 +1,11 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { ThemeProvider as NextThemesProvider } from "next-themes"
|
||||
|
||||
export function ThemeProvider({
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof NextThemesProvider>) {
|
||||
return <NextThemesProvider {...props}>{children}</NextThemesProvider>
|
||||
}
|
||||
19
src/shared/constants/apiEndpoints.ts
Normal file
19
src/shared/constants/apiEndpoints.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
export const API_URL = process.env.API_URL
|
||||
|
||||
export const CATEGORIES = "/categories"
|
||||
export const BRANDS = "/brands"
|
||||
export const COMPILATIONS = "/compilations"
|
||||
export const PARTNERS = "/partners"
|
||||
export const PRODUCTS = "/products"
|
||||
export const SERVICES = "/services"
|
||||
export const USEFUL_INFORMATION = "/useful-information"
|
||||
export const OAUTH = '/oauth/'
|
||||
export const OAUTH_VERIFY = '/oauth/verify'
|
||||
export const SUPPORT = '/support'
|
||||
export const USER_ORDERS = '/user/orders'
|
||||
export const USER_REQUESTS = '/user/requests'
|
||||
export const FEEDBACK = '/feedback'
|
||||
export const PAGE_POLICY= "page/policy"
|
||||
export const USER_ME= "/user/me"
|
||||
export const REGIONS = "/regions"
|
||||
export const CHECKOUT = "/checkout"
|
||||
26
src/shared/constants/data.ts
Normal file
26
src/shared/constants/data.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
const PRODUCT_INFO = {
|
||||
name: "Get Green",
|
||||
description: "Generated by create next app",
|
||||
logo: "/getgreen.png",
|
||||
favicon: "/favicon.png",
|
||||
url: "https://getgreen.uz",
|
||||
socials: {
|
||||
telegram: "https://t.me/usmanov_dev",
|
||||
instagram: "https://t.me/usmanov_dev",
|
||||
youtube: "https://t.me/usmanov_dev",
|
||||
linkedin: "https://www.linkedin.com/in/usmonov-azizbek/",
|
||||
},
|
||||
contact: {
|
||||
phone: "+998555067788",
|
||||
email: "contact@fias.uz",
|
||||
location: "Yunusabadskiy rayon, ulisa Iftixor, 1"
|
||||
},
|
||||
terms_of_use: "",
|
||||
creator: "Get Green",
|
||||
app: {
|
||||
ios: "/",
|
||||
android: "/"
|
||||
}
|
||||
}
|
||||
|
||||
export {PRODUCT_INFO}
|
||||
7
src/shared/constants/index.ts
Normal file
7
src/shared/constants/index.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import {PRODUCT_INFO} from "@/shared/constants/data";
|
||||
import {profileSidebarMenu} from "@/shared/constants/profileSidebar";
|
||||
export * from "./apiEndpoints"
|
||||
export {
|
||||
PRODUCT_INFO,
|
||||
profileSidebarMenu
|
||||
}
|
||||
39
src/shared/constants/profileSidebar.ts
Normal file
39
src/shared/constants/profileSidebar.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import {Calendar, Home, Inbox, Search, Settings, PhoneCall, Newspaper} from "lucide-react";
|
||||
|
||||
export const profileSidebarMenu = [
|
||||
{
|
||||
label: "Bo'limlar",
|
||||
menus: [
|
||||
{
|
||||
title: "Profil ma'lumotlari",
|
||||
url: "/profile",
|
||||
icon: Home,
|
||||
},
|
||||
{
|
||||
title: "Buyurtmalarim",
|
||||
url: "/profile/orders",
|
||||
icon: Inbox,
|
||||
},
|
||||
{
|
||||
title: "Arizalarim",
|
||||
url: "/profile/applications",
|
||||
icon: Calendar,
|
||||
},
|
||||
]
|
||||
},
|
||||
{
|
||||
label: "Sozlamalar",
|
||||
menus: [
|
||||
{
|
||||
title: "Offerta va foydalanish shartlari",
|
||||
url: "/profile/terms",
|
||||
icon: Newspaper,
|
||||
},
|
||||
{
|
||||
title: "Bog'lanish",
|
||||
url: "/profile/contact",
|
||||
icon: PhoneCall,
|
||||
},
|
||||
]
|
||||
}
|
||||
]
|
||||
14
src/shared/hooks/use-locale.ts
Normal file
14
src/shared/hooks/use-locale.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
export const useLocale = ()=>{
|
||||
const locale = window.location.pathname.split('/')[1]
|
||||
const setLocale = (newLocale: string) => {
|
||||
const segments = window.location.pathname.split('/');
|
||||
segments[1] = newLocale;
|
||||
const newPath = segments.join('/');
|
||||
window.history.pushState({}, '', newPath);
|
||||
window.location.reload();
|
||||
}
|
||||
return {
|
||||
locale,
|
||||
setLocale
|
||||
}
|
||||
}
|
||||
19
src/shared/hooks/use-mobile.ts
Normal file
19
src/shared/hooks/use-mobile.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import * as React from "react"
|
||||
|
||||
const MOBILE_BREAKPOINT = 768
|
||||
|
||||
export function useIsMobile() {
|
||||
const [isMobile, setIsMobile] = React.useState<boolean | undefined>(undefined)
|
||||
|
||||
React.useEffect(() => {
|
||||
const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`)
|
||||
const onChange = () => {
|
||||
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
|
||||
}
|
||||
mql.addEventListener("change", onChange)
|
||||
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
|
||||
return () => mql.removeEventListener("change", onChange)
|
||||
}, [])
|
||||
|
||||
return !!isMobile
|
||||
}
|
||||
27
src/shared/hooks/use-query-string.ts
Normal file
27
src/shared/hooks/use-query-string.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
'use client';
|
||||
|
||||
import { useSearchParams, useRouter, usePathname } from 'next/navigation';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
export function useQueryString() {
|
||||
const searchParams = useSearchParams();
|
||||
const router = useRouter();
|
||||
const pathname = usePathname();
|
||||
|
||||
const createQueryString = useCallback(
|
||||
(name: string, value: string) => {
|
||||
const params = new URLSearchParams(searchParams.toString());
|
||||
params.set(name, value);
|
||||
|
||||
return params.toString();
|
||||
},
|
||||
[searchParams]
|
||||
);
|
||||
|
||||
return {
|
||||
searchParams,
|
||||
router,
|
||||
pathname,
|
||||
createQueryString,
|
||||
};
|
||||
}
|
||||
5
src/shared/lib/formatNumberWithSpace.ts
Normal file
5
src/shared/lib/formatNumberWithSpace.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
const formatNumberWithSpaces = (number: number | string = "") => {
|
||||
return number.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ' ');
|
||||
}
|
||||
|
||||
export default formatNumberWithSpaces
|
||||
38
src/shared/lib/formatPhone.ts
Normal file
38
src/shared/lib/formatPhone.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
/**
|
||||
* Format the number (+998 00 111-22-33)
|
||||
* @param value Number to be formatted
|
||||
* @returns string +998 00 111-22-33
|
||||
*/
|
||||
const formatPhone = (value: string) => {
|
||||
// Keep only numbers
|
||||
const digits = value.replace(/\D/g, '');
|
||||
|
||||
// Return empty string if data is not available
|
||||
if (digits.length === 0) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const prefix = digits.startsWith('998') ? '+998 ' : '+998 ';
|
||||
|
||||
let formattedNumber = prefix;
|
||||
|
||||
if (digits.length > 3) {
|
||||
formattedNumber += digits.slice(3, 5);
|
||||
}
|
||||
|
||||
if (digits.length > 5) {
|
||||
formattedNumber += ' ' + digits.slice(5, 8);
|
||||
}
|
||||
|
||||
if (digits.length > 8) {
|
||||
formattedNumber += '-' + digits.slice(8, 10);
|
||||
}
|
||||
|
||||
if (digits.length > 10) {
|
||||
formattedNumber += '-' + digits.slice(10, 12);
|
||||
}
|
||||
|
||||
return formattedNumber.trim();
|
||||
};
|
||||
|
||||
export default formatPhone;
|
||||
9
src/shared/lib/getCurrentLocale.ts
Normal file
9
src/shared/lib/getCurrentLocale.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
export const getCurrentLocale = (): string | undefined => {
|
||||
if (typeof window !== "undefined" && typeof document !== "undefined") {
|
||||
const match = document.cookie
|
||||
.split("; ")
|
||||
.find(row => row.startsWith("NEXT_LOCALE="));
|
||||
return match?.split("=")[1];
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
6
src/shared/lib/utils.ts
Normal file
6
src/shared/lib/utils.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { clsx, type ClassValue } from "clsx"
|
||||
import { twMerge } from "tailwind-merge"
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs))
|
||||
}
|
||||
24
src/shared/providers/PrivateRouteProvider.tsx
Normal file
24
src/shared/providers/PrivateRouteProvider.tsx
Normal file
@@ -0,0 +1,24 @@
|
||||
"use client";
|
||||
import React, {useEffect} from 'react';
|
||||
import {useRouter} from 'next/navigation';
|
||||
import {useAuthStore} from '@/shared/store/authStore';
|
||||
import {isBrowser} from "motion/react";
|
||||
|
||||
const PrivateRoute = ({children}: { children: React.ReactNode }) => {
|
||||
const {user, isAuthenticated} = useAuthStore();
|
||||
const router = useRouter();
|
||||
|
||||
useEffect(() => {
|
||||
if (isBrowser && (!user || !isAuthenticated)) {
|
||||
router.push('/auth/login');
|
||||
}
|
||||
}, [user, isAuthenticated, router]);
|
||||
|
||||
if (!isAuthenticated) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <>{children}</>;
|
||||
};
|
||||
|
||||
export default PrivateRoute;
|
||||
21
src/shared/providers/QueryProvider.tsx
Normal file
21
src/shared/providers/QueryProvider.tsx
Normal file
@@ -0,0 +1,21 @@
|
||||
"use client"
|
||||
import {ReactNode, useState} from "react";
|
||||
import {QueryClient, QueryClientProvider} from "@tanstack/react-query";
|
||||
|
||||
const QueryProvider = ({children}: {children: ReactNode})=> {
|
||||
const [queryClient] = useState(() => new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
refetchOnWindowFocus: false,
|
||||
retry: 1,
|
||||
staleTime: 1000 * 60 * 5,
|
||||
refetchOnReconnect: false,
|
||||
refetchOnMount: false,
|
||||
refetchInterval: 1000 * 60 * 5,
|
||||
},
|
||||
},
|
||||
}));
|
||||
return <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>;
|
||||
}
|
||||
|
||||
export default QueryProvider;
|
||||
2
src/shared/providers/index.ts
Normal file
2
src/shared/providers/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from "./PrivateRouteProvider"
|
||||
export * from "./QueryProvider"
|
||||
41
src/shared/store/authStore.ts
Normal file
41
src/shared/store/authStore.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import {create} from 'zustand';
|
||||
import {persist} from 'zustand/middleware';
|
||||
import {isBrowser} from "motion/react";
|
||||
|
||||
interface AuthData {
|
||||
id: number;
|
||||
phone: string;
|
||||
access_token: string;
|
||||
}
|
||||
|
||||
interface AuthState {
|
||||
isAuthenticated: boolean;
|
||||
user: AuthData | null;
|
||||
login: (data: AuthData) => void;
|
||||
logout: () => void;
|
||||
}
|
||||
|
||||
export const useAuthStore = create<AuthState>()(
|
||||
persist(
|
||||
(set) => ({
|
||||
user: isBrowser && typeof window !== "undefined" ? JSON.parse(localStorage.getItem('auth-store') || 'null') : null,
|
||||
login: (data) => {
|
||||
set({user: data});
|
||||
if (isBrowser && typeof window !== "undefined") {
|
||||
localStorage.setItem('auth-store', JSON.stringify(data));
|
||||
}
|
||||
},
|
||||
logout: () => {
|
||||
set({user: null});
|
||||
if (isBrowser && typeof window !== "undefined") {
|
||||
localStorage.removeItem('auth-store');
|
||||
}
|
||||
},
|
||||
isAuthenticated: isBrowser && typeof window !== "undefined" && localStorage.getItem("auth-store") !== null,
|
||||
}),
|
||||
{
|
||||
name: 'auth-store',
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
23
src/shared/style/custom-utils.css
Normal file
23
src/shared/style/custom-utils.css
Normal file
@@ -0,0 +1,23 @@
|
||||
@layer utilities {
|
||||
.my-container {
|
||||
@apply container mx-auto transition-all duration-300;
|
||||
}
|
||||
.section-title{
|
||||
@apply text-2xl max-sm:text-[28px] font-bold mb-4 text-[2.8rem] leading-[1.17];
|
||||
}
|
||||
.section-subtitle{
|
||||
@apply text-base leading-[1.6];
|
||||
}
|
||||
.section-wrapper{
|
||||
@apply py-24
|
||||
}
|
||||
.profile-section-wrapper{
|
||||
@apply bg-white rounded-xl w-full p-4 h-fit
|
||||
}
|
||||
.profile-section-title{
|
||||
@apply text-xl font-semibold
|
||||
}
|
||||
.profile-section-subtitle{
|
||||
@apply text-sm font-semibold text-gray-500
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user