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 |