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