change package managers

This commit is contained in:
azizziy
2025-12-27 09:32:01 +05:00
commit 05dc099df9
189 changed files with 13706 additions and 0 deletions

41
.gitignore vendored Normal file
View 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
View File

@@ -0,0 +1 @@
npx lint-staged

1
.husky/pre-push Normal file
View File

@@ -0,0 +1 @@
npm run build

6
.npmrc Normal file
View File

@@ -0,0 +1,6 @@
# pnpm configuration
audit=true
ignore-scripts=false
strict-ssl=true
minimum-release-age=262974

36
README.md Normal file
View File

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

21
components.json Normal file
View File

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

54
eslint.config.mjs Normal file
View 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
View 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
View 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
View 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

File diff suppressed because it is too large Load Diff

5
postcss.config.mjs Normal file
View File

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

9
prettier.config.cjs Normal file
View File

@@ -0,0 +1,9 @@
/** @type {import("prettier").Config} */
module.exports = {
semi: true, // Har satrda nuqta-vergul bolishi
singleQuote: true, // ' ' ishlatilsin, " " emas
trailingComma: 'all', // songgi vergullar qoyilsin
tabWidth: 2, // Indent 2 bolsin
bracketSpacing: true, // { a: 1 } ichida bosh joy qoldirsin
arrowParens: 'always', // (x) => {...}
};

BIN
public/favicon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 203 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 257 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 372 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 89 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 785 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 270 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 820 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 757 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 202 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 284 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 169 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

BIN
public/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 131 KiB

8
public/robots.txt Normal file
View 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
View 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>

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

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

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

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

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

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

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

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

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

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

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

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

View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 162 KiB

106
src/app/globals.css Normal file
View 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
View File

@@ -0,0 +1,11 @@
import { ReactNode } from 'react';
type Props = {
children: ReactNode;
};
// Since we have a `not-found.tsx` page on the root, a layout file
// is required, even if it's just passing children through.
export default function RootLayout({ children }: Props) {
return children;
}

6
src/app/page.tsx Normal file
View File

@@ -0,0 +1,6 @@
import { redirect } from 'next/navigation';
// This page only renders when the app is built statically (output: 'export')
export default function RootPage() {
redirect('/ru');
}

View File

@@ -0,0 +1 @@
// Ushbu fayl loyiha structurasi uchun qo'shilgan. O'chirib tashlasangiz bo'ladi

View File

@@ -0,0 +1 @@
// Ushbu fayl loyiha structurasi uchun qo'shilgan. O'chirib tashlasangiz bo'ladi

View File

@@ -0,0 +1 @@
// Ushbu fayl loyiha structurasi uchun qo'shilgan. O'chirib tashlasangiz bo'ladi

1
src/features/index.ts Normal file
View File

@@ -0,0 +1 @@
// Ushbu fayl loyiha structurasi uchun qo'shilgan. O'chirib tashlasangiz bo'ladi

11
src/middleware.ts Normal file
View File

@@ -0,0 +1,11 @@
import createMiddleware from 'next-intl/middleware';
import { routing } from './shared/config/i18n/routing';
export default createMiddleware(routing);
export const config = {
// Match all pathnames except for
// - … if they start with `/api`, `/trpc`, `/_next` or `/_vercel`
// - … the ones containing a dot (e.g. `favicon.ico`)
matcher: '/((?!api|trpc|_next|_vercel|.*\\..*).*)',
};

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

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

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

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

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

View 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',
});

View File

@@ -0,0 +1,6 @@
{
"HomePage": {
"title": "Salom dunyo! (Kiril)",
"about": "Go to the about page"
}
}

View File

@@ -0,0 +1,6 @@
{
"HomePage": {
"title": "Hello world!",
"about": "Go to the about page"
}
}

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

View File

@@ -0,0 +1,6 @@
{
"HomePage": {
"title": "Salom dunyo!",
"about": "Go to the about page"
}
}

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

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

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

View File

@@ -0,0 +1,5 @@
export enum LanguageRoutes {
UZ = 'uz', // o'zbekcha
RU = 'ru', // ruscha
KI = 'ki', // kirilcha
}

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

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

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

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

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

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

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

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

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

View 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
? ' сўм'
: ' som'
: '';
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;

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

Binary file not shown.

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 136 KiB

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