change
42
.gitignore
vendored
Normal 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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
69
package.json
Normal 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
3
pnpm-workspace.yaml
Normal file
@@ -0,0 +1,3 @@
|
||||
onlyBuiltDependencies:
|
||||
- sharp
|
||||
- unrs-resolver
|
||||
5
postcss.config.mjs
Normal file
@@ -0,0 +1,5 @@
|
||||
const config = {
|
||||
plugins: ["@tailwindcss/postcss"],
|
||||
};
|
||||
|
||||
export default config;
|
||||
BIN
public/favicon.png
Normal file
|
After Width: | Height: | Size: 3.4 KiB |
37
public/felix.svg
Normal file
|
After Width: | Height: | Size: 47 KiB |
BIN
public/getgreen.png
Normal file
|
After Width: | Height: | Size: 5.9 KiB |
BIN
public/hero-solar-panel.png
Normal file
|
After Width: | Height: | Size: 322 KiB |
BIN
public/images/aboutus.png
Normal file
|
After Width: | Height: | Size: 75 KiB |
17
public/images/app-store-light.svg
Normal file
|
After Width: | Height: | Size: 29 KiB |
12
public/images/google-play-light.svg
Normal file
|
After Width: | Height: | Size: 30 KiB |
BIN
public/images/hero-bg.jpg
Normal file
|
After Width: | Height: | Size: 222 KiB |
BIN
public/images/profit.png
Normal file
|
After Width: | Height: | Size: 185 KiB |
BIN
public/images/recommendation-bg.jpg
Normal file
|
After Width: | Height: | Size: 426 KiB |
BIN
public/images/screenshot-home.png
Normal file
|
After Width: | Height: | Size: 382 KiB |
BIN
public/og-banner.png
Normal file
|
After Width: | Height: | Size: 76 KiB |
9
public/robots.txt
Normal 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
@@ -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
@@ -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
@@ -0,0 +1,8 @@
|
||||
import React from 'react'
|
||||
|
||||
const Custom500 = () => {
|
||||
return (
|
||||
<div>500</div>
|
||||
)
|
||||
}
|
||||
export default Custom500
|
||||
26
src/app/Error.tsx
Normal file
@@ -0,0 +1,26 @@
|
||||
import React from 'react';
|
||||
|
||||
interface ErrorProps {
|
||||
statusCode?: number;
|
||||
}
|
||||
|
||||
const ErrorPage: React.FC<ErrorProps> = ({statusCode}) => {
|
||||
return (
|
||||
<div>
|
||||
<p>
|
||||
{statusCode
|
||||
? `An error ${statusCode} occurred on server`
|
||||
: 'An error occurred on client'}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
export async function getServerSideProps() {
|
||||
return {
|
||||
props: {statusCode: 500},
|
||||
};
|
||||
}
|
||||
|
||||
export default ErrorPage;
|
||||
21
src/app/[locale]/auth/login/page.tsx
Normal file
@@ -0,0 +1,21 @@
|
||||
"use client"
|
||||
import React from 'react'
|
||||
import LoginSection from "@/features/auth/ui/login-section";
|
||||
import {useAuthStore} from "@/shared/store/authStore";
|
||||
|
||||
const Page = () => {
|
||||
const {user, isAuthenticated} = useAuthStore();
|
||||
|
||||
if(user || isAuthenticated) {
|
||||
window.location.href = '/profile';
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex min-h-svh w-full items-center justify-center p-6 md:p-10">
|
||||
<div className="w-full max-w-sm">
|
||||
<LoginSection/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default Page
|
||||
8
src/app/[locale]/auth/register/page.tsx
Normal file
@@ -0,0 +1,8 @@
|
||||
import React from 'react'
|
||||
|
||||
const Page = () => {
|
||||
return (
|
||||
<div>Page</div>
|
||||
)
|
||||
}
|
||||
export default Page
|
||||
32
src/app/[locale]/brand/[brandId]/page.tsx
Normal file
@@ -0,0 +1,32 @@
|
||||
import React from "react";
|
||||
import { getBrandProducts } from "@/shared/api/brandsSvc";
|
||||
import ProductsList from "@/features/brand-products/ui/products-list";
|
||||
import MyPagionation from "@/shared/ui/my-pagionation";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
type PageProps = {
|
||||
params: {
|
||||
brandId: number;
|
||||
};
|
||||
searchParams: { page?: string };
|
||||
};
|
||||
|
||||
const Page = async ({ params, searchParams }: Readonly<PageProps>) => {
|
||||
const { brandId } = await params;
|
||||
const { page } = await searchParams;
|
||||
const products = await getBrandProducts(brandId);
|
||||
|
||||
return (
|
||||
<div className={"section-wrapper"}>
|
||||
<div className={"section-wrapper"}>
|
||||
<ProductsList products={products.data} />
|
||||
</div>
|
||||
<MyPagionation
|
||||
currentPage={Number(page)}
|
||||
totalPages={products.pagination.total}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
export default Page;
|
||||
36
src/app/[locale]/category/[categoryId]/page.tsx
Normal file
@@ -0,0 +1,36 @@
|
||||
import React from "react";
|
||||
import ProductsSection from "@/features/category-details/ui/products-section";
|
||||
import MyPagionation from "@/shared/ui/my-pagionation";
|
||||
import { getProducts } from "@/shared/api/productSvc";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
type PageProps = {
|
||||
params: {
|
||||
categoryId: number;
|
||||
};
|
||||
searchParams: { page?: string };
|
||||
};
|
||||
|
||||
const Page = async ({ params, searchParams }: Readonly<PageProps>) => {
|
||||
const { page } = await searchParams;
|
||||
|
||||
const { categoryId } = await params;
|
||||
const { data: products } = await getProducts({
|
||||
categoryId,
|
||||
currentPage: Number(page),
|
||||
});
|
||||
|
||||
return (
|
||||
<div className={"section-wrapper"}>
|
||||
<div className={"section-wrapper"}>
|
||||
<ProductsSection products={products.data} />
|
||||
</div>
|
||||
<MyPagionation
|
||||
currentPage={Number(page)}
|
||||
totalPages={products.pagination.total}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
export default Page;
|
||||
14
src/app/[locale]/category/page.tsx
Normal file
@@ -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
@@ -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
@@ -0,0 +1,40 @@
|
||||
import {
|
||||
ContactSection,
|
||||
DownloadAppSection,
|
||||
FreeRecommendationSection,
|
||||
HeroSection,
|
||||
PartnersSection,
|
||||
ProfitSection,
|
||||
} from "@/features/home/ui";
|
||||
import { getCompilation } from "@/shared/api/compilationsSvc";
|
||||
import ProductsSection from "@/features/category-details/ui/products-section";
|
||||
import SectionsSection from "@/features/home/ui/sections-section";
|
||||
import { getBrands } from "@/shared/api/brandsSvc";
|
||||
|
||||
const Page = async () => {
|
||||
const { data } = await getCompilation();
|
||||
const { data: partners } = await getBrands();
|
||||
|
||||
return (
|
||||
<div>
|
||||
<HeroSection />
|
||||
<SectionsSection />
|
||||
{data.map((section) => (
|
||||
<div
|
||||
key={section.id}
|
||||
className={"my-container section-wrapper px-4 md:px-0"}
|
||||
>
|
||||
<h1 className={"section-title"}>{section.title}</h1>
|
||||
<ProductsSection products={section.products} />
|
||||
</div>
|
||||
))}
|
||||
<FreeRecommendationSection />
|
||||
<ProfitSection />
|
||||
<PartnersSection partners={partners} />
|
||||
<ContactSection />
|
||||
<DownloadAppSection />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Page;
|
||||
13
src/app/[locale]/partners/page.tsx
Normal file
@@ -0,0 +1,13 @@
|
||||
import React from "react";
|
||||
import PartnersSection from "@/features/partners/ui/partners-section/partners-section";
|
||||
import { getPartners } from "@/shared/api/partnersSvc";
|
||||
|
||||
const Page = async () => {
|
||||
const { data: partners } = await getPartners();
|
||||
return (
|
||||
<div className={"section-wrapper"}>
|
||||
<PartnersSection partners={partners} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
export default Page;
|
||||
32
src/app/[locale]/product/[productId]/page.tsx
Normal file
@@ -0,0 +1,32 @@
|
||||
import React from 'react'
|
||||
import ProductDetailsSection from "@/features/product-details/ui/product-details-section";
|
||||
import {ContactSection, DownloadAppSection, FreeRecommendationSection, PartnersSection} from "@/features/home/ui";
|
||||
import {getProductById} from "@/shared/api/productSvc";
|
||||
import { getBrands } from '@/shared/api/brandsSvc';
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
type PageProps = {
|
||||
params: {
|
||||
productId: number
|
||||
},
|
||||
};
|
||||
|
||||
const Page = async ({params}: Readonly<PageProps>) => {
|
||||
const {productId} = await params;
|
||||
const {data: product} = await getProductById(productId)
|
||||
const { data: partners } = await getBrands();
|
||||
|
||||
return (
|
||||
<div className={"section-wrapper bg-white"}>
|
||||
<ProductDetailsSection product={product.data}/>
|
||||
<FreeRecommendationSection/>
|
||||
<div className="mt-24">
|
||||
<PartnersSection partners={partners}/>
|
||||
</div>
|
||||
<ContactSection/>
|
||||
<DownloadAppSection/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default Page
|
||||
9
src/app/[locale]/profile/applications/page.tsx
Normal file
@@ -0,0 +1,9 @@
|
||||
import React from 'react'
|
||||
import ApplicationsSections from "@/features/profile/ui/applications-section";
|
||||
|
||||
const Page = () => {
|
||||
return (
|
||||
<ApplicationsSections/>
|
||||
)
|
||||
}
|
||||
export default Page
|
||||
9
src/app/[locale]/profile/contact/page.tsx
Normal file
@@ -0,0 +1,9 @@
|
||||
import React from 'react'
|
||||
import ContactSection from "@/features/profile/ui/contact-section";
|
||||
|
||||
const Page = () => {
|
||||
return (
|
||||
<ContactSection/>
|
||||
)
|
||||
}
|
||||
export default Page
|
||||
20
src/app/[locale]/profile/layout.tsx
Normal file
@@ -0,0 +1,20 @@
|
||||
import React from 'react'
|
||||
import {ProfileSidebar} from "@/widgets/profile-sidebar/profile-sidebar";
|
||||
import {SidebarProvider} from "@/shared/ui/sidebar";
|
||||
import PrivateRoute from "@/shared/providers/PrivateRouteProvider";
|
||||
|
||||
const Layout = ({children}: Readonly<{ children: React.ReactNode }>) => {
|
||||
return (
|
||||
<PrivateRoute>
|
||||
<div className="my-12 min-h-screen">
|
||||
<div className="my-container section-wrapper">
|
||||
<SidebarProvider className={"gap-4 !min-h-auto"}>
|
||||
<ProfileSidebar/>
|
||||
{children}
|
||||
</SidebarProvider>
|
||||
</div>
|
||||
</div>
|
||||
</PrivateRoute>
|
||||
)
|
||||
}
|
||||
export default Layout
|
||||
10
src/app/[locale]/profile/orders/page.tsx
Normal file
@@ -0,0 +1,10 @@
|
||||
"use client"
|
||||
import React from 'react'
|
||||
import OrdersSection from "@/features/profile/ui/orders-section";
|
||||
|
||||
const Page = () => {
|
||||
return (
|
||||
<OrdersSection/>
|
||||
)
|
||||
}
|
||||
export default Page
|
||||
9
src/app/[locale]/profile/page.tsx
Normal file
@@ -0,0 +1,9 @@
|
||||
import React from 'react'
|
||||
import InformationSection from "@/features/profile/ui/information-section";
|
||||
|
||||
const Page = () => {
|
||||
return (
|
||||
<InformationSection/>
|
||||
)
|
||||
}
|
||||
export default Page
|
||||
13
src/app/[locale]/profile/settings/page.tsx
Normal file
@@ -0,0 +1,13 @@
|
||||
import { useTranslations } from 'next-intl'
|
||||
import React from 'react'
|
||||
|
||||
const Page = () => {
|
||||
const t = useTranslations("")
|
||||
return (
|
||||
<div className={"bg-white rounded-xl w-full p-4"}>
|
||||
<h1 className={"text-md font-semibold"}>{t("Sozlamalar")}</h1>
|
||||
<span className={"text-sm font-semibold text-gray-500"}>{t("Ma'lumotlarni yangilash")}</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default Page
|
||||
9
src/app/[locale]/profile/terms/page.tsx
Normal file
@@ -0,0 +1,9 @@
|
||||
import React from 'react'
|
||||
import TermsSection from "@/features/profile/ui/terms-section";
|
||||
|
||||
const Page = () => {
|
||||
return (
|
||||
<TermsSection/>
|
||||
)
|
||||
}
|
||||
export default Page
|
||||
13
src/app/[locale]/services/page.tsx
Normal file
@@ -0,0 +1,13 @@
|
||||
import React from "react";
|
||||
import { getServices } from "@/shared/api/servicesSvc";
|
||||
import ServicesSection from "@/features/services/ui/services-section/services-section";
|
||||
|
||||
const Page = async () => {
|
||||
const { data: services } = await getServices();
|
||||
return (
|
||||
<div className={"section-wrapper"}>
|
||||
<ServicesSection services={services} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
export default Page;
|
||||
13
src/app/[locale]/useful/page.tsx
Normal file
@@ -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
|
After Width: | Height: | Size: 3.4 KiB |
129
src/app/globals.css
Normal file
@@ -0,0 +1,129 @@
|
||||
@import "tailwindcss";
|
||||
@import "tw-animate-css";
|
||||
@import "../shared/style/custom-utils.css";
|
||||
|
||||
@custom-variant dark (&:is(.dark *));
|
||||
|
||||
@theme inline {
|
||||
--color-background: var(--background);
|
||||
--color-foreground: var(--foreground);
|
||||
--font-sans: var(--font-golos-text);
|
||||
--color-sidebar-ring: var(--sidebar-ring);
|
||||
--color-sidebar-border: var(--sidebar-border);
|
||||
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
||||
--color-sidebar-accent: var(--sidebar-accent);
|
||||
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
||||
--color-sidebar-primary: var(--sidebar-primary);
|
||||
--color-sidebar-foreground: var(--sidebar-foreground);
|
||||
--color-sidebar: var(--sidebar);
|
||||
--color-chart-5: var(--chart-5);
|
||||
--color-chart-4: var(--chart-4);
|
||||
--color-chart-3: var(--chart-3);
|
||||
--color-chart-2: var(--chart-2);
|
||||
--color-chart-1: var(--chart-1);
|
||||
--color-ring: var(--ring);
|
||||
--color-input: var(--input);
|
||||
--color-border: var(--border);
|
||||
--color-destructive: var(--destructive);
|
||||
--color-accent-foreground: var(--accent-foreground);
|
||||
--color-accent: var(--accent);
|
||||
--color-muted-foreground: var(--muted-foreground);
|
||||
--color-muted: var(--muted);
|
||||
--color-secondary-foreground: var(--secondary-foreground);
|
||||
--color-secondary: var(--secondary);
|
||||
--color-primary-foreground: var(--primary-foreground);
|
||||
--color-primary: var(--primary);
|
||||
--color-popover-foreground: var(--popover-foreground);
|
||||
--color-popover: var(--popover);
|
||||
--color-card-foreground: var(--card-foreground);
|
||||
--color-card: var(--card);
|
||||
--radius-sm: calc(var(--radius) - 4px);
|
||||
--radius-md: calc(var(--radius) - 2px);
|
||||
--radius-lg: var(--radius);
|
||||
--radius-xl: calc(var(--radius) + 4px);
|
||||
}
|
||||
|
||||
/* Ranglarni ko'rish uchun https://oklch.com/ saytidan foydalaning */
|
||||
:root {
|
||||
--radius: 0.625rem;
|
||||
--background: oklch(0.98 0 0);
|
||||
--foreground: oklch(0.141 0.005 285.823);
|
||||
--card: oklch(1 0 0);
|
||||
--card-foreground: oklch(0.141 0.005 285.823);
|
||||
--popover: oklch(1 0 0);
|
||||
--popover-foreground: oklch(0.141 0.005 285.823);
|
||||
--primary: var(--chart-2);
|
||||
--primary-foreground: oklch(0.985 0 0);
|
||||
--secondary: oklch(0.967 0.001 286.375);
|
||||
--secondary-foreground: oklch(0.21 0.006 285.885);
|
||||
--muted: oklch(0.967 0.001 286.375);
|
||||
--muted-foreground: oklch(0.552 0.016 285.938);
|
||||
--accent: oklch(0.967 0.001 286.375);
|
||||
--accent-foreground: oklch(0.21 0.006 285.885);
|
||||
--destructive: oklch(0.577 0.245 27.325);
|
||||
--border: oklch(0.92 0.004 286.32);
|
||||
--input: oklch(0.92 0.004 286.32);
|
||||
--ring: oklch(0.705 0.015 286.067);
|
||||
--chart-1: oklch(0.646 0.222 41.116);
|
||||
--chart-2: oklch(0.6 0.118 184.704);
|
||||
--chart-3: oklch(0.398 0.07 227.392);
|
||||
--chart-4: oklch(0.828 0.189 84.429);
|
||||
--chart-5: oklch(0.769 0.188 70.08);
|
||||
--sidebar: oklch(0.985 0 0);
|
||||
--sidebar-foreground: oklch(0.141 0.005 285.823);
|
||||
--sidebar-primary: oklch(0.21 0.006 285.885);
|
||||
--sidebar-primary-foreground: oklch(0.985 0 0);
|
||||
--sidebar-accent: oklch(0.967 0.001 286.375);
|
||||
--sidebar-accent-foreground: oklch(0.21 0.006 285.885);
|
||||
--sidebar-border: oklch(0.92 0.004 286.32);
|
||||
--sidebar-ring: oklch(0.705 0.015 286.067);
|
||||
}
|
||||
|
||||
.dark {
|
||||
--background: oklch(0.141 0.005 285.823);
|
||||
--foreground: oklch(0.985 0 0);
|
||||
--card: oklch(0.21 0.006 285.885);
|
||||
--card-foreground: oklch(0.985 0 0);
|
||||
--popover: oklch(0.21 0.006 285.885);
|
||||
--popover-foreground: oklch(0.985 0 0);
|
||||
--primary: oklch(0.62 0.1532 154.89);
|
||||
--primary-foreground: oklch(0.21 0.006 285.885);
|
||||
--secondary: oklch(0.274 0.006 286.033);
|
||||
--secondary-foreground: oklch(0.985 0 0);
|
||||
--muted: oklch(0.274 0.006 286.033);
|
||||
--muted-foreground: oklch(0.705 0.015 286.067);
|
||||
--accent: oklch(0.274 0.006 286.033);
|
||||
--accent-foreground: oklch(0.985 0 0);
|
||||
--destructive: oklch(0.704 0.191 22.216);
|
||||
--border: oklch(1 0 0 / 10%);
|
||||
--input: oklch(1 0 0 / 15%);
|
||||
--ring: oklch(0.552 0.016 285.938);
|
||||
--chart-1: oklch(0.488 0.243 264.376);
|
||||
--chart-2: oklch(0.696 0.17 162.48);
|
||||
--chart-3: oklch(0.769 0.188 70.08);
|
||||
--chart-4: oklch(0.627 0.265 303.9);
|
||||
--chart-5: oklch(0.645 0.246 16.439);
|
||||
--sidebar: oklch(0.21 0.006 285.885);
|
||||
--sidebar-foreground: oklch(0.985 0 0);
|
||||
--sidebar-primary: oklch(0.488 0.243 264.376);
|
||||
--sidebar-primary-foreground: oklch(0.985 0 0);
|
||||
--sidebar-accent: oklch(0.274 0.006 286.033);
|
||||
--sidebar-accent-foreground: oklch(0.985 0 0);
|
||||
--sidebar-border: oklch(1 0 0 / 10%);
|
||||
--sidebar-ring: oklch(0.552 0.016 285.938);
|
||||
}
|
||||
|
||||
*{
|
||||
/*border: 1px solid red;*/
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
|
||||
@layer base {
|
||||
* {
|
||||
@apply border-border outline-ring/50;
|
||||
}
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
}
|
||||
}
|
||||
|
||||
11
src/app/layout.tsx
Normal file
@@ -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
@@ -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
@@ -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
@@ -0,0 +1,6 @@
|
||||
import { redirect } from 'next/navigation';
|
||||
|
||||
// This page only renders when the app is built statically (output: 'export')
|
||||
export default function RootPage() {
|
||||
redirect('/uz');
|
||||
}
|
||||
172
src/features/auth/ui/login-section.tsx
Normal file
@@ -0,0 +1,172 @@
|
||||
"use client";
|
||||
|
||||
import { cn } from "@/shared/lib/utils";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/shared/ui/card";
|
||||
import { Label } from "@radix-ui/react-label";
|
||||
import { Input } from "@/shared/ui/input";
|
||||
import { Button } from "@/shared/ui/button";
|
||||
import Image from "next/image";
|
||||
import { FormEvent, useState } from "react";
|
||||
import { InputOTP, InputOTPGroup, InputOTPSlot } from "@/shared/ui/input-otp";
|
||||
import { REGEXP_ONLY_DIGITS_AND_CHARS } from "input-otp";
|
||||
import { Checkbox } from "@/shared/ui/checkbox";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/shared/ui/dialog";
|
||||
import { sendPhoneNumber, verifyCode } from "@/shared/api";
|
||||
import { useRouter } from "@/shared/config/i18n/navigation";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
const LoginSection = ({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentPropsWithoutRef<"div">) => {
|
||||
const [step, setStep] = useState(1);
|
||||
const [phone, setPhone] = useState("");
|
||||
const [code, setCode] = useState("");
|
||||
const router = useRouter();
|
||||
const t = useTranslations("");
|
||||
|
||||
const handlePhoneSubmit = async (e: FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
await sendPhoneNumber(phone);
|
||||
setStep(2);
|
||||
};
|
||||
|
||||
const handleVerifySubmit = async (e: FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
await verifyCode(phone, parseInt(code));
|
||||
router.push("/profile");
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={cn("flex flex-col gap-6", className)} {...props}>
|
||||
{step === 1 ? (
|
||||
<Card className="shadow-none border-none">
|
||||
<CardHeader>
|
||||
<Image
|
||||
className="mx-auto mb-10"
|
||||
src="/getgreen.png"
|
||||
alt=""
|
||||
width={300}
|
||||
height={300}
|
||||
/>
|
||||
<CardTitle className="text-2xl">{t("Login")}</CardTitle>
|
||||
<CardDescription>
|
||||
{t("Hisobingizga kirish uchun telefon raqamingizni kiriting")}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<form onSubmit={handlePhoneSubmit}>
|
||||
<div className="flex flex-col gap-6">
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="phone">{t("Telefon")}</Label>
|
||||
<Input
|
||||
id="phone"
|
||||
type="tel"
|
||||
name="phone"
|
||||
value={phone}
|
||||
onChange={(e) => setPhone(e.target.value)}
|
||||
placeholder="+998 94 456 78 90"
|
||||
required
|
||||
/>
|
||||
<div className="flex items-center space-x-2 mt-2">
|
||||
<Checkbox id="terms" required />
|
||||
{t.rich("terms_of_use", {
|
||||
tag: (chunks) => (
|
||||
<label
|
||||
htmlFor="terms"
|
||||
className="text-sm font-medium leading-none"
|
||||
>
|
||||
<TermsOfUse /> {chunks}
|
||||
</label>
|
||||
),
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
<Button type="submit" className="w-full">
|
||||
{t("login")}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</CardContent>
|
||||
</Card>
|
||||
) : (
|
||||
<Card className="shadow-none border-none">
|
||||
<CardHeader>
|
||||
<Image
|
||||
className="mx-auto mb-10"
|
||||
src="/getgreen.png"
|
||||
alt=""
|
||||
width={300}
|
||||
height={300}
|
||||
/>
|
||||
<CardTitle className="text-2xl">OTP</CardTitle>
|
||||
<CardDescription>{t("Tasdiqlash kodini kiriting")}</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<form onSubmit={handleVerifySubmit}>
|
||||
<div className="flex flex-col gap-6">
|
||||
<div className="grid gap-2">
|
||||
<InputOTP
|
||||
maxLength={5}
|
||||
pattern={REGEXP_ONLY_DIGITS_AND_CHARS}
|
||||
value={code}
|
||||
onChange={setCode}
|
||||
>
|
||||
<InputOTPGroup>
|
||||
{[0, 1, 2, 3, 4].map((i) => (
|
||||
<InputOTPSlot key={i} index={i} />
|
||||
))}
|
||||
</InputOTPGroup>
|
||||
</InputOTP>
|
||||
</div>
|
||||
<Button type="submit" className="w-full">
|
||||
{t("Tasdiqlash")}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default LoginSection;
|
||||
|
||||
const TermsOfUse = () => {
|
||||
const t = useTranslations("")
|
||||
return (
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>
|
||||
<span className="border-b border-primary cursor-pointer">
|
||||
{t("Offer va shartlar")}
|
||||
</span>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="min-w-6/12">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{t("Offerta shartlari")}</DialogTitle>
|
||||
<DialogDescription>
|
||||
{t("Bu yerda offerta shartlarini o'qib chiqishingiz mumkin")}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="overflow-y-scroll h-[70vh]">
|
||||
{/* Replace with actual offer content */}
|
||||
Lorem ipsum dolor sit amet, consectetur...
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
46
src/features/brand-products/ui/products-list.tsx
Normal file
@@ -0,0 +1,46 @@
|
||||
import React from "react";
|
||||
import ProductCard from "@/shared/ui/product-card";
|
||||
import { Product } from "@/shared/types/product";
|
||||
import { BrandProductsResultType } from "@/shared/types/brands";
|
||||
|
||||
interface ProductSectionProps {
|
||||
products: BrandProductsResultType[];
|
||||
}
|
||||
|
||||
const ProductsList = ({ products }: ProductSectionProps) => {
|
||||
return (
|
||||
<div className={"my-container"}>
|
||||
<section id={"invertor-section"} className={"my-container"}>
|
||||
<div className={"flex flex-col justify-center"}>
|
||||
<div>
|
||||
<div
|
||||
className={
|
||||
"grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 "
|
||||
}
|
||||
>
|
||||
{products.map((i) => {
|
||||
const product: Product = {
|
||||
id: i.id,
|
||||
name: i.name,
|
||||
price: i.price,
|
||||
price_usd: i.price,
|
||||
price_discount: i.price_discount,
|
||||
discount_percent: i.discount_percent,
|
||||
is_leader_of_sales: i.is_leader_of_sales,
|
||||
poster: i.poster,
|
||||
poster_thumb: i.poster_thumb,
|
||||
is_favorite: i.is_favorite,
|
||||
is_cart: i.is_cart,
|
||||
count: i.count,
|
||||
power: i.power,
|
||||
};
|
||||
return <ProductCard key={product.id} product={product} />;
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
export default ProductsList;
|
||||
30
src/features/category-details/ui/products-section.tsx
Normal file
@@ -0,0 +1,30 @@
|
||||
import React from 'react'
|
||||
import {CategoryCard} from "@/shared/ui/category-card";
|
||||
import {Button} from "@/shared/ui/button";
|
||||
import ProductCard from "@/shared/ui/product-card";
|
||||
import {Product} from "@/shared/types/product";
|
||||
|
||||
interface ProductSectionProps{
|
||||
products: Product[]
|
||||
}
|
||||
|
||||
const ProductsSection = ({products}: ProductSectionProps) => {
|
||||
return (
|
||||
<div className={"my-container"}>
|
||||
<section id={"invertor-section"} className={"my-container"}>
|
||||
<div className={"flex flex-col justify-center"}>
|
||||
<div>
|
||||
<div className={"grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 "}>
|
||||
{
|
||||
products.map(product=>(
|
||||
<ProductCard key={product.id} product={product}/>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default ProductsSection
|
||||
29
src/features/home/lib/data.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import { List, MailIcon, MapPin, PhoneCall } from "lucide-react";
|
||||
import { CardProps } from "../models/types";
|
||||
import formatPhone from "@/shared/lib/formatPhone";
|
||||
import { PRODUCT_INFO } from "@/shared/constants";
|
||||
|
||||
const contactData: CardProps[] = [
|
||||
{
|
||||
icon: PhoneCall,
|
||||
title: "Telefon raqam",
|
||||
value: formatPhone(PRODUCT_INFO.contact.phone),
|
||||
},
|
||||
{
|
||||
icon: MailIcon,
|
||||
title: "Email",
|
||||
value: PRODUCT_INFO.contact.email,
|
||||
},
|
||||
{
|
||||
icon: MapPin,
|
||||
title: "Adress",
|
||||
value: "office",
|
||||
},
|
||||
{
|
||||
icon: List,
|
||||
title: "Ish vaqtlari",
|
||||
value: "9:00 - 18:00",
|
||||
},
|
||||
];
|
||||
|
||||
export { contactData };
|
||||
7
src/features/home/models/types.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { LucideProps } from "lucide-react";
|
||||
|
||||
export interface CardProps {
|
||||
icon: React.ComponentType<LucideProps>,
|
||||
title: string;
|
||||
value: string;
|
||||
}
|
||||
34
src/features/home/ui/aboutus-section.tsx
Normal file
@@ -0,0 +1,34 @@
|
||||
import React from "react";
|
||||
import Image from "next/image";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
const AboutusSection = () => {
|
||||
const t = useTranslations("");
|
||||
return (
|
||||
<section id={"about-section"} className={"bg-slate-900 py-12"}>
|
||||
<div
|
||||
className={
|
||||
"max-w-7xl mx-auto grid grid-cols-1 md:grid-cols-2 justify-between items-center py-10 gap-12"
|
||||
}
|
||||
>
|
||||
<div className={"relative w-full"}>
|
||||
<Image
|
||||
src={"/images/aboutus.png"}
|
||||
alt={"About us image"}
|
||||
layout="responsive"
|
||||
width={500}
|
||||
height={500}
|
||||
/>
|
||||
</div>
|
||||
<div className={"text-white px-4"}>
|
||||
<h1 className="section-title">{t("Biz haqimizda")}</h1>
|
||||
<p className="section-subtitle">{t("about_us_subtitle")}</p>
|
||||
<br aria-hidden />
|
||||
<p className="section-subtitle">{t("about_us_desc")}</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default AboutusSection;
|
||||
81
src/features/home/ui/category-section.tsx
Normal file
@@ -0,0 +1,81 @@
|
||||
"use client"
|
||||
import {Accordion, AccordionContent, AccordionItem, AccordionTrigger} from "@/shared/ui/accordion";
|
||||
import {Link} from "@/shared/config/i18n/navigation";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
|
||||
interface Category {
|
||||
id: number;
|
||||
name: string;
|
||||
image: string;
|
||||
parent_id: number | null;
|
||||
parents: Category[];
|
||||
}
|
||||
|
||||
interface CategoriesProps {
|
||||
categories: Category[];
|
||||
}
|
||||
|
||||
const CategoriesSection: React.FC<CategoriesProps> = ({categories}) => {
|
||||
const t = useTranslations("")
|
||||
const renderCategory = (category: Category, level: number = 0) => {
|
||||
const itemValue = `category-${category.id}`;
|
||||
const hasChildren = category.parents && category.parents.length > 0;
|
||||
const hasImage =
|
||||
level === 0 && category.image && !category.image.includes("no_brend.png")
|
||||
? category.image
|
||||
: null;
|
||||
|
||||
return (
|
||||
<AccordionItem
|
||||
key={category.id}
|
||||
value={itemValue}
|
||||
className={`border-b ${level === 0 ? "border-gray-200" : "border-gray-100"}`}
|
||||
>
|
||||
<AccordionTrigger
|
||||
showArrowIcon={hasChildren}
|
||||
className={`flex items-center gap-4 p-4 hover:bg-gray-50 transition-colors ${
|
||||
level === 0 ? "text-lg font-semibold" : "text-base font-medium"
|
||||
}`}
|
||||
>
|
||||
<div className={"flex items-center justify-center gap-10"}>
|
||||
{hasImage && (
|
||||
<img
|
||||
src={category.image}
|
||||
alt={category.name}
|
||||
className="w-12 h-12 object-cover rounded-md"
|
||||
/>
|
||||
)}
|
||||
{hasChildren ? (
|
||||
<span>{category.name}</span>
|
||||
) : (
|
||||
<Link href={`/category/${category.id}`}>
|
||||
{category.name}
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
</AccordionTrigger>
|
||||
{hasChildren && (
|
||||
<AccordionContent className="pl-6">
|
||||
<Accordion type="multiple" className="w-full">
|
||||
{category.parents.map((child) => renderCategory(child, level + 1))}
|
||||
</Accordion>
|
||||
</AccordionContent>
|
||||
)}
|
||||
</AccordionItem>
|
||||
);
|
||||
};
|
||||
return (
|
||||
<div className="my-container section-wrapper mx-auto p-4">
|
||||
<h1 className="section-title text-center">{t("Kategoriyalar")}</h1>
|
||||
<Accordion
|
||||
type="multiple"
|
||||
className="w-full rounded-lg grid space-y-10"
|
||||
>
|
||||
{categories.map((category) => renderCategory(category))}
|
||||
</Accordion>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default CategoriesSection;
|
||||
43
src/features/home/ui/contact-section.tsx
Normal file
@@ -0,0 +1,43 @@
|
||||
import React from "react";
|
||||
import { CardProps } from "../models/types";
|
||||
import { contactData } from "../lib/data";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
function ContactCard({ icon: Icon, title, value }: CardProps) {
|
||||
const t = useTranslations("");
|
||||
return (
|
||||
<div className={"bg-white rounded-4xl p-10 flex items-center gap-10"}>
|
||||
<Icon size={70} />
|
||||
<div className={"flex flex-col"}>
|
||||
<span className={"text-2xl font-bold"}>{t(title)}</span>
|
||||
<span>{t(value)}</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const ContactSection = () => {
|
||||
const t = useTranslations("");
|
||||
return (
|
||||
<section id={"contact-section.tsx"} className={"section-wrapper"}>
|
||||
<div className="bg-primary py-24 px-4">
|
||||
<div className={"my-container"}>
|
||||
<h1 className={"section-title text-center pb-10 text-white"}>
|
||||
{t("Kontaktlar")}
|
||||
</h1>
|
||||
<div className={"grid grid-cols-2 max-sm:grid-cols-1 gap-8"}>
|
||||
{contactData.map((e, i) => (
|
||||
<ContactCard
|
||||
icon={e.icon}
|
||||
title={e.title}
|
||||
value={e.value}
|
||||
key={i}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
export default ContactSection;
|
||||
75
src/features/home/ui/download-app-section.tsx
Normal file
@@ -0,0 +1,75 @@
|
||||
import React from "react";
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import { PRODUCT_INFO } from "@/shared/constants";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
const DownloadAppSection = () => {
|
||||
const t = useTranslations("")
|
||||
return (
|
||||
<section
|
||||
id={"download-app-section"}
|
||||
className={
|
||||
"bg-slate-900 my-container rounded-4xl max-md:rounded-3xl mb-20 relative overflow-hidden max-md:pt-12"
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="bg-radial from-blue-950 via-transparent to-transparent w-[700px] h-[700px] rounded-full absolute -bottom-60 -left-60"
|
||||
aria-hidden
|
||||
/>
|
||||
|
||||
<div
|
||||
className="bg-radial from-indigo-900 via-transparent to-transparent w-[700px] h-[700px] rounded-full absolute -top-80 -right-80"
|
||||
aria-hidden
|
||||
/>
|
||||
<div
|
||||
className={
|
||||
"max-w-7xl mx-auto grid grid-cols-1 md:grid-cols-2 justify-between items-center gap-12 relative z-20"
|
||||
}
|
||||
>
|
||||
<div className={"text-white px-4"}>
|
||||
<h1 className="section-title">{t("Ilovamizni yuklab oling")}</h1>
|
||||
<p className="section-subtitle">
|
||||
{t("download_our_app_desc")}
|
||||
</p>
|
||||
<br aria-hidden />
|
||||
<div className={"flex gap-4 mt-5"}>
|
||||
<Link href={PRODUCT_INFO.app.ios}>
|
||||
<Image
|
||||
src={"/images/app-store-light.svg"}
|
||||
alt={""}
|
||||
width={200}
|
||||
height={200}
|
||||
className="max-md:w-40"
|
||||
/>
|
||||
</Link>
|
||||
<Link href={PRODUCT_INFO.app.android}>
|
||||
<Image
|
||||
src={"/images/google-play-light.svg"}
|
||||
alt={""}
|
||||
width={200}
|
||||
height={200}
|
||||
className="max-md:w-40"
|
||||
/>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className={
|
||||
"relative flex justify-end max-md:justify-center pt-24 max-md:pt-20"
|
||||
}
|
||||
>
|
||||
<Image
|
||||
className={"-mb-96"}
|
||||
src={"/images/screenshot-home.png"}
|
||||
alt={"About us image"}
|
||||
width={500}
|
||||
height={500}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default DownloadAppSection;
|
||||
76
src/features/home/ui/free-recommendation-section.tsx
Normal file
@@ -0,0 +1,76 @@
|
||||
"use client";
|
||||
|
||||
import React, { FormEvent, useState } from "react";
|
||||
import { Input } from "@/shared/ui/input";
|
||||
import { Button } from "@/shared/ui/button";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { contactInfoSubmit } from "@/shared/api/contactSvs";
|
||||
import formatPhone from "@/shared/lib/formatPhone";
|
||||
import { toast } from "sonner";
|
||||
|
||||
const FreeRecommendationSection = () => {
|
||||
const t = useTranslations("");
|
||||
const [phone, setPhone] = useState("+998 ");
|
||||
const [name, setName] = useState("");
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [isError, setIsError] = useState("")
|
||||
|
||||
const onContactSubmit = async (e: FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
if (phone.length === 17 || name.length > 2) {
|
||||
setIsLoading(true);
|
||||
setIsError("")
|
||||
try {
|
||||
await contactInfoSubmit(phone, name);
|
||||
setName("");
|
||||
setPhone("+998 ");
|
||||
toast.success("Muvaffaqiyatli yuborildi");
|
||||
setIsLoading(false);
|
||||
} catch (error) {
|
||||
setIsLoading(false);
|
||||
}
|
||||
} else {
|
||||
setIsError("Ma'lumotlarni to'ldiring")
|
||||
}
|
||||
};
|
||||
return (
|
||||
<section
|
||||
id={"free-recommendation-section"}
|
||||
className={
|
||||
"relative overflow-hidden section-wrapper bg-[url('/images/recommendation-bg.jpg')] bg-cover bg-no-repeat bg-center"
|
||||
}
|
||||
>
|
||||
<div className={"flex justify-center items-center p-16 max-sm:p-10"}>
|
||||
<div
|
||||
className={"bg-slate-950/35 w-full h-screen absolute top-0 left-0"}
|
||||
/>
|
||||
<div className="bg-white rounded-xl p-10 relative z-20 w-full max-w-[600px]">
|
||||
<h1 className="text-center text-2xl mb-2 font-bold pb-5">
|
||||
{t("Bepul maslahat uchun ma'lumotlaringizni kiriting")}
|
||||
</h1>
|
||||
<form
|
||||
action="w-full"
|
||||
className={"space-y-5"}
|
||||
onSubmit={onContactSubmit}
|
||||
>
|
||||
<Input
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
placeholder={t("Ismingiz")}
|
||||
/>
|
||||
<Input
|
||||
value={formatPhone(phone)}
|
||||
onChange={(e) => setPhone(formatPhone(e.target.value))}
|
||||
placeholder={t("Telefon raqamingiz")}
|
||||
/>
|
||||
<Button className={"w-full"} type="submit" disabled={isLoading}>
|
||||
{!isLoading ? t("Yuborish") : t("Yuborilmoqda")}
|
||||
</Button>
|
||||
</form>
|
||||
{isError && <p className="text-center text-red-500 mt-4">{t(isError)}</p>}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
export default FreeRecommendationSection;
|
||||
45
src/features/home/ui/hero-section.tsx
Normal file
@@ -0,0 +1,45 @@
|
||||
"use client"
|
||||
|
||||
import React from "react";
|
||||
import Image from "next/image";
|
||||
import { Button } from "@/shared/ui/button";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { useRouter } from "next/navigation";
|
||||
|
||||
const HeroSection = () => {
|
||||
const t = useTranslations("")
|
||||
const router = useRouter()
|
||||
return (
|
||||
<section className={"relative overflow-hidden"}>
|
||||
<div
|
||||
className={
|
||||
"bg-[url('/images/hero-bg.jpg')] flex justify-between items-center bg-cover bg-no-repeat bg-center h-[calc(100vh-4rem)]"
|
||||
}
|
||||
>
|
||||
<div
|
||||
className={"bg-slate-950/75 w-full h-screen absolute top-0 left-0"}
|
||||
/>
|
||||
<div className="my-container relative z-20 flex justify-between items-center max-lg:flex-col pt-24 px-4">
|
||||
<div className={"w-1/2 max-lg:w-full"}>
|
||||
<h1 className="section-title text-white">
|
||||
{t("Quyosh uskunalarini ulgurji narxlarda sotib oling!")}
|
||||
</h1>
|
||||
<p className="section-subtitle text-white">
|
||||
{t("Quyosh elektr stansiyasi uchun hamma narsani bir joyda va eng yaxshi narxda sotib oling")}
|
||||
</p>
|
||||
<Button onClick={() => router.push("/category")} className={"px-10 mt-5 z-30"}>
|
||||
{t("Batafsil")}
|
||||
</Button>
|
||||
</div>
|
||||
<Image
|
||||
src={"/hero-solar-panel.png"}
|
||||
alt={""}
|
||||
width={900}
|
||||
height={900}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
export default HeroSection;
|
||||
8
src/features/home/ui/index.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
export { default as HeroSection } from "@/features/home/ui/hero-section";
|
||||
export { default as AboutusSection } from "@/features/home/ui/aboutus-section";
|
||||
export { default as CategorySection } from "@/features/home/ui/category-section";
|
||||
export { default as FreeRecommendationSection } from "@/features/home/ui/free-recommendation-section";
|
||||
export { default as ProfitSection } from "@/features/home/ui/profit-section";
|
||||
export { default as PartnersSection } from "@/features/home/ui/partners-section";
|
||||
export { default as ContactSection } from "@/features/home/ui/contact-section";
|
||||
export { default as DownloadAppSection } from "@/features/home/ui/download-app-section";
|
||||
32
src/features/home/ui/invertor-section.tsx
Normal file
@@ -0,0 +1,32 @@
|
||||
import React from 'react'
|
||||
import {Button} from "@/shared/ui/button";
|
||||
import ProductCard from "@/shared/ui/product-card";
|
||||
import { useTranslations } from 'next-intl';
|
||||
|
||||
|
||||
const InvertorSection = () => {
|
||||
const t = useTranslations("")
|
||||
return (
|
||||
<section id={"invertor-section"} className={"my-container section-wrapper"}>
|
||||
<div className={"flex flex-col justify-center"}>
|
||||
<h1 className="section-title uppercase text-center pb-5">{t("Quyosh panellari")}</h1>
|
||||
<div>
|
||||
<div className={"grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4"}>
|
||||
<ProductCard/>
|
||||
<ProductCard/>
|
||||
<ProductCard/>
|
||||
<ProductCard/>
|
||||
<ProductCard/>
|
||||
<ProductCard/>
|
||||
<ProductCard/>
|
||||
<ProductCard/>
|
||||
</div>
|
||||
</div>
|
||||
<Button className={"mx-auto mt-10 px-16"}>
|
||||
{t("Hammasini ko'rish")}
|
||||
</Button>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
export default InvertorSection
|
||||
42
src/features/home/ui/partners-section.tsx
Normal file
@@ -0,0 +1,42 @@
|
||||
import React from "react";
|
||||
import Image from "next/image";
|
||||
import { BrandsResult } from "@/shared/types/brands";
|
||||
import { useTranslations } from "next-intl";
|
||||
import Link from "next/link";
|
||||
|
||||
interface Props {
|
||||
partners: BrandsResult[];
|
||||
}
|
||||
|
||||
const PartnersSection = ({ partners }: Props) => {
|
||||
const t = useTranslations("")
|
||||
return (
|
||||
<section
|
||||
id={"partners-section"}
|
||||
className={"my-container section-wrapper bg-white rounded-4xl"}
|
||||
>
|
||||
<div className={"flex flex-col justify-center"}>
|
||||
<h1 className="section-title uppercase text-center pb-5">
|
||||
{t("Hamkorlarimiz")}
|
||||
</h1>
|
||||
<div>
|
||||
<div
|
||||
className={"grid grid-cols-2 md:grid-cols-4 lg:grid-cols-5 gap-4"}
|
||||
>
|
||||
{partners.map((e, i) => (
|
||||
<Link href={`/brand/${e.id}`} key={i} className={"w-full h-full flex items-center justify-center"}>
|
||||
<Image
|
||||
src={e.image}
|
||||
alt={"category"}
|
||||
width={150}
|
||||
height={150}
|
||||
/>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
export default PartnersSection;
|
||||
31
src/features/home/ui/profit-section.tsx
Normal file
@@ -0,0 +1,31 @@
|
||||
import React from 'react'
|
||||
import Image from "next/image";
|
||||
import { useTranslations } from 'next-intl';
|
||||
|
||||
const ProfitSection = () => {
|
||||
const t = useTranslations("")
|
||||
return (
|
||||
<section id={"profit-section"} className={"py-12"}>
|
||||
<div className={"max-w-7xl mx-auto grid grid-cols-1 md:grid-cols-2 justify-between items-center py-10 gap-12"}>
|
||||
<div className={"relative w-full"}>
|
||||
<Image src={"/images/profit.png"} alt={"About us image"} layout="responsive" width={500} height={500}/>
|
||||
</div>
|
||||
<div className={"px-4"}>
|
||||
<p className="section-subtitle">
|
||||
{t("profit_1_desc")}
|
||||
</p>
|
||||
<br aria-hidden/>
|
||||
<p className="section-subtitle">
|
||||
{t("profit_2_desc")}
|
||||
</p>
|
||||
<br aria-hidden/>
|
||||
<p className="section-subtitle">
|
||||
{t("profit_3_desc")}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
export default ProfitSection;
|
||||
67
src/features/home/ui/sections-section.tsx
Normal file
@@ -0,0 +1,67 @@
|
||||
import React from "react";
|
||||
import { Link } from "@/shared/config/i18n/navigation";
|
||||
import {
|
||||
BlocksIcon,
|
||||
BookMarkedIcon,
|
||||
BoxesIcon,
|
||||
HandshakeIcon,
|
||||
} from "lucide-react";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
const SectionsSection = () => {
|
||||
const t = useTranslations("");
|
||||
return (
|
||||
<div className={"my-container section-wrapper"}>
|
||||
<div className={"grid grid-cols-4 max-md:grid-cols-2 gap-6 max-sm:gap-4"}>
|
||||
<Link href={"/category"} className={"block w-full"}>
|
||||
<div
|
||||
className={"bg-white p-12 w-full flex flex-col items-center gap-2"}
|
||||
>
|
||||
<BlocksIcon strokeWidth={1} size={64} className={"text-primary"} />
|
||||
<h1 className={"text-xl font-bold text-center"}>{t("Katalog")}</h1>
|
||||
</div>
|
||||
</Link>
|
||||
|
||||
<Link href={"/services"} className={"block w-full"}>
|
||||
<div
|
||||
className={"bg-white p-12 w-full flex flex-col items-center gap-2"}
|
||||
>
|
||||
<BoxesIcon strokeWidth={1} size={64} className={"text-primary"} />
|
||||
<h1 className={"text-xl font-bold text-center"}>
|
||||
{t("Xizmatlar")}
|
||||
</h1>
|
||||
</div>
|
||||
</Link>
|
||||
|
||||
<Link href={"/partners"} className={"block w-full"}>
|
||||
<div
|
||||
className={"bg-white p-12 w-full flex flex-col items-center gap-2"}
|
||||
>
|
||||
<HandshakeIcon
|
||||
strokeWidth={1}
|
||||
size={64}
|
||||
className={"text-primary"}
|
||||
/>
|
||||
<h1 className={"text-xl font-bold text-center"}>
|
||||
{t("Hamkorlik")}
|
||||
</h1>
|
||||
</div>
|
||||
</Link>
|
||||
|
||||
<Link href={"/useful"} className={"block w-full"}>
|
||||
<div
|
||||
className={"bg-white p-12 w-full flex flex-col items-center gap-2"}
|
||||
>
|
||||
<BookMarkedIcon
|
||||
strokeWidth={1}
|
||||
size={64}
|
||||
className={"text-primary"}
|
||||
/>
|
||||
<h1 className={"text-xl font-bold text-center"}>{t("Foydali")}</h1>
|
||||
</div>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
export default SectionsSection;
|
||||
74
src/features/partners/ui/partners-section/partner-modal.tsx
Normal file
@@ -0,0 +1,74 @@
|
||||
"use client"
|
||||
import {Partners} from "@/shared/types/partners";
|
||||
import {Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle} from "@/shared/ui/dialog";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectGroup,
|
||||
SelectItem,
|
||||
SelectLabel,
|
||||
SelectTrigger,
|
||||
SelectValue
|
||||
} from "@/shared/ui/select";
|
||||
import {Input} from "@/shared/ui/input";
|
||||
import {Textarea} from "@/shared/ui/textarea";
|
||||
import React from "react";
|
||||
import {Button} from "@/shared/ui/button";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
interface PartnerModalProps {
|
||||
selectedPartner: Partners | null;
|
||||
setSelectedPartner: (partner: Partners | null) => void;
|
||||
}
|
||||
|
||||
const PartnerModal = ({selectedPartner, setSelectedPartner}: PartnerModalProps) => {
|
||||
const t = useTranslations("")
|
||||
return (
|
||||
<Dialog open={!!selectedPartner} onOpenChange={
|
||||
(open) => {
|
||||
if (!open) {
|
||||
setSelectedPartner(null)
|
||||
}
|
||||
}
|
||||
}>
|
||||
<DialogContent className={"min-w-4/12"}>
|
||||
<DialogHeader>
|
||||
<DialogTitle>{selectedPartner?.name}</DialogTitle>
|
||||
<DialogDescription className={"space-y-5 mt-5"}>
|
||||
<Select>
|
||||
<SelectTrigger className={"w-full"}>
|
||||
<SelectValue placeholder={t("Viloyat")}/>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
<SelectLabel>{t("Viloyat")}</SelectLabel>
|
||||
<SelectItem value="pineapple">Pineapple</SelectItem>
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Select>
|
||||
<SelectTrigger className={"w-full"}>
|
||||
<SelectValue placeholder={t("Tuman")}/>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
<SelectLabel>{t("Tuman")}</SelectLabel>
|
||||
<SelectItem value="pineapple">Pineapple</SelectItem>
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
<Input placeholder={t("Telefon raqamingiz")}/>
|
||||
<Input placeholder={"full_name"}/>
|
||||
<Textarea placeholder="Type your message here."/>
|
||||
<div className={"text-end"}>
|
||||
<Button size={"lg"}>{t("Ariza yuborish")}</Button>
|
||||
</div>
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
|
||||
export default PartnerModal
|
||||
@@ -0,0 +1,52 @@
|
||||
"use client";
|
||||
import React, { useState } from "react";
|
||||
import { Partners } from "@/shared/types/partners";
|
||||
import Image from "next/image";
|
||||
import PartnerModal from "@/features/partners/ui/partners-section/partner-modal";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
interface PartnersSectionProps {
|
||||
partners: Partners[];
|
||||
}
|
||||
|
||||
const PartnersSection = ({ partners }: PartnersSectionProps) => {
|
||||
const t = useTranslations("")
|
||||
const [selectedPartner, setSelectedPartner] = useState<Partners | null>(null);
|
||||
return (
|
||||
<div className={"my-container section-wrapper min-h-[70vh]"}>
|
||||
<h1 className={"section-title text-center"}>{t("Hamkorlik")}</h1>
|
||||
<div
|
||||
className={
|
||||
"grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 mx-4 md:mx-0 gap-6 mt-12"
|
||||
}
|
||||
>
|
||||
{partners.map((partner) => (
|
||||
<div
|
||||
key={partner.id}
|
||||
onClick={() => {
|
||||
setSelectedPartner(partner);
|
||||
}}
|
||||
className={
|
||||
"cursor-pointer border hover:border-primary transition-all duration-400 bg-white p-12 w-full flex flex-col justify-center items-center gap-2 h-full"
|
||||
}
|
||||
>
|
||||
<Image
|
||||
src={partner.image}
|
||||
alt={partner.name}
|
||||
width={100}
|
||||
height={100}
|
||||
/>
|
||||
<h1 className={"text-xl font-bold text-center"}>{partner.name}</h1>
|
||||
</div>
|
||||
))}
|
||||
{selectedPartner && (
|
||||
<PartnerModal
|
||||
selectedPartner={selectedPartner!}
|
||||
setSelectedPartner={setSelectedPartner}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
export default PartnersSection;
|
||||
47
src/features/product-details/ui/buyForm.tsx
Normal file
@@ -0,0 +1,47 @@
|
||||
import { Product } from "@/shared/types/product";
|
||||
import { Button } from "@/shared/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogTrigger,
|
||||
} from "@/shared/ui/dialog";
|
||||
import { Tabs, TabsList, TabsTrigger } from "@/shared/ui/tabs";
|
||||
import React, { useState } from "react";
|
||||
import PhysicalTab from "./physicalTab";
|
||||
import LegalTab from "./legalTab";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
interface Props {
|
||||
product: Product;
|
||||
}
|
||||
|
||||
const BuyForm = ({ product }: Props) => {
|
||||
const t = useTranslations("")
|
||||
const [isDialogOpen, setIsDialogOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button className="mt-8 px-16" onClick={() => setIsDialogOpen(true)}>
|
||||
{t("Sotib olish")}
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="sm:max-w-6/12 max-h-[90vh] overflow-y-auto">
|
||||
<Tabs defaultValue="physical" className="w-full mt-4">
|
||||
<TabsList className="grid w-full grid-cols-2">
|
||||
<TabsTrigger value="physical">{t("Jismoniy shaxs")}</TabsTrigger>
|
||||
<TabsTrigger value="legal">{t("Yuridik shaxs")}</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
{/* Physical */}
|
||||
<PhysicalTab product={product} setIsDialogOpen={setIsDialogOpen} />
|
||||
|
||||
{/* Legal */}
|
||||
<LegalTab product={product} setIsDialogOpen={setIsDialogOpen} />
|
||||
</Tabs>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default BuyForm;
|
||||
272
src/features/product-details/ui/legalTab.tsx
Normal file
@@ -0,0 +1,272 @@
|
||||
import { getRegions } from "@/shared/api/regionSvc";
|
||||
import { createUserOrder } from "@/shared/api/userOrdersSvc";
|
||||
import formatPhone from "@/shared/lib/formatPhone";
|
||||
import { Product } from "@/shared/types/product";
|
||||
import { Button } from "@/shared/ui/button";
|
||||
import { DialogFooter } from "@/shared/ui/dialog";
|
||||
import { Input } from "@/shared/ui/input";
|
||||
import { Label } from "@/shared/ui/label";
|
||||
import { RadioGroup, RadioGroupItem } from "@/shared/ui/radio-group";
|
||||
import { TabsContent } from "@/shared/ui/tabs";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { Form, Formik } from "formik";
|
||||
import { Loader2 } from "lucide-react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import React, { useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
|
||||
interface Props {
|
||||
product: Product;
|
||||
setIsDialogOpen: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
}
|
||||
|
||||
const LegalTab = ({ product, setIsDialogOpen }: Props) => {
|
||||
const t = useTranslations("")
|
||||
const { data: regions } = useQuery({
|
||||
queryKey: ["getRegions"],
|
||||
queryFn: getRegions,
|
||||
});
|
||||
|
||||
const [isOrderCreating, setIsOrderCreating] = useState(false);
|
||||
const [corpDistricts, setCorpDistricts] = useState<
|
||||
{ id: number; name: string }[] | null
|
||||
>(null);
|
||||
|
||||
const handleCorpRegionChange = (regionId: number) => {
|
||||
const selectedRegion = regions?.data?.find(
|
||||
(region) => region.id === regionId
|
||||
);
|
||||
setCorpDistricts(selectedRegion?.cities || []);
|
||||
};
|
||||
|
||||
return (
|
||||
<Formik
|
||||
initialValues={{
|
||||
phone: "+998 ",
|
||||
director_full_name: "",
|
||||
company_name: "",
|
||||
inn: "",
|
||||
bank_name: "",
|
||||
mfo: "",
|
||||
oked: "",
|
||||
payment_account: "",
|
||||
address: "",
|
||||
home: "",
|
||||
landmark: "",
|
||||
city_id: "",
|
||||
branch_id: 1,
|
||||
with_installation: true,
|
||||
delivery_type: "delivery",
|
||||
payment_type: "bank",
|
||||
with_didox: true,
|
||||
products: [{ id: product.id, count: 1 }],
|
||||
}}
|
||||
onSubmit={async (values, helpers) => {
|
||||
setIsOrderCreating(true);
|
||||
let payload = {
|
||||
branch_id: 1,
|
||||
type: "ready_solutions",
|
||||
delivery_type: values.delivery_type,
|
||||
client_type: "legal",
|
||||
client_information: {
|
||||
director_full_name: values.director_full_name,
|
||||
company_name: values.company_name,
|
||||
inn: values.inn,
|
||||
bank_name: values.bank_name,
|
||||
mfo: values.mfo,
|
||||
oked: values.oked,
|
||||
payment_account: values.payment_account,
|
||||
address: values.address,
|
||||
email: "",
|
||||
phone: Number(values.phone.replace(/\D/g, "")),
|
||||
},
|
||||
address: {
|
||||
city_id: Number(values.city_id),
|
||||
address: values.address,
|
||||
home: values.home,
|
||||
landmark: values.landmark,
|
||||
},
|
||||
with_installation: values.with_installation,
|
||||
payment_type: values.payment_type,
|
||||
with_didox: values.with_didox,
|
||||
products: values.products,
|
||||
};
|
||||
|
||||
try {
|
||||
await createUserOrder(payload);
|
||||
toast.success(t("Buyurtma muvaffaqiyatli yaratildi!"), {
|
||||
description: t("Siz bilan tez orada bog'lanamiz"),
|
||||
});
|
||||
setIsDialogOpen(false);
|
||||
setIsOrderCreating(false);
|
||||
} catch (e: any) {
|
||||
toast.error(t("Buyurtma yaratishda xatolik!"), {
|
||||
description:
|
||||
t("Ma'lumotlar to'liq kiritilganligiga ishonch hosil qiling yoki qaytadan urinib ko'ring"),
|
||||
});
|
||||
setIsOrderCreating(false);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{(formikProps) => (
|
||||
<Form>
|
||||
{/* Legal */}
|
||||
<TabsContent value="legal" className="space-y-4 mt-4">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<Input
|
||||
placeholder={t("Kompaniya nomi")}
|
||||
onChange={formikProps.handleChange("company_name")}
|
||||
/>
|
||||
<Input
|
||||
placeholder={t("Direktor")}
|
||||
onChange={formikProps.handleChange("director_full_name")}
|
||||
/>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<Input
|
||||
placeholder={t("Yuridik manzil")}
|
||||
onChange={formikProps.handleChange("address")}
|
||||
/>
|
||||
<Input
|
||||
placeholder={t("Telefon raqam")}
|
||||
onChange={(e) =>
|
||||
formikProps.setFieldValue(
|
||||
"phone",
|
||||
formatPhone(e.target.value)
|
||||
)
|
||||
}
|
||||
value={formikProps.values.phone}
|
||||
/>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<Input
|
||||
placeholder="INN"
|
||||
onChange={(e) =>
|
||||
formikProps.setFieldValue(
|
||||
"inn",
|
||||
e.target.value.replace(/\D/g, "")
|
||||
)
|
||||
}
|
||||
value={formikProps.values.inn}
|
||||
/>
|
||||
<Input
|
||||
placeholder={t("Bank nomi")}
|
||||
onChange={formikProps.handleChange("bank_name")}
|
||||
/>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<Input
|
||||
placeholder="MFO"
|
||||
onChange={formikProps.handleChange("mfo")}
|
||||
/>
|
||||
<Input
|
||||
placeholder="OKED"
|
||||
onChange={formikProps.handleChange("oked")}
|
||||
/>
|
||||
</div>
|
||||
<Input placeholder={t("Hisob raqam")} onChange={formikProps.handleChange("payment_account")}/>
|
||||
<Label className={"mb-2"}>{t("Yetkazib berish")}</Label>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<select
|
||||
onChange={(e) => handleCorpRegionChange(Number(e.target.value))}
|
||||
className="w-full p-2 rounded-md border"
|
||||
>
|
||||
<option value="">{t("Viloyatni tanlang")}</option>
|
||||
{regions?.data.map((region) => (
|
||||
<option key={region.id} value={region.id}>
|
||||
{region.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
|
||||
<select
|
||||
onChange={formikProps.handleChange("city_id")}
|
||||
className="w-full p-2 rounded-md border"
|
||||
disabled={!corpDistricts}
|
||||
>
|
||||
<option value="">{t("Tuman/shahar")}</option>
|
||||
{corpDistricts?.map((district) => (
|
||||
<option key={district.id} value={district.id}>
|
||||
{district.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<Input
|
||||
placeholder={t("Manzil")}
|
||||
onChange={formikProps.handleChange("address")}
|
||||
/>
|
||||
<Input
|
||||
placeholder={t("Uy raqami")}
|
||||
onChange={formikProps.handleChange("home")}
|
||||
/>
|
||||
<Input
|
||||
placeholder={t("Mo'ljal")}
|
||||
onChange={formikProps.handleChange("landmark")}
|
||||
/>
|
||||
<div>
|
||||
<Label className="mb-2 block">{t("O‘rnatish xizmati kerakmi?")}</Label>
|
||||
<RadioGroup
|
||||
onValueChange={(value) =>
|
||||
formikProps.setFieldValue(
|
||||
"with_installation",
|
||||
value === "yes"
|
||||
)
|
||||
}
|
||||
defaultValue="yes"
|
||||
className="flex gap-4"
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<RadioGroupItem value="yes" id="c-install-yes" />
|
||||
<Label htmlFor="c-install-yes">{t("Ha")}</Label>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<RadioGroupItem value="no" id="c-install-no" />
|
||||
<Label htmlFor="c-install-no">{t("Yo‘q")}</Label>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label className="mb-2 block">{t("Yetkazib berish kerakmi")}</Label>
|
||||
<RadioGroup
|
||||
onValueChange={(value) =>
|
||||
formikProps.setFieldValue("delivery_type", value)
|
||||
}
|
||||
defaultValue="delivery"
|
||||
className="flex gap-4"
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<RadioGroupItem value="delivery" id="c-install-yes" />
|
||||
<Label htmlFor="c-install-yes">{t("Ha")}</Label>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<RadioGroupItem value="pickup" id="c-install-no" />
|
||||
<Label htmlFor="c-install-no">{t("Yo‘q o'zim olib ketaman")}</Label>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
</div>
|
||||
|
||||
<DialogFooter className="mt-4">
|
||||
<Button
|
||||
size="lg"
|
||||
type="submit"
|
||||
className="w-full"
|
||||
disabled={isOrderCreating}
|
||||
>
|
||||
{isOrderCreating ? (
|
||||
<Loader2 className="animate-spin" />
|
||||
) : (
|
||||
t("Yuborish")
|
||||
)}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
|
||||
</TabsContent>
|
||||
</Form>
|
||||
)}
|
||||
</Formik>
|
||||
);
|
||||
};
|
||||
|
||||
export default LegalTab;
|
||||
247
src/features/product-details/ui/physicalTab.tsx
Normal file
@@ -0,0 +1,247 @@
|
||||
import { getRegions } from "@/shared/api/regionSvc";
|
||||
import { createUserOrder } from "@/shared/api/userOrdersSvc";
|
||||
import formatPhone from "@/shared/lib/formatPhone";
|
||||
import { Product } from "@/shared/types/product";
|
||||
import { Button } from "@/shared/ui/button";
|
||||
import { DialogFooter } from "@/shared/ui/dialog";
|
||||
import { Input } from "@/shared/ui/input";
|
||||
import { Label } from "@/shared/ui/label";
|
||||
import { RadioGroup, RadioGroupItem } from "@/shared/ui/radio-group";
|
||||
import { TabsContent } from "@/shared/ui/tabs";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { Form, Formik } from "formik";
|
||||
import { Loader2 } from "lucide-react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import React, { useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
|
||||
interface Props {
|
||||
product: Product;
|
||||
setIsDialogOpen: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
}
|
||||
|
||||
const PhysicalTab = ({ product, setIsDialogOpen }: Props) => {
|
||||
const t = useTranslations("")
|
||||
const { data: regions } = useQuery({
|
||||
queryKey: ["getRegions"],
|
||||
queryFn: getRegions,
|
||||
});
|
||||
|
||||
const [isOrderCreating, setIsOrderCreating] = useState(false);
|
||||
const [districts, setDistricts] = useState<
|
||||
{ id: number; name: string }[] | null
|
||||
>(null);
|
||||
const handleRegionChange = (regionId: number) => {
|
||||
const selectedRegion = regions?.data?.find(
|
||||
(region) => region.id === regionId
|
||||
);
|
||||
setDistricts(selectedRegion?.cities || []);
|
||||
};
|
||||
|
||||
return (
|
||||
<Formik
|
||||
initialValues={{
|
||||
full_name: "",
|
||||
phone: "+998 ",
|
||||
jshir: "",
|
||||
series: "",
|
||||
address: "",
|
||||
home: "",
|
||||
landmark: "",
|
||||
city_id: "",
|
||||
branch_id: 1,
|
||||
with_installation: true,
|
||||
delivery_type: "delivery",
|
||||
payment_type: "bank",
|
||||
with_didox: true,
|
||||
products: [{ id: product.id, count: 1 }],
|
||||
}}
|
||||
onSubmit={async (values, helpers) => {
|
||||
setIsOrderCreating(true);
|
||||
let payload = {
|
||||
branch_id: 1,
|
||||
series: 1,
|
||||
type: "ready_solutions",
|
||||
delivery_type: values.delivery_type,
|
||||
client_type: "physical",
|
||||
client_information: {
|
||||
full_name: values.full_name,
|
||||
jshir: values.jshir,
|
||||
series: values.series,
|
||||
phone: Number(values.phone.replace(/\D/g, "")),
|
||||
},
|
||||
address: {
|
||||
city_id: Number(values.city_id),
|
||||
address: values.address,
|
||||
home: values.home,
|
||||
landmark: values.landmark,
|
||||
},
|
||||
with_installation: values.with_installation,
|
||||
payment_type: values.payment_type,
|
||||
with_didox: values.with_didox,
|
||||
products: values.products,
|
||||
};
|
||||
|
||||
try {
|
||||
await createUserOrder(payload);
|
||||
toast.success(t("Buyurtma muvaffaqiyatli yaratildi!"), {
|
||||
description: t("Siz bilan tez orada bog'lanamiz"),
|
||||
});
|
||||
setIsDialogOpen(false);
|
||||
setIsOrderCreating(false);
|
||||
} catch (e: any) {
|
||||
toast.error(t("Buyurtma yaratishda xatolik!"), {
|
||||
description:
|
||||
t("Ma'lumotlar to'liq kiritilganligiga ishonch hosil qiling yoki qaytadan urinib ko'ring"),
|
||||
});
|
||||
setIsOrderCreating(false);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{(formikProps) => (
|
||||
<Form>
|
||||
{/* Physical */}
|
||||
<TabsContent value="physical" className="space-y-4 mt-4">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<Input
|
||||
placeholder={t("full_name")}
|
||||
onChange={formikProps.handleChange("full_name")}
|
||||
/>
|
||||
{formikProps.errors.full_name && (
|
||||
<p className="text-red-500 text-sm">
|
||||
{formikProps.errors.full_name}
|
||||
</p>
|
||||
)}
|
||||
<Input
|
||||
placeholder={t("Telefon raqam")}
|
||||
onChange={(e) =>
|
||||
formikProps.setFieldValue(
|
||||
"phone",
|
||||
formatPhone(e.target.value)
|
||||
)
|
||||
}
|
||||
value={formikProps.values.phone}
|
||||
/>
|
||||
</div>
|
||||
<div className={"grid grid-cols-2 gap-4"}>
|
||||
<Input
|
||||
placeholder="JShShIR"
|
||||
value={formikProps.values.jshir}
|
||||
onChange={(e) =>
|
||||
formikProps.setFieldValue(
|
||||
"jshir",
|
||||
e.target.value.replace(/\D/g, "")
|
||||
)
|
||||
}
|
||||
/>
|
||||
<Input
|
||||
placeholder={t("Passport seriya va raqami")}
|
||||
onChange={formikProps.handleChange("series")}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Label className={"mb-2"}>{t("Yetkazib berish")}</Label>
|
||||
<div className={"grid grid-cols-2 gap-4"}>
|
||||
<select
|
||||
onChange={(e) => handleRegionChange(Number(e.target.value))}
|
||||
className="w-full p-2 rounded-md border"
|
||||
>
|
||||
<option value="">{t("Viloyatni tanlang")}</option>
|
||||
{regions?.data?.map((region) => (
|
||||
<option key={region.id} value={region.id}>
|
||||
{region.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
|
||||
<select
|
||||
onChange={formikProps.handleChange("city_id")}
|
||||
className="w-full p-2 rounded-md border"
|
||||
disabled={!districts}
|
||||
>
|
||||
<option value="">{t("Tuman/shahar")}</option>
|
||||
{districts?.map((district) => (
|
||||
<option key={district.id} value={district.id}>
|
||||
{district.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<Input
|
||||
placeholder={t("Manzil")}
|
||||
onChange={formikProps.handleChange("address")}
|
||||
/>
|
||||
<Input
|
||||
placeholder={t("Uy raqami")}
|
||||
onChange={formikProps.handleChange("home")}
|
||||
/>
|
||||
<Input
|
||||
placeholder={t("Mo'ljal")}
|
||||
onChange={formikProps.handleChange("landmark")}
|
||||
/>
|
||||
|
||||
<div>
|
||||
<Label className="mb-2 block">{t("O‘rnatish xizmati kerakmi?")}</Label>
|
||||
<RadioGroup
|
||||
onValueChange={(value) =>
|
||||
formikProps.setFieldValue(
|
||||
"with_installation",
|
||||
value === "yes"
|
||||
)
|
||||
}
|
||||
defaultValue="yes"
|
||||
className="flex gap-4"
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<RadioGroupItem value="yes" id="install-yes" />
|
||||
<Label htmlFor="install-yes">{t("Ha")}</Label>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<RadioGroupItem value="no" id="install-no" />
|
||||
<Label htmlFor="install-no">{t("Yo‘q")}</Label>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
</div>
|
||||
<div>
|
||||
<Label className="mb-2 block">{t("Yetkazib berish kerakmi?")}</Label>
|
||||
<RadioGroup
|
||||
onValueChange={(value) =>
|
||||
formikProps.setFieldValue("delivery_type", value)
|
||||
}
|
||||
defaultValue="yes"
|
||||
className="flex gap-4"
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<RadioGroupItem value="delivery" id="c-install-yes" />
|
||||
<Label htmlFor="c-install-yes">{t("Ha")}</Label>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<RadioGroupItem value="pickup" id="c-install-no" />
|
||||
<Label htmlFor="c-install-no">{t("Yo‘q o'zim olib ketaman")}</Label>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
</div>
|
||||
|
||||
<DialogFooter className="mt-4">
|
||||
<Button
|
||||
size="lg"
|
||||
type="submit"
|
||||
className="w-full"
|
||||
disabled={isOrderCreating}
|
||||
>
|
||||
{isOrderCreating ? (
|
||||
<Loader2 className="animate-spin" />
|
||||
) : (
|
||||
t("Yuborish")
|
||||
)}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
|
||||
</TabsContent>
|
||||
</Form>
|
||||
)}
|
||||
</Formik>
|
||||
);
|
||||
};
|
||||
|
||||
export default PhysicalTab;
|
||||
82
src/features/product-details/ui/product-details-section.tsx
Normal file
@@ -0,0 +1,82 @@
|
||||
"use client";
|
||||
|
||||
import React from "react";
|
||||
import Image from "next/image";
|
||||
import { Button } from "@/shared/ui/button";
|
||||
import { Product } from "@/shared/types/product";
|
||||
import formatNumberWithSpaces from "@/shared/lib/formatNumberWithSpace";
|
||||
import { useAuthStore } from "@/shared/store/authStore";
|
||||
import { Link } from "@/shared/config/i18n/navigation";
|
||||
import BuyForm from "./buyForm";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
interface ProductDetailsSectionProps {
|
||||
product: Product;
|
||||
}
|
||||
|
||||
const ProductDetailsSection = ({ product }: ProductDetailsSectionProps) => {
|
||||
const { isAuthenticated } = useAuthStore();
|
||||
const t = useTranslations("")
|
||||
|
||||
return (
|
||||
<div className="my-container px-4">
|
||||
<section className="my-container">
|
||||
<div className="flex flex-col justify-center">
|
||||
<div className="p-24 grid grid-cols-2 max-md:grid-cols-1 items-center max-md:p-0">
|
||||
<div className="flex items-center justify-center">
|
||||
<Image
|
||||
className="w-10/12 max-md:w-full rounded-4xl"
|
||||
src={product.poster}
|
||||
alt={product.name}
|
||||
width={200}
|
||||
height={200}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="font-bold text-4xl leading-relaxed">
|
||||
{product.name}
|
||||
</h2>
|
||||
<div
|
||||
className="my-5"
|
||||
dangerouslySetInnerHTML={{ __html: product.short_description! }}
|
||||
/>
|
||||
<div className="gap-5">
|
||||
<span className="text-3xl font-bold">
|
||||
{formatNumberWithSpaces(product.price)} {t("so'm")}
|
||||
<span className="text-base font-light"> {t("QQS bilan")}</span>
|
||||
</span>
|
||||
<span className="block">
|
||||
{formatNumberWithSpaces(product.price_usd)} y.e.
|
||||
</span>
|
||||
{product.discount_percent > 0 && (
|
||||
<>
|
||||
<span className="text-xl font-bold text-red-500 line-through">
|
||||
{formatNumberWithSpaces(product.price_discount)} {t("so'm")}
|
||||
</span>
|
||||
<span className="text-xl font-bold text-red-500">
|
||||
{product.discount_percent}% {t("chegirma")}
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{isAuthenticated ? (
|
||||
<BuyForm product={product} />
|
||||
) : (
|
||||
<Button className="mt-8 px-16" asChild>
|
||||
<Link href={"/auth/login"}>{t("Sotib olish")}</Link>
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="text-base flex flex-col justify-center items-center mb-24 mt-12"
|
||||
dangerouslySetInnerHTML={{ __html: product.description! }}
|
||||
/>
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
export default ProductDetailsSection;
|
||||
30
src/features/product-details/ui/related-products-section.tsx
Normal file
@@ -0,0 +1,30 @@
|
||||
import React from 'react'
|
||||
import ProductCard from "@/shared/ui/product-card";
|
||||
import {Product} from "@/shared/types/product";
|
||||
import { useTranslations } from 'next-intl';
|
||||
|
||||
interface RelatedProductsSectionProps {
|
||||
products: Product[]
|
||||
}
|
||||
|
||||
const RelatedProductsSection = ({products}: RelatedProductsSectionProps) => {
|
||||
const t = useTranslations("")
|
||||
return (
|
||||
<div className={"bg-background section-wrapper"}>
|
||||
<div className={"my-container"}>
|
||||
<h1 className="section-title uppercase text-center pb-5">{t("Boshqa mahsulotlar")}</h1>
|
||||
<div className={"mb-24 grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4"}>
|
||||
{
|
||||
products.map((product) => (
|
||||
<ProductCard
|
||||
key={product.id}
|
||||
product={product}
|
||||
/>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default RelatedProductsSection
|
||||
125
src/features/profile/ui/applications-section.tsx
Normal file
@@ -0,0 +1,125 @@
|
||||
"use client"
|
||||
import React, {useState} from 'react'
|
||||
import {Card, CardContent, CardDescription, CardTitle} from "@/shared/ui/card";
|
||||
import {Separator} from "@/shared/ui/separator";
|
||||
import {Badge} from "@/shared/ui/badge";
|
||||
import {Accordion, AccordionContent, AccordionItem, AccordionTrigger} from "@/shared/ui/accordion";
|
||||
import {useQuery} from "@tanstack/react-query";
|
||||
import {getUserRequests, getUserRequestsById} from "@/shared/api/userRequestsSvc";
|
||||
import Loader from "@/shared/ui/loader";
|
||||
import { useTranslations } from 'next-intl';
|
||||
|
||||
const ApplicationsSections = () => {
|
||||
const t = useTranslations("")
|
||||
const [selectedRequest, setSelectedRequest] = useState<number>(0)
|
||||
const {data: requests, isLoading: requestsIsLoading} = useQuery({
|
||||
queryKey: ["getUserRequests"],
|
||||
queryFn: getUserRequests,
|
||||
})
|
||||
|
||||
const {data: requestDetails} = useQuery({
|
||||
queryKey: ["getUserRequestsById", selectedRequest],
|
||||
queryFn: () => getUserRequestsById(selectedRequest),
|
||||
enabled: !!selectedRequest,
|
||||
})
|
||||
return (
|
||||
<div className={"profile-section-wrapper"}>
|
||||
<h1 className={"profile-section-title"}>{t("Mening arizalarim")}</h1>
|
||||
<span className={"profile-section-subtitle"}>
|
||||
{t("Sizning arizalaringiz va ularning holati haqida ma'lumotlar")}
|
||||
</span>
|
||||
<div className={"mt-4 space-y-4"}>
|
||||
{
|
||||
requestsIsLoading ? <Loader height={"h-[30vh]"}/> :requests?.data?.map(request => (
|
||||
<Accordion onClick={
|
||||
() => setSelectedRequest(request.id)
|
||||
} type="single" className={"border px-5 rounded-xl"} collapsible>
|
||||
<AccordionItem value="item-1">
|
||||
<AccordionTrigger className={" cursor-pointer"}>
|
||||
<div className={"w-full"}>
|
||||
<div className={"flex justify-between items-center"}>
|
||||
<div>
|
||||
<CardTitle className={"text-lg"}>{t("Ariza")} #{request.id}</CardTitle>
|
||||
<CardDescription>{t("Yaratilish vaqti")}: {request.created_at}</CardDescription>
|
||||
</div>
|
||||
<Badge style={{ backgroundColor: request.status.bg_color, color: request.status.font_color }}>
|
||||
{request.status.translation}
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
</AccordionTrigger>
|
||||
<AccordionContent>
|
||||
<Card key={request.id} className={"shadow-none border-none p-0 mt-5 rounded-none"}>
|
||||
<CardContent className={"p-0"}>
|
||||
<CardTitle>{t("Ariza tafsilotlari")}</CardTitle>
|
||||
<div className={"mt-5 space-y-3"}>
|
||||
<div className={"flex justify-between items-center"}>
|
||||
<CardDescription>{t("Ariza turi")}</CardDescription>
|
||||
<CardDescription>{requestDetails?.data.service.name}</CardDescription>
|
||||
</div>
|
||||
<div className={"flex justify-between items-center"}>
|
||||
<CardDescription>{t("Ariza raqami")}</CardDescription>
|
||||
<CardDescription>#{requestDetails?.data.id}</CardDescription>
|
||||
</div>
|
||||
<div className={"flex justify-between items-center"}>
|
||||
<CardDescription>{t("Ariza holati")}</CardDescription>
|
||||
<Badge style={{ backgroundColor: request.status.bg_color, color: request.status.font_color }}>
|
||||
{request.status.translation}
|
||||
</Badge>
|
||||
</div>
|
||||
<div className={"flex justify-between items-center"}>
|
||||
<CardDescription>{t("Yaratilish vaqti")}</CardDescription>
|
||||
<CardDescription>{
|
||||
request.created_at
|
||||
}</CardDescription>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
<Separator/>
|
||||
<CardContent className={"p-0"}>
|
||||
<CardTitle>{t("Xizmat tafsilotlari")}</CardTitle>
|
||||
<div className={"mt-5 space-y-3"}>
|
||||
<div className={"flex justify-between items-center"}>
|
||||
<CardDescription>{t("Quvvat")}</CardDescription>
|
||||
<CardDescription>
|
||||
{requestDetails?.data.power.name}
|
||||
</CardDescription>
|
||||
</div>
|
||||
<div className={"flex justify-between items-center"}>
|
||||
<CardDescription>{t("Manzil")}</CardDescription>
|
||||
<CardDescription>
|
||||
{requestDetails?.data.city.name}, {" "}
|
||||
{requestDetails?.data.city.region.name}
|
||||
</CardDescription>
|
||||
</div>
|
||||
<div className={"flex justify-between items-center"}>
|
||||
<CardDescription>{t("full_name")}</CardDescription>
|
||||
<CardDescription>{
|
||||
requestDetails?.data.full_name
|
||||
}</CardDescription>
|
||||
</div>
|
||||
<div className={"flex justify-between items-center"}>
|
||||
<CardDescription>{t("Telefon raqami")}</CardDescription>
|
||||
<CardDescription>{
|
||||
requestDetails?.data.phone
|
||||
}</CardDescription>
|
||||
</div>
|
||||
<div className={"flex justify-between items-center"}>
|
||||
<CardDescription>{t("Izoh")}</CardDescription>
|
||||
<CardDescription>
|
||||
{requestDetails?.data.comment}
|
||||
</CardDescription>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default ApplicationsSections
|
||||
58
src/features/profile/ui/contact-section.tsx
Normal file
@@ -0,0 +1,58 @@
|
||||
"use client";
|
||||
import React from "react";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { getFeedback } from "@/shared/api/feedbackSvc";
|
||||
import Link from "next/link";
|
||||
import Loader from "@/shared/ui/loader";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
const ContactSection = () => {
|
||||
const t = useTranslations("");
|
||||
const { data: feedback, isLoading: feedbackIsLoading } = useQuery({
|
||||
queryKey: ["getFeedback"],
|
||||
queryFn: getFeedback,
|
||||
});
|
||||
return (
|
||||
<div className={"profile-section-wrapper"}>
|
||||
<h1 className={"profile-section-title"}>{t("Bog'lanish")}</h1>
|
||||
<span className={"profile-section-subtitle"}>
|
||||
{t(
|
||||
"Biz bilan bog'lanish uchun quyidagi usullardan foydalanishingiz mumkin"
|
||||
)}
|
||||
</span>
|
||||
<div>
|
||||
{feedbackIsLoading ? (
|
||||
<Loader height={"h-[30vh]"} />
|
||||
) : (
|
||||
<div className={"grid grid-cols-2 items-center justify-between my-4"}>
|
||||
<div className={"flex flex-col"}>
|
||||
<span className={"text-lg font-bold"}>{t("Call Center")}</span>
|
||||
<Link
|
||||
className={"w-fit text-primary"}
|
||||
href={`tel:${feedback?.data.phone}`}
|
||||
>
|
||||
{feedback?.data.phone}
|
||||
</Link>
|
||||
</div>
|
||||
<div className={"flex flex-col"}>
|
||||
<span className={"text-lg font-bold"}>Telegram</span>
|
||||
<Link
|
||||
className={"w-fit text-primary"}
|
||||
href={feedback?.data.telegram_support || ""}
|
||||
>
|
||||
{feedback?.data.telegram_support}
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{/*<div className={"space-x-4"}>*/}
|
||||
{/* <Button className={""} size={"lg"}><InstagramIcon/> @Instagram</Button>*/}
|
||||
{/* <Button className={""} size={"lg"}><YoutubeIcon/> @Instagram</Button>*/}
|
||||
{/* <Button className={""} size={"lg"}><InstagramIcon/> @Instagram</Button>*/}
|
||||
{/* <Button className={""} size={"lg"}><YoutubeIcon/> @Instagram</Button>*/}
|
||||
{/*</div>*/}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
export default ContactSection;
|
||||
98
src/features/profile/ui/information-section.tsx
Normal file
@@ -0,0 +1,98 @@
|
||||
"use client";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { Input } from "@/shared/ui/input";
|
||||
import { Button } from "@/shared/ui/button";
|
||||
import { useQuery, useMutation } from "@tanstack/react-query";
|
||||
import { getUserMe, updateUserMe } from "@/shared/api/userMeSvc";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
const InformationSection = () => {
|
||||
const t = useTranslations("");
|
||||
const { data: user } = useQuery({
|
||||
queryKey: ["getUserMe"],
|
||||
queryFn: getUserMe,
|
||||
});
|
||||
|
||||
const [formData, setFormData] = useState({
|
||||
first_name: "",
|
||||
last_name: "",
|
||||
middle_name: "",
|
||||
phone: "",
|
||||
gender: true,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (user?.data) {
|
||||
setFormData({
|
||||
first_name: user.data.first_name || "",
|
||||
last_name: user.data.last_name || "",
|
||||
middle_name: user.data.middle_name || "",
|
||||
phone: user.data.phone || "",
|
||||
gender: true,
|
||||
});
|
||||
}
|
||||
}, [user]);
|
||||
|
||||
const mutation = useMutation({
|
||||
mutationFn: updateUserMe,
|
||||
onError: () => {
|
||||
alert(t("Xatolik yuz berdi"));
|
||||
},
|
||||
});
|
||||
|
||||
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const { name, value } = e.target;
|
||||
setFormData((prev) => ({ ...prev, [name]: value }));
|
||||
};
|
||||
|
||||
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
mutation.mutate(formData);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={"profile-section-wrapper"}>
|
||||
<h1 className={"profile-section-title"}>{t("Profil ma'lumotlari")}</h1>
|
||||
<span className={"profile-section-subtitle"}>
|
||||
{t("Sizning profil ma'lumotlaringiz va ularni o'zgartirish")}
|
||||
</span>
|
||||
<form onSubmit={handleSubmit} className={"space-y-5 mt-5 text-end"}>
|
||||
<div className={"grid grid-cols-2 gap-5"}>
|
||||
<Input
|
||||
name="first_name"
|
||||
placeholder={t("Ismingiz")}
|
||||
value={formData.first_name}
|
||||
onChange={handleChange}
|
||||
required
|
||||
/>
|
||||
<Input
|
||||
name="last_name"
|
||||
placeholder={t("Familiyangiz")}
|
||||
value={formData.last_name}
|
||||
onChange={handleChange}
|
||||
required
|
||||
/>
|
||||
<Input
|
||||
name="middle_name"
|
||||
placeholder={t("Sharif")}
|
||||
value={formData.middle_name}
|
||||
onChange={handleChange}
|
||||
required
|
||||
/>
|
||||
<Input
|
||||
name="phone"
|
||||
placeholder={t("Telefon raqam")}
|
||||
value={formData.phone}
|
||||
onChange={handleChange}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<Button size={"lg"} type="submit" disabled={mutation.isPending}>
|
||||
{mutation.isPending ? t("Saqlanmoqda") : t("Saqlash")}
|
||||
</Button>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default InformationSection;
|
||||
154
src/features/profile/ui/orders-section.tsx
Normal file
@@ -0,0 +1,154 @@
|
||||
import React, {useState} from 'react'
|
||||
import {Card, CardContent, CardDescription, CardFooter, CardTitle} from "@/shared/ui/card";
|
||||
import {Separator} from "@/shared/ui/separator";
|
||||
import {Badge} from "@/shared/ui/badge";
|
||||
import {Button} from "@/shared/ui/button";
|
||||
import {Accordion, AccordionContent, AccordionItem, AccordionTrigger} from "@/shared/ui/accordion";
|
||||
import {useQuery} from "@tanstack/react-query";
|
||||
import {getUserOrders, getUserOrdersById} from "@/shared/api/userOrdersSvc";
|
||||
import formatNumberWithSpaces from "@/shared/lib/formatNumberWithSpace";
|
||||
import Link from "next/link";
|
||||
import Loader from "@/shared/ui/loader";
|
||||
import { useTranslations } from 'next-intl';
|
||||
|
||||
const OrdersSection = () => {
|
||||
const t = useTranslations("")
|
||||
const [selectedOrder, setSelectedOrder] = useState<number>(0)
|
||||
const {data: orders, isLoading: ordersIsLoading} = useQuery({
|
||||
queryKey: ["getUserOrders"],
|
||||
queryFn: getUserOrders,
|
||||
})
|
||||
|
||||
const {data: orderDetails} = useQuery({
|
||||
queryKey: ["getUserOrdersById", selectedOrder],
|
||||
queryFn: () => getUserOrdersById(selectedOrder),
|
||||
enabled: !!selectedOrder,
|
||||
})
|
||||
|
||||
return (
|
||||
<div className={"profile-section-wrapper"}>
|
||||
<h1 className={"profile-section-title"}>{t("Mening buyurtmalarim")}</h1>
|
||||
<span className={"profile-section-subtitle"}>
|
||||
{t("Sizning buyurtmalaringiz va ularning holati haqida ma'lumotlar")}
|
||||
</span>
|
||||
<div className={"mt-4 space-y-4"}>
|
||||
{ordersIsLoading ? <Loader height={"h-[30vh]"}/>:
|
||||
orders?.data?.map(order => (
|
||||
<Accordion key={order.id} onClick={()=>setSelectedOrder(order.id)} type="single" className={"border px-5 rounded-xl"} collapsible>
|
||||
<AccordionItem value="item-1">
|
||||
<AccordionTrigger className={"cursor-pointer"}>
|
||||
<div className={"w-full"}>
|
||||
<div className={"flex justify-between items-center mb-5"}>
|
||||
<div>
|
||||
<CardTitle className={"text-lg"}>{t("Buyurtma")} #{order.id}</CardTitle>
|
||||
<CardDescription>{t("Yaratilish vaqti")}: {order.created_at}</CardDescription>
|
||||
</div>
|
||||
<div className={"text-end"}>
|
||||
<CardDescription className={"text-lg"}>{formatNumberWithSpaces(order.total_amount!)} so'm</CardDescription>
|
||||
<Badge style={{ backgroundColor: order.status.bg_color, color: order.status.font_color }}>
|
||||
{order.status.translation}
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</AccordionTrigger>
|
||||
<AccordionContent>
|
||||
<Card key={order.id} className={"shadow-none border-none p-0 mt-5 rounded-none"}>
|
||||
<CardContent className={"p-0"}>
|
||||
<CardTitle>{t("Buyurtma tafsilotlari")}</CardTitle>
|
||||
<div className={"mt-5 space-y-3"}>
|
||||
<div className={"flex justify-between items-center"}>
|
||||
<CardDescription>{t("Buyurtma raqami")}</CardDescription>
|
||||
<CardDescription>#{orderDetails?.data.id}</CardDescription>
|
||||
</div>
|
||||
<div className={"flex justify-between items-center"}>
|
||||
<CardDescription>{t("Buyurtma holati")}</CardDescription>
|
||||
<Badge
|
||||
style={{ backgroundColor: orderDetails?.data.status.bg_color, color: orderDetails?.data.status.font_color }}
|
||||
>{orderDetails?.data.status.translation}</Badge>
|
||||
</div>
|
||||
<div className={"flex justify-between items-center"}>
|
||||
<CardDescription>{t("To'lov holati")}</CardDescription>
|
||||
<Badge
|
||||
style={{ backgroundColor: orderDetails?.data.payment_status.bg_color, color: orderDetails?.data.payment_status.font_color }}
|
||||
>{orderDetails?.data.payment_status.translation}</Badge>
|
||||
</div>
|
||||
<div className={"flex justify-between items-center"}>
|
||||
<CardDescription>{t("Yaratilish vaqti")}</CardDescription>
|
||||
<CardDescription>
|
||||
{orderDetails?.data.created_at}
|
||||
</CardDescription>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
<Separator/>
|
||||
<CardContent className={"p-0"}>
|
||||
<CardTitle>{t("Xaridlar ro'yxati")}</CardTitle>
|
||||
<div className={"mt-5 space-y-3"}>
|
||||
{
|
||||
orderDetails?.data.products.map(product => (
|
||||
<div key={product.id} className={"flex justify-between w-full items-center"}>
|
||||
<CardDescription>{product.name}</CardDescription>
|
||||
<CardDescription>{product.count} x {formatNumberWithSpaces(product.price || product.total_price)} {t("so'm")}</CardDescription>
|
||||
</div>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
</CardContent>
|
||||
<Separator/>
|
||||
<CardContent className={"p-0"}>
|
||||
<CardTitle>{t("Mijoz ma'lumotlari")}</CardTitle>
|
||||
<div className={"mt-5 space-y-3"}>
|
||||
<div className={"flex justify-between items-center"}>
|
||||
<CardDescription>{t("Mijoz turi")}</CardDescription>
|
||||
<CardDescription>{orderDetails?.data.client_type}</CardDescription>
|
||||
</div>
|
||||
<div className={"flex justify-between items-center"}>
|
||||
<CardDescription>{t("Mijoz turi")}</CardDescription>
|
||||
<CardDescription>{orderDetails?.data.client_information.full_name}</CardDescription>
|
||||
</div>
|
||||
<div className={"flex justify-between items-center"}>
|
||||
<CardDescription>{t("Mijoz telefon")}</CardDescription>
|
||||
<CardDescription>{orderDetails?.data.client_information.phone}</CardDescription>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
<Separator/>
|
||||
<CardContent className={"p-0"}>
|
||||
<CardTitle>{t("Yetkazib berish")}</CardTitle>
|
||||
<div className={"mt-5 space-y-3"}>
|
||||
<div className={"flex justify-between items-center"}>
|
||||
<CardDescription>{t("Yetkazib berish turi")}</CardDescription>
|
||||
<CardDescription>{orderDetails?.data.delivery_type}</CardDescription>
|
||||
</div>
|
||||
<div className={"flex justify-between items-center"}>
|
||||
<CardDescription>{t("Yetkazib berish manzili")}</CardDescription>
|
||||
<CardDescription>{orderDetails?.data.address.city.region.name}, {orderDetails?.data.address.city.name}</CardDescription>
|
||||
</div>
|
||||
<div className={"flex justify-between items-center"}>
|
||||
<CardDescription>{t("Yetkazib berish narxi")}</CardDescription>
|
||||
<CardDescription>{formatNumberWithSpaces(orderDetails?.data.price_delivery!)} so'm</CardDescription>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
<Separator/>
|
||||
<CardFooter className={"flex justify-between p-0 pb-2"}>
|
||||
<span/>
|
||||
<Button size={"lg"} asChild>
|
||||
<Link href={orderDetails?.data.pay_url! || ""} target={"_blank"}>
|
||||
{t("To'lash")}
|
||||
</Link>
|
||||
</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
|
||||
))
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default OrdersSection
|
||||
26
src/features/profile/ui/terms-section.tsx
Normal file
@@ -0,0 +1,26 @@
|
||||
"use client"
|
||||
import React from 'react'
|
||||
import {useQuery} from "@tanstack/react-query";
|
||||
import {getPolicy} from "@/shared/api/policySvc";
|
||||
import Loader from "@/shared/ui/loader";
|
||||
import { useTranslations } from 'next-intl';
|
||||
|
||||
const TermsSection = () => {
|
||||
const t = useTranslations("")
|
||||
const {data: policy, isLoading: policyIsLoading} = useQuery({
|
||||
queryKey: ["getPolicy"],
|
||||
queryFn: getPolicy
|
||||
})
|
||||
return (
|
||||
<div className={"profile-section-wrapper"}>
|
||||
<div className={"flex justify-between items-center mb-4"}>
|
||||
<div>
|
||||
<h1 className={"profile-section-title"}>{policy?.data.name}</h1>
|
||||
<span className={"profile-section-subtitle"}>{t("Offerta shartlari")}</span>
|
||||
</div>
|
||||
</div>
|
||||
{policyIsLoading ? <Loader height={"h-[30vh]"}/> : <div dangerouslySetInnerHTML={{__html: policy?.data?.body!}}/>}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default TermsSection
|
||||
74
src/features/services/ui/services-section/service-modal.tsx
Normal file
@@ -0,0 +1,74 @@
|
||||
"use client"
|
||||
import {Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle} from "@/shared/ui/dialog";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectGroup,
|
||||
SelectItem,
|
||||
SelectLabel,
|
||||
SelectTrigger,
|
||||
SelectValue
|
||||
} from "@/shared/ui/select";
|
||||
import {Input} from "@/shared/ui/input";
|
||||
import {Textarea} from "@/shared/ui/textarea";
|
||||
import React from "react";
|
||||
import {Button} from "@/shared/ui/button";
|
||||
import {Service} from "@/shared/types/services";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
interface ServiceModalProps {
|
||||
selectedService: Service | null;
|
||||
setSelectedService: (service: Service | null) => void;
|
||||
}
|
||||
|
||||
const ServiceModal = ({selectedService, setSelectedService}: ServiceModalProps) => {
|
||||
const t = useTranslations("")
|
||||
return (
|
||||
<Dialog open={!!selectedService} onOpenChange={
|
||||
(open) => {
|
||||
if (!open) {
|
||||
setSelectedService(null)
|
||||
}
|
||||
}
|
||||
}>
|
||||
<DialogContent className={"min-w-4/12"}>
|
||||
<DialogHeader>
|
||||
<DialogTitle>{selectedService?.name}</DialogTitle>
|
||||
<DialogDescription className={"space-y-5 mt-5"}>
|
||||
<Select>
|
||||
<SelectTrigger className={"w-full"}>
|
||||
<SelectValue placeholder={t("Viloyat")}/>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
<SelectLabel>{t("Viloyat")}</SelectLabel>
|
||||
<SelectItem value="pineapple">Pineapple</SelectItem>
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Select>
|
||||
<SelectTrigger className={"w-full"}>
|
||||
<SelectValue placeholder={t("Tuman")}/>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
<SelectLabel>{t("Tuman")}</SelectLabel>
|
||||
<SelectItem value="pineapple">Pineapple</SelectItem>
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
<Input placeholder={t("Telefon raqamingiz")}/>
|
||||
<Input placeholder={t("full_name")}/>
|
||||
<Textarea placeholder="Type your message here."/>
|
||||
<div className={"text-end"}>
|
||||
<Button size={"lg"}>{t("Ariza yuborish")}</Button>
|
||||
</div>
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
|
||||
export default ServiceModal
|
||||
@@ -0,0 +1,47 @@
|
||||
"use client";
|
||||
import React, { useState } from "react";
|
||||
import { Service } from "@/shared/types/services";
|
||||
import Image from "next/image";
|
||||
import ServiceModal from "@/features/services/ui/services-section/service-modal";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
interface ServiceSectionProps {
|
||||
services: Service[];
|
||||
}
|
||||
|
||||
const ServicesSection = ({ services }: Readonly<ServiceSectionProps>) => {
|
||||
const t = useTranslations("");
|
||||
const [selectedService, setSelectedService] = useState<Service | null>(null);
|
||||
return (
|
||||
<div className={"my-container section-wrapper"}>
|
||||
<h1 className={"section-title text-center"}>{t("Xizmatlar")}</h1>
|
||||
<div className={"grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 mx-4 md:mx-0 gap-6 mt-12"}>
|
||||
{services.map((service) => (
|
||||
<div
|
||||
className={
|
||||
"cursor-pointer border hover:border-primary transition-all duration-400 bg-white p-12 w-full flex flex-col justify-center items-center gap-2 h-full"
|
||||
}
|
||||
key={service.id}
|
||||
onClick={() => {
|
||||
setSelectedService(service);
|
||||
}}
|
||||
>
|
||||
<div className={"w-full flex flex-col items-center gap-2"}>
|
||||
<Image src={service.image} alt={""} width={100} height={100} />
|
||||
<h1 className={"text-xl font-bold text-center"}>
|
||||
{service.name}
|
||||
</h1>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
{selectedService && (
|
||||
<ServiceModal
|
||||
selectedService={selectedService}
|
||||
setSelectedService={setSelectedService}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
export default ServicesSection;
|
||||
124
src/features/useful/ui/useful-section.tsx
Normal file
@@ -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
@@ -0,0 +1,11 @@
|
||||
import createMiddleware from "next-intl/middleware";
|
||||
import { routing } from "./shared/config/i18n/routing";
|
||||
|
||||
export default createMiddleware(routing);
|
||||
|
||||
export const config = {
|
||||
// Match all pathnames except for
|
||||
// - … if they start with `/apiClient`, `/trpc`, `/_next` or `/_vercel`
|
||||
// - … the ones containing a dot (e.g. `favicon.ico`)
|
||||
matcher: "/((?!apiClient|trpc|_next|_vercel|.*\\..*).*)",
|
||||
};
|
||||
53
src/shared/api/apiClient.ts
Normal file
@@ -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
@@ -0,0 +1,14 @@
|
||||
import {POST} from "@/shared/api/apiClient";
|
||||
import {useAuthStore} from "@/shared/store/authStore";
|
||||
import {OAUTH, OAUTH_VERIFY} from "@/shared/constants";
|
||||
|
||||
export const sendPhoneNumber = async (phone: string) => {
|
||||
await POST(OAUTH, { phone });
|
||||
};
|
||||
|
||||
export const verifyCode = async (phone: string, verify_code: number): Promise<any> => {
|
||||
const response = await POST(OAUTH_VERIFY, { phone, verify_code });
|
||||
const {data} = response?.data;
|
||||
useAuthStore.getState().login(data);
|
||||
return data;
|
||||
};
|
||||
13
src/shared/api/brandsSvc.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import {BRANDS, PRODUCTS} from "@/shared/constants/apiEndpoints";
|
||||
import {GET} from "@/shared/api/apiClient";
|
||||
import { BrandProductsType, Brands } from "../types/brands";
|
||||
|
||||
export const getBrands = async (): Promise<Brands> => {
|
||||
const res = await GET<Brands>(BRANDS);
|
||||
return res.data;
|
||||
}
|
||||
|
||||
export const getBrandProducts = async (brandId: number): Promise<BrandProductsType> => {
|
||||
const res = await GET<BrandProductsType>(`${brandId}${PRODUCTS}`);
|
||||
return res.data;
|
||||
}
|
||||
8
src/shared/api/compilationsSvc.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import {GET} from "@/shared/api/apiClient";
|
||||
import {COMPILATIONS} from "@/shared/constants/apiEndpoints";
|
||||
import {GetCompilationResponse} from "@/shared/types/compilations";
|
||||
|
||||
export const getCompilation = async () => {
|
||||
const res = await GET<GetCompilationResponse>(COMPILATIONS)
|
||||
return res.data;
|
||||
}
|
||||
11
src/shared/api/contactSvs.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { AxiosResponse } from "axios";
|
||||
import { SUPPORT } from "../constants";
|
||||
import { POST } from "./apiClient";
|
||||
|
||||
export const contactInfoSubmit = async (
|
||||
phone: string,
|
||||
name: string
|
||||
): Promise<AxiosResponse> => {
|
||||
const response = await POST(SUPPORT, { phone, name });
|
||||
return response;
|
||||
};
|
||||
12
src/shared/api/feedbackSvc.ts
Normal file
@@ -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
@@ -0,0 +1,5 @@
|
||||
import apiClient from "@/shared/api/apiClient";
|
||||
export * from "./authSvc"
|
||||
export {
|
||||
apiClient
|
||||
}
|
||||
13
src/shared/api/partnersSvc.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import {PARTNERS} from "@/shared/constants/apiEndpoints";
|
||||
import {GET} from "@/shared/api/apiClient";
|
||||
import {GetPartnersResponse} from "@/shared/types/partners";
|
||||
|
||||
export const getPartners = async (): Promise<GetPartnersResponse> => {
|
||||
const res = await GET<GetPartnersResponse>(PARTNERS);
|
||||
return res.data;
|
||||
}
|
||||
|
||||
export const getPartnerById = async (id: number): Promise<GetPartnersResponse> => {
|
||||
const res = await GET<GetPartnersResponse>(`${PARTNERS}/${id}`);
|
||||
return res.data;
|
||||
}
|
||||
10
src/shared/api/policySvc.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import {GET} from "@/shared/api/apiClient";
|
||||
import {PAGE_POLICY} from "@/shared/constants";
|
||||
|
||||
export const getPolicy = async ()=>{
|
||||
const {data} = await GET<{data: {
|
||||
name: string
|
||||
body: string
|
||||
}}>(PAGE_POLICY)
|
||||
return data
|
||||
}
|
||||
8
src/shared/api/productCategorySvc.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import {ProductCategory} from "../types/productCategory"
|
||||
import {CATEGORIES} from "@/shared/constants/apiEndpoints";
|
||||
import {GET} from "@/shared/api/apiClient";
|
||||
|
||||
export const getCategory = async (): Promise<{data:ProductCategory[]}>=>{
|
||||
const res = await GET<{data:ProductCategory[]}>(CATEGORIES);
|
||||
return res.data;
|
||||
}
|
||||
23
src/shared/api/productSvc.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import {CATEGORIES, PRODUCTS} from "@/shared/constants/apiEndpoints";
|
||||
import {GetProductsResponse} from "@/shared/types/product";
|
||||
import {GET} from "@/shared/api/apiClient";
|
||||
import { BrandProductsType } from "../types/brands";
|
||||
|
||||
interface GetProductsProps {
|
||||
categoryId: number
|
||||
currentPage?: number
|
||||
}
|
||||
|
||||
export const getProducts = async ({categoryId, currentPage}: GetProductsProps): Promise<{
|
||||
data: GetProductsResponse
|
||||
}> => {
|
||||
const res = await GET<GetProductsResponse>(`${CATEGORIES}/${categoryId}${PRODUCTS}`, {
|
||||
...(currentPage !== undefined && { page: currentPage })
|
||||
});
|
||||
return res;
|
||||
}
|
||||
|
||||
export const getProductById = async (productId: number): Promise<any> => {
|
||||
const res = await GET(`${PRODUCTS}/${productId}`);
|
||||
return res;
|
||||
}
|
||||
8
src/shared/api/regionSvc.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import {GET} from "@/shared/api/apiClient";
|
||||
import {REGIONS} from "@/shared/constants";
|
||||
import {GetRegionsResponse} from "@/shared/types/region";
|
||||
|
||||
export const getRegions = async ()=>{
|
||||
const {data} = await GET<GetRegionsResponse>(REGIONS)
|
||||
return data
|
||||
}
|
||||
13
src/shared/api/servicesSvc.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import {SERVICES} from "@/shared/constants/apiEndpoints";
|
||||
import {GET} from "@/shared/api/apiClient";
|
||||
import {GetServiceByIdResponse, GetServicesResponse} from "@/shared/types/services";
|
||||
|
||||
export const getServices = async (): Promise<GetServicesResponse> => {
|
||||
const {data} = await GET<GetServicesResponse>(`${SERVICES}`);
|
||||
return data
|
||||
}
|
||||
|
||||
export const getServiceById = async (id: number): Promise<GetServiceByIdResponse> => {
|
||||
const {data} = await GET<GetServiceByIdResponse>(`${SERVICES}/${id}`);
|
||||
return data
|
||||
}
|
||||
18
src/shared/api/usefulSvc.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import {GET} from "@/shared/api/apiClient";
|
||||
import {USEFUL_INFORMATION} from "@/shared/constants/apiEndpoints";
|
||||
import {GetUsefulResponse} from "@/shared/types/useful";
|
||||
|
||||
export const getUseful = async (): Promise<GetUsefulResponse> => {
|
||||
const {data} = await GET<GetUsefulResponse>(USEFUL_INFORMATION)
|
||||
return data
|
||||
}
|
||||
|
||||
export const getUsefulById = async (id: number): Promise<any> => {
|
||||
const {data} = await GET(`${USEFUL_INFORMATION}/${id}`)
|
||||
return data
|
||||
}
|
||||
|
||||
export const getUsefulItems = async (useful_id: number, items_id:number): Promise<any> => {
|
||||
const {data} = await GET(`${USEFUL_INFORMATION}/${useful_id}/items/${items_id}`)
|
||||
return data
|
||||
}
|
||||
17
src/shared/api/userMeSvc.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import {GET, POST, PUT} from "@/shared/api/apiClient";
|
||||
import {USER_ME} from "@/shared/constants";
|
||||
import {GetUserMeResponse} from "@/shared/types/user";
|
||||
|
||||
export const getUserMe = async () => {
|
||||
const {data} = await GET<GetUserMeResponse>(USER_ME)
|
||||
return data
|
||||
}
|
||||
export const updateUserMe = async (userData: {
|
||||
first_name?: string
|
||||
last_name?: string
|
||||
middle_name?: string
|
||||
phone?: string
|
||||
}) => {
|
||||
const {data} = await PUT(USER_ME, userData)
|
||||
return data
|
||||
}
|
||||
18
src/shared/api/userOrdersSvc.ts
Normal file
@@ -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
|
||||
}
|
||||