This commit is contained in:
2026-04-15 11:19:45 +00:00
commit acb79b2db7
183 changed files with 22067 additions and 0 deletions

42
.gitignore vendored Normal file
View File

@@ -0,0 +1,42 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.idea
/.pnp
.pnp.*
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/versions
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*
# env files (can opt-in for committing if needed)
.env*
# vercel
.vercel
# typescript
*.tsbuildinfo
next-env.d.ts

36
README.md Normal file
View File

@@ -0,0 +1,36 @@
This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).
## Getting Started
First, run the development server:
```bash
npm run dev
# or
yarn dev
# or
pnpm dev
# or
bun dev
```
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.
## Learn More
To learn more about Next.js, take a look at the following resources:
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
## Deploy on Vercel
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.

21
components.json Normal file
View File

@@ -0,0 +1,21 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "new-york",
"rsc": true,
"tsx": true,
"tailwind": {
"config": "",
"css": "src/app/globals.css",
"baseColor": "zinc",
"cssVariables": true,
"prefix": ""
},
"aliases": {
"components": "@/widgets",
"utils": "@/shared/lib/utils",
"ui": "@/shared/ui",
"lib": "@/shared/lib",
"hooks": "@/shared/hooks"
},
"iconLibrary": "lucide"
}

16
eslint.config.mjs Normal file
View File

@@ -0,0 +1,16 @@
import { dirname } from "path";
import { fileURLToPath } from "url";
import { FlatCompat } from "@eslint/eslintrc";
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const compat = new FlatCompat({
baseDirectory: __dirname,
});
const eslintConfig = [
...compat.extends("next/core-web-vitals", "next/typescript"),
];
export default eslintConfig;

38
next-sitemap.config.js Normal file
View File

@@ -0,0 +1,38 @@
/** @type {import('next-sitemap').IConfig} */
const siteUrl = "https://getgreen.uz";
const locales = ["uz", "ru"];
module.exports = {
siteUrl,
generateRobotsTxt: true,
changefreq: "daily",
priority: 0.7,
exclude: [],
alternateRefs: locales.map((locale) => ({
href: `${siteUrl}/${locale}`,
hreflang: locale === 'uz' ? 'uz_UZ' : 'ru_RU',
})),
additionalPaths: async (config) => {
const paths = ['/about', '/services', '/useful', '/category'];
const entries = [];
for (const path of paths) {
for (const locale of locales) {
entries.push({
loc: `${siteUrl}/${locale}${path}`,
changefreq: 'daily',
priority: 0.7,
alternateRefs: locales.map((altLocale) => ({
href: `${siteUrl}/${altLocale}${path}`,
hreflang: altLocale === 'uz' ? 'uz_UZ' : 'ru_RU',
})),
});
}
}
return entries;
},
};

37
next.config.ts Normal file
View File

@@ -0,0 +1,37 @@
import type { NextConfig } from "next";
import createNextIntlPlugin from "next-intl/plugin"
const nextConfig: NextConfig = {
images:{
remotePatterns: [
{
protocol: 'https',
hostname: 'getgreen.uz',
},
{
protocol: "https",
hostname: "felix-s3.jscorp.uz"
},
{
protocol: "https",
hostname: "minio.quyoshli.uz"
},
{
protocol: 'http',
hostname: '185.228.88.227',
port: '9000',
pathname: '/**',
},
]
},
devIndicators: false,
eslint: {
ignoreDuringBuilds: true,
},
typescript: {
ignoreBuildErrors: true,
},
};
const withNextIntl = createNextIntlPlugin("./src/shared/config/i18n/request.ts")
export default withNextIntl(nextConfig);

8393
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

69
package.json Normal file
View File

