change package managers
41
.gitignore
vendored
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||||
|
|
||||||
|
# dependencies
|
||||||
|
/node_modules
|
||||||
|
/.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
|
||||||
1
.husky/pre-commit
Normal file
@@ -0,0 +1 @@
|
|||||||
|
npx lint-staged
|
||||||
1
.husky/pre-push
Normal file
@@ -0,0 +1 @@
|
|||||||
|
npm run build
|
||||||
6
.npmrc
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
# pnpm configuration
|
||||||
|
|
||||||
|
audit=true
|
||||||
|
ignore-scripts=false
|
||||||
|
strict-ssl=true
|
||||||
|
minimum-release-age=262974
|
||||||
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"
|
||||||
|
}
|
||||||
54
eslint.config.mjs
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
import { dirname } from 'path';
|
||||||
|
import { fileURLToPath } from 'url';
|
||||||
|
import { FlatCompat } from '@eslint/eslintrc';
|
||||||
|
import prettierPlugin from 'eslint-plugin-prettier'; // Statik import
|
||||||
|
|
||||||
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
|
const __dirname = dirname(__filename);
|
||||||
|
|
||||||
|
const compat = new FlatCompat({
|
||||||
|
baseDirectory: __dirname,
|
||||||
|
});
|
||||||
|
|
||||||
|
const eslintConfig = [
|
||||||
|
{
|
||||||
|
ignores: [
|
||||||
|
'node_modules/*',
|
||||||
|
'dist/*',
|
||||||
|
'build/*',
|
||||||
|
'coverage/*',
|
||||||
|
'*.min.js',
|
||||||
|
'*.log',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
...compat.extends('next/core-web-vitals', 'next/typescript', 'prettier'),
|
||||||
|
{
|
||||||
|
files: ['**/*.{js,ts,jsx,tsx}'],
|
||||||
|
plugins: {
|
||||||
|
prettier: prettierPlugin,
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
'prettier/prettier': 'warn',
|
||||||
|
'max-len': [
|
||||||
|
'error',
|
||||||
|
{
|
||||||
|
code: 200,
|
||||||
|
ignoreUrls: true,
|
||||||
|
ignoreComments: true,
|
||||||
|
ignoreStrings: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
'no-console': ['warn', { allow: ['error'] }],
|
||||||
|
eqeqeq: 'warn',
|
||||||
|
'no-duplicate-imports': 'error',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
files: ['src/shared/ui/**/*.{js,ts,jsx,tsx}'],
|
||||||
|
rules: {
|
||||||
|
'max-len': 'off',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export default eslintConfig;
|
||||||
17
next-sitemap.config.js
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
/** @type {import('next-sitemap').IConfig} */
|
||||||
|
module.exports = {
|
||||||
|
siteUrl: 'https://can-prom.com',
|
||||||
|
generateRobotsTxt: false,
|
||||||
|
changefreq: 'daily',
|
||||||
|
priority: 0.7,
|
||||||
|
sitemapSize: 5000,
|
||||||
|
robotsTxtOptions: {
|
||||||
|
policies: [
|
||||||
|
{
|
||||||
|
userAgent: '*',
|
||||||
|
allow: '/',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
additionalSitemaps: ['https://can-prom.com/sitemap.xml'],
|
||||||
|
},
|
||||||
|
};
|
||||||
21
next.config.ts
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import type { NextConfig } from 'next';
|
||||||
|
import createNextIntlPlugin from 'next-intl/plugin';
|
||||||
|
|
||||||
|
const nextConfig: NextConfig = {
|
||||||
|
/* config options here */
|
||||||
|
devIndicators: false,
|
||||||
|
images: {
|
||||||
|
domains: ['food.felixits.uz'],
|
||||||
|
},
|
||||||
|
// eslint: {
|
||||||
|
// ignoreDuringBuilds: true,
|
||||||
|
// },
|
||||||
|
};
|
||||||
|
const withNextIntl = createNextIntlPlugin({
|
||||||
|
requestConfig: './src/shared/config/i18n/request.ts',
|
||||||
|
experimental: {
|
||||||
|
createMessagesDeclaration: './src/shared/config/i18n/messages/uz.json',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default withNextIntl(nextConfig);
|
||||||
84
package.json
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
{
|
||||||
|
"name": "zt-food",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"dev": "next dev --turbopack",
|
||||||
|
"build": "next build",
|
||||||
|
"start": "next start",
|
||||||
|
"prettier": "prettier src --write",
|
||||||
|
"lint": "eslint src --fix",
|
||||||
|
"prepare": "husky"
|
||||||
|
},
|
||||||
|
"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",
|
||||||
|
"@hookform/resolvers": "^5.1.1",
|
||||||
|
"@radix-ui/react-accordion": "^1.2.11",
|
||||||
|
"@radix-ui/react-alert-dialog": "^1.1.14",
|
||||||
|
"@radix-ui/react-avatar": "^1.1.7",
|
||||||
|
"@radix-ui/react-checkbox": "^1.2.3",
|
||||||
|
"@radix-ui/react-dialog": "^1.1.14",
|
||||||
|
"@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-select": "^2.2.2",
|
||||||
|
"@radix-ui/react-separator": "^1.1.4",
|
||||||
|
"@radix-ui/react-slot": "^1.2.3",
|
||||||
|
"@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.76.0",
|
||||||
|
"@tanstack/react-table": "^8.21.3",
|
||||||
|
"axios": "^1.9.0",
|
||||||
|
"class-variance-authority": "^0.7.1",
|
||||||
|
"clsx": "^2.1.1",
|
||||||
|
"dayjs": "^1.11.13",
|
||||||
|
"dompurify": "^3.2.6",
|
||||||
|
"framer-motion": "^12.16.0",
|
||||||
|
"lucide-react": "^0.503.0",
|
||||||
|
"next": "15.3.8",
|
||||||
|
"next-intl": "^4.1.0",
|
||||||
|
"next-themes": "^0.4.6",
|
||||||
|
"react": "19.0.1",
|
||||||
|
"react-dom": "19.0.1",
|
||||||
|
"react-hook-form": "^7.57.0",
|
||||||
|
"react-icons": "^5.5.0",
|
||||||
|
"recharts": "^2.15.3",
|
||||||
|
"sonner": "^2.0.3",
|
||||||
|
"sweetalert2": "^11.22.0",
|
||||||
|
"swiper": "^11.2.8",
|
||||||
|
"tailwind-merge": "^3.2.0",
|
||||||
|
"vaul": "^1.1.2",
|
||||||
|
"zod": "^3.25.64",
|
||||||
|
"zustand": "^5.0.5"
|
||||||
|
},
|
||||||
|
"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",
|
||||||
|
"eslint-config-prettier": "^10.1.2",
|
||||||
|
"eslint-plugin-prettier": "^5.2.6",
|
||||||
|
"husky": "^9.1.7",
|
||||||
|
"lint-staged": "^15.5.1",
|
||||||
|
"prettier": "^3.5.3",
|
||||||
|
"tailwindcss": "^4",
|
||||||
|
"tw-animate-css": "^1.2.8",
|
||||||
|
"typescript": "^5"
|
||||||
|
},
|
||||||
|
"lint-staged": {
|
||||||
|
"*.{js,ts,jsx,tsx}": [
|
||||||
|
"prettier --write",
|
||||||
|
"eslint src --fix"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"packageManager": "pnpm@9.15.4"
|
||||||
|
}
|
||||||
6472
pnpm-lock.yaml
generated
Normal file
5
postcss.config.mjs
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
const config = {
|
||||||
|
plugins: ['@tailwindcss/postcss'],
|
||||||
|
};
|
||||||
|
|
||||||
|
export default config;
|
||||||
9
prettier.config.cjs
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
/** @type {import("prettier").Config} */
|
||||||
|
module.exports = {
|
||||||
|
semi: true, // Har satrda nuqta-vergul bo‘lishi
|
||||||
|
singleQuote: true, // ' ' ishlatilsin, " " emas
|
||||||
|
trailingComma: 'all', // so‘nggi vergullar qo‘yilsin
|
||||||
|
tabWidth: 2, // Indent 2 bo‘lsin
|
||||||
|
bracketSpacing: true, // { a: 1 } ichida bo‘sh joy qoldirsin
|
||||||
|
arrowParens: 'always', // (x) => {...}
|
||||||
|
};
|
||||||
BIN
public/favicon.png
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
public/images/about-us/about-img-1.webp
Normal file
|
After Width: | Height: | Size: 203 KiB |
BIN
public/images/about-us/about-img-2.jpg
Normal file
|
After Width: | Height: | Size: 257 KiB |
BIN
public/images/about-us/our-comp/about-company-1.webp
Normal file
|
After Width: | Height: | Size: 372 KiB |
BIN
public/images/about-us/our-comp/about-company-4.webp
Normal file
|
After Width: | Height: | Size: 89 KiB |
BIN
public/images/about-us/our-comp/about-company-5.webp
Normal file
|
After Width: | Height: | Size: 785 KiB |
BIN
public/images/about-us/our-fact/company-1.webp
Normal file
|
After Width: | Height: | Size: 270 KiB |
BIN
public/images/about-us/our-fact/sertificate.png
Normal file
|
After Width: | Height: | Size: 820 KiB |
BIN
public/images/footer/footer-galery-1.webp
Normal file
|
After Width: | Height: | Size: 3.1 KiB |
BIN
public/images/footer/footer-galery-2.webp
Normal file
|
After Width: | Height: | Size: 2.9 KiB |
BIN
public/images/footer/footer-galery-3.webp
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
BIN
public/images/footer/footer-galery-4.webp
Normal file
|
After Width: | Height: | Size: 2.6 KiB |
BIN
public/images/footer/footer-galery-5.webp
Normal file
|
After Width: | Height: | Size: 2.9 KiB |
BIN
public/images/footer/footer-galery-6.webp
Normal file
|
After Width: | Height: | Size: 2.6 KiB |
BIN
public/images/footer/footer-shape-1.webp
Normal file
|
After Width: | Height: | Size: 9.6 KiB |
BIN
public/images/footer/footer-shape-2.webp
Normal file
|
After Width: | Height: | Size: 3.9 KiB |
BIN
public/images/footer/site-footer-bg.webp
Normal file
|
After Width: | Height: | Size: 757 KiB |
BIN
public/images/footer/site-footer-ripped.webp
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
public/images/header-banner/header-banner.webp
Normal file
|
After Width: | Height: | Size: 202 KiB |
BIN
public/images/home-about/about-bg-shape1.webp
Normal file
|
After Width: | Height: | Size: 6.6 KiB |
BIN
public/images/home-about/about-bg-shape2.webp
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
public/images/home-about/home-about1.webp
Normal file
|
After Width: | Height: | Size: 284 KiB |
BIN
public/images/home-about/home-about2.webp
Normal file
|
After Width: | Height: | Size: 169 KiB |
BIN
public/images/home-conditions/1.png
Normal file
|
After Width: | Height: | Size: 22 KiB |
BIN
public/images/home-conditions/2.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
public/images/home-conditions/3.png
Normal file
|
After Width: | Height: | Size: 27 KiB |
BIN
public/images/home-new-product/new-pro-bg1.webp
Normal file
|
After Width: | Height: | Size: 4.9 KiB |
BIN
public/images/home-new-product/new-pro-bg2.webp
Normal file
|
After Width: | Height: | Size: 20 KiB |
BIN
public/images/home-process-card/card-bg.webp
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
public/images/home-process-card/home-process-1.png
Normal file
|
After Width: | Height: | Size: 32 KiB |
BIN
public/images/home-process-card/home-process-2.png
Normal file
|
After Width: | Height: | Size: 49 KiB |
BIN
public/images/home-process-card/home-process-3.png
Normal file
|
After Width: | Height: | Size: 55 KiB |
BIN
public/images/home-process-card/home-process-4.png
Normal file
|
After Width: | Height: | Size: 34 KiB |
BIN
public/logo.png
Normal file
|
After Width: | Height: | Size: 131 KiB |
8
public/robots.txt
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
# Qidiruv tizimlar uchun umumiy ruxsat
|
||||||
|
User-agent: *
|
||||||
|
Allow: /
|
||||||
|
|
||||||
|
Crawl-delay: 5
|
||||||
|
|
||||||
|
# XML sitemap joylashuvi
|
||||||
|
Sitemap: https://can-prom.com/sitemap.xml
|
||||||
115
public/sitemap.xml
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<urlset
|
||||||
|
xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"
|
||||||
|
>
|
||||||
|
<url>
|
||||||
|
<loc>https://can-prom.com</loc>
|
||||||
|
<lastmod>2025-06-27</lastmod>
|
||||||
|
<changefreq>daily</changefreq>
|
||||||
|
<priority>1.0</priority>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://can-prom.com/about-us</loc>
|
||||||
|
<lastmod>2025-06-27</lastmod>
|
||||||
|
<changefreq>weekly</changefreq>
|
||||||
|
<priority>0.7</priority>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://can-prom.com/about-us/our-company</loc>
|
||||||
|
<lastmod>2025-06-27</lastmod>
|
||||||
|
<changefreq>weekly</changefreq>
|
||||||
|
<priority>0.7</priority>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://can-prom.com/about-us/our-factory</loc>
|
||||||
|
<lastmod>2025-06-27</lastmod>
|
||||||
|
<changefreq>weekly</changefreq>
|
||||||
|
<priority>0.7</priority>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://can-prom.com/contact-us</loc>
|
||||||
|
<lastmod>2025-06-27</lastmod>
|
||||||
|
<changefreq>monthly</changefreq>
|
||||||
|
<priority>0.6</priority>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://can-prom.com/faq</loc>
|
||||||
|
<lastmod>2025-06-27</lastmod>
|
||||||
|
<changefreq>monthly</changefreq>
|
||||||
|
<priority>0.6</priority>
|
||||||
|
</url>
|
||||||
|
|
||||||
|
<url>
|
||||||
|
<loc>https://can-prom.com/ru/</loc>
|
||||||
|
<lastmod>2025-06-27</lastmod>
|
||||||
|
<changefreq>daily</changefreq>
|
||||||
|
<priority>1.0</priority>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://can-prom.com/ru/about-us</loc>
|
||||||
|
<lastmod>2025-06-27</lastmod>
|
||||||
|
<changefreq>weekly</changefreq>
|
||||||
|
<priority>0.7</priority>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://can-prom.com/ru/about-us/our-company</loc>
|
||||||
|
<lastmod>2025-06-27</lastmod>
|
||||||
|
<changefreq>weekly</changefreq>
|
||||||
|
<priority>0.7</priority>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://can-prom.com/ru/about-us/our-factory</loc>
|
||||||
|
<lastmod>2025-06-27</lastmod>
|
||||||
|
<changefreq>weekly</changefreq>
|
||||||
|
<priority>0.7</priority>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://can-prom.com/ru/contact-us</loc>
|
||||||
|
<lastmod>2025-06-27</lastmod>
|
||||||
|
<changefreq>monthly</changefreq>
|
||||||
|
<priority>0.6</priority>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://can-prom.com/ru/faq</loc>
|
||||||
|
<lastmod>2025-06-27</lastmod>
|
||||||
|
<changefreq>monthly</changefreq>
|
||||||
|
<priority>0.6</priority>
|
||||||
|
</url>
|
||||||
|
|
||||||
|
<url>
|
||||||
|
<loc>https://can-prom.com/uz/</loc>
|
||||||
|
<lastmod>2025-06-27</lastmod>
|
||||||
|
<changefreq>daily</changefreq>
|
||||||
|
<priority>1.0</priority>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://can-prom.com/uz/about-us</loc>
|
||||||
|
<lastmod>2025-06-27</lastmod>
|
||||||
|
<changefreq>weekly</changefreq>
|
||||||
|
<priority>0.7</priority>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://can-prom.com/uz/about-us/our-company</loc>
|
||||||
|
<lastmod>2025-06-27</lastmod>
|
||||||
|
<changefreq>weekly</changefreq>
|
||||||
|
<priority>0.7</priority>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://can-prom.com/uz/about-us/our-factory</loc>
|
||||||
|
<lastmod>2025-06-27</lastmod>
|
||||||
|
<changefreq>weekly</changefreq>
|
||||||
|
<priority>0.7</priority>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://can-prom.com/uz/contact-us</loc>
|
||||||
|
<lastmod>2025-06-27</lastmod>
|
||||||
|
<changefreq>monthly</changefreq>
|
||||||
|
<priority>0.6</priority>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://can-prom.com/uz/faq</loc>
|
||||||
|
<lastmod>2025-06-27</lastmod>
|
||||||
|
<changefreq>monthly</changefreq>
|
||||||
|
<priority>0.6</priority>
|
||||||
|
</url>
|
||||||
|
</urlset>
|
||||||
15
src/app/[locale]/about-us/layout.tsx
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import HeaderBanner from '@/widgets/header-banner/ui';
|
||||||
|
import React, { ReactNode } from 'react';
|
||||||
|
|
||||||
|
const About = ({ children }: { children: ReactNode }) => {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div>
|
||||||
|
<HeaderBanner />
|
||||||
|
</div>
|
||||||
|
<div>{children}</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default About;
|
||||||
38
src/app/[locale]/about-us/our-company/page.tsx
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
'use client';
|
||||||
|
import { IBreadcrumbItem, pageStore } from '@/shared/store/pageStore';
|
||||||
|
import OurCompany from '@/widgets/about/our-company/ui';
|
||||||
|
import React, { useEffect } from 'react';
|
||||||
|
|
||||||
|
const AboutCompany = () => {
|
||||||
|
const breadcrumsItem: IBreadcrumbItem[] = [
|
||||||
|
{
|
||||||
|
label: 'Главная',
|
||||||
|
href: '/',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'О нас',
|
||||||
|
href: '/about-us',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Наша компания',
|
||||||
|
href: '/about-us/our-company',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const { onChangePage } = pageStore((state) => state);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
onChangePage({
|
||||||
|
title: 'Наша компания',
|
||||||
|
breadcrumbs: breadcrumsItem,
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<OurCompany />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AboutCompany;
|
||||||
37
src/app/[locale]/about-us/our-factory/page.tsx
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
'use client';
|
||||||
|
import React, { useEffect } from 'react';
|
||||||
|
import OurFactory from '@/widgets/about/our-factory/ui';
|
||||||
|
import { IBreadcrumbItem, pageStore } from '@/shared/store/pageStore';
|
||||||
|
|
||||||
|
const AboutCompany = () => {
|
||||||
|
const breadcrumsItem: IBreadcrumbItem[] = [
|
||||||
|
{
|
||||||
|
label: 'Главная',
|
||||||
|
href: '/',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'О нас',
|
||||||
|
href: '/about-us',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Наш завод',
|
||||||
|
href: '/about-us/our-factory',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const { onChangePage } = pageStore((state) => state);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
onChangePage({
|
||||||
|
title: 'Наш завод',
|
||||||
|
breadcrumbs: breadcrumsItem,
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<OurFactory />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AboutCompany;
|
||||||
33
src/app/[locale]/about-us/page.tsx
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
'use client';
|
||||||
|
import { IBreadcrumbItem, pageStore } from '@/shared/store/pageStore';
|
||||||
|
import AboutUs from '@/widgets/about';
|
||||||
|
import React, { useEffect } from 'react';
|
||||||
|
|
||||||
|
const About = () => {
|
||||||
|
const breadcrumsItem: IBreadcrumbItem[] = [
|
||||||
|
{
|
||||||
|
label: 'Главная',
|
||||||
|
href: '/',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'О нас',
|
||||||
|
href: '/about-us',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const { onChangePage } = pageStore((state) => state);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
onChangePage({
|
||||||
|
title: 'О нас',
|
||||||
|
breadcrumbs: breadcrumsItem,
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<AboutUs />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default About;
|
||||||
15
src/app/[locale]/contact-us/layout.tsx
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import HeaderBanner from '@/widgets/header-banner/ui';
|
||||||
|
import React, { ReactNode } from 'react';
|
||||||
|
|
||||||
|
const ContactUs = ({ children }: { children: ReactNode }) => {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div>
|
||||||
|
<HeaderBanner />
|
||||||
|
</div>
|
||||||
|
<div>{children}</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ContactUs;
|
||||||
33
src/app/[locale]/contact-us/page.tsx
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
'use client';
|
||||||
|
import { IBreadcrumbItem, pageStore } from '@/shared/store/pageStore';
|
||||||
|
import ContactContent from '@/widgets/contact/ui';
|
||||||
|
import React, { useEffect } from 'react';
|
||||||
|
|
||||||
|
const ContactUs = () => {
|
||||||
|
const breadcrumsItem: IBreadcrumbItem[] = [
|
||||||
|
{
|
||||||
|
label: 'Главная',
|
||||||
|
href: '/',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Контакты',
|
||||||
|
href: '/contact-us',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const { onChangePage } = pageStore((state) => state);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
onChangePage({
|
||||||
|
title: 'Контакты',
|
||||||
|
breadcrumbs: breadcrumsItem,
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<ContactContent />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ContactUs;
|
||||||
15
src/app/[locale]/faq/layout.tsx
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import HeaderBanner from '@/widgets/header-banner/ui';
|
||||||
|
import React, { ReactNode } from 'react';
|
||||||
|
|
||||||
|
const FAQ = ({ children }: { children: ReactNode }) => {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div>
|
||||||
|
<HeaderBanner />
|
||||||
|
</div>
|
||||||
|
<div>{children}</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default FAQ;
|
||||||
33
src/app/[locale]/faq/page.tsx
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
'use client';
|
||||||
|
import { IBreadcrumbItem, pageStore } from '@/shared/store/pageStore';
|
||||||
|
import FaqContent from '@/widgets/faq/ui';
|
||||||
|
import React, { useEffect } from 'react';
|
||||||
|
|
||||||
|
const FAQ = () => {
|
||||||
|
const breadcrumsItem: IBreadcrumbItem[] = [
|
||||||
|
{
|
||||||
|
label: 'Главная',
|
||||||
|
href: '/',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Вопросы и ответы',
|
||||||
|
href: '/faq',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const { onChangePage } = pageStore((state) => state);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
onChangePage({
|
||||||
|
title: 'Вопросы и ответы',
|
||||||
|
breadcrumbs: breadcrumsItem,
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<FaqContent />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default FAQ;
|
||||||
74
src/app/[locale]/layout.tsx
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
import type { Metadata } from 'next';
|
||||||
|
import '../globals.css';
|
||||||
|
import { ThemeProvider } from '@/shared/config/theme-provider';
|
||||||
|
import { PRODUCT_INFO } from '@/shared/constants/data';
|
||||||
|
import { hasLocale, Locale, NextIntlClientProvider } from 'next-intl';
|
||||||
|
import { routing } from '@/shared/config/i18n/routing';
|
||||||
|
import { notFound } from 'next/navigation';
|
||||||
|
import Footer from '@/widgets/footer/ui';
|
||||||
|
import Navbar from '@/widgets/navbar/ui';
|
||||||
|
import { ReactNode } from 'react';
|
||||||
|
import { setRequestLocale } from 'next-intl/server';
|
||||||
|
import QueryProvider from '@/shared/config/react-query/QueryProvider';
|
||||||
|
import Script from 'next/script';
|
||||||
|
import { caveat, manrope } from '@/shared/config/fonts';
|
||||||
|
import Sidebar from '@/widgets/sidebar/ui';
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: PRODUCT_INFO.name,
|
||||||
|
description: PRODUCT_INFO.description,
|
||||||
|
icons: PRODUCT_INFO.favicon,
|
||||||
|
};
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
children: ReactNode;
|
||||||
|
params: Promise<{ locale: Locale }>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function generateStaticParams() {
|
||||||
|
return routing.locales.map((locale) => ({ locale }));
|
||||||
|
}
|
||||||
|
|
||||||
|
export default async function RootLayout({ children, params }: Props) {
|
||||||
|
const { locale } = await params;
|
||||||
|
if (!hasLocale(routing.locales, locale)) {
|
||||||
|
notFound();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enable static rendering
|
||||||
|
setRequestLocale(locale);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<html
|
||||||
|
lang={locale}
|
||||||
|
className={`${manrope.variable} ${caveat.variable}`}
|
||||||
|
suppressHydrationWarning
|
||||||
|
>
|
||||||
|
<body className={'antialiased'}>
|
||||||
|
<NextIntlClientProvider locale={locale}>
|
||||||
|
<ThemeProvider
|
||||||
|
attribute={'class'}
|
||||||
|
defaultTheme="light"
|
||||||
|
enableSystem
|
||||||
|
disableTransitionOnChange
|
||||||
|
>
|
||||||
|
<QueryProvider>
|
||||||
|
<Navbar />
|
||||||
|
<Sidebar />
|
||||||
|
<div className="overflow-x-hidden">
|
||||||
|
{children}
|
||||||
|
<Footer />
|
||||||
|
</div>
|
||||||
|
</QueryProvider>
|
||||||
|
</ThemeProvider>
|
||||||
|
</NextIntlClientProvider>
|
||||||
|
</body>
|
||||||
|
<Script
|
||||||
|
src="https://buttons.github.io/buttons.js"
|
||||||
|
strategy="lazyOnload"
|
||||||
|
async
|
||||||
|
defer
|
||||||
|
/>
|
||||||
|
</html>
|
||||||
|
);
|
||||||
|
}
|
||||||
16
src/app/[locale]/page.tsx
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import { getBanner, getCategory } from '@/shared/config/api/testApi';
|
||||||
|
import Welcome from '@/widgets/welcome';
|
||||||
|
|
||||||
|
export const dynamic = 'force-dynamic';
|
||||||
|
|
||||||
|
export default async function Home() {
|
||||||
|
const resBanner = await getBanner();
|
||||||
|
const bannerData = resBanner?.results || [];
|
||||||
|
const resCategory = await getCategory();
|
||||||
|
const categoryData = resCategory?.results || [];
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Welcome data={{ bannerData, categoryData }} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
60
src/app/[locale]/products/[detail]/page.tsx
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
'use client';
|
||||||
|
import { getOneProduct } from '@/shared/config/api/testApi';
|
||||||
|
import { IBreadcrumbItem, pageStore } from '@/shared/store/pageStore';
|
||||||
|
import { IProductDetail } from '@/shared/types/testApi';
|
||||||
|
import PageNotFound from '@/widgets/not-fount/ui';
|
||||||
|
import ProductDetails from '@/widgets/product-detail/ui/ProductDetail';
|
||||||
|
import { useQuery } from '@tanstack/react-query';
|
||||||
|
import { useParams } from 'next/navigation';
|
||||||
|
import React, { useEffect } from 'react';
|
||||||
|
|
||||||
|
const ProductDetail = () => {
|
||||||
|
const { detail } = useParams();
|
||||||
|
const id = Number(detail as unknown as string);
|
||||||
|
const { data: resProductsDetail, isLoading: loadProducts } = useQuery({
|
||||||
|
queryKey: ['productItem', id],
|
||||||
|
queryFn: () => getOneProduct(id),
|
||||||
|
});
|
||||||
|
|
||||||
|
const detailData: IProductDetail | undefined = resProductsDetail;
|
||||||
|
const breadcrumsItem: IBreadcrumbItem[] = [
|
||||||
|
{
|
||||||
|
label: 'Главная',
|
||||||
|
href: '/',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Продукты',
|
||||||
|
href: '/products',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Подробности о продукте',
|
||||||
|
href: '/products',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const { onChangePage } = pageStore((state) => state);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
onChangePage({
|
||||||
|
title: 'Подробности о продукте',
|
||||||
|
breadcrumbs: breadcrumsItem,
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
if (loadProducts)
|
||||||
|
return (
|
||||||
|
<div className="">
|
||||||
|
<div className="custom-container min-h-[400px] flex justify-center items-center text-muted font-medium">
|
||||||
|
Загрузка...
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{detailData ? <ProductDetails item={detailData} /> : <PageNotFound />}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ProductDetail;
|
||||||
15
src/app/[locale]/products/layout.tsx
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import HeaderBanner from '@/widgets/header-banner/ui';
|
||||||
|
import React, { ReactNode } from 'react';
|
||||||
|
|
||||||
|
const Products = ({ children }: { children: ReactNode }) => {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div>
|
||||||
|
<HeaderBanner />
|
||||||
|
</div>
|
||||||
|
<div>{children}</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Products;
|
||||||
48
src/app/[locale]/products/page.tsx
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
'use client';
|
||||||
|
import { useCategoryStore } from '@/shared/store/categoryStore';
|
||||||
|
import useFilterStore from '@/shared/store/filterStore';
|
||||||
|
import { IBreadcrumbItem, pageStore } from '@/shared/store/pageStore';
|
||||||
|
import ProductsCategory from '@/widgets/products/ui';
|
||||||
|
import React, { useEffect } from 'react';
|
||||||
|
|
||||||
|
const Products = () => {
|
||||||
|
const breadcrumsItem: IBreadcrumbItem[] = [
|
||||||
|
{
|
||||||
|
label: 'Главная',
|
||||||
|
href: '/',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Продукты',
|
||||||
|
href: '/products',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const { onChangePage } = pageStore((state) => state);
|
||||||
|
const { categoryId } = useFilterStore();
|
||||||
|
const { categories } = useCategoryStore();
|
||||||
|
useEffect(() => {
|
||||||
|
if (categoryId) {
|
||||||
|
const categoryName = categories.find(
|
||||||
|
(item) => item.id === categoryId,
|
||||||
|
)?.title;
|
||||||
|
onChangePage({
|
||||||
|
title: categoryName || 'Все продукты',
|
||||||
|
breadcrumbs: breadcrumsItem,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
onChangePage({
|
||||||
|
title: 'Все продукты',
|
||||||
|
breadcrumbs: breadcrumsItem,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [categoryId]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<ProductsCategory />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Products;
|
||||||
62
src/app/api/send-to-bot/route.ts
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
import { NextResponse } from 'next/server';
|
||||||
|
|
||||||
|
export async function POST(req: Request) {
|
||||||
|
const body = await req.json();
|
||||||
|
const { name, phone, company, email, message } = body;
|
||||||
|
|
||||||
|
const BOT_TOKEN = process.env.API_BOT_TOKEN;
|
||||||
|
const CHAT_IDS = process.env.API_BOT_CHAT_IDS?.split(',') || [];
|
||||||
|
|
||||||
|
if (!BOT_TOKEN || CHAT_IDS.length === 0) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ success: false, message: 'Server not configured properly' },
|
||||||
|
{ status: 500 },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const text1 = `
|
||||||
|
📩 *Новая заявка на обратный звонок*
|
||||||
|
|
||||||
|
👤 *Имя:* _${name}_
|
||||||
|
📞 *Телефон:* _${phone}_
|
||||||
|
`;
|
||||||
|
|
||||||
|
const text2 = `
|
||||||
|
📩 *Новая заявка*
|
||||||
|
|
||||||
|
👤 *Имя:* _${name}_
|
||||||
|
🏢 *Компания:* _${company}_
|
||||||
|
📧 *Email:* _${email}_
|
||||||
|
📞 *Телефон:* _${phone}_
|
||||||
|
|
||||||
|
📝 *Сообщение:*
|
||||||
|
_${message}_
|
||||||
|
`;
|
||||||
|
|
||||||
|
const sendMessage = async (chatId: string) => {
|
||||||
|
return fetch(`https://api.telegram.org/bot${BOT_TOKEN}/sendMessage`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
chat_id: chatId,
|
||||||
|
text: message ? text2 : text1,
|
||||||
|
parse_mode: 'Markdown',
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const results = await Promise.allSettled(
|
||||||
|
CHAT_IDS.map((id) => sendMessage(id)),
|
||||||
|
);
|
||||||
|
|
||||||
|
const hasSuccess = results.some((r) => r.status === 'fulfilled');
|
||||||
|
|
||||||
|
if (!hasSuccess) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ success: false, message: 'Telegram error' },
|
||||||
|
{ status: 500 },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return NextResponse.json({ success: true });
|
||||||
|
}
|
||||||
BIN
src/app/favicon.ico
Normal file
|
After Width: | Height: | Size: 162 KiB |
106
src/app/globals.css
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
@import 'tailwindcss';
|
||||||
|
@import 'tw-animate-css';
|
||||||
|
|
||||||
|
@custom-variant dark (&:is(.dark *));
|
||||||
|
|
||||||
|
@theme inline {
|
||||||
|
--font-manrope: var(--font-manrope-text);
|
||||||
|
--font-caveat: var(--font-caveat-text);
|
||||||
|
--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-6: var(--chart-6);
|
||||||
|
--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(1 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(0.9625 0.0079 106.55);
|
||||||
|
--popover-foreground: oklch(0.141 0.005 285.823);
|
||||||
|
--primary: oklch(0.6499 0.1778 138.27);
|
||||||
|
--primary-foreground: oklch(0.985 0 0);
|
||||||
|
--secondary: oklch(0.8179 0.1493 85.39);
|
||||||
|
--secondary-foreground: oklch(1 0 none);
|
||||||
|
--muted: oklch(0.6152 0.0288 136.39);
|
||||||
|
--muted-foreground: oklch(0.9625 0.0079 106.55);
|
||||||
|
--accent: oklch(0.3695 0.0789 137.33);
|
||||||
|
--accent-foreground: oklch(0.99 0 none);
|
||||||
|
--destructive: oklch(0.577 0.245 27.325);
|
||||||
|
--border: oklch(0.95 0 none);
|
||||||
|
--input: oklch(0.92 0.004 286.32);
|
||||||
|
--ring: oklch(0.705 0.015 286.067);
|
||||||
|
--chart-1: oklch(0.6307 0.1373 304.27);
|
||||||
|
--chart-2: oklch(0.7326 0.0991 160.82);
|
||||||
|
--chart-3: oklch(0.6499 0.1778 138.27);
|
||||||
|
--chart-4: oklch(0.7605 0.1145 80.36);
|
||||||
|
--chart-5: oklch(0.61 0.1408 263.03);
|
||||||
|
--chart-6: oklch(0.6574 0.1526 9.38);
|
||||||
|
--sidebar: oklch(0.3695 0.0789 137.33);
|
||||||
|
--sidebar-foreground: oklch(1 0 none);
|
||||||
|
--sidebar-primary: oklch(0.6499 0.1778 138.27);
|
||||||
|
--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);
|
||||||
|
}
|
||||||
|
|
||||||
|
@layer base {
|
||||||
|
* {
|
||||||
|
@apply border-border outline-ring/50;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: var(--font-manrope);
|
||||||
|
@apply bg-background text-foreground;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@layer components {
|
||||||
|
.custom-container {
|
||||||
|
@apply w-[90%] mx-auto max-w-[1200px];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.swiper-pagination-bullet {
|
||||||
|
background-color: white !important;
|
||||||
|
}
|
||||||
|
|
||||||
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;
|
||||||
|
}
|
||||||
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('/ru');
|
||||||
|
}
|
||||||
1
src/features/auth/lib/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
// Ushbu fayl loyiha structurasi uchun qo'shilgan. O'chirib tashlasangiz bo'ladi
|
||||||
1
src/features/auth/model/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
// Ushbu fayl loyiha structurasi uchun qo'shilgan. O'chirib tashlasangiz bo'ladi
|
||||||
1
src/features/auth/ui/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
// Ushbu fayl loyiha structurasi uchun qo'shilgan. O'chirib tashlasangiz bo'ladi
|
||||||
1
src/features/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
// Ushbu fayl loyiha structurasi uchun qo'shilgan. O'chirib tashlasangiz bo'ladi
|
||||||
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 `/api`, `/trpc`, `/_next` or `/_vercel`
|
||||||
|
// - … the ones containing a dot (e.g. `favicon.ico`)
|
||||||
|
matcher: '/((?!api|trpc|_next|_vercel|.*\\..*).*)',
|
||||||
|
};
|
||||||
7
src/shared/config/api/URLs.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
const BASE_URL = process.env.NEXT_PUBLIC_API_URL || 'https://food.felixits.uz/';
|
||||||
|
|
||||||
|
const ENDP_BANNER = '/banner/';
|
||||||
|
const ENDP_CATEGORY = '/category/';
|
||||||
|
const ENDP_PRODUCT = '/products/';
|
||||||
|
|
||||||
|
export { BASE_URL, ENDP_BANNER, ENDP_CATEGORY, ENDP_PRODUCT };
|
||||||
35
src/shared/config/api/httpClient.ts
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import getLocaleCS from '@/shared/lib/getLocaleCS';
|
||||||
|
import axios from 'axios';
|
||||||
|
import { getLocale } from 'next-intl/server';
|
||||||
|
import { LanguageRoutes } from '../i18n/types';
|
||||||
|
import { BASE_URL } from './URLs';
|
||||||
|
|
||||||
|
const httpClient = axios.create({
|
||||||
|
baseURL: BASE_URL,
|
||||||
|
timeout: 10000,
|
||||||
|
});
|
||||||
|
|
||||||
|
httpClient.interceptors.request.use(
|
||||||
|
async (config) => {
|
||||||
|
let language = LanguageRoutes.RU;
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (typeof window === 'undefined') {
|
||||||
|
// Serverda ishlayapti
|
||||||
|
language = (await getLocale()) as LanguageRoutes;
|
||||||
|
} else {
|
||||||
|
// Clientda ishlayapti
|
||||||
|
language = getLocaleCS() || LanguageRoutes.RU;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.log('Locale olishda xato:', e);
|
||||||
|
}
|
||||||
|
|
||||||
|
config.headers['Accept-Language'] = language;
|
||||||
|
|
||||||
|
return config;
|
||||||
|
},
|
||||||
|
(error) => Promise.reject(error),
|
||||||
|
);
|
||||||
|
|
||||||
|
export default httpClient;
|
||||||
46
src/shared/config/api/sendToBot.ts
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
import { NextResponse } from 'next/server';
|
||||||
|
|
||||||
|
export async function POST(req: Request) {
|
||||||
|
const body = await req.json();
|
||||||
|
const { name, phone } = body;
|
||||||
|
|
||||||
|
const BOT_TOKEN = process.env.BOT_TOKEN;
|
||||||
|
const CHAT_IDS = process.env.BOT_CHAT_IDS?.split(',') || [];
|
||||||
|
|
||||||
|
if (!BOT_TOKEN || CHAT_IDS.length === 0) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ success: false, message: 'Server not configured properly' },
|
||||||
|
{ status: 500 },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const text = `
|
||||||
|
📩 *Новая заявка на обратный звонок*
|
||||||
|
|
||||||
|
👤 *Имя:* _${name}_
|
||||||
|
📞 *Телефон:* _${phone}_
|
||||||
|
`;
|
||||||
|
|
||||||
|
const sendMessage = async (chatId: string) => {
|
||||||
|
return fetch(`https://api.telegram.org/bot${BOT_TOKEN}/sendMessage`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ chat_id: chatId, text, parse_mode: 'Markdown' }),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const results = await Promise.allSettled(
|
||||||
|
CHAT_IDS.map((id) => sendMessage(id)),
|
||||||
|
);
|
||||||
|
|
||||||
|
const hasSuccess = results.some((r) => r.status === 'fulfilled');
|
||||||
|
|
||||||
|
if (!hasSuccess) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ success: false, message: 'Telegram error' },
|
||||||
|
{ status: 500 },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return NextResponse.json({ success: true });
|
||||||
|
}
|
||||||
101
src/shared/config/api/testApi.ts
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
import {
|
||||||
|
ReqWithPagination,
|
||||||
|
ResWithPagination,
|
||||||
|
ResWithPaginationOneItem,
|
||||||
|
} from './types';
|
||||||
|
import { AxiosResponse } from 'axios';
|
||||||
|
import {
|
||||||
|
IBanner,
|
||||||
|
ICategory,
|
||||||
|
IProduct,
|
||||||
|
IProductDetail,
|
||||||
|
} from '@/shared/types/testApi';
|
||||||
|
import httpClient from './httpClient';
|
||||||
|
import { ENDP_BANNER, ENDP_CATEGORY, ENDP_PRODUCT } from './URLs';
|
||||||
|
|
||||||
|
const getBanner = async (
|
||||||
|
pagination?: ReqWithPagination,
|
||||||
|
): Promise<ResWithPagination<IBanner>['data']> => {
|
||||||
|
const response: AxiosResponse<ResWithPagination<IBanner>> =
|
||||||
|
await httpClient.get(ENDP_BANNER, { params: pagination });
|
||||||
|
return response.data.data;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getCategory = async (
|
||||||
|
pagination?: ReqWithPagination,
|
||||||
|
): Promise<ResWithPagination<ICategory>['data']> => {
|
||||||
|
const response: AxiosResponse<ResWithPagination<ICategory>> =
|
||||||
|
await httpClient.get(ENDP_CATEGORY, { params: pagination });
|
||||||
|
return response.data.data;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getAllProduct = async (
|
||||||
|
pagination?: ReqWithPagination,
|
||||||
|
): Promise<ResWithPagination<IProduct>['data']> => {
|
||||||
|
const firstResponse: AxiosResponse<ResWithPagination<IProduct>> =
|
||||||
|
await httpClient.get(ENDP_PRODUCT, { params: { ...pagination, page: 1 } });
|
||||||
|
const pageCount = firstResponse.data.data.total_pages;
|
||||||
|
const allResults = [...firstResponse.data.data.results];
|
||||||
|
const requests = [];
|
||||||
|
|
||||||
|
for (let i = 2; i <= pageCount; i++) {
|
||||||
|
requests.push(
|
||||||
|
httpClient.get<ResWithPagination<IProduct>>(ENDP_PRODUCT, {
|
||||||
|
params: { ...pagination, page: i },
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const responses = await Promise.allSettled(requests);
|
||||||
|
|
||||||
|
for (const res of responses) {
|
||||||
|
if (res.status === 'fulfilled') {
|
||||||
|
allResults.push(...res.value.data.data.results);
|
||||||
|
} else {
|
||||||
|
console.error('Error:', res.reason);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...firstResponse.data.data,
|
||||||
|
results: allResults,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const getProduct = async (
|
||||||
|
pagination?: ReqWithPagination,
|
||||||
|
): Promise<ResWithPagination<IProduct>['data']> => {
|
||||||
|
const response: AxiosResponse<ResWithPagination<IProduct>> =
|
||||||
|
await httpClient.get(ENDP_PRODUCT, {
|
||||||
|
params: pagination,
|
||||||
|
});
|
||||||
|
return response.data.data;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getBestProduct = async (
|
||||||
|
pagination?: ReqWithPagination,
|
||||||
|
): Promise<ResWithPagination<IProduct>['data']> => {
|
||||||
|
const response: AxiosResponse<ResWithPagination<IProduct>> =
|
||||||
|
await httpClient.get(ENDP_PRODUCT, {
|
||||||
|
params: pagination,
|
||||||
|
});
|
||||||
|
return response.data.data;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getOneProduct = async (
|
||||||
|
id?: number,
|
||||||
|
pagination?: ReqWithPagination,
|
||||||
|
): Promise<IProductDetail> => {
|
||||||
|
const response: AxiosResponse<ResWithPaginationOneItem<IProductDetail>> =
|
||||||
|
await httpClient.get(`${ENDP_PRODUCT}${id}`, { params: pagination });
|
||||||
|
return response.data.data;
|
||||||
|
};
|
||||||
|
|
||||||
|
export {
|
||||||
|
getBanner,
|
||||||
|
getCategory,
|
||||||
|
getProduct,
|
||||||
|
getAllProduct,
|
||||||
|
getOneProduct,
|
||||||
|
getBestProduct,
|
||||||
|
};
|
||||||
28
src/shared/config/api/types.ts
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
export interface ResWithPagination<T> {
|
||||||
|
data: {
|
||||||
|
links: Links;
|
||||||
|
total_items: number;
|
||||||
|
total_pages: number;
|
||||||
|
page_size: number;
|
||||||
|
current_page: number;
|
||||||
|
results: T[];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ResWithPaginationOneItem<T> {
|
||||||
|
data: T;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Links {
|
||||||
|
next: number | null;
|
||||||
|
previous: number | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ReqWithPagination {
|
||||||
|
_start?: number;
|
||||||
|
_limit?: number;
|
||||||
|
page?: number;
|
||||||
|
category?: number | undefined;
|
||||||
|
popular?: boolean;
|
||||||
|
search?: string;
|
||||||
|
}
|
||||||
15
src/shared/config/fonts.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import { Caveat, Manrope } from 'next/font/google';
|
||||||
|
|
||||||
|
export const manrope = Manrope({
|
||||||
|
weight: ['400', '500', '600', '700', '800'],
|
||||||
|
subsets: ['latin', 'cyrillic'],
|
||||||
|
variable: '--font-manrope-text',
|
||||||
|
display: 'swap',
|
||||||
|
});
|
||||||
|
|
||||||
|
export const caveat = Caveat({
|
||||||
|
weight: ['400', '500', '600', '700'],
|
||||||
|
subsets: ['latin', 'cyrillic'],
|
||||||
|
variable: '--font-caveat-text',
|
||||||
|
display: 'swap',
|
||||||
|
});
|
||||||
6
src/shared/config/i18n/messages/ki.json
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"HomePage": {
|
||||||
|
"title": "Salom dunyo! (Kiril)",
|
||||||
|
"about": "Go to the about page"
|
||||||
|
}
|
||||||
|
}
|
||||||
6
src/shared/config/i18n/messages/ru.json
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"HomePage": {
|
||||||
|
"title": "Hello world!",
|
||||||
|
"about": "Go to the about page"
|
||||||
|
}
|
||||||
|
}
|
||||||
10
src/shared/config/i18n/messages/uz.d.json.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
// This file is auto-generated by next-intl, do not edit directly.
|
||||||
|
// See: https://next-intl.dev/docs/workflows/typescript#messages-arguments
|
||||||
|
|
||||||
|
declare const messages: {
|
||||||
|
HomePage: {
|
||||||
|
title: 'Salom dunyo!';
|
||||||
|
about: 'Go to the about page';
|
||||||
|
};
|
||||||
|
};
|
||||||
|
export default messages;
|
||||||
6
src/shared/config/i18n/messages/uz.json
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"HomePage": {
|
||||||
|
"title": "Salom dunyo!",
|
||||||
|
"about": "Go to the about page"
|
||||||
|
}
|
||||||
|
}
|
||||||
7
src/shared/config/i18n/navigation.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import { createNavigation } from 'next-intl/navigation';
|
||||||
|
import { routing } from './routing';
|
||||||
|
|
||||||
|
// Lightweight wrappers around Next.js' navigation
|
||||||
|
// APIs that consider the routing configuration
|
||||||
|
export const { Link, redirect, usePathname, useRouter, getPathname } =
|
||||||
|
createNavigation(routing);
|
||||||
16
src/shared/config/i18n/request.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import { getRequestConfig } from 'next-intl/server';
|
||||||
|
import { hasLocale } from 'next-intl';
|
||||||
|
import { routing } from './routing';
|
||||||
|
|
||||||
|
export default getRequestConfig(async ({ requestLocale }) => {
|
||||||
|
// Typically corresponds to the `[locale]` segment
|
||||||
|
const requested = await requestLocale;
|
||||||
|
const locale = hasLocale(routing.locales, requested)
|
||||||
|
? requested
|
||||||
|
: routing.defaultLocale;
|
||||||
|
|
||||||
|
return {
|
||||||
|
locale,
|
||||||
|
messages: (await import(`./messages/${locale}.json`)).default,
|
||||||
|
};
|
||||||
|
});
|
||||||
11
src/shared/config/i18n/routing.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import { defineRouting } from 'next-intl/routing';
|
||||||
|
import { LanguageRoutes } from './types';
|
||||||
|
|
||||||
|
export const routing = defineRouting({
|
||||||
|
// A list of all locales that are supported
|
||||||
|
locales: [LanguageRoutes.UZ, LanguageRoutes.RU, LanguageRoutes.KI],
|
||||||
|
|
||||||
|
// Used when no locale matches
|
||||||
|
defaultLocale: LanguageRoutes.RU,
|
||||||
|
localeDetection: false,
|
||||||
|
});
|
||||||
5
src/shared/config/i18n/types.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
export enum LanguageRoutes {
|
||||||
|
UZ = 'uz', // o'zbekcha
|
||||||
|
RU = 'ru', // ruscha
|
||||||
|
KI = 'ki', // kirilcha
|
||||||
|
}
|
||||||
27
src/shared/config/react-query/QueryProvider.tsx
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { ReactNode, useState } from 'react';
|
||||||
|
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||||
|
|
||||||
|
const QueryProvider = ({ children }: { children: ReactNode }) => {
|
||||||
|
const [queryClient] = useState(
|
||||||
|
() =>
|
||||||
|
new QueryClient({
|
||||||
|
defaultOptions: {
|
||||||
|
queries: {
|
||||||
|
refetchOnWindowFocus: false,
|
||||||
|
retry: 1,
|
||||||
|
staleTime: 1000 * 60 * 5,
|
||||||
|
refetchOnReconnect: false,
|
||||||
|
refetchOnMount: false,
|
||||||
|
refetchInterval: 1000 * 60 * 5,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default QueryProvider;
|
||||||
11
src/shared/config/theme-provider.tsx
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import * as React from 'react';
|
||||||
|
import { ThemeProvider as NextThemesProvider } from 'next-themes';
|
||||||
|
|
||||||
|
export function ThemeProvider({
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof NextThemesProvider>) {
|
||||||
|
return <NextThemesProvider {...props}>{children}</NextThemesProvider>;
|
||||||
|
}
|
||||||
27
src/shared/constants/data.ts
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
const PRODUCT_INFO = {
|
||||||
|
name: 'ZHANGZHOU ZHENTIAN FOOD',
|
||||||
|
description: 'ОТ ФЕРМЫ ДО БАНКИ. ПОЛУФАБРИКАТЫ ДЛЯ КОНСЕРВНОГО ПРИОЗВОДСТВА',
|
||||||
|
logo: '/logo.png',
|
||||||
|
logoTitle: 'ZHANGZHOU ZHENTIAN FOOD',
|
||||||
|
favicon: '/favicon.png',
|
||||||
|
url: 'https://www.shadcnblocks.com',
|
||||||
|
socials: {
|
||||||
|
telegram: 'https://t.me/Can_prom',
|
||||||
|
instagram: 'https://www.instagram.com/username',
|
||||||
|
wechat: 'https://wechat.com/username',
|
||||||
|
viber: 'https://invite.viber.com/?g=I-swr9CY6VQns1HWA1PJvUYXbOLkR6aw',
|
||||||
|
facebook: 'https://www.facebook.com/username',
|
||||||
|
whatsapp: 'https://wa.me/998774074324',
|
||||||
|
},
|
||||||
|
|
||||||
|
contact: {
|
||||||
|
phone: '+998774074324',
|
||||||
|
email: 'info@can-prom.com',
|
||||||
|
address:
|
||||||
|
'1-1603 Oriental Cambridge Estate, Haicheng Town, Longhai City, Fujian Province, China.',
|
||||||
|
},
|
||||||
|
terms_of_use: '',
|
||||||
|
creator: 'FIAS App',
|
||||||
|
};
|
||||||
|
|
||||||
|
export { PRODUCT_INFO };
|
||||||
39
src/shared/hooks/use-closer.ts
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
import React, { useEffect } from 'react';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hook for closing some items when they are unnecessary to the user
|
||||||
|
* @param ref For an item that needs to be closed when the outer part is pressed
|
||||||
|
* @param closeFunction Closing function
|
||||||
|
* @param scrollClose If it shouldn't close when scrolling, false will be sent. Default true
|
||||||
|
*/
|
||||||
|
|
||||||
|
const useCloser = (
|
||||||
|
ref: React.RefObject<HTMLElement>,
|
||||||
|
closeFunction: () => void,
|
||||||
|
scrollClose: boolean = true,
|
||||||
|
) => {
|
||||||
|
useEffect(() => {
|
||||||
|
// call function when click outside is ref element
|
||||||
|
function handleClickOutside(event: MouseEvent) {
|
||||||
|
if (ref.current && !ref.current.contains(event.target as Node)) {
|
||||||
|
closeFunction();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// call function when page is scrolling
|
||||||
|
function handleScroll() {
|
||||||
|
if (scrollClose) {
|
||||||
|
closeFunction();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener('mousedown', handleClickOutside);
|
||||||
|
document.addEventListener('scroll', handleScroll);
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener('mousedown', handleClickOutside);
|
||||||
|
document.removeEventListener('scroll', handleScroll);
|
||||||
|
};
|
||||||
|
}, [ref, closeFunction]);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useCloser;
|
||||||
27
src/shared/hooks/use-mobile.ts
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
|
||||||
|
const MOBILE_BREAKPOINT = 768;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine if it's on the current mobile screen (768px)
|
||||||
|
* @returns boolean
|
||||||
|
*/
|
||||||
|
const useIsMobile = () => {
|
||||||
|
const [isMobile, setIsMobile] = React.useState<boolean | undefined>(
|
||||||
|
undefined,
|
||||||
|
);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`);
|
||||||
|
const onChange = () => {
|
||||||
|
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
|
||||||
|
};
|
||||||
|
mql.addEventListener('change', onChange);
|
||||||
|
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
|
||||||
|
return () => mql.removeEventListener('change', onChange);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return !!isMobile;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useIsMobile;
|
||||||
38
src/shared/hooks/use-window-size.ts
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
interface ISize {
|
||||||
|
width: number | undefined;
|
||||||
|
height: number | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Screen size determination
|
||||||
|
* @returns number
|
||||||
|
*/
|
||||||
|
const useWindowSize = () => {
|
||||||
|
const [size, setSize] = useState<ISize>({
|
||||||
|
width: undefined,
|
||||||
|
height: undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const getScreenSize = () => {
|
||||||
|
setSize({
|
||||||
|
width: window.innerWidth,
|
||||||
|
height: window.innerHeight,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
getScreenSize();
|
||||||
|
|
||||||
|
window.addEventListener('resize', getScreenSize);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('resize', getScreenSize);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return size;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useWindowSize;
|
||||||
10
src/shared/lib/addBaseUrl.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
/**
|
||||||
|
* Add base url to url
|
||||||
|
* @param url Current url
|
||||||
|
* @returns string
|
||||||
|
*/
|
||||||
|
const addBaseUrl = (url: string) => {
|
||||||
|
return process.env.NEXT_PUBLIC_API_URL + url;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default addBaseUrl;
|
||||||
89
src/shared/lib/formatDate.ts
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
import dayjs from 'dayjs';
|
||||||
|
import 'dayjs/locale/uz-latn';
|
||||||
|
import 'dayjs/locale/uz';
|
||||||
|
import 'dayjs/locale/ru';
|
||||||
|
import localizedFormat from 'dayjs/plugin/localizedFormat';
|
||||||
|
import relativeTime from 'dayjs/plugin/relativeTime';
|
||||||
|
import { getLocale } from 'next-intl/server';
|
||||||
|
|
||||||
|
// Install Dayjs plugins
|
||||||
|
dayjs.extend(localizedFormat);
|
||||||
|
dayjs.extend(relativeTime);
|
||||||
|
|
||||||
|
// Find locale
|
||||||
|
const getCurrentLocale = async () => {
|
||||||
|
const locale = await getLocale();
|
||||||
|
switch (locale) {
|
||||||
|
case 'ki':
|
||||||
|
return 'uz';
|
||||||
|
case 'uz':
|
||||||
|
return 'uz-latn';
|
||||||
|
case 'ru':
|
||||||
|
return 'ru';
|
||||||
|
|
||||||
|
default:
|
||||||
|
return 'uz-latn';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatDate = {
|
||||||
|
/**
|
||||||
|
* Show date in specified format
|
||||||
|
* @param time Date object or string or number
|
||||||
|
* @param format type
|
||||||
|
* @param locale Language (optional)
|
||||||
|
* @returns string
|
||||||
|
*/
|
||||||
|
to: async (
|
||||||
|
time: Date | string | number,
|
||||||
|
format: string,
|
||||||
|
locale?: string,
|
||||||
|
): Promise<string> => {
|
||||||
|
const currentLocale = locale || (await getCurrentLocale());
|
||||||
|
return dayjs(time).locale(currentLocale).format(format);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sync date in specified format (for client-side)
|
||||||
|
* @param time Date object or string or number
|
||||||
|
* @param format type
|
||||||
|
* @param locale Language (optional, standard Uzbek)
|
||||||
|
* @returns string
|
||||||
|
*/
|
||||||
|
format: (
|
||||||
|
time: Date | string | number,
|
||||||
|
format: string,
|
||||||
|
locale: string = 'uz',
|
||||||
|
): string => {
|
||||||
|
return dayjs(time).locale(locale).format(format);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show date in relative time format (today, yesterday, 2 days ago,...)
|
||||||
|
* @param time Date object or string or number
|
||||||
|
* @param locale Language (optional, standard Uzbek)
|
||||||
|
* @returns string
|
||||||
|
*/
|
||||||
|
relative: async (
|
||||||
|
time: Date | string | number,
|
||||||
|
locale?: string,
|
||||||
|
): Promise<string> => {
|
||||||
|
const currentLocale = locale || (await getCurrentLocale());
|
||||||
|
return dayjs(time).locale(currentLocale).fromNow();
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show relative time synchronously (for client-side)
|
||||||
|
* @param time Date object or string or number
|
||||||
|
* @param locale Language (optional, standard Uzbek)
|
||||||
|
* @returns string
|
||||||
|
*/
|
||||||
|
relativeFormat: (
|
||||||
|
time: Date | string | number,
|
||||||
|
locale: string = 'uz',
|
||||||
|
): string => {
|
||||||
|
return dayjs(time).locale(locale).fromNow();
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default formatDate;
|
||||||
38
src/shared/lib/formatPhone.ts
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
/**
|
||||||
|
* Format the number (+998 00 111-22-33)
|
||||||
|
* @param value Number to be formatted
|
||||||
|
* @returns string +998 00 111-22-33
|
||||||
|
*/
|
||||||
|
const formatPhone = (value: string) => {
|
||||||
|
// Keep only numbers
|
||||||
|
const digits = value.replace(/\D/g, '');
|
||||||
|
|
||||||
|
// Return empty string if data is not available
|
||||||
|
if (digits.length === 0) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
const prefix = digits.startsWith('998') ? '+998 ' : '+998 ';
|
||||||
|
|
||||||
|
let formattedNumber = prefix;
|
||||||
|
|
||||||
|
if (digits.length > 3) {
|
||||||
|
formattedNumber += digits.slice(3, 5);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (digits.length > 5) {
|
||||||
|
formattedNumber += ' ' + digits.slice(5, 8);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (digits.length > 8) {
|
||||||
|
formattedNumber += '-' + digits.slice(8, 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (digits.length > 10) {
|
||||||
|
formattedNumber += '-' + digits.slice(10, 12);
|
||||||
|
}
|
||||||
|
|
||||||
|
return formattedNumber.trim();
|
||||||
|
};
|
||||||
|
|
||||||
|
export default formatPhone;
|
||||||
32
src/shared/lib/formatPrice.ts
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import { LanguageRoutes } from '../config/i18n/types';
|
||||||
|
import { getLocale } from 'next-intl/server';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format price. With label.
|
||||||
|
* @param amount Price
|
||||||
|
* @param withLabel Show label. Default false
|
||||||
|
* @returns string. Ex. X XXX XXX sum
|
||||||
|
*/
|
||||||
|
const formatPrice = async (amount: number | string, withLabel?: boolean) => {
|
||||||
|
const locale = (await getLocale()) as LanguageRoutes;
|
||||||
|
const label = withLabel
|
||||||
|
? locale == LanguageRoutes.RU
|
||||||
|
? ' сум'
|
||||||
|
: locale == LanguageRoutes.KI
|
||||||
|
? ' сўм'
|
||||||
|
: ' so‘m'
|
||||||
|
: '';
|
||||||
|
const parts = String(amount).split('.');
|
||||||
|
const dollars = parts[0];
|
||||||
|
const cents = parts.length > 1 ? parts[1] : '00';
|
||||||
|
|
||||||
|
const formattedDollars = dollars.replace(/\B(?=(\d{3})+(?!\d))/g, ' ');
|
||||||
|
|
||||||
|
if (String(amount).length == 0) {
|
||||||
|
return formattedDollars + '.' + cents + label;
|
||||||
|
} else {
|
||||||
|
return formattedDollars + label;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default formatPrice;
|
||||||
13
src/shared/lib/getLocaleCS.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import { LanguageRoutes } from '../config/i18n/types';
|
||||||
|
|
||||||
|
const getLocaleCS = (): LanguageRoutes | undefined => {
|
||||||
|
if (typeof window !== 'undefined' && typeof document !== 'undefined') {
|
||||||
|
const match = document.cookie
|
||||||
|
.split('; ')
|
||||||
|
.find((row) => row.startsWith('NEXT_LOCALE='));
|
||||||
|
return match?.split('=')[1] as LanguageRoutes;
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default getLocaleCS;
|
||||||
6
src/shared/lib/utils.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
import { clsx, type ClassValue } from 'clsx';
|
||||||
|
import { twMerge } from 'tailwind-merge';
|
||||||
|
|
||||||
|
export function cn(...inputs: ClassValue[]) {
|
||||||
|
return twMerge(clsx(inputs));
|
||||||
|
}
|
||||||
BIN
src/shared/ogenix-icons/fonts/icomoon.eot@orkqwr
Normal file
60
src/shared/ogenix-icons/fonts/icomoon.svg@orkqwr
Normal file
|
After Width: | Height: | Size: 136 KiB |