@@ -0,0 +1,69 @@
{
"name": "fias",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev --turbopack",
"build": "next build",
"start": "next start",
"lint": "next lint",
"postbuild": "next-sitemap"
},
"dependencies": {
"@dnd-kit/core": "^6.3.1",
"@dnd-kit/modifiers": "^9.0.0",
"@dnd-kit/sortable": "^10.0.0",
"@dnd-kit/utilities": "^3.2.2",
"@radix-ui/react-accordion": "^1.2.8",
"@radix-ui/react-avatar": "^1.1.7",
"@radix-ui/react-checkbox": "^1.2.3",
"@radix-ui/react-dialog": "^1.1.11",
"@radix-ui/react-dropdown-menu": "^2.1.12",
"@radix-ui/react-label": "^2.1.4",
"@radix-ui/react-navigation-menu": "^1.2.10",
"@radix-ui/react-radio-group": "^1.3.6",
"@radix-ui/react-select": "^2.2.2",
"@radix-ui/react-separator": "^1.1.4",
"@radix-ui/react-slot": "^1.2.0",
"@radix-ui/react-tabs": "^1.1.9",
"@radix-ui/react-toggle": "^1.1.6",
"@radix-ui/react-toggle-group": "^1.1.7",
"@radix-ui/react-tooltip": "^1.2.4",
"@tabler/icons-react": "^3.31.0",
"@tanstack/react-query": "^5.74.11",
"@tanstack/react-table": "^8.21.3",
"axios": "^1.9.0",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"formik": "^2.4.6",
"input-otp": "^1.4.2",
"lucide-react": "^0.503.0",
"motion": "^12.11.0",
"next": "15.3.8",
"next-intl": "^4.1.0",
"next-nprogress-bar": "^2.4.7",
"next-sitemap": "^4.2.3",
"next-themes": "^0.4.6",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"recharts": "^2.15.3",
"sonner": "^2.0.3",
"tailwind-merge": "^3.2.0",
"vaul": "^1.1.2",
"zod": "^3.24.3",
"zustand": "^5.0.4",
"zustand-persist": "^0.4.0"
},
"devDependencies": {
"@eslint/eslintrc": "^3",
"@tailwindcss/postcss": "^4",
"@types/node": "^20",
"@types/react": "^19",
"@types/react-dom": "^19",
"eslint": "^9",
"eslint-config-next": "15.3.1",
"tailwindcss": "^4",
"tw-animate-css": "^1.2.8",
"typescript": "^5"
}
}

5780
pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff

3
pnpm-workspace.yaml Normal file
View File

@@ -0,0 +1,3 @@
onlyBuiltDependencies:
- sharp
- unrs-resolver

5
postcss.config.mjs Normal file
View File

@@ -0,0 +1,5 @@
const config = {
plugins: ["@tailwindcss/postcss"],
};
export default config;

BIN
public/favicon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

37
public/felix.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 47 KiB

BIN
public/getgreen.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

BIN
public/hero-solar-panel.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 322 KiB

BIN
public/images/aboutus.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 75 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 29 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 30 KiB

BIN
public/images/hero-bg.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 222 KiB

BIN
public/images/profit.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 185 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 426 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 382 KiB

BIN
public/og-banner.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

9
public/robots.txt Normal file
View File

@@ -0,0 +1,9 @@
# *
User-agent: *
Allow: /
# Host
Host: https://getgreen.uz
# Sitemaps
Sitemap: https://getgreen.uz/sitemap.xml

11
public/sitemap-0.xml Normal file
View File

@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:news="http://www.google.com/schemas/sitemap-news/0.9" xmlns:xhtml="http://www.w3.org/1999/xhtml" xmlns:mobile="http://www.google.com/schemas/sitemap-mobile/1.0" xmlns:image="http://www.google.com/schemas/sitemap-image/1.1" xmlns:video="http://www.google.com/schemas/sitemap-video/1.1">
<url><loc>https://getgreen.uz/uz/about</loc><changefreq>daily</changefreq><priority>0.7</priority><xhtml:link rel="alternate" hreflang="uz_UZ" href="https://getgreen.uz/uz/about"/><xhtml:link rel="alternate" hreflang="ru_RU" href="https://getgreen.uz/uz/about"/></url>
<url><loc>https://getgreen.uz/ru/about</loc><changefreq>daily</changefreq><priority>0.7</priority><xhtml:link rel="alternate" hreflang="uz_UZ" href="https://getgreen.uz/ru/about"/><xhtml:link rel="alternate" hreflang="ru_RU" href="https://getgreen.uz/ru/about"/></url>
<url><loc>https://getgreen.uz/uz/services</loc><changefreq>daily</changefreq><priority>0.7</priority><xhtml:link rel="alternate" hreflang="uz_UZ" href="https://getgreen.uz/uz/services"/><xhtml:link rel="alternate" hreflang="ru_RU" href="https://getgreen.uz/uz/services"/></url>
<url><loc>https://getgreen.uz/ru/services</loc><changefreq>daily</changefreq><priority>0.7</priority><xhtml:link rel="alternate" hreflang="uz_UZ" href="https://getgreen.uz/ru/services"/><xhtml:link rel="alternate" hreflang="ru_RU" href="https://getgreen.uz/ru/services"/></url>
<url><loc>https://getgreen.uz/uz/useful</loc><changefreq>daily</changefreq><priority>0.7</priority><xhtml:link rel="alternate" hreflang="uz_UZ" href="https://getgreen.uz/uz/useful"/><xhtml:link rel="alternate" hreflang="ru_RU" href="https://getgreen.uz/uz/useful"/></url>
<url><loc>https://getgreen.uz/ru/useful</loc><changefreq>daily</changefreq><priority>0.7</priority><xhtml:link rel="alternate" hreflang="uz_UZ" href="https://getgreen.uz/ru/useful"/><xhtml:link rel="alternate" hreflang="ru_RU" href="https://getgreen.uz/ru/useful"/></url>
<url><loc>https://getgreen.uz/uz/category</loc><changefreq>daily</changefreq><priority>0.7</priority><xhtml:link rel="alternate" hreflang="uz_UZ" href="https://getgreen.uz/uz/category"/><xhtml:link rel="alternate" hreflang="ru_RU" href="https://getgreen.uz/uz/category"/></url>
<url><loc>https://getgreen.uz/ru/category</loc><changefreq>daily</changefreq><priority>0.7</priority><xhtml:link rel="alternate" hreflang="uz_UZ" href="https://getgreen.uz/ru/category"/><xhtml:link rel="alternate" hreflang="ru_RU" href="https://getgreen.uz/ru/category"/></url>
</urlset>

4
public/sitemap.xml Normal file
View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<sitemap><loc>https://getgreen.uz/sitemap-0.xml</loc></sitemap>
</sitemapindex>

8
src/app/500.tsx Normal file
View 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
View 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;

View 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

View File

@@ -0,0 +1,8 @@
import React from 'react'
const Page = () => {
return (
<div>Page</div>
)
}
export default Page

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

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

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

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

View 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

View File

@@ -0,0 +1,9 @@
import React from 'react'
import ApplicationsSections from "@/features/profile/ui/applications-section";
const Page = () => {
return (
<ApplicationsSections/>
)
}
export default Page

View File

@@ -0,0 +1,9 @@
import React from 'react'
import ContactSection from "@/features/profile/ui/contact-section";
const Page = () => {
return (
<ContactSection/>
)
}
export default Page

View 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

View 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

View File

@@ -0,0 +1,9 @@
import React from 'react'
import InformationSection from "@/features/profile/ui/information-section";
const Page = () => {
return (
<InformationSection/>
)
}
export default Page

View 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

View File

@@ -0,0 +1,9 @@
import React from 'react'
import TermsSection from "@/features/profile/ui/terms-section";
const Page = () => {
return (
<TermsSection/>
)
}
export default Page

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

View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

129
src/app/globals.css Normal file
View 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
View 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
View 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
View 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
View 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');
}

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

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

View 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

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

View File

@@ -0,0 +1,7 @@
import { LucideProps } from "lucide-react";
export interface CardProps {
icon: React.ComponentType<LucideProps>,
title: string;
value: string;
}

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

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

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

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

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

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

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

View 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

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

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

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

View 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

View File

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

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

View 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("Ornatish 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("Yoq")}</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("Yoq 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;

View 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("Ornatish 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("Yoq")}</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("Yoq 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;

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

View 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

View 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

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

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

View 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

View 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

View 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

View File

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

View 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
View 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|.*\\..*).*)",
};

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

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

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

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

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

@@ -0,0 +1,5 @@
import apiClient from "@/shared/api/apiClient";
export * from "./authSvc"
export {
apiClient
}

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

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

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

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

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

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

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

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

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

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