This commit is contained in:
2026-04-15 11:19:45 +00:00
commit acb79b2db7
183 changed files with 22067 additions and 0 deletions

8
src/app/500.tsx Normal file
View File

@@ -0,0 +1,8 @@
import React from 'react'
const Custom500 = () => {
return (
<div>500</div>
)
}
export default Custom500

26
src/app/Error.tsx Normal file
View File

@@ -0,0 +1,26 @@
import React from 'react';
interface ErrorProps {
statusCode?: number;
}
const ErrorPage: React.FC<ErrorProps> = ({statusCode}) => {
return (
<div>
<p>
{statusCode
? `An error ${statusCode} occurred on server`
: 'An error occurred on client'}
</p>
</div>
);
};
export async function getServerSideProps() {
return {
props: {statusCode: 500},
};
}
export default ErrorPage;

View File

@@ -0,0 +1,21 @@
"use client"
import React from 'react'
import LoginSection from "@/features/auth/ui/login-section";
import {useAuthStore} from "@/shared/store/authStore";
const Page = () => {
const {user, isAuthenticated} = useAuthStore();
if(user || isAuthenticated) {
window.location.href = '/profile';
}
return (
<div className="flex min-h-svh w-full items-center justify-center p-6 md:p-10">
<div className="w-full max-w-sm">
<LoginSection/>
</div>
</div>
)
}
export default Page

View File

@@ -0,0 +1,8 @@
import React from 'react'
const Page = () => {
return (
<div>Page</div>
)
}
export default Page

View File

@@ -0,0 +1,32 @@
import React from "react";
import { getBrandProducts } from "@/shared/api/brandsSvc";
import ProductsList from "@/features/brand-products/ui/products-list";
import MyPagionation from "@/shared/ui/my-pagionation";
export const dynamic = "force-dynamic";
type PageProps = {
params: {
brandId: number;
};
searchParams: { page?: string };
};
const Page = async ({ params, searchParams }: Readonly<PageProps>) => {
const { brandId } = await params;
const { page } = await searchParams;
const products = await getBrandProducts(brandId);
return (
<div className={"section-wrapper"}>
<div className={"section-wrapper"}>
<ProductsList products={products.data} />
</div>
<MyPagionation
currentPage={Number(page)}
totalPages={products.pagination.total}
/>
</div>
);
};
export default Page;

View File

@@ -0,0 +1,36 @@
import React from "react";
import ProductsSection from "@/features/category-details/ui/products-section";
import MyPagionation from "@/shared/ui/my-pagionation";
import { getProducts } from "@/shared/api/productSvc";
export const dynamic = "force-dynamic";
type PageProps = {
params: {
categoryId: number;
};
searchParams: { page?: string };
};
const Page = async ({ params, searchParams }: Readonly<PageProps>) => {
const { page } = await searchParams;
const { categoryId } = await params;
const { data: products } = await getProducts({
categoryId,
currentPage: Number(page),
});
return (
<div className={"section-wrapper"}>
<div className={"section-wrapper"}>
<ProductsSection products={products.data} />
</div>
<MyPagionation
currentPage={Number(page)}
totalPages={products.pagination.total}
/>
</div>
);
};
export default Page;

View File

@@ -0,0 +1,14 @@
import React from "react";
import CategorySection from "../../../features/home/ui/category-section";
import { getCategory } from "@/shared/api/productCategorySvc";
const Page = async () => {
const { data: categoryData } = await getCategory();
return (
<div className={"section-wrapper"}>
<CategorySection categories={categoryData} />
</div>
);
};
export default Page;

113
src/app/[locale]/layout.tsx Normal file
View File

@@ -0,0 +1,113 @@
import React from "react";
import "../globals.css";
import { notFound } from "next/navigation";
import { golosText, routing } from "@/shared/config";
import { Locale } from "@/shared/types/locale";
import { LanguageRoutes } from "@/shared/config/i18n/types";
import { Navbar } from "@/widgets";
import Footer from "@/widgets/footer/footer";
import { Metadata } from "next";
import { PRODUCT_INFO } from "@/shared/constants";
import { hasLocale, NextIntlClientProvider } from "next-intl";
import { setRequestLocale } from "next-intl/server";
import QueryProvider from "@/shared/providers/QueryProvider";
import ProgressBar from "@/shared/ui/progressbar";
import { Toaster } from "@/shared/ui/sonner";
import Script from "next/script";
export const metadata: Metadata = {
title: PRODUCT_INFO.name,
description: PRODUCT_INFO.description,
icons: PRODUCT_INFO.favicon,
keywords: [
"get green",
"green energy",
"get green energy trade",
"quyosh uskunalari",
"солнечное оборудование",
],
metadataBase: new URL(PRODUCT_INFO.url),
alternates: {
canonical: PRODUCT_INFO.url,
languages: {
uz: `${PRODUCT_INFO.url}/${LanguageRoutes.UZ}`,
ru: `${PRODUCT_INFO.url}/${LanguageRoutes.RU}`,
},
},
applicationName: PRODUCT_INFO.name,
authors: [{ name: PRODUCT_INFO.creator, url: "https://felix-its.uz/" }],
category: "website",
openGraph: {
title: PRODUCT_INFO.name,
url: PRODUCT_INFO.url,
description: PRODUCT_INFO.description,
type: "website",
countryName: "O'zbekiston",
siteName: PRODUCT_INFO.name,
images: {
url: "/og-banner.png",
alt: "get green",
width: 1200,
height: 630,
},
alternateLocale: [LanguageRoutes.UZ, LanguageRoutes.RU],
},
};
type LayoutProps = {
children: React.ReactNode;
params: Promise<{ locale: Locale }>;
};
export default async function LocaleLayout({ children, params }: LayoutProps) {
const { locale } = await params;
if (!hasLocale(routing.locales, locale)) {
notFound();
}
// Enable static rendering
setRequestLocale(locale);
return (
<html lang={locale} suppressHydrationWarning>
<head>
{/* Google tag (gtag.js) */}
<Script
src="https://www.googletagmanager.com/gtag/js?id=AW-17219198796"
strategy="afterInteractive"
/>
<Script id="gtag-init" strategy="afterInteractive">
{`
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', 'AW-17219198796');
`}
</Script>
{/* Google Ads Conversion Tracking */}
<Script id="conversion-tracking" strategy="afterInteractive">
{`
gtag('event', 'conversion', {
send_to: 'AW-17219198796/SQJ6CJuH8dwaEMy-4JJA',
value: 1.0,
currency: 'USD'
});
`}
</Script>
</head>
<body
className={`${golosText.variable} ${golosText.className} font-poppins antialiased`}
>
<NextIntlClientProvider locale={locale as LanguageRoutes}>
<QueryProvider>
<ProgressBar />
<Navbar />
{children}
<Footer />
<Toaster position="bottom-center" richColors />
</QueryProvider>
</NextIntlClientProvider>
</body>
</html>
);
}

40
src/app/[locale]/page.tsx Normal file
View File

@@ -0,0 +1,40 @@
import {
ContactSection,
DownloadAppSection,
FreeRecommendationSection,
HeroSection,
PartnersSection,
ProfitSection,
} from "@/features/home/ui";
import { getCompilation } from "@/shared/api/compilationsSvc";
import ProductsSection from "@/features/category-details/ui/products-section";
import SectionsSection from "@/features/home/ui/sections-section";
import { getBrands } from "@/shared/api/brandsSvc";
const Page = async () => {
const { data } = await getCompilation();
const { data: partners } = await getBrands();
return (
<div>
<HeroSection />
<SectionsSection />
{data.map((section) => (
<div
key={section.id}
className={"my-container section-wrapper px-4 md:px-0"}
>
<h1 className={"section-title"}>{section.title}</h1>
<ProductsSection products={section.products} />
</div>
))}
<FreeRecommendationSection />
<ProfitSection />
<PartnersSection partners={partners} />
<ContactSection />
<DownloadAppSection />
</div>
);
};
export default Page;

View File

@@ -0,0 +1,13 @@
import React from "react";
import PartnersSection from "@/features/partners/ui/partners-section/partners-section";
import { getPartners } from "@/shared/api/partnersSvc";
const Page = async () => {
const { data: partners } = await getPartners();
return (
<div className={"section-wrapper"}>
<PartnersSection partners={partners} />
</div>
);
};
export default Page;

View File

@@ -0,0 +1,32 @@
import React from 'react'
import ProductDetailsSection from "@/features/product-details/ui/product-details-section";
import {ContactSection, DownloadAppSection, FreeRecommendationSection, PartnersSection} from "@/features/home/ui";
import {getProductById} from "@/shared/api/productSvc";
import { getBrands } from '@/shared/api/brandsSvc';
export const dynamic = "force-dynamic";
type PageProps = {
params: {
productId: number
},
};
const Page = async ({params}: Readonly<PageProps>) => {
const {productId} = await params;
const {data: product} = await getProductById(productId)
const { data: partners } = await getBrands();
return (
<div className={"section-wrapper bg-white"}>
<ProductDetailsSection product={product.data}/>
<FreeRecommendationSection/>
<div className="mt-24">
<PartnersSection partners={partners}/>
</div>
<ContactSection/>
<DownloadAppSection/>
</div>
)
}
export default Page

View File

@@ -0,0 +1,9 @@
import React from 'react'
import ApplicationsSections from "@/features/profile/ui/applications-section";
const Page = () => {
return (
<ApplicationsSections/>
)
}
export default Page

View File

@@ -0,0 +1,9 @@
import React from 'react'
import ContactSection from "@/features/profile/ui/contact-section";
const Page = () => {
return (
<ContactSection/>
)
}
export default Page

View File

@@ -0,0 +1,20 @@
import React from 'react'
import {ProfileSidebar} from "@/widgets/profile-sidebar/profile-sidebar";
import {SidebarProvider} from "@/shared/ui/sidebar";
import PrivateRoute from "@/shared/providers/PrivateRouteProvider";
const Layout = ({children}: Readonly<{ children: React.ReactNode }>) => {
return (
<PrivateRoute>
<div className="my-12 min-h-screen">
<div className="my-container section-wrapper">
<SidebarProvider className={"gap-4 !min-h-auto"}>
<ProfileSidebar/>
{children}
</SidebarProvider>
</div>
</div>
</PrivateRoute>
)
}
export default Layout

View File

@@ -0,0 +1,10 @@
"use client"
import React from 'react'
import OrdersSection from "@/features/profile/ui/orders-section";
const Page = () => {
return (
<OrdersSection/>
)
}
export default Page

View File

@@ -0,0 +1,9 @@
import React from 'react'
import InformationSection from "@/features/profile/ui/information-section";
const Page = () => {
return (
<InformationSection/>
)
}
export default Page

View File

@@ -0,0 +1,13 @@
import { useTranslations } from 'next-intl'
import React from 'react'
const Page = () => {
const t = useTranslations("")
return (
<div className={"bg-white rounded-xl w-full p-4"}>
<h1 className={"text-md font-semibold"}>{t("Sozlamalar")}</h1>
<span className={"text-sm font-semibold text-gray-500"}>{t("Ma'lumotlarni yangilash")}</span>
</div>
)
}
export default Page

View File

@@ -0,0 +1,9 @@
import React from 'react'
import TermsSection from "@/features/profile/ui/terms-section";
const Page = () => {
return (
<TermsSection/>
)
}
export default Page

View File

@@ -0,0 +1,13 @@
import React from "react";
import { getServices } from "@/shared/api/servicesSvc";
import ServicesSection from "@/features/services/ui/services-section/services-section";
const Page = async () => {
const { data: services } = await getServices();
return (
<div className={"section-wrapper"}>
<ServicesSection services={services} />
</div>
);
};
export default Page;

View File

@@ -0,0 +1,13 @@
import React from 'react'
import {getUseful} from "@/shared/api/usefulSvc";
import UsefulSection from "@/features/useful/ui/useful-section";
const Page = async() => {
const {data: usefuls} = await getUseful()
return (
<div className={"section-wrapper"}>
<UsefulSection usefuls={usefuls}/>
</div>
)
}
export default Page

BIN
src/app/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

129
src/app/globals.css Normal file
View File

@@ -0,0 +1,129 @@
@import "tailwindcss";
@import "tw-animate-css";
@import "../shared/style/custom-utils.css";
@custom-variant dark (&:is(.dark *));
@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
--font-sans: var(--font-golos-text);
--color-sidebar-ring: var(--sidebar-ring);
--color-sidebar-border: var(--sidebar-border);
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
--color-sidebar-accent: var(--sidebar-accent);
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
--color-sidebar-primary: var(--sidebar-primary);
--color-sidebar-foreground: var(--sidebar-foreground);
--color-sidebar: var(--sidebar);
--color-chart-5: var(--chart-5);
--color-chart-4: var(--chart-4);
--color-chart-3: var(--chart-3);
--color-chart-2: var(--chart-2);
--color-chart-1: var(--chart-1);
--color-ring: var(--ring);
--color-input: var(--input);
--color-border: var(--border);
--color-destructive: var(--destructive);
--color-accent-foreground: var(--accent-foreground);
--color-accent: var(--accent);
--color-muted-foreground: var(--muted-foreground);
--color-muted: var(--muted);
--color-secondary-foreground: var(--secondary-foreground);
--color-secondary: var(--secondary);
--color-primary-foreground: var(--primary-foreground);
--color-primary: var(--primary);
--color-popover-foreground: var(--popover-foreground);
--color-popover: var(--popover);
--color-card-foreground: var(--card-foreground);
--color-card: var(--card);
--radius-sm: calc(var(--radius) - 4px);
--radius-md: calc(var(--radius) - 2px);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) + 4px);
}
/* Ranglarni ko'rish uchun https://oklch.com/ saytidan foydalaning */
:root {
--radius: 0.625rem;
--background: oklch(0.98 0 0);
--foreground: oklch(0.141 0.005 285.823);
--card: oklch(1 0 0);
--card-foreground: oklch(0.141 0.005 285.823);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.141 0.005 285.823);
--primary: var(--chart-2);
--primary-foreground: oklch(0.985 0 0);
--secondary: oklch(0.967 0.001 286.375);
--secondary-foreground: oklch(0.21 0.006 285.885);
--muted: oklch(0.967 0.001 286.375);
--muted-foreground: oklch(0.552 0.016 285.938);
--accent: oklch(0.967 0.001 286.375);
--accent-foreground: oklch(0.21 0.006 285.885);
--destructive: oklch(0.577 0.245 27.325);
--border: oklch(0.92 0.004 286.32);
--input: oklch(0.92 0.004 286.32);
--ring: oklch(0.705 0.015 286.067);
--chart-1: oklch(0.646 0.222 41.116);
--chart-2: oklch(0.6 0.118 184.704);
--chart-3: oklch(0.398 0.07 227.392);
--chart-4: oklch(0.828 0.189 84.429);
--chart-5: oklch(0.769 0.188 70.08);
--sidebar: oklch(0.985 0 0);
--sidebar-foreground: oklch(0.141 0.005 285.823);
--sidebar-primary: oklch(0.21 0.006 285.885);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.967 0.001 286.375);
--sidebar-accent-foreground: oklch(0.21 0.006 285.885);
--sidebar-border: oklch(0.92 0.004 286.32);
--sidebar-ring: oklch(0.705 0.015 286.067);
}
.dark {
--background: oklch(0.141 0.005 285.823);
--foreground: oklch(0.985 0 0);
--card: oklch(0.21 0.006 285.885);
--card-foreground: oklch(0.985 0 0);
--popover: oklch(0.21 0.006 285.885);
--popover-foreground: oklch(0.985 0 0);
--primary: oklch(0.62 0.1532 154.89);
--primary-foreground: oklch(0.21 0.006 285.885);
--secondary: oklch(0.274 0.006 286.033);
--secondary-foreground: oklch(0.985 0 0);
--muted: oklch(0.274 0.006 286.033);
--muted-foreground: oklch(0.705 0.015 286.067);
--accent: oklch(0.274 0.006 286.033);
--accent-foreground: oklch(0.985 0 0);
--destructive: oklch(0.704 0.191 22.216);
--border: oklch(1 0 0 / 10%);
--input: oklch(1 0 0 / 15%);
--ring: oklch(0.552 0.016 285.938);
--chart-1: oklch(0.488 0.243 264.376);
--chart-2: oklch(0.696 0.17 162.48);
--chart-3: oklch(0.769 0.188 70.08);
--chart-4: oklch(0.627 0.265 303.9);
--chart-5: oklch(0.645 0.246 16.439);
--sidebar: oklch(0.21 0.006 285.885);
--sidebar-foreground: oklch(0.985 0 0);
--sidebar-primary: oklch(0.488 0.243 264.376);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.274 0.006 286.033);
--sidebar-accent-foreground: oklch(0.985 0 0);
--sidebar-border: oklch(1 0 0 / 10%);
--sidebar-ring: oklch(0.552 0.016 285.938);
}
*{
/*border: 1px solid red;*/
scroll-behavior: smooth;
}
@layer base {
* {
@apply border-border outline-ring/50;
}
body {
@apply bg-background text-foreground;
}
}

11
src/app/layout.tsx Normal file
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;
}

35
src/app/loading.tsx Normal file
View File

@@ -0,0 +1,35 @@
"use client";
import React from "react";
import { motion } from "motion/react";
const Loading = () => {
return (
<html>
<body>
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 1 }}
className="h-screen w-full flex flex-col gap-4 justify-center items-center bg-primary"
>
<motion.div
animate={{
scale: [1, 1.2, 1],
rotate: [0, 360, 0],
}}
transition={{
duration: 2,
repeat: Infinity,
ease: "easeInOut",
}}
className="w-10 h-10 bg-white rounded-full"
/>
</motion.div>
</body>
</html>
);
};
export default Loading;

29
src/app/not-found.tsx Normal file
View File

@@ -0,0 +1,29 @@
import React from "react";
import { Link } from "@/shared/config/i18n/navigation";
import { useTranslations } from "next-intl";
const Custom404 = () => {
const t = useTranslations("");
return (
<html>
<body>
<div className="flex items-center justify-center h-screen w-full text-center">
<div>
<h1 className={"text-4xl font-bold text-primary"}>404</h1>
<p className={"my-5"}>
{t("Sahifa topilmadi, Iltimos qayta urinib ko`ring")}
</p>
<Link
href="/"
className="mt-6 px-4 py-2 bg-primary text-white rounded-lg hover:bg-primary/90"
>
{t("Orga qaytish")}
</Link>
</div>
</div>
</body>
</html>
);
};
export default Custom404;

6
src/app/page.tsx Normal file
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('/uz');
}

View File

@@ -0,0 +1,172 @@
"use client";
import { cn } from "@/shared/lib/utils";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/shared/ui/card";
import { Label } from "@radix-ui/react-label";
import { Input } from "@/shared/ui/input";
import { Button } from "@/shared/ui/button";
import Image from "next/image";
import { FormEvent, useState } from "react";
import { InputOTP, InputOTPGroup, InputOTPSlot } from "@/shared/ui/input-otp";
import { REGEXP_ONLY_DIGITS_AND_CHARS } from "input-otp";
import { Checkbox } from "@/shared/ui/checkbox";
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/shared/ui/dialog";
import { sendPhoneNumber, verifyCode } from "@/shared/api";
import { useRouter } from "@/shared/config/i18n/navigation";
import { useTranslations } from "next-intl";
const LoginSection = ({
className,
...props
}: React.ComponentPropsWithoutRef<"div">) => {
const [step, setStep] = useState(1);
const [phone, setPhone] = useState("");
const [code, setCode] = useState("");
const router = useRouter();
const t = useTranslations("");
const handlePhoneSubmit = async (e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
await sendPhoneNumber(phone);
setStep(2);
};
const handleVerifySubmit = async (e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
await verifyCode(phone, parseInt(code));
router.push("/profile");
};
return (
<div className={cn("flex flex-col gap-6", className)} {...props}>
{step === 1 ? (
<Card className="shadow-none border-none">
<CardHeader>
<Image
className="mx-auto mb-10"
src="/getgreen.png"
alt=""
width={300}
height={300}
/>
<CardTitle className="text-2xl">{t("Login")}</CardTitle>
<CardDescription>
{t("Hisobingizga kirish uchun telefon raqamingizni kiriting")}
</CardDescription>
</CardHeader>
<CardContent>
<form onSubmit={handlePhoneSubmit}>
<div className="flex flex-col gap-6">
<div className="grid gap-2">
<Label htmlFor="phone">{t("Telefon")}</Label>
<Input
id="phone"
type="tel"
name="phone"
value={phone}
onChange={(e) => setPhone(e.target.value)}
placeholder="+998 94 456 78 90"
required
/>
<div className="flex items-center space-x-2 mt-2">
<Checkbox id="terms" required />
{t.rich("terms_of_use", {
tag: (chunks) => (
<label
htmlFor="terms"
className="text-sm font-medium leading-none"
>
<TermsOfUse /> {chunks}
</label>
),
})}
</div>
</div>
<Button type="submit" className="w-full">
{t("login")}
</Button>
</div>
</form>
</CardContent>
</Card>
) : (
<Card className="shadow-none border-none">
<CardHeader>
<Image
className="mx-auto mb-10"
src="/getgreen.png"
alt=""
width={300}
height={300}
/>
<CardTitle className="text-2xl">OTP</CardTitle>
<CardDescription>{t("Tasdiqlash kodini kiriting")}</CardDescription>
</CardHeader>
<CardContent>
<form onSubmit={handleVerifySubmit}>
<div className="flex flex-col gap-6">
<div className="grid gap-2">
<InputOTP
maxLength={5}
pattern={REGEXP_ONLY_DIGITS_AND_CHARS}
value={code}
onChange={setCode}
>
<InputOTPGroup>
{[0, 1, 2, 3, 4].map((i) => (
<InputOTPSlot key={i} index={i} />
))}
</InputOTPGroup>
</InputOTP>
</div>
<Button type="submit" className="w-full">
{t("Tasdiqlash")}
</Button>
</div>
</form>
</CardContent>
</Card>
)}
</div>
);
};
export default LoginSection;
const TermsOfUse = () => {
const t = useTranslations("")
return (
<Dialog>
<DialogTrigger asChild>
<span className="border-b border-primary cursor-pointer">
{t("Offer va shartlar")}
</span>
</DialogTrigger>
<DialogContent className="min-w-6/12">
<DialogHeader>
<DialogTitle>{t("Offerta shartlari")}</DialogTitle>
<DialogDescription>
{t("Bu yerda offerta shartlarini o'qib chiqishingiz mumkin")}
</DialogDescription>
</DialogHeader>
<div className="overflow-y-scroll h-[70vh]">
{/* Replace with actual offer content */}
Lorem ipsum dolor sit amet, consectetur...
</div>
</DialogContent>
</Dialog>
)
}

View File

@@ -0,0 +1,46 @@
import React from "react";
import ProductCard from "@/shared/ui/product-card";
import { Product } from "@/shared/types/product";
import { BrandProductsResultType } from "@/shared/types/brands";
interface ProductSectionProps {
products: BrandProductsResultType[];
}
const ProductsList = ({ products }: ProductSectionProps) => {
return (
<div className={"my-container"}>
<section id={"invertor-section"} className={"my-container"}>
<div className={"flex flex-col justify-center"}>
<div>
<div
className={
"grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 "
}
>
{products.map((i) => {
const product: Product = {
id: i.id,
name: i.name,
price: i.price,
price_usd: i.price,
price_discount: i.price_discount,
discount_percent: i.discount_percent,
is_leader_of_sales: i.is_leader_of_sales,
poster: i.poster,
poster_thumb: i.poster_thumb,
is_favorite: i.is_favorite,
is_cart: i.is_cart,
count: i.count,
power: i.power,
};
return <ProductCard key={product.id} product={product} />;
})}
</div>
</div>
</div>
</section>
</div>
);
};
export default ProductsList;

View File

@@ -0,0 +1,30 @@
import React from 'react'
import {CategoryCard} from "@/shared/ui/category-card";
import {Button} from "@/shared/ui/button";
import ProductCard from "@/shared/ui/product-card";
import {Product} from "@/shared/types/product";
interface ProductSectionProps{
products: Product[]
}
const ProductsSection = ({products}: ProductSectionProps) => {
return (
<div className={"my-container"}>
<section id={"invertor-section"} className={"my-container"}>
<div className={"flex flex-col justify-center"}>
<div>
<div className={"grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 "}>
{
products.map(product=>(
<ProductCard key={product.id} product={product}/>
))
}
</div>
</div>
</div>
</section>
</div>
)
}
export default ProductsSection

View File

@@ -0,0 +1,29 @@
import { List, MailIcon, MapPin, PhoneCall } from "lucide-react";
import { CardProps } from "../models/types";
import formatPhone from "@/shared/lib/formatPhone";
import { PRODUCT_INFO } from "@/shared/constants";
const contactData: CardProps[] = [
{
icon: PhoneCall,
title: "Telefon raqam",
value: formatPhone(PRODUCT_INFO.contact.phone),
},
{
icon: MailIcon,
title: "Email",
value: PRODUCT_INFO.contact.email,
},
{
icon: MapPin,
title: "Adress",
value: "office",
},
{
icon: List,
title: "Ish vaqtlari",
value: "9:00 - 18:00",
},
];
export { contactData };

View File

@@ -0,0 +1,7 @@
import { LucideProps } from "lucide-react";
export interface CardProps {
icon: React.ComponentType<LucideProps>,
title: string;
value: string;
}

View File

@@ -0,0 +1,34 @@
import React from "react";
import Image from "next/image";
import { useTranslations } from "next-intl";
const AboutusSection = () => {
const t = useTranslations("");
return (
<section id={"about-section"} className={"bg-slate-900 py-12"}>
<div
className={
"max-w-7xl mx-auto grid grid-cols-1 md:grid-cols-2 justify-between items-center py-10 gap-12"
}
>
<div className={"relative w-full"}>
<Image
src={"/images/aboutus.png"}
alt={"About us image"}
layout="responsive"
width={500}
height={500}
/>
</div>
<div className={"text-white px-4"}>
<h1 className="section-title">{t("Biz haqimizda")}</h1>
<p className="section-subtitle">{t("about_us_subtitle")}</p>
<br aria-hidden />
<p className="section-subtitle">{t("about_us_desc")}</p>
</div>
</div>
</section>
);
};
export default AboutusSection;

View File

@@ -0,0 +1,81 @@
"use client"
import {Accordion, AccordionContent, AccordionItem, AccordionTrigger} from "@/shared/ui/accordion";
import {Link} from "@/shared/config/i18n/navigation";
import { useTranslations } from "next-intl";
interface Category {
id: number;
name: string;
image: string;
parent_id: number | null;
parents: Category[];
}
interface CategoriesProps {
categories: Category[];
}
const CategoriesSection: React.FC<CategoriesProps> = ({categories}) => {
const t = useTranslations("")
const renderCategory = (category: Category, level: number = 0) => {
const itemValue = `category-${category.id}`;
const hasChildren = category.parents && category.parents.length > 0;
const hasImage =
level === 0 && category.image && !category.image.includes("no_brend.png")
? category.image
: null;
return (
<AccordionItem
key={category.id}
value={itemValue}
className={`border-b ${level === 0 ? "border-gray-200" : "border-gray-100"}`}
>
<AccordionTrigger
showArrowIcon={hasChildren}
className={`flex items-center gap-4 p-4 hover:bg-gray-50 transition-colors ${
level === 0 ? "text-lg font-semibold" : "text-base font-medium"
}`}
>
<div className={"flex items-center justify-center gap-10"}>
{hasImage && (
<img
src={category.image}
alt={category.name}
className="w-12 h-12 object-cover rounded-md"
/>
)}
{hasChildren ? (
<span>{category.name}</span>
) : (
<Link href={`/category/${category.id}`}>
{category.name}
</Link>
)}
</div>
</AccordionTrigger>
{hasChildren && (
<AccordionContent className="pl-6">
<Accordion type="multiple" className="w-full">
{category.parents.map((child) => renderCategory(child, level + 1))}
</Accordion>
</AccordionContent>
)}
</AccordionItem>
);
};
return (
<div className="my-container section-wrapper mx-auto p-4">
<h1 className="section-title text-center">{t("Kategoriyalar")}</h1>
<Accordion
type="multiple"
className="w-full rounded-lg grid space-y-10"
>
{categories.map((category) => renderCategory(category))}
</Accordion>
</div>
);
};
export default CategoriesSection;

View File

@@ -0,0 +1,43 @@
import React from "react";
import { CardProps } from "../models/types";
import { contactData } from "../lib/data";
import { useTranslations } from "next-intl";
function ContactCard({ icon: Icon, title, value }: CardProps) {
const t = useTranslations("");
return (
<div className={"bg-white rounded-4xl p-10 flex items-center gap-10"}>
<Icon size={70} />
<div className={"flex flex-col"}>
<span className={"text-2xl font-bold"}>{t(title)}</span>
<span>{t(value)}</span>
</div>
</div>
);
}
const ContactSection = () => {
const t = useTranslations("");
return (
<section id={"contact-section.tsx"} className={"section-wrapper"}>
<div className="bg-primary py-24 px-4">
<div className={"my-container"}>
<h1 className={"section-title text-center pb-10 text-white"}>
{t("Kontaktlar")}
</h1>
<div className={"grid grid-cols-2 max-sm:grid-cols-1 gap-8"}>
{contactData.map((e, i) => (
<ContactCard
icon={e.icon}
title={e.title}
value={e.value}
key={i}
/>
))}
</div>
</div>
</div>
</section>
);
};
export default ContactSection;

View File

@@ -0,0 +1,75 @@
import React from "react";
import Image from "next/image";
import Link from "next/link";
import { PRODUCT_INFO } from "@/shared/constants";
import { useTranslations } from "next-intl";
const DownloadAppSection = () => {
const t = useTranslations("")
return (
<section
id={"download-app-section"}
className={
"bg-slate-900 my-container rounded-4xl max-md:rounded-3xl mb-20 relative overflow-hidden max-md:pt-12"
}
>
<div
className="bg-radial from-blue-950 via-transparent to-transparent w-[700px] h-[700px] rounded-full absolute -bottom-60 -left-60"
aria-hidden
/>
<div
className="bg-radial from-indigo-900 via-transparent to-transparent w-[700px] h-[700px] rounded-full absolute -top-80 -right-80"
aria-hidden
/>
<div
className={
"max-w-7xl mx-auto grid grid-cols-1 md:grid-cols-2 justify-between items-center gap-12 relative z-20"
}
>
<div className={"text-white px-4"}>
<h1 className="section-title">{t("Ilovamizni yuklab oling")}</h1>
<p className="section-subtitle">
{t("download_our_app_desc")}
</p>
<br aria-hidden />
<div className={"flex gap-4 mt-5"}>
<Link href={PRODUCT_INFO.app.ios}>
<Image
src={"/images/app-store-light.svg"}
alt={""}
width={200}
height={200}
className="max-md:w-40"
/>
</Link>
<Link href={PRODUCT_INFO.app.android}>
<Image
src={"/images/google-play-light.svg"}
alt={""}
width={200}
height={200}
className="max-md:w-40"
/>
</Link>
</div>
</div>
<div
className={
"relative flex justify-end max-md:justify-center pt-24 max-md:pt-20"
}
>
<Image
className={"-mb-96"}
src={"/images/screenshot-home.png"}
alt={"About us image"}
width={500}
height={500}
/>
</div>
</div>
</section>
);
};
export default DownloadAppSection;

View File

@@ -0,0 +1,76 @@
"use client";
import React, { FormEvent, useState } from "react";
import { Input } from "@/shared/ui/input";
import { Button } from "@/shared/ui/button";
import { useTranslations } from "next-intl";
import { contactInfoSubmit } from "@/shared/api/contactSvs";
import formatPhone from "@/shared/lib/formatPhone";
import { toast } from "sonner";
const FreeRecommendationSection = () => {
const t = useTranslations("");
const [phone, setPhone] = useState("+998 ");
const [name, setName] = useState("");
const [isLoading, setIsLoading] = useState(false);
const [isError, setIsError] = useState("")
const onContactSubmit = async (e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
if (phone.length === 17 || name.length > 2) {
setIsLoading(true);
setIsError("")
try {
await contactInfoSubmit(phone, name);
setName("");
setPhone("+998 ");
toast.success("Muvaffaqiyatli yuborildi");
setIsLoading(false);
} catch (error) {
setIsLoading(false);
}
} else {
setIsError("Ma'lumotlarni to'ldiring")
}
};
return (
<section
id={"free-recommendation-section"}
className={
"relative overflow-hidden section-wrapper bg-[url('/images/recommendation-bg.jpg')] bg-cover bg-no-repeat bg-center"
}
>
<div className={"flex justify-center items-center p-16 max-sm:p-10"}>
<div
className={"bg-slate-950/35 w-full h-screen absolute top-0 left-0"}
/>
<div className="bg-white rounded-xl p-10 relative z-20 w-full max-w-[600px]">
<h1 className="text-center text-2xl mb-2 font-bold pb-5">
{t("Bepul maslahat uchun ma'lumotlaringizni kiriting")}
</h1>
<form
action="w-full"
className={"space-y-5"}
onSubmit={onContactSubmit}
>
<Input
value={name}
onChange={(e) => setName(e.target.value)}
placeholder={t("Ismingiz")}
/>
<Input
value={formatPhone(phone)}
onChange={(e) => setPhone(formatPhone(e.target.value))}
placeholder={t("Telefon raqamingiz")}
/>
<Button className={"w-full"} type="submit" disabled={isLoading}>
{!isLoading ? t("Yuborish") : t("Yuborilmoqda")}
</Button>
</form>
{isError && <p className="text-center text-red-500 mt-4">{t(isError)}</p>}
</div>
</div>
</section>
);
};
export default FreeRecommendationSection;

View File

@@ -0,0 +1,45 @@
"use client"
import React from "react";
import Image from "next/image";
import { Button } from "@/shared/ui/button";
import { useTranslations } from "next-intl";
import { useRouter } from "next/navigation";
const HeroSection = () => {
const t = useTranslations("")
const router = useRouter()
return (
<section className={"relative overflow-hidden"}>
<div
className={
"bg-[url('/images/hero-bg.jpg')] flex justify-between items-center bg-cover bg-no-repeat bg-center h-[calc(100vh-4rem)]"
}
>
<div
className={"bg-slate-950/75 w-full h-screen absolute top-0 left-0"}
/>
<div className="my-container relative z-20 flex justify-between items-center max-lg:flex-col pt-24 px-4">
<div className={"w-1/2 max-lg:w-full"}>
<h1 className="section-title text-white">
{t("Quyosh uskunalarini ulgurji narxlarda sotib oling!")}
</h1>
<p className="section-subtitle text-white">
{t("Quyosh elektr stansiyasi uchun hamma narsani bir joyda va eng yaxshi narxda sotib oling")}
</p>
<Button onClick={() => router.push("/category")} className={"px-10 mt-5 z-30"}>
{t("Batafsil")}
</Button>
</div>
<Image
src={"/hero-solar-panel.png"}
alt={""}
width={900}
height={900}
/>
</div>
</div>
</section>
);
};
export default HeroSection;

View File

@@ -0,0 +1,8 @@
export { default as HeroSection } from "@/features/home/ui/hero-section";
export { default as AboutusSection } from "@/features/home/ui/aboutus-section";
export { default as CategorySection } from "@/features/home/ui/category-section";
export { default as FreeRecommendationSection } from "@/features/home/ui/free-recommendation-section";
export { default as ProfitSection } from "@/features/home/ui/profit-section";
export { default as PartnersSection } from "@/features/home/ui/partners-section";
export { default as ContactSection } from "@/features/home/ui/contact-section";
export { default as DownloadAppSection } from "@/features/home/ui/download-app-section";

View File

@@ -0,0 +1,32 @@
import React from 'react'
import {Button} from "@/shared/ui/button";
import ProductCard from "@/shared/ui/product-card";
import { useTranslations } from 'next-intl';
const InvertorSection = () => {
const t = useTranslations("")
return (
<section id={"invertor-section"} className={"my-container section-wrapper"}>
<div className={"flex flex-col justify-center"}>
<h1 className="section-title uppercase text-center pb-5">{t("Quyosh panellari")}</h1>
<div>
<div className={"grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4"}>
<ProductCard/>
<ProductCard/>
<ProductCard/>
<ProductCard/>
<ProductCard/>
<ProductCard/>
<ProductCard/>
<ProductCard/>
</div>
</div>
<Button className={"mx-auto mt-10 px-16"}>
{t("Hammasini ko'rish")}
</Button>
</div>
</section>
)
}
export default InvertorSection

View File

@@ -0,0 +1,42 @@
import React from "react";
import Image from "next/image";
import { BrandsResult } from "@/shared/types/brands";
import { useTranslations } from "next-intl";
import Link from "next/link";
interface Props {
partners: BrandsResult[];
}
const PartnersSection = ({ partners }: Props) => {
const t = useTranslations("")
return (
<section
id={"partners-section"}
className={"my-container section-wrapper bg-white rounded-4xl"}
>
<div className={"flex flex-col justify-center"}>
<h1 className="section-title uppercase text-center pb-5">
{t("Hamkorlarimiz")}
</h1>
<div>
<div
className={"grid grid-cols-2 md:grid-cols-4 lg:grid-cols-5 gap-4"}
>
{partners.map((e, i) => (
<Link href={`/brand/${e.id}`} key={i} className={"w-full h-full flex items-center justify-center"}>
<Image
src={e.image}
alt={"category"}
width={150}
height={150}
/>
</Link>
))}
</div>
</div>
</div>
</section>
);
};
export default PartnersSection;

View File

@@ -0,0 +1,31 @@
import React from 'react'
import Image from "next/image";
import { useTranslations } from 'next-intl';
const ProfitSection = () => {
const t = useTranslations("")
return (
<section id={"profit-section"} className={"py-12"}>
<div className={"max-w-7xl mx-auto grid grid-cols-1 md:grid-cols-2 justify-between items-center py-10 gap-12"}>
<div className={"relative w-full"}>
<Image src={"/images/profit.png"} alt={"About us image"} layout="responsive" width={500} height={500}/>
</div>
<div className={"px-4"}>
<p className="section-subtitle">
{t("profit_1_desc")}
</p>
<br aria-hidden/>
<p className="section-subtitle">
{t("profit_2_desc")}
</p>
<br aria-hidden/>
<p className="section-subtitle">
{t("profit_3_desc")}
</p>
</div>
</div>
</section>
)
}
export default ProfitSection;

View File

@@ -0,0 +1,67 @@
import React from "react";
import { Link } from "@/shared/config/i18n/navigation";
import {
BlocksIcon,
BookMarkedIcon,
BoxesIcon,
HandshakeIcon,
} from "lucide-react";
import { useTranslations } from "next-intl";
const SectionsSection = () => {
const t = useTranslations("");
return (
<div className={"my-container section-wrapper"}>
<div className={"grid grid-cols-4 max-md:grid-cols-2 gap-6 max-sm:gap-4"}>
<Link href={"/category"} className={"block w-full"}>
<div
className={"bg-white p-12 w-full flex flex-col items-center gap-2"}
>
<BlocksIcon strokeWidth={1} size={64} className={"text-primary"} />
<h1 className={"text-xl font-bold text-center"}>{t("Katalog")}</h1>
</div>
</Link>
<Link href={"/services"} className={"block w-full"}>
<div
className={"bg-white p-12 w-full flex flex-col items-center gap-2"}
>
<BoxesIcon strokeWidth={1} size={64} className={"text-primary"} />
<h1 className={"text-xl font-bold text-center"}>
{t("Xizmatlar")}
</h1>
</div>
</Link>
<Link href={"/partners"} className={"block w-full"}>
<div
className={"bg-white p-12 w-full flex flex-col items-center gap-2"}
>
<HandshakeIcon
strokeWidth={1}
size={64}
className={"text-primary"}
/>
<h1 className={"text-xl font-bold text-center"}>
{t("Hamkorlik")}
</h1>
</div>
</Link>
<Link href={"/useful"} className={"block w-full"}>
<div
className={"bg-white p-12 w-full flex flex-col items-center gap-2"}
>
<BookMarkedIcon
strokeWidth={1}
size={64}
className={"text-primary"}
/>
<h1 className={"text-xl font-bold text-center"}>{t("Foydali")}</h1>
</div>
</Link>
</div>
</div>
);
};
export default SectionsSection;

View File

@@ -0,0 +1,74 @@
"use client"
import {Partners} from "@/shared/types/partners";
import {Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle} from "@/shared/ui/dialog";
import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectLabel,
SelectTrigger,
SelectValue
} from "@/shared/ui/select";
import {Input} from "@/shared/ui/input";
import {Textarea} from "@/shared/ui/textarea";
import React from "react";
import {Button} from "@/shared/ui/button";
import { useTranslations } from "next-intl";
interface PartnerModalProps {
selectedPartner: Partners | null;
setSelectedPartner: (partner: Partners | null) => void;
}
const PartnerModal = ({selectedPartner, setSelectedPartner}: PartnerModalProps) => {
const t = useTranslations("")
return (
<Dialog open={!!selectedPartner} onOpenChange={
(open) => {
if (!open) {
setSelectedPartner(null)
}
}
}>
<DialogContent className={"min-w-4/12"}>
<DialogHeader>
<DialogTitle>{selectedPartner?.name}</DialogTitle>
<DialogDescription className={"space-y-5 mt-5"}>
<Select>
<SelectTrigger className={"w-full"}>
<SelectValue placeholder={t("Viloyat")}/>
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectLabel>{t("Viloyat")}</SelectLabel>
<SelectItem value="pineapple">Pineapple</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
<Select>
<SelectTrigger className={"w-full"}>
<SelectValue placeholder={t("Tuman")}/>
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectLabel>{t("Tuman")}</SelectLabel>
<SelectItem value="pineapple">Pineapple</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
<Input placeholder={t("Telefon raqamingiz")}/>
<Input placeholder={"full_name"}/>
<Textarea placeholder="Type your message here."/>
<div className={"text-end"}>
<Button size={"lg"}>{t("Ariza yuborish")}</Button>
</div>
</DialogDescription>
</DialogHeader>
</DialogContent>
</Dialog>
)
}
export default PartnerModal

View File

@@ -0,0 +1,52 @@
"use client";
import React, { useState } from "react";
import { Partners } from "@/shared/types/partners";
import Image from "next/image";
import PartnerModal from "@/features/partners/ui/partners-section/partner-modal";
import { useTranslations } from "next-intl";
interface PartnersSectionProps {
partners: Partners[];
}
const PartnersSection = ({ partners }: PartnersSectionProps) => {
const t = useTranslations("")
const [selectedPartner, setSelectedPartner] = useState<Partners | null>(null);
return (
<div className={"my-container section-wrapper min-h-[70vh]"}>
<h1 className={"section-title text-center"}>{t("Hamkorlik")}</h1>
<div
className={
"grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 mx-4 md:mx-0 gap-6 mt-12"
}
>
{partners.map((partner) => (
<div
key={partner.id}
onClick={() => {
setSelectedPartner(partner);
}}
className={
"cursor-pointer border hover:border-primary transition-all duration-400 bg-white p-12 w-full flex flex-col justify-center items-center gap-2 h-full"
}
>
<Image
src={partner.image}
alt={partner.name}
width={100}
height={100}
/>
<h1 className={"text-xl font-bold text-center"}>{partner.name}</h1>
</div>
))}
{selectedPartner && (
<PartnerModal
selectedPartner={selectedPartner!}
setSelectedPartner={setSelectedPartner}
/>
)}
</div>
</div>
);
};
export default PartnersSection;

View File

@@ -0,0 +1,47 @@
import { Product } from "@/shared/types/product";
import { Button } from "@/shared/ui/button";
import {
Dialog,
DialogContent,
DialogTrigger,
} from "@/shared/ui/dialog";
import { Tabs, TabsList, TabsTrigger } from "@/shared/ui/tabs";
import React, { useState } from "react";
import PhysicalTab from "./physicalTab";
import LegalTab from "./legalTab";
import { useTranslations } from "next-intl";
interface Props {
product: Product;
}
const BuyForm = ({ product }: Props) => {
const t = useTranslations("")
const [isDialogOpen, setIsDialogOpen] = useState(false);
return (
<Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
<DialogTrigger asChild>
<Button className="mt-8 px-16" onClick={() => setIsDialogOpen(true)}>
{t("Sotib olish")}
</Button>
</DialogTrigger>
<DialogContent className="sm:max-w-6/12 max-h-[90vh] overflow-y-auto">
<Tabs defaultValue="physical" className="w-full mt-4">
<TabsList className="grid w-full grid-cols-2">
<TabsTrigger value="physical">{t("Jismoniy shaxs")}</TabsTrigger>
<TabsTrigger value="legal">{t("Yuridik shaxs")}</TabsTrigger>
</TabsList>
{/* Physical */}
<PhysicalTab product={product} setIsDialogOpen={setIsDialogOpen} />
{/* Legal */}
<LegalTab product={product} setIsDialogOpen={setIsDialogOpen} />
</Tabs>
</DialogContent>
</Dialog>
);
};
export default BuyForm;

View File

@@ -0,0 +1,272 @@
import { getRegions } from "@/shared/api/regionSvc";
import { createUserOrder } from "@/shared/api/userOrdersSvc";
import formatPhone from "@/shared/lib/formatPhone";
import { Product } from "@/shared/types/product";
import { Button } from "@/shared/ui/button";
import { DialogFooter } from "@/shared/ui/dialog";
import { Input } from "@/shared/ui/input";
import { Label } from "@/shared/ui/label";
import { RadioGroup, RadioGroupItem } from "@/shared/ui/radio-group";
import { TabsContent } from "@/shared/ui/tabs";
import { useQuery } from "@tanstack/react-query";
import { Form, Formik } from "formik";
import { Loader2 } from "lucide-react";
import { useTranslations } from "next-intl";
import React, { useState } from "react";
import { toast } from "sonner";
interface Props {
product: Product;
setIsDialogOpen: React.Dispatch<React.SetStateAction<boolean>>;
}
const LegalTab = ({ product, setIsDialogOpen }: Props) => {
const t = useTranslations("")
const { data: regions } = useQuery({
queryKey: ["getRegions"],
queryFn: getRegions,
});
const [isOrderCreating, setIsOrderCreating] = useState(false);
const [corpDistricts, setCorpDistricts] = useState<
{ id: number; name: string }[] | null
>(null);
const handleCorpRegionChange = (regionId: number) => {
const selectedRegion = regions?.data?.find(
(region) => region.id === regionId
);
setCorpDistricts(selectedRegion?.cities || []);
};
return (
<Formik
initialValues={{
phone: "+998 ",
director_full_name: "",
company_name: "",
inn: "",
bank_name: "",
mfo: "",
oked: "",
payment_account: "",
address: "",
home: "",
landmark: "",
city_id: "",
branch_id: 1,
with_installation: true,
delivery_type: "delivery",
payment_type: "bank",
with_didox: true,
products: [{ id: product.id, count: 1 }],
}}
onSubmit={async (values, helpers) => {
setIsOrderCreating(true);
let payload = {
branch_id: 1,
type: "ready_solutions",
delivery_type: values.delivery_type,
client_type: "legal",
client_information: {
director_full_name: values.director_full_name,
company_name: values.company_name,
inn: values.inn,
bank_name: values.bank_name,
mfo: values.mfo,
oked: values.oked,
payment_account: values.payment_account,
address: values.address,
email: "",
phone: Number(values.phone.replace(/\D/g, "")),
},
address: {
city_id: Number(values.city_id),
address: values.address,
home: values.home,
landmark: values.landmark,
},
with_installation: values.with_installation,
payment_type: values.payment_type,
with_didox: values.with_didox,
products: values.products,
};
try {
await createUserOrder(payload);
toast.success(t("Buyurtma muvaffaqiyatli yaratildi!"), {
description: t("Siz bilan tez orada bog'lanamiz"),
});
setIsDialogOpen(false);
setIsOrderCreating(false);
} catch (e: any) {
toast.error(t("Buyurtma yaratishda xatolik!"), {
description:
t("Ma'lumotlar to'liq kiritilganligiga ishonch hosil qiling yoki qaytadan urinib ko'ring"),
});
setIsOrderCreating(false);
}
}}
>
{(formikProps) => (
<Form>
{/* Legal */}
<TabsContent value="legal" className="space-y-4 mt-4">
<div className="grid grid-cols-2 gap-4">
<Input
placeholder={t("Kompaniya nomi")}
onChange={formikProps.handleChange("company_name")}
/>
<Input
placeholder={t("Direktor")}
onChange={formikProps.handleChange("director_full_name")}
/>
</div>
<div className="grid grid-cols-2 gap-4">
<Input
placeholder={t("Yuridik manzil")}
onChange={formikProps.handleChange("address")}
/>
<Input
placeholder={t("Telefon raqam")}
onChange={(e) =>
formikProps.setFieldValue(
"phone",
formatPhone(e.target.value)
)
}
value={formikProps.values.phone}
/>
</div>
<div className="grid grid-cols-2 gap-4">
<Input
placeholder="INN"
onChange={(e) =>
formikProps.setFieldValue(
"inn",
e.target.value.replace(/\D/g, "")
)
}
value={formikProps.values.inn}
/>
<Input
placeholder={t("Bank nomi")}
onChange={formikProps.handleChange("bank_name")}
/>
</div>
<div className="grid grid-cols-2 gap-4">
<Input
placeholder="MFO"
onChange={formikProps.handleChange("mfo")}
/>
<Input
placeholder="OKED"
onChange={formikProps.handleChange("oked")}
/>
</div>
<Input placeholder={t("Hisob raqam")} onChange={formikProps.handleChange("payment_account")}/>
<Label className={"mb-2"}>{t("Yetkazib berish")}</Label>
<div className="grid grid-cols-2 gap-4">
<select
onChange={(e) => handleCorpRegionChange(Number(e.target.value))}
className="w-full p-2 rounded-md border"
>
<option value="">{t("Viloyatni tanlang")}</option>
{regions?.data.map((region) => (
<option key={region.id} value={region.id}>
{region.name}
</option>
))}
</select>
<select
onChange={formikProps.handleChange("city_id")}
className="w-full p-2 rounded-md border"
disabled={!corpDistricts}
>
<option value="">{t("Tuman/shahar")}</option>
{corpDistricts?.map((district) => (
<option key={district.id} value={district.id}>
{district.name}
</option>
))}
</select>
</div>
<Input
placeholder={t("Manzil")}
onChange={formikProps.handleChange("address")}
/>
<Input
placeholder={t("Uy raqami")}
onChange={formikProps.handleChange("home")}
/>
<Input
placeholder={t("Mo'ljal")}
onChange={formikProps.handleChange("landmark")}
/>
<div>
<Label className="mb-2 block">{t("Ornatish xizmati kerakmi?")}</Label>
<RadioGroup
onValueChange={(value) =>
formikProps.setFieldValue(
"with_installation",
value === "yes"
)
}
defaultValue="yes"
className="flex gap-4"
>
<div className="flex items-center gap-2">
<RadioGroupItem value="yes" id="c-install-yes" />
<Label htmlFor="c-install-yes">{t("Ha")}</Label>
</div>
<div className="flex items-center gap-2">
<RadioGroupItem value="no" id="c-install-no" />
<Label htmlFor="c-install-no">{t("Yoq")}</Label>
</div>
</RadioGroup>
</div>
<div>
<Label className="mb-2 block">{t("Yetkazib berish kerakmi")}</Label>
<RadioGroup
onValueChange={(value) =>
formikProps.setFieldValue("delivery_type", value)
}
defaultValue="delivery"
className="flex gap-4"
>
<div className="flex items-center gap-2">
<RadioGroupItem value="delivery" id="c-install-yes" />
<Label htmlFor="c-install-yes">{t("Ha")}</Label>
</div>
<div className="flex items-center gap-2">
<RadioGroupItem value="pickup" id="c-install-no" />
<Label htmlFor="c-install-no">{t("Yoq o'zim olib ketaman")}</Label>
</div>
</RadioGroup>
</div>
<DialogFooter className="mt-4">
<Button
size="lg"
type="submit"
className="w-full"
disabled={isOrderCreating}
>
{isOrderCreating ? (
<Loader2 className="animate-spin" />
) : (
t("Yuborish")
)}
</Button>
</DialogFooter>
</TabsContent>
</Form>
)}
</Formik>
);
};
export default LegalTab;

View File

@@ -0,0 +1,247 @@
import { getRegions } from "@/shared/api/regionSvc";
import { createUserOrder } from "@/shared/api/userOrdersSvc";
import formatPhone from "@/shared/lib/formatPhone";
import { Product } from "@/shared/types/product";
import { Button } from "@/shared/ui/button";
import { DialogFooter } from "@/shared/ui/dialog";
import { Input } from "@/shared/ui/input";
import { Label } from "@/shared/ui/label";
import { RadioGroup, RadioGroupItem } from "@/shared/ui/radio-group";
import { TabsContent } from "@/shared/ui/tabs";
import { useQuery } from "@tanstack/react-query";
import { Form, Formik } from "formik";
import { Loader2 } from "lucide-react";
import { useTranslations } from "next-intl";
import React, { useState } from "react";
import { toast } from "sonner";
interface Props {
product: Product;
setIsDialogOpen: React.Dispatch<React.SetStateAction<boolean>>;
}
const PhysicalTab = ({ product, setIsDialogOpen }: Props) => {
const t = useTranslations("")
const { data: regions } = useQuery({
queryKey: ["getRegions"],
queryFn: getRegions,
});
const [isOrderCreating, setIsOrderCreating] = useState(false);
const [districts, setDistricts] = useState<
{ id: number; name: string }[] | null
>(null);
const handleRegionChange = (regionId: number) => {
const selectedRegion = regions?.data?.find(
(region) => region.id === regionId
);
setDistricts(selectedRegion?.cities || []);
};
return (
<Formik
initialValues={{
full_name: "",
phone: "+998 ",
jshir: "",
series: "",
address: "",
home: "",
landmark: "",
city_id: "",
branch_id: 1,
with_installation: true,
delivery_type: "delivery",
payment_type: "bank",
with_didox: true,
products: [{ id: product.id, count: 1 }],
}}
onSubmit={async (values, helpers) => {
setIsOrderCreating(true);
let payload = {
branch_id: 1,
series: 1,
type: "ready_solutions",
delivery_type: values.delivery_type,
client_type: "physical",
client_information: {
full_name: values.full_name,
jshir: values.jshir,
series: values.series,
phone: Number(values.phone.replace(/\D/g, "")),
},
address: {
city_id: Number(values.city_id),
address: values.address,
home: values.home,
landmark: values.landmark,
},
with_installation: values.with_installation,
payment_type: values.payment_type,
with_didox: values.with_didox,
products: values.products,
};
try {
await createUserOrder(payload);
toast.success(t("Buyurtma muvaffaqiyatli yaratildi!"), {
description: t("Siz bilan tez orada bog'lanamiz"),
});
setIsDialogOpen(false);
setIsOrderCreating(false);
} catch (e: any) {
toast.error(t("Buyurtma yaratishda xatolik!"), {
description:
t("Ma'lumotlar to'liq kiritilganligiga ishonch hosil qiling yoki qaytadan urinib ko'ring"),
});
setIsOrderCreating(false);
}
}}
>
{(formikProps) => (
<Form>
{/* Physical */}
<TabsContent value="physical" className="space-y-4 mt-4">
<div className="grid grid-cols-2 gap-4">
<Input
placeholder={t("full_name")}
onChange={formikProps.handleChange("full_name")}
/>
{formikProps.errors.full_name && (
<p className="text-red-500 text-sm">
{formikProps.errors.full_name}
</p>
)}
<Input
placeholder={t("Telefon raqam")}
onChange={(e) =>
formikProps.setFieldValue(
"phone",
formatPhone(e.target.value)
)
}
value={formikProps.values.phone}
/>
</div>
<div className={"grid grid-cols-2 gap-4"}>
<Input
placeholder="JShShIR"
value={formikProps.values.jshir}
onChange={(e) =>
formikProps.setFieldValue(
"jshir",
e.target.value.replace(/\D/g, "")
)
}
/>
<Input
placeholder={t("Passport seriya va raqami")}
onChange={formikProps.handleChange("series")}
/>
</div>
<Label className={"mb-2"}>{t("Yetkazib berish")}</Label>
<div className={"grid grid-cols-2 gap-4"}>
<select
onChange={(e) => handleRegionChange(Number(e.target.value))}
className="w-full p-2 rounded-md border"
>
<option value="">{t("Viloyatni tanlang")}</option>
{regions?.data?.map((region) => (
<option key={region.id} value={region.id}>
{region.name}
</option>
))}
</select>
<select
onChange={formikProps.handleChange("city_id")}
className="w-full p-2 rounded-md border"
disabled={!districts}
>
<option value="">{t("Tuman/shahar")}</option>
{districts?.map((district) => (
<option key={district.id} value={district.id}>
{district.name}
</option>
))}
</select>
</div>
<Input
placeholder={t("Manzil")}
onChange={formikProps.handleChange("address")}
/>
<Input
placeholder={t("Uy raqami")}
onChange={formikProps.handleChange("home")}
/>
<Input
placeholder={t("Mo'ljal")}
onChange={formikProps.handleChange("landmark")}
/>
<div>
<Label className="mb-2 block">{t("Ornatish xizmati kerakmi?")}</Label>
<RadioGroup
onValueChange={(value) =>
formikProps.setFieldValue(
"with_installation",
value === "yes"
)
}
defaultValue="yes"
className="flex gap-4"
>
<div className="flex items-center gap-2">
<RadioGroupItem value="yes" id="install-yes" />
<Label htmlFor="install-yes">{t("Ha")}</Label>
</div>
<div className="flex items-center gap-2">
<RadioGroupItem value="no" id="install-no" />
<Label htmlFor="install-no">{t("Yoq")}</Label>
</div>
</RadioGroup>
</div>
<div>
<Label className="mb-2 block">{t("Yetkazib berish kerakmi?")}</Label>
<RadioGroup
onValueChange={(value) =>
formikProps.setFieldValue("delivery_type", value)
}
defaultValue="yes"
className="flex gap-4"
>
<div className="flex items-center gap-2">
<RadioGroupItem value="delivery" id="c-install-yes" />
<Label htmlFor="c-install-yes">{t("Ha")}</Label>
</div>
<div className="flex items-center gap-2">
<RadioGroupItem value="pickup" id="c-install-no" />
<Label htmlFor="c-install-no">{t("Yoq o'zim olib ketaman")}</Label>
</div>
</RadioGroup>
</div>
<DialogFooter className="mt-4">
<Button
size="lg"
type="submit"
className="w-full"
disabled={isOrderCreating}
>
{isOrderCreating ? (
<Loader2 className="animate-spin" />
) : (
t("Yuborish")
)}
</Button>
</DialogFooter>
</TabsContent>
</Form>
)}
</Formik>
);
};
export default PhysicalTab;

View File

@@ -0,0 +1,82 @@
"use client";
import React from "react";
import Image from "next/image";
import { Button } from "@/shared/ui/button";
import { Product } from "@/shared/types/product";
import formatNumberWithSpaces from "@/shared/lib/formatNumberWithSpace";
import { useAuthStore } from "@/shared/store/authStore";
import { Link } from "@/shared/config/i18n/navigation";
import BuyForm from "./buyForm";
import { useTranslations } from "next-intl";
interface ProductDetailsSectionProps {
product: Product;
}
const ProductDetailsSection = ({ product }: ProductDetailsSectionProps) => {
const { isAuthenticated } = useAuthStore();
const t = useTranslations("")
return (
<div className="my-container px-4">
<section className="my-container">
<div className="flex flex-col justify-center">
<div className="p-24 grid grid-cols-2 max-md:grid-cols-1 items-center max-md:p-0">
<div className="flex items-center justify-center">
<Image
className="w-10/12 max-md:w-full rounded-4xl"
src={product.poster}
alt={product.name}
width={200}
height={200}
/>
</div>
<div>
<h2 className="font-bold text-4xl leading-relaxed">
{product.name}
</h2>
<div
className="my-5"
dangerouslySetInnerHTML={{ __html: product.short_description! }}
/>
<div className="gap-5">
<span className="text-3xl font-bold">
{formatNumberWithSpaces(product.price)} {t("so'm")}
<span className="text-base font-light"> {t("QQS bilan")}</span>
</span>
<span className="block">
{formatNumberWithSpaces(product.price_usd)} y.e.
</span>
{product.discount_percent > 0 && (
<>
<span className="text-xl font-bold text-red-500 line-through">
{formatNumberWithSpaces(product.price_discount)} {t("so'm")}
</span>
<span className="text-xl font-bold text-red-500">
{product.discount_percent}% {t("chegirma")}
</span>
</>
)}
</div>
{isAuthenticated ? (
<BuyForm product={product} />
) : (
<Button className="mt-8 px-16" asChild>
<Link href={"/auth/login"}>{t("Sotib olish")}</Link>
</Button>
)}
</div>
</div>
</div>
<div
className="text-base flex flex-col justify-center items-center mb-24 mt-12"
dangerouslySetInnerHTML={{ __html: product.description! }}
/>
</section>
</div>
);
};
export default ProductDetailsSection;

View File

@@ -0,0 +1,30 @@
import React from 'react'
import ProductCard from "@/shared/ui/product-card";
import {Product} from "@/shared/types/product";
import { useTranslations } from 'next-intl';
interface RelatedProductsSectionProps {
products: Product[]
}
const RelatedProductsSection = ({products}: RelatedProductsSectionProps) => {
const t = useTranslations("")
return (
<div className={"bg-background section-wrapper"}>
<div className={"my-container"}>
<h1 className="section-title uppercase text-center pb-5">{t("Boshqa mahsulotlar")}</h1>
<div className={"mb-24 grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4"}>
{
products.map((product) => (
<ProductCard
key={product.id}
product={product}
/>
))
}
</div>
</div>
</div>
)
}
export default RelatedProductsSection

View File

@@ -0,0 +1,125 @@
"use client"
import React, {useState} from 'react'
import {Card, CardContent, CardDescription, CardTitle} from "@/shared/ui/card";
import {Separator} from "@/shared/ui/separator";
import {Badge} from "@/shared/ui/badge";
import {Accordion, AccordionContent, AccordionItem, AccordionTrigger} from "@/shared/ui/accordion";
import {useQuery} from "@tanstack/react-query";
import {getUserRequests, getUserRequestsById} from "@/shared/api/userRequestsSvc";
import Loader from "@/shared/ui/loader";
import { useTranslations } from 'next-intl';
const ApplicationsSections = () => {
const t = useTranslations("")
const [selectedRequest, setSelectedRequest] = useState<number>(0)
const {data: requests, isLoading: requestsIsLoading} = useQuery({
queryKey: ["getUserRequests"],
queryFn: getUserRequests,
})
const {data: requestDetails} = useQuery({
queryKey: ["getUserRequestsById", selectedRequest],
queryFn: () => getUserRequestsById(selectedRequest),
enabled: !!selectedRequest,
})
return (
<div className={"profile-section-wrapper"}>
<h1 className={"profile-section-title"}>{t("Mening arizalarim")}</h1>
<span className={"profile-section-subtitle"}>
{t("Sizning arizalaringiz va ularning holati haqida ma'lumotlar")}
</span>
<div className={"mt-4 space-y-4"}>
{
requestsIsLoading ? <Loader height={"h-[30vh]"}/> :requests?.data?.map(request => (
<Accordion onClick={
() => setSelectedRequest(request.id)
} type="single" className={"border px-5 rounded-xl"} collapsible>
<AccordionItem value="item-1">
<AccordionTrigger className={" cursor-pointer"}>
<div className={"w-full"}>
<div className={"flex justify-between items-center"}>
<div>
<CardTitle className={"text-lg"}>{t("Ariza")} #{request.id}</CardTitle>
<CardDescription>{t("Yaratilish vaqti")}: {request.created_at}</CardDescription>
</div>
<Badge style={{ backgroundColor: request.status.bg_color, color: request.status.font_color }}>
{request.status.translation}
</Badge>
</div>
</div>
</AccordionTrigger>
<AccordionContent>
<Card key={request.id} className={"shadow-none border-none p-0 mt-5 rounded-none"}>
<CardContent className={"p-0"}>
<CardTitle>{t("Ariza tafsilotlari")}</CardTitle>
<div className={"mt-5 space-y-3"}>
<div className={"flex justify-between items-center"}>
<CardDescription>{t("Ariza turi")}</CardDescription>
<CardDescription>{requestDetails?.data.service.name}</CardDescription>
</div>
<div className={"flex justify-between items-center"}>
<CardDescription>{t("Ariza raqami")}</CardDescription>
<CardDescription>#{requestDetails?.data.id}</CardDescription>
</div>
<div className={"flex justify-between items-center"}>
<CardDescription>{t("Ariza holati")}</CardDescription>
<Badge style={{ backgroundColor: request.status.bg_color, color: request.status.font_color }}>
{request.status.translation}
</Badge>
</div>
<div className={"flex justify-between items-center"}>
<CardDescription>{t("Yaratilish vaqti")}</CardDescription>
<CardDescription>{
request.created_at
}</CardDescription>
</div>
</div>
</CardContent>
<Separator/>
<CardContent className={"p-0"}>
<CardTitle>{t("Xizmat tafsilotlari")}</CardTitle>
<div className={"mt-5 space-y-3"}>
<div className={"flex justify-between items-center"}>
<CardDescription>{t("Quvvat")}</CardDescription>
<CardDescription>
{requestDetails?.data.power.name}
</CardDescription>
</div>
<div className={"flex justify-between items-center"}>
<CardDescription>{t("Manzil")}</CardDescription>
<CardDescription>
{requestDetails?.data.city.name}, {" "}
{requestDetails?.data.city.region.name}
</CardDescription>
</div>
<div className={"flex justify-between items-center"}>
<CardDescription>{t("full_name")}</CardDescription>
<CardDescription>{
requestDetails?.data.full_name
}</CardDescription>
</div>
<div className={"flex justify-between items-center"}>
<CardDescription>{t("Telefon raqami")}</CardDescription>
<CardDescription>{
requestDetails?.data.phone
}</CardDescription>
</div>
<div className={"flex justify-between items-center"}>
<CardDescription>{t("Izoh")}</CardDescription>
<CardDescription>
{requestDetails?.data.comment}
</CardDescription>
</div>
</div>
</CardContent>
</Card>
</AccordionContent>
</AccordionItem>
</Accordion>
))
}
</div>
</div>
)
}
export default ApplicationsSections

View File

@@ -0,0 +1,58 @@
"use client";
import React from "react";
import { useQuery } from "@tanstack/react-query";
import { getFeedback } from "@/shared/api/feedbackSvc";
import Link from "next/link";
import Loader from "@/shared/ui/loader";
import { useTranslations } from "next-intl";
const ContactSection = () => {
const t = useTranslations("");
const { data: feedback, isLoading: feedbackIsLoading } = useQuery({
queryKey: ["getFeedback"],
queryFn: getFeedback,
});
return (
<div className={"profile-section-wrapper"}>
<h1 className={"profile-section-title"}>{t("Bog'lanish")}</h1>
<span className={"profile-section-subtitle"}>
{t(
"Biz bilan bog'lanish uchun quyidagi usullardan foydalanishingiz mumkin"
)}
</span>
<div>
{feedbackIsLoading ? (
<Loader height={"h-[30vh]"} />
) : (
<div className={"grid grid-cols-2 items-center justify-between my-4"}>
<div className={"flex flex-col"}>
<span className={"text-lg font-bold"}>{t("Call Center")}</span>
<Link
className={"w-fit text-primary"}
href={`tel:${feedback?.data.phone}`}
>
{feedback?.data.phone}
</Link>
</div>
<div className={"flex flex-col"}>
<span className={"text-lg font-bold"}>Telegram</span>
<Link
className={"w-fit text-primary"}
href={feedback?.data.telegram_support || ""}
>
{feedback?.data.telegram_support}
</Link>
</div>
</div>
)}
{/*<div className={"space-x-4"}>*/}
{/* <Button className={""} size={"lg"}><InstagramIcon/> @Instagram</Button>*/}
{/* <Button className={""} size={"lg"}><YoutubeIcon/> @Instagram</Button>*/}
{/* <Button className={""} size={"lg"}><InstagramIcon/> @Instagram</Button>*/}
{/* <Button className={""} size={"lg"}><YoutubeIcon/> @Instagram</Button>*/}
{/*</div>*/}
</div>
</div>
);
};
export default ContactSection;

View File

@@ -0,0 +1,98 @@
"use client";
import React, { useEffect, useState } from "react";
import { Input } from "@/shared/ui/input";
import { Button } from "@/shared/ui/button";
import { useQuery, useMutation } from "@tanstack/react-query";
import { getUserMe, updateUserMe } from "@/shared/api/userMeSvc";
import { useTranslations } from "next-intl";
const InformationSection = () => {
const t = useTranslations("");
const { data: user } = useQuery({
queryKey: ["getUserMe"],
queryFn: getUserMe,
});
const [formData, setFormData] = useState({
first_name: "",
last_name: "",
middle_name: "",
phone: "",
gender: true,
});
useEffect(() => {
if (user?.data) {
setFormData({
first_name: user.data.first_name || "",
last_name: user.data.last_name || "",
middle_name: user.data.middle_name || "",
phone: user.data.phone || "",
gender: true,
});
}
}, [user]);
const mutation = useMutation({
mutationFn: updateUserMe,
onError: () => {
alert(t("Xatolik yuz berdi"));
},
});
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const { name, value } = e.target;
setFormData((prev) => ({ ...prev, [name]: value }));
};
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
mutation.mutate(formData);
};
return (
<div className={"profile-section-wrapper"}>
<h1 className={"profile-section-title"}>{t("Profil ma'lumotlari")}</h1>
<span className={"profile-section-subtitle"}>
{t("Sizning profil ma'lumotlaringiz va ularni o'zgartirish")}
</span>
<form onSubmit={handleSubmit} className={"space-y-5 mt-5 text-end"}>
<div className={"grid grid-cols-2 gap-5"}>
<Input
name="first_name"
placeholder={t("Ismingiz")}
value={formData.first_name}
onChange={handleChange}
required
/>
<Input
name="last_name"
placeholder={t("Familiyangiz")}
value={formData.last_name}
onChange={handleChange}
required
/>
<Input
name="middle_name"
placeholder={t("Sharif")}
value={formData.middle_name}
onChange={handleChange}
required
/>
<Input
name="phone"
placeholder={t("Telefon raqam")}
value={formData.phone}
onChange={handleChange}
required
/>
</div>
<Button size={"lg"} type="submit" disabled={mutation.isPending}>
{mutation.isPending ? t("Saqlanmoqda") : t("Saqlash")}
</Button>
</form>
</div>
);
};
export default InformationSection;

View File

@@ -0,0 +1,154 @@
import React, {useState} from 'react'
import {Card, CardContent, CardDescription, CardFooter, CardTitle} from "@/shared/ui/card";
import {Separator} from "@/shared/ui/separator";
import {Badge} from "@/shared/ui/badge";
import {Button} from "@/shared/ui/button";
import {Accordion, AccordionContent, AccordionItem, AccordionTrigger} from "@/shared/ui/accordion";
import {useQuery} from "@tanstack/react-query";
import {getUserOrders, getUserOrdersById} from "@/shared/api/userOrdersSvc";
import formatNumberWithSpaces from "@/shared/lib/formatNumberWithSpace";
import Link from "next/link";
import Loader from "@/shared/ui/loader";
import { useTranslations } from 'next-intl';
const OrdersSection = () => {
const t = useTranslations("")
const [selectedOrder, setSelectedOrder] = useState<number>(0)
const {data: orders, isLoading: ordersIsLoading} = useQuery({
queryKey: ["getUserOrders"],
queryFn: getUserOrders,
})
const {data: orderDetails} = useQuery({
queryKey: ["getUserOrdersById", selectedOrder],
queryFn: () => getUserOrdersById(selectedOrder),
enabled: !!selectedOrder,
})
return (
<div className={"profile-section-wrapper"}>
<h1 className={"profile-section-title"}>{t("Mening buyurtmalarim")}</h1>
<span className={"profile-section-subtitle"}>
{t("Sizning buyurtmalaringiz va ularning holati haqida ma'lumotlar")}
</span>
<div className={"mt-4 space-y-4"}>
{ordersIsLoading ? <Loader height={"h-[30vh]"}/>:
orders?.data?.map(order => (
<Accordion key={order.id} onClick={()=>setSelectedOrder(order.id)} type="single" className={"border px-5 rounded-xl"} collapsible>
<AccordionItem value="item-1">
<AccordionTrigger className={"cursor-pointer"}>
<div className={"w-full"}>
<div className={"flex justify-between items-center mb-5"}>
<div>
<CardTitle className={"text-lg"}>{t("Buyurtma")} #{order.id}</CardTitle>
<CardDescription>{t("Yaratilish vaqti")}: {order.created_at}</CardDescription>
</div>
<div className={"text-end"}>
<CardDescription className={"text-lg"}>{formatNumberWithSpaces(order.total_amount!)} so'm</CardDescription>
<Badge style={{ backgroundColor: order.status.bg_color, color: order.status.font_color }}>
{order.status.translation}
</Badge>
</div>
</div>
</div>
</AccordionTrigger>
<AccordionContent>
<Card key={order.id} className={"shadow-none border-none p-0 mt-5 rounded-none"}>
<CardContent className={"p-0"}>
<CardTitle>{t("Buyurtma tafsilotlari")}</CardTitle>
<div className={"mt-5 space-y-3"}>
<div className={"flex justify-between items-center"}>
<CardDescription>{t("Buyurtma raqami")}</CardDescription>
<CardDescription>#{orderDetails?.data.id}</CardDescription>
</div>
<div className={"flex justify-between items-center"}>
<CardDescription>{t("Buyurtma holati")}</CardDescription>
<Badge
style={{ backgroundColor: orderDetails?.data.status.bg_color, color: orderDetails?.data.status.font_color }}
>{orderDetails?.data.status.translation}</Badge>
</div>
<div className={"flex justify-between items-center"}>
<CardDescription>{t("To'lov holati")}</CardDescription>
<Badge
style={{ backgroundColor: orderDetails?.data.payment_status.bg_color, color: orderDetails?.data.payment_status.font_color }}
>{orderDetails?.data.payment_status.translation}</Badge>
</div>
<div className={"flex justify-between items-center"}>
<CardDescription>{t("Yaratilish vaqti")}</CardDescription>
<CardDescription>
{orderDetails?.data.created_at}
</CardDescription>
</div>
</div>
</CardContent>
<Separator/>
<CardContent className={"p-0"}>
<CardTitle>{t("Xaridlar ro'yxati")}</CardTitle>
<div className={"mt-5 space-y-3"}>
{
orderDetails?.data.products.map(product => (
<div key={product.id} className={"flex justify-between w-full items-center"}>
<CardDescription>{product.name}</CardDescription>
<CardDescription>{product.count} x {formatNumberWithSpaces(product.price || product.total_price)} {t("so'm")}</CardDescription>
</div>
))
}
</div>
</CardContent>
<Separator/>
<CardContent className={"p-0"}>
<CardTitle>{t("Mijoz ma'lumotlari")}</CardTitle>
<div className={"mt-5 space-y-3"}>
<div className={"flex justify-between items-center"}>
<CardDescription>{t("Mijoz turi")}</CardDescription>
<CardDescription>{orderDetails?.data.client_type}</CardDescription>
</div>
<div className={"flex justify-between items-center"}>
<CardDescription>{t("Mijoz turi")}</CardDescription>
<CardDescription>{orderDetails?.data.client_information.full_name}</CardDescription>
</div>
<div className={"flex justify-between items-center"}>
<CardDescription>{t("Mijoz telefon")}</CardDescription>
<CardDescription>{orderDetails?.data.client_information.phone}</CardDescription>
</div>
</div>
</CardContent>
<Separator/>
<CardContent className={"p-0"}>
<CardTitle>{t("Yetkazib berish")}</CardTitle>
<div className={"mt-5 space-y-3"}>
<div className={"flex justify-between items-center"}>
<CardDescription>{t("Yetkazib berish turi")}</CardDescription>
<CardDescription>{orderDetails?.data.delivery_type}</CardDescription>
</div>
<div className={"flex justify-between items-center"}>
<CardDescription>{t("Yetkazib berish manzili")}</CardDescription>
<CardDescription>{orderDetails?.data.address.city.region.name}, {orderDetails?.data.address.city.name}</CardDescription>
</div>
<div className={"flex justify-between items-center"}>
<CardDescription>{t("Yetkazib berish narxi")}</CardDescription>
<CardDescription>{formatNumberWithSpaces(orderDetails?.data.price_delivery!)} so'm</CardDescription>
</div>
</div>
</CardContent>
<Separator/>
<CardFooter className={"flex justify-between p-0 pb-2"}>
<span/>
<Button size={"lg"} asChild>
<Link href={orderDetails?.data.pay_url! || ""} target={"_blank"}>
{t("To'lash")}
</Link>
</Button>
</CardFooter>
</Card>
</AccordionContent>
</AccordionItem>
</Accordion>
))
}
</div>
</div>
)
}
export default OrdersSection

View File

@@ -0,0 +1,26 @@
"use client"
import React from 'react'
import {useQuery} from "@tanstack/react-query";
import {getPolicy} from "@/shared/api/policySvc";
import Loader from "@/shared/ui/loader";
import { useTranslations } from 'next-intl';
const TermsSection = () => {
const t = useTranslations("")
const {data: policy, isLoading: policyIsLoading} = useQuery({
queryKey: ["getPolicy"],
queryFn: getPolicy
})
return (
<div className={"profile-section-wrapper"}>
<div className={"flex justify-between items-center mb-4"}>
<div>
<h1 className={"profile-section-title"}>{policy?.data.name}</h1>
<span className={"profile-section-subtitle"}>{t("Offerta shartlari")}</span>
</div>
</div>
{policyIsLoading ? <Loader height={"h-[30vh]"}/> : <div dangerouslySetInnerHTML={{__html: policy?.data?.body!}}/>}
</div>
)
}
export default TermsSection

View File

@@ -0,0 +1,74 @@
"use client"
import {Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle} from "@/shared/ui/dialog";
import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectLabel,
SelectTrigger,
SelectValue
} from "@/shared/ui/select";
import {Input} from "@/shared/ui/input";
import {Textarea} from "@/shared/ui/textarea";
import React from "react";
import {Button} from "@/shared/ui/button";
import {Service} from "@/shared/types/services";
import { useTranslations } from "next-intl";
interface ServiceModalProps {
selectedService: Service | null;
setSelectedService: (service: Service | null) => void;
}
const ServiceModal = ({selectedService, setSelectedService}: ServiceModalProps) => {
const t = useTranslations("")
return (
<Dialog open={!!selectedService} onOpenChange={
(open) => {
if (!open) {
setSelectedService(null)
}
}
}>
<DialogContent className={"min-w-4/12"}>
<DialogHeader>
<DialogTitle>{selectedService?.name}</DialogTitle>
<DialogDescription className={"space-y-5 mt-5"}>
<Select>
<SelectTrigger className={"w-full"}>
<SelectValue placeholder={t("Viloyat")}/>
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectLabel>{t("Viloyat")}</SelectLabel>
<SelectItem value="pineapple">Pineapple</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
<Select>
<SelectTrigger className={"w-full"}>
<SelectValue placeholder={t("Tuman")}/>
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectLabel>{t("Tuman")}</SelectLabel>
<SelectItem value="pineapple">Pineapple</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
<Input placeholder={t("Telefon raqamingiz")}/>
<Input placeholder={t("full_name")}/>
<Textarea placeholder="Type your message here."/>
<div className={"text-end"}>
<Button size={"lg"}>{t("Ariza yuborish")}</Button>
</div>
</DialogDescription>
</DialogHeader>
</DialogContent>
</Dialog>
)
}
export default ServiceModal

View File

@@ -0,0 +1,47 @@
"use client";
import React, { useState } from "react";
import { Service } from "@/shared/types/services";
import Image from "next/image";
import ServiceModal from "@/features/services/ui/services-section/service-modal";
import { useTranslations } from "next-intl";
interface ServiceSectionProps {
services: Service[];
}
const ServicesSection = ({ services }: Readonly<ServiceSectionProps>) => {
const t = useTranslations("");
const [selectedService, setSelectedService] = useState<Service | null>(null);
return (
<div className={"my-container section-wrapper"}>
<h1 className={"section-title text-center"}>{t("Xizmatlar")}</h1>
<div className={"grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 mx-4 md:mx-0 gap-6 mt-12"}>
{services.map((service) => (
<div
className={
"cursor-pointer border hover:border-primary transition-all duration-400 bg-white p-12 w-full flex flex-col justify-center items-center gap-2 h-full"
}
key={service.id}
onClick={() => {
setSelectedService(service);
}}
>
<div className={"w-full flex flex-col items-center gap-2"}>
<Image src={service.image} alt={""} width={100} height={100} />
<h1 className={"text-xl font-bold text-center"}>
{service.name}
</h1>
</div>
</div>
))}
</div>
{selectedService && (
<ServiceModal
selectedService={selectedService}
setSelectedService={setSelectedService}
/>
)}
</div>
);
};
export default ServicesSection;

View File

@@ -0,0 +1,124 @@
"use client"
import {getUsefulById} from "@/shared/api/usefulSvc";
import {UsefulItem} from "@/shared/types/useful";
import {useState} from "react";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger
} from "@/shared/ui/dialog";
import {Accordion, AccordionContent, AccordionItem, AccordionTrigger} from "@/shared/ui/accordion";
import {Button} from "@/shared/ui/button";
import Image from "next/image";
import { useTranslations } from "next-intl";
interface UsefulItemData {
id: number
name: string
description: string
video_url?: string
link_url?: string
file_url?: string
}
interface UsefulSectionProps {
usefuls: UsefulItem[]
}
const UsefulSection = ({ usefuls }: UsefulSectionProps) => {
const [items, setItems] = useState<UsefulItemData[]>([])
const [openId, setOpenId] = useState<number | null>(null)
const t = useTranslations("")
const fetchItems = async (id: number) => {
try {
const { data } = await getUsefulById(id);
setItems(data);
} catch (error) {
console.error("Error fetching items", error);
}
}
const handleDialogOpen = (isOpen: boolean, id: number) => {
if (isOpen && id !== openId) {
setOpenId(id);
fetchItems(id);
}
}
return (
<div className={"my-container section-wrapper"}>
<h1 className={"section-title text-center"}>{t("Foydali")}</h1>
<div className={"grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mt-12 px-4 md:px-0"}>
{usefuls.map((useful) => (
<div key={useful.id} className={"cursor-pointer border hover:border-primary transition-all duration-400 bg-white p-12 w-full flex flex-col justify-center items-center gap-2 h-full"}>
<Dialog onOpenChange={(isOpen) => handleDialogOpen(isOpen, useful.id)}>
<DialogTrigger asChild>
<div className={"p-12 w-full flex flex-col items-center gap-2"}>
<Image src={useful.image} alt={useful.name} width={100} height={100} />
<h1 className={"text-xl font-bold text-center"}>{useful.name}</h1>
</div>
</DialogTrigger>
<DialogContent className="sm:max-w-6/12">
<DialogHeader>
<DialogTitle>{useful.name}</DialogTitle>
<DialogDescription>
{t("Foydali ma'lumotlar ro'yxati")}
</DialogDescription>
</DialogHeader>
<Accordion type="single" collapsible className="w-full">
{items.map((item) => (
<AccordionItem value={`item-${item.id}`} key={item.id}>
<AccordionTrigger>{item.name}</AccordionTrigger>
<AccordionContent>
<div className={`${item.video_url && "grid grid-cols-2"}`}>
<p className="mb-2">{item.description}</p>
{item.video_url && (
<iframe
width="100%"
height="315"
src={item.video_url.replace("watch?v=", "embed/")}
title={item.name}
className={"rounded-xl"}
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
allowFullScreen
></iframe>
)}
</div>
{item.file_url && (
<a href={item.file_url} target="_blank" rel="noopener noreferrer">
<Button size="sm">{t("Download PDF")}</Button>
</a>
)}
{item.link_url && (
<a href={item.link_url} target="_blank" rel="noopener noreferrer">
<Button size="sm">{t("Download PDF")}</Button>
</a>
)}
</AccordionContent>
</AccordionItem>
))}
</Accordion>
<DialogFooter>
<Button size={"lg"} type="button" onClick={() => setOpenId(null)}>
{t("Yopish")}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
))}
</div>
</div>
);
}
export default UsefulSection;

11
src/middleware.ts Normal file
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 `/apiClient`, `/trpc`, `/_next` or `/_vercel`
// - … the ones containing a dot (e.g. `favicon.ico`)
matcher: "/((?!apiClient|trpc|_next|_vercel|.*\\..*).*)",
};

View File

@@ -0,0 +1,53 @@
import axios, { AxiosResponse } from "axios";
import { API_URL } from "@/shared/constants/apiEndpoints";
import { getLocale } from "next-intl/server";
import { getCurrentLocale } from "@/shared/lib/getCurrentLocale";
import { useAuthStore } from "@/shared/store/authStore";
const apiClient = axios.create({
baseURL: API_URL || "https://api.quyoshli.uz/api",
});
apiClient.interceptors.request.use(async (config) => {
console.log("API request", config);
const token = useAuthStore.getState().user?.access_token;
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
let language = "uz";
try {
language = await getLocale();
} catch (e) {
language = getCurrentLocale() || "uz";
}
config.headers["Accept-Language"] = language;
return config;
});
apiClient.interceptors.response.use(
(response) => response,
(error) => {
console.error("API error:", error);
return Promise.reject(error);
}
);
export const GET = <T>(
url: string,
params?: object
): Promise<AxiosResponse<T>> => apiClient.get(url, { params });
export const POST = <T>(url: string, data: object): Promise<AxiosResponse<T>> =>
apiClient.post(url, data);
export const PUT = <T>(url: string, data: object): Promise<AxiosResponse<T>> =>
apiClient.put(url, data);
export const PATCH = <T>(
url: string,
data: object
): Promise<AxiosResponse<T>> => apiClient.patch(url, data);
export const DELETE = <T>(
url: string,
data: object
): Promise<AxiosResponse<T>> => apiClient.delete(url, data);
export default apiClient;

14
src/shared/api/authSvc.ts Normal file
View File

@@ -0,0 +1,14 @@
import {POST} from "@/shared/api/apiClient";
import {useAuthStore} from "@/shared/store/authStore";
import {OAUTH, OAUTH_VERIFY} from "@/shared/constants";
export const sendPhoneNumber = async (phone: string) => {
await POST(OAUTH, { phone });
};
export const verifyCode = async (phone: string, verify_code: number): Promise<any> => {
const response = await POST(OAUTH_VERIFY, { phone, verify_code });
const {data} = response?.data;
useAuthStore.getState().login(data);
return data;
};

View File

@@ -0,0 +1,13 @@
import {BRANDS, PRODUCTS} from "@/shared/constants/apiEndpoints";
import {GET} from "@/shared/api/apiClient";
import { BrandProductsType, Brands } from "../types/brands";
export const getBrands = async (): Promise<Brands> => {
const res = await GET<Brands>(BRANDS);
return res.data;
}
export const getBrandProducts = async (brandId: number): Promise<BrandProductsType> => {
const res = await GET<BrandProductsType>(`${brandId}${PRODUCTS}`);
return res.data;
}

View File

@@ -0,0 +1,8 @@
import {GET} from "@/shared/api/apiClient";
import {COMPILATIONS} from "@/shared/constants/apiEndpoints";
import {GetCompilationResponse} from "@/shared/types/compilations";
export const getCompilation = async () => {
const res = await GET<GetCompilationResponse>(COMPILATIONS)
return res.data;
}

View File

@@ -0,0 +1,11 @@
import { AxiosResponse } from "axios";
import { SUPPORT } from "../constants";
import { POST } from "./apiClient";
export const contactInfoSubmit = async (
phone: string,
name: string
): Promise<AxiosResponse> => {
const response = await POST(SUPPORT, { phone, name });
return response;
};

View File

@@ -0,0 +1,12 @@
import {GET} from "@/shared/api/apiClient";
import {FEEDBACK} from "@/shared/constants";
export const getFeedback = async ()=>{
const {data}= await GET<{
data: {
"phone": string
"telegram_support": string
}
}>(FEEDBACK)
return data
}

5
src/shared/api/index.ts Normal file
View File

@@ -0,0 +1,5 @@
import apiClient from "@/shared/api/apiClient";
export * from "./authSvc"
export {
apiClient
}

View File

@@ -0,0 +1,13 @@
import {PARTNERS} from "@/shared/constants/apiEndpoints";
import {GET} from "@/shared/api/apiClient";
import {GetPartnersResponse} from "@/shared/types/partners";
export const getPartners = async (): Promise<GetPartnersResponse> => {
const res = await GET<GetPartnersResponse>(PARTNERS);
return res.data;
}
export const getPartnerById = async (id: number): Promise<GetPartnersResponse> => {
const res = await GET<GetPartnersResponse>(`${PARTNERS}/${id}`);
return res.data;
}

View File

@@ -0,0 +1,10 @@
import {GET} from "@/shared/api/apiClient";
import {PAGE_POLICY} from "@/shared/constants";
export const getPolicy = async ()=>{
const {data} = await GET<{data: {
name: string
body: string
}}>(PAGE_POLICY)
return data
}

View File

@@ -0,0 +1,8 @@
import {ProductCategory} from "../types/productCategory"
import {CATEGORIES} from "@/shared/constants/apiEndpoints";
import {GET} from "@/shared/api/apiClient";
export const getCategory = async (): Promise<{data:ProductCategory[]}>=>{
const res = await GET<{data:ProductCategory[]}>(CATEGORIES);
return res.data;
}

View File

@@ -0,0 +1,23 @@
import {CATEGORIES, PRODUCTS} from "@/shared/constants/apiEndpoints";
import {GetProductsResponse} from "@/shared/types/product";
import {GET} from "@/shared/api/apiClient";
import { BrandProductsType } from "../types/brands";
interface GetProductsProps {
categoryId: number
currentPage?: number
}
export const getProducts = async ({categoryId, currentPage}: GetProductsProps): Promise<{
data: GetProductsResponse
}> => {
const res = await GET<GetProductsResponse>(`${CATEGORIES}/${categoryId}${PRODUCTS}`, {
...(currentPage !== undefined && { page: currentPage })
});
return res;
}
export const getProductById = async (productId: number): Promise<any> => {
const res = await GET(`${PRODUCTS}/${productId}`);
return res;
}

View File

@@ -0,0 +1,8 @@
import {GET} from "@/shared/api/apiClient";
import {REGIONS} from "@/shared/constants";
import {GetRegionsResponse} from "@/shared/types/region";
export const getRegions = async ()=>{
const {data} = await GET<GetRegionsResponse>(REGIONS)
return data
}

View File

@@ -0,0 +1,13 @@
import {SERVICES} from "@/shared/constants/apiEndpoints";
import {GET} from "@/shared/api/apiClient";
import {GetServiceByIdResponse, GetServicesResponse} from "@/shared/types/services";
export const getServices = async (): Promise<GetServicesResponse> => {
const {data} = await GET<GetServicesResponse>(`${SERVICES}`);
return data
}
export const getServiceById = async (id: number): Promise<GetServiceByIdResponse> => {
const {data} = await GET<GetServiceByIdResponse>(`${SERVICES}/${id}`);
return data
}

View File

@@ -0,0 +1,18 @@
import {GET} from "@/shared/api/apiClient";
import {USEFUL_INFORMATION} from "@/shared/constants/apiEndpoints";
import {GetUsefulResponse} from "@/shared/types/useful";
export const getUseful = async (): Promise<GetUsefulResponse> => {
const {data} = await GET<GetUsefulResponse>(USEFUL_INFORMATION)
return data
}
export const getUsefulById = async (id: number): Promise<any> => {
const {data} = await GET(`${USEFUL_INFORMATION}/${id}`)
return data
}
export const getUsefulItems = async (useful_id: number, items_id:number): Promise<any> => {
const {data} = await GET(`${USEFUL_INFORMATION}/${useful_id}/items/${items_id}`)
return data
}

View File

@@ -0,0 +1,17 @@
import {GET, POST, PUT} from "@/shared/api/apiClient";
import {USER_ME} from "@/shared/constants";
import {GetUserMeResponse} from "@/shared/types/user";
export const getUserMe = async () => {
const {data} = await GET<GetUserMeResponse>(USER_ME)
return data
}
export const updateUserMe = async (userData: {
first_name?: string
last_name?: string
middle_name?: string
phone?: string
}) => {
const {data} = await PUT(USER_ME, userData)
return data
}

View File

@@ -0,0 +1,18 @@
import {GET, POST} from "@/shared/api/apiClient";
import {CHECKOUT, USER_ORDERS} from "@/shared/constants";
import {GetUserOrderByIdResponse, GetUserOrdersResponse} from "@/shared/types/userOrders";
export const getUserOrders = async ():Promise<GetUserOrdersResponse>=>{
const {data} = await GET<GetUserOrdersResponse>(USER_ORDERS)
return data
}
export const getUserOrdersById = async (id: number):Promise<GetUserOrderByIdResponse>=>{
const {data} = await GET<GetUserOrderByIdResponse>(`${USER_ORDERS}/${id}`)
return data
}
export const createUserOrder = async (data: any)=>{
const res = await POST(CHECKOUT, data)
return res
}

View File

@@ -0,0 +1,13 @@
import {GET} from "@/shared/api/apiClient";
import {USER_ORDERS, USER_REQUESTS} from "@/shared/constants";
import {GetUserRequestByIdResponse, GetUserRequestsResponse} from "@/shared/types/userRequests";
export const getUserRequests = async ():Promise<GetUserRequestsResponse>=>{
const {data} = await GET<GetUserRequestsResponse>(USER_REQUESTS)
return data
}
export const getUserRequestsById = async (id: number):Promise<GetUserRequestByIdResponse>=>{
const {data} = await GET<GetUserRequestByIdResponse>(`${USER_REQUESTS}/${id}`)
return data
}

View File

@@ -0,0 +1,9 @@
import {Golos_Text} from "next/font/google";
const golosText = Golos_Text({
weight: ["400", "500", "600", "700", "800"],
variable: "--font-golos-text",
subsets: ["latin"],
});
export { golosText };

View File

@@ -0,0 +1,130 @@
{
"Sozlamalar": "Настройки",
"Ma'lumotlarni yangilash": "Обновление данных",
"Sahifa topilmadi, Iltimos qayta urinib ko`ring": "Страница не найдена, попробуйте еще раз",
"Orga qaytish": "Возвращаться",
"Login": "Авторизоваться",
"Hisobingizga kirish uchun telefon raqamingizni kiriting": "Введите свой номер телефона, чтобы получить доступ к вашей учетной записи",
"Telefon": "Телефон",
"terms_of_use": "Я ознакомился с <tag>{tag}</tag>",
"Tasdiqlash kodini kiriting": "Введите код подтверждения",
"Tasdiqlash": "Подтвердить",
"Offer va shartlar": "Предложение и условия",
"Offerta shartlari": "Офферта термины",
"Bu yerda offerta shartlarini o'qib chiqishingiz mumkin": "Вы можете прочитать здесь термины Offerta.",
"Telefon raqam": "Номер телефона",
"Email": "Электронная почта",
"Adress": "Адрес",
"Ish vaqtlari": "Рабочее время",
"Biz haqimizda": "О нас",
"about_us_subtitle": "Наш интернет-магазин предлагает полный ассортимент оборудования и своевременный доступ ко всем новым моделям. Здесь вы найдете все необходимое для себя и своего дома, принимая во внимание последние обновления и скидки. Таким образом, вы всегда получите самый новый продукт по самым оптимальным ценам.",
"about_us_desc": "Наши каталоги постоянно пополняются новыми брендами и их продуктами, и вы всегда будете знать о сетевых поджелудочных зачинках, солнечных батареях и недавних разработках во всех спектрах услуг, связанных с энергией.",
"Kategoriyalar": "Категории",
"Kontaktlar": "Контакты",
"Ilovamizni yuklab oling": "Загрузите наше приложение",
"download_our_app_desc": "Наш интернет - магазин предоставляет вам полный ассортимент продукции для солнечного оборудования и уникальный доступ к соответствующей модели. Здесь вы найдете все необходимое для себя и своего дома, принимая во внимание последние обновления и скидки. Таким образом, вы всегда получите самый новый продукт по самым оптимальным ценам.",
"Bepul maslahat uchun ro'yxatdan o'ting": "Зарегистрируйтесь на бесплатный совет",
"Ro'yxatdan o'tish": "Зарегистрироваться",
"Ismingiz": "Ваше имя",
"Telefon raqamingiz": "Ваш номер телефона",
"Quyosh uskunalarini ulgurji narxlarda sotib oling!": "Купите солнечное оборудование по оптовым ценам!",
"Quyosh elektr stansiyasi uchun hamma narsani bir joyda va eng yaxshi narxda sotib oling": "Купите всё для солнечной электростанции у нас по лучшим ценам.",
"Batafsil": "Подробно",
"Quyosh panellari": "Солнечные панели",
"Hammasini ko'rish": "Просмотреть все",
"Hamkorlarimiz": "Наши партнеры",
"profit_1_desc": "Возьмите прибыль от энергии, производимой каждый день солнечными батареями. Вы можете подключиться к «зелёному» тарифу и продавать излишки солнечной энергии в сеть. Обычно оборудовано солнечными батареями, сетевым инвертором, установкой для солнечных батарей, защитой и переключением оборудования.",
"profit_2_desc": "Всегда будет свет - когда вы являетесь владельцем автономной или резервной солнечной электростанции. Даже если отключена внешняя сеть, вы продолжаете обеспечивать электроэнергией свой дом. Такие станции при необходимости можно дополнительно оснастить аккумуляторами.",
"profit_3_desc": "Вы можете купить все необходимые компоненты для солнечной электростанции в нашем интернет -магазине. Мы устанавливаем оборудование, которое предлагаем. Мы продаём только такое оборудование, которое приносит пользователю реальную экономию.",
"Katalog": "Каталог",
"Xizmatlar": "Услуги",
"Hamkorlik": "Сотрудничество",
"Foydali": "Полезный",
"Viloyat": "Область",
"Tuman": "Район",
"Ariza yuborish": "Отправить заявление",
"Sotib olish": "Покупка",
"Jismoniy shaxs": "Индивидуальный",
"Yuridik shaxs": "Юридическое лицо",
"Buyurtma muvaffaqiyatli yaratildi!": "Порядок был успешно создан!",
"Siz bilan tez orada bog'lanamiz": "Мы скоро свяжемся с вами.",
"Buyurtma yaratishda xatolik!": "Ошибка в создании порядка!",
"Ma'lumotlar to'liq kiritilganligiga ishonch hosil qiling yoki qaytadan urinib ko'ring": "Убедитесь, что данные введены и попробуйте еще раз.",
"Kompaniya nomi": "Название компании",
"Direktor": "Директор Ф.И.О.",
"Yuridik manzil": "Юридический адрес",
"Bank nomi": "Банк название",
"Hisob raqam": "Номер счета",
"Yetkazib berish": "Доставка",
"Viloyatni tanlang": "Выберите регион",
"Tuman/shahar": "Район / Город",
"Manzil": "Адрес",
"Uy raqami": "Домашний номер",
"Mo'ljal": "Цель",
"Ornatish xizmati kerakmi?": "Нужна служба установки?",
"Ha": "Да",
"Yoq": "Нет",
"Yetkazib berish kerakmi": "Вам нужно доставить",
"Yoq o'zim olib ketaman": "Нет, я возьму себя",
"Yuborish": "Отправка",
"full_name": "Ф.И.О.",
"Passport seriya va raqami": "Серия паспорта и номер",
"Yetkazib berish kerakmi?": "Стоит ли вам доставить?",
"so'm": "Сум",
"QQS bilan": "С НДС",
"chegirma": "скидка",
"Boshqa mahsulotlar": "Другие продукты",
"Mening arizalarim": "Мои приложения",
"Sizning arizalaringiz va ularning holati haqida ma'lumotlar": "Информация о ваших приложениях и их статусе",
"Ariza": "Приложение",
"Yaratilish vaqti": "Время создать",
"Ariza tafsilotlari": "Детали приложения",
"Ariza turi": "Тип приложения",
"Ariza raqami": "Номер приложения",
"Ariza holati": "Статус приложения",
"Xizmat tafsilotlari": "Служба детали",
"Quvvat": "Власть",
"Telefon raqami": "Номер телефона",
"Izoh": "Комментарий",
"Bog'lanish": "Связаться с нами",
"Biz bilan bog'lanish uchun quyidagi usullardan foydalanishingiz mumkin": "Вы можете использовать следующие способы связаться с нами",
"Call Center": "Колл-центр",
"Xatolik yuz berdi": "Произошла ошибка",
"Profil ma'lumotlari": "Информация о профиле",
"Sizning profil ma'lumotlaringiz va ularni o'zgartirish": "Информация о вашем профиле и их изменение",
"Familiyangiz": "Ваша фамилия",
"Sharif": "Шариф",
"Saqlanmoqda": "Сохранение...",
"Saqlash": "Хранилище",
"Mening buyurtmalarim": "Мои заказы",
"Sizning buyurtmalaringiz va ularning holati haqida ma'lumotlar": "Информация о ваших заказах и их статусе",
"Buyurtma": "Заказ",
"Buyurtma tafsilotlari": "Подробная информация о заказе",
"Buyurtma raqami": "Номер заказа",
"Buyurtma holati": "Статус заказа",
"To'lov holati": "Статус оплаты",
"Xaridlar ro'yxati": "Платный список",
"Mijoz ma'lumotlari": "Информация о клиенте",
"Mijoz turi": "Тип клиента",
"Mijoz telefon": "Номер телефона клиента",
"Yetkazib berish turi": "Тип доставки",
"Yetkazib berish manzili": "Адрес доставки",
"Yetkazib berish narxi": "Стоимость доставки",
"To'lash": "Платить",
"Foydali ma'lumotlar ro'yxati": "Полезный список информации",
"Download PDF": "Загрузите файл",
"Yopish": "Закрытие",
"Bo'limlar": "Разделы",
"Buyurtmalarim": "Мои заказы",
"Arizalarim": "Мои приложения",
"Offerta va foydalanish shartlari": "Офферта и Условия использования",
"GET-GREEN ENERGY TRADE - Barcha huquqlar himoyalangan": "© GETGREEN ENERGY TRADE. Все права защищены.",
"Asosiy": "Главная",
"Market": "Магазин",
"Kirish": "Войти",
"Bepul maslahat uchun ma'lumotlaringizni kiriting": "Введите свои данные, чтобы получить бесплатную консультацию",
"Yuborilmoqda": "Отправить",
"Ma'lumotlarni to'ldiring": "Заполните данные",
"Narx": "Цена",
"office": "Юнусабадский район, улица Ифтихор, 1, Ташкент, Узбекистан"
}

View File

@@ -0,0 +1,130 @@
{
"Sozlamalar": "Sozlamalar",
"Ma'lumotlarni yangilash": "Ma'lumotlarni yangilash",
"Sahifa topilmadi, Iltimos qayta urinib ko`ring": "Sahifa topilmadi, Iltimos qayta urinib ko`ring",
"Orga qaytish": "Orga qaytish",
"Login": "Login",
"Hisobingizga kirish uchun telefon raqamingizni kiriting": "Hisobingizga kirish uchun telefon raqamingizni kiriting",
"Telefon": "Telefon",
"terms_of_use": "<tag>{tag}</tag> bilan tanishdim",
"Tasdiqlash kodini kiriting": "Tasdiqlash kodini kiriting",
"Tasdiqlash": "Tasdiqlash",
"Offer va shartlar": "Offer va shartlar",
"Offerta shartlari": "Offerta shartlari",
"Bu yerda offerta shartlarini o'qib chiqishingiz mumkin": "Bu yerda offerta shartlarini o'qib chiqishingiz mumkin.",
"Telefon raqam": "Telefon raqam",
"Email": "Email",
"Adress": "Adress",
"Ish vaqtlari": "Ish vaqtlari",
"Biz haqimizda": "Biz haqimizda",
"about_us_subtitle": "Bizning onlayn-do'konimiz sizga quyosh uskunalari uchun mahsulotlarning to'liq assortimenti va mos modelni sotib olishning noyob imkoniyatini beradi. Bu yerda siz so'nggi yangilanishlar va chegirmalarni hisobga olgan holda o'zingiz va uyingiz uchun kerak bo'lgan hamma narsani topasiz. Shunday qilib, siz har doim eng yangi mahsulotni eng maqbul narxlarda olasiz.",
"about_us_desc": "Bizning kataloglarimiz doimiy ravishda yangi brendlar va ularning mahsulotlari bilan to'ldirilib boriladi, biz bilan birga bo'lib, siz doimo tarmoq invertorlari, quyosh panellari va muqobil energiya bilan bog'liq xizmatlarning barcha spektri sohasidagi so'nggi ishlanmalardan xabardor bo'lasiz.",
"Kategoriyalar": "Kategoriyalar",
"Kontaktlar": "Kontaktlar",
"Ilovamizni yuklab oling": "Ilovamizni yuklab oling",
"download_our_app_desc": "Bizning onlayn-do'konimiz sizga quyosh uskunalari uchun mahsulotlarning to'liq assortimenti va mos modelni sotib olishning noyob imkoniyatini beradi. Bu yerda siz so'nggi yangilanishlar va chegirmalarni hisobga olgan holda o'zingiz va uyingiz uchun kerak bo'lgan hamma narsani topasiz. Shunday qilib, siz har doim eng yangi mahsulotni eng maqbul narxlarda olasiz.",
"Bepul maslahat uchun ro'yxatdan o'ting": "Bepul maslahat uchun ro'yxatdan o'ting",
"Ro'yxatdan o'tish": "Ro'yxatdan o'tish",
"Ismingiz": "Ismingiz",
"Telefon raqamingiz": "Telefon raqamingiz",
"Quyosh uskunalarini ulgurji narxlarda sotib oling!": "Quyosh uskunalarini ulgurji narxlarda sotib oling!",
"Quyosh elektr stansiyasi uchun hamma narsani bir joyda va eng yaxshi narxda sotib oling": "Quyosh elektr stansiyasi uchun hamma narsani bir joyda va eng yaxshi narxda sotib oling.",
"Batafsil": "Batafsil",
"Quyosh panellari": "Quyosh panellari",
"Hammasini ko'rish": "Hammasini ko'rish",
"Hamkorlarimiz": "Hamkorlarimiz",
"profit_1_desc": "Quyosh panellari tomonidan har kuni ishlab chiqariladigan energiyadan foyda oling. Bu 'yashil tarif' bo'yicha tarmoq quyosh elektr stansiyasi tomonidan ta'minlanadi. Odatda quyosh panellari, tarmoq inverteri, quyosh panellari uchun o'rnatish to'plami, himoya va kommutatsiya uskunalari bilan jihozlangan.",
"profit_2_desc": "Har doim yorug'lik bo'ladi - avtonom yoki zaxira quyosh elektr stantsiyasining egasi bo'lganingizda. Tarmoq o'chirilgan bo'lsa ham o'zingizni va uyingizni elektr energiyasi bilan ta'minlaysiz. Bunday quyosh stantsiyalari qo'shimcha ravishda qayta zaryadlanuvchi batareyalar bilan jihozlangan.",
"profit_3_desc": "Quyosh elektr stansiyasi uchun barcha kerakli komponentlarni bizning onlayn do'konimizda xarid qilishingiz mumkin. Biz o'zimiz taklif qilayotgan uskunani o'rnatamiz. Va biz faqat foydalanuvchiga haqiqiy foyda keltiradigan narsalarni sotamiz.",
"Katalog": "Katalog",
"Xizmatlar": "Xizmatlar",
"Hamkorlik": "Hamkorlik",
"Foydali": "Foydali",
"Viloyat": "Viloyat",
"Tuman": "Tuman",
"Ariza yuborish": "Ariza yuborish",
"Sotib olish": "Sotib olish",
"Jismoniy shaxs": "Jismoniy shaxs",
"Yuridik shaxs": "Yuridik shaxs",
"Buyurtma muvaffaqiyatli yaratildi!": "Buyurtma muvaffaqiyatli yaratildi!",
"Siz bilan tez orada bog'lanamiz": "Siz bilan tez orada bog'lanamiz.",
"Buyurtma yaratishda xatolik!": "Buyurtma yaratishda xatolik!",
"Ma'lumotlar to'liq kiritilganligiga ishonch hosil qiling yoki qaytadan urinib ko'ring": "Ma'lumotlar to'liq kiritilganligiga ishonch hosil qiling yoki qaytadan urinib ko'ring.",
"Kompaniya nomi": "Kompaniya nomi",
"Direktor": "Direktor F.I.Sh.",
"Yuridik manzil": "Yuridik manzil",
"Bank nomi": "Bank nomi",
"Hisob raqam": "Hisob raqam",
"Yetkazib berish": "Yetkazib berish",
"Viloyatni tanlang": "Viloyatni tanlang",
"Tuman/shahar": "Tuman/shahar",
"Manzil": "Manzil",
"Uy raqami": "Uy raqami",
"Mo'ljal": "Mo'ljal",
"Ornatish xizmati kerakmi?": "Ornatish xizmati kerakmi?",
"Ha": "Ha",
"Yoq": "Yoq",
"Yetkazib berish kerakmi": "Yetkazib berish kerakmi",
"Yoq o'zim olib ketaman": "Yoq o'zim olib ketaman",
"Yuborish": "Yuborish",
"full_name": "F.I.Sh.",
"Passport seriya va raqami": "Passport seriya va raqami",
"Yetkazib berish kerakmi?": "Yetkazib berish kerakmi?",
"so'm": "so'm",
"QQS bilan": "QQS bilan",
"chegirma": "chegirma",
"Boshqa mahsulotlar": "Boshqa mahsulotlar",
"Mening arizalarim": "Mening arizalarim",
"Sizning arizalaringiz va ularning holati haqida ma'lumotlar": "Sizning arizalaringiz va ularning holati haqida ma'lumotlar",
"Ariza": "Ariza",
"Yaratilish vaqti": "Yaratilish vaqti",
"Ariza tafsilotlari": "Ariza tafsilotlari",
"Ariza turi": "Ariza turi",
"Ariza raqami": "Ariza raqami",
"Ariza holati": "Ariza holati",
"Xizmat tafsilotlari": "Xizmat tafsilotlari",
"Quvvat": "Quvvat",
"Telefon raqami": "Telefon raqami",
"Izoh": "Izoh",
"Bog'lanish": "Biz bilan bog'lanish",
"Biz bilan bog'lanish uchun quyidagi usullardan foydalanishingiz mumkin": "Biz bilan bog'lanish uchun quyidagi usullardan foydalanishingiz mumkin",
"Call Center": "Call Center",
"Xatolik yuz berdi": "Xatolik yuz berdi",
"Profil ma'lumotlari": "Profil ma'lumotlari",
"Sizning profil ma'lumotlaringiz va ularni o'zgartirish": "Sizning profil ma'lumotlaringiz va ularni o'zgartirish",
"Familiyangiz": "Familiyangiz",
"Sharif": "Sharif",
"Saqlanmoqda": "Saqlanmoqda...",
"Saqlash": "Saqlash",
"Mening buyurtmalarim": "Mening buyurtmalarim",
"Sizning buyurtmalaringiz va ularning holati haqida ma'lumotlar": "Sizning buyurtmalaringiz va ularning holati haqida ma'lumotlar",
"Buyurtma": "Buyurtma",
"Buyurtma tafsilotlari": "Buyurtma tafsilotlari",
"Buyurtma raqami": "Buyurtma raqami",
"Buyurtma holati": "Buyurtma holati",
"To'lov holati": "To'lov holati",
"Xaridlar ro'yxati": "Xaridlar ro'yxati",
"Mijoz ma'lumotlari": "Mijoz ma'lumotlari",
"Mijoz turi": "Mijoz turi",
"Mijoz telefon": "Mijoz telefon raqami",
"Yetkazib berish turi": "Yetkazib berish turi",
"Yetkazib berish manzili": "Yetkazib berish manzili",
"Yetkazib berish narxi": "Yetkazib berish narxi",
"To'lash": "To'lash",
"Foydali ma'lumotlar ro'yxati": "Foydali ma'lumotlar ro'yxati",
"Download PDF": "Faylni yuklab olish",
"Yopish": "Yopish",
"Bo'limlar": "Bo'limlar",
"Buyurtmalarim": "Buyurtmalarim",
"Arizalarim": "Arizalarim",
"Offerta va foydalanish shartlari": "Offerta va foydalanish shartlari",
"GET-GREEN ENERGY TRADE - Barcha huquqlar himoyalangan": "GET-GREEN ENERGY TRADE - Barcha huquqlar himoyalangan",
"Asosiy": "Asosiy",
"Market": "Market",
"Kirish": "Kirish",
"Bepul maslahat uchun ma'lumotlaringizni kiriting": "Bepul maslahat uchun ma'lumotlaringizni kiriting",
"Yuborilmoqda": "Yuborilmoqda...",
"Ma'lumotlarni to'ldiring": "Ma'lumotlarni to'ldiring",
"Narx": "Narx",
"office": "Yunusobod tumani, Iftixor kochasi, 1-uy, Toshkent, Ozbekiston"
}

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],
// Used when no locale matches
defaultLocale: LanguageRoutes.UZ,
localeDetection: false,
});

View File

@@ -0,0 +1,4 @@
export enum LanguageRoutes {
UZ = "uz", // o'zbekcha
RU = "ru", // ruscha
}

View File

@@ -0,0 +1,7 @@
import {golosText} from "@/shared/config/fonts";
import {routing} from "@/shared/config/i18n/routing";
export {
golosText,
routing
}

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,19 @@
export const API_URL = process.env.API_URL
export const CATEGORIES = "/categories"
export const BRANDS = "/brands"
export const COMPILATIONS = "/compilations"
export const PARTNERS = "/partners"
export const PRODUCTS = "/products"
export const SERVICES = "/services"
export const USEFUL_INFORMATION = "/useful-information"
export const OAUTH = '/oauth/'
export const OAUTH_VERIFY = '/oauth/verify'
export const SUPPORT = '/support'
export const USER_ORDERS = '/user/orders'
export const USER_REQUESTS = '/user/requests'
export const FEEDBACK = '/feedback'
export const PAGE_POLICY= "page/policy"
export const USER_ME= "/user/me"
export const REGIONS = "/regions"
export const CHECKOUT = "/checkout"

View File

@@ -0,0 +1,26 @@
const PRODUCT_INFO = {
name: "Get Green",
description: "Generated by create next app",
logo: "/getgreen.png",
favicon: "/favicon.png",
url: "https://getgreen.uz",
socials: {
telegram: "https://t.me/usmanov_dev",
instagram: "https://t.me/usmanov_dev",
youtube: "https://t.me/usmanov_dev",
linkedin: "https://www.linkedin.com/in/usmonov-azizbek/",
},
contact: {
phone: "+998555067788",
email: "contact@fias.uz",
location: "Yunusabadskiy rayon, ulisa Iftixor, 1"
},
terms_of_use: "",
creator: "Get Green",
app: {
ios: "/",
android: "/"
}
}
export {PRODUCT_INFO}

View File

@@ -0,0 +1,7 @@
import {PRODUCT_INFO} from "@/shared/constants/data";
import {profileSidebarMenu} from "@/shared/constants/profileSidebar";
export * from "./apiEndpoints"
export {
PRODUCT_INFO,
profileSidebarMenu
}

View File

@@ -0,0 +1,39 @@
import {Calendar, Home, Inbox, Search, Settings, PhoneCall, Newspaper} from "lucide-react";
export const profileSidebarMenu = [
{
label: "Bo'limlar",
menus: [
{
title: "Profil ma'lumotlari",
url: "/profile",
icon: Home,
},
{
title: "Buyurtmalarim",
url: "/profile/orders",
icon: Inbox,
},
{
title: "Arizalarim",
url: "/profile/applications",
icon: Calendar,
},
]
},
{
label: "Sozlamalar",
menus: [
{
title: "Offerta va foydalanish shartlari",
url: "/profile/terms",
icon: Newspaper,
},
{
title: "Bog'lanish",
url: "/profile/contact",
icon: PhoneCall,
},
]
}
]

View File

@@ -0,0 +1,14 @@
export const useLocale = ()=>{
const locale = window.location.pathname.split('/')[1]
const setLocale = (newLocale: string) => {
const segments = window.location.pathname.split('/');
segments[1] = newLocale;
const newPath = segments.join('/');
window.history.pushState({}, '', newPath);
window.location.reload();
}
return {
locale,
setLocale
}
}

View File

@@ -0,0 +1,19 @@
import * as React from "react"
const MOBILE_BREAKPOINT = 768
export function 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
}

View File

@@ -0,0 +1,27 @@
'use client';
import { useSearchParams, useRouter, usePathname } from 'next/navigation';
import { useCallback } from 'react';
export function useQueryString() {
const searchParams = useSearchParams();
const router = useRouter();
const pathname = usePathname();
const createQueryString = useCallback(
(name: string, value: string) => {
const params = new URLSearchParams(searchParams.toString());
params.set(name, value);
return params.toString();
},
[searchParams]
);
return {
searchParams,
router,
pathname,
createQueryString,
};
}

View File

@@ -0,0 +1,5 @@
const formatNumberWithSpaces = (number: number | string = "") => {
return number.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ' ');
}
export default formatNumberWithSpaces

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,9 @@
export const getCurrentLocale = (): string | undefined => {
if (typeof window !== "undefined" && typeof document !== "undefined") {
const match = document.cookie
.split("; ")
.find(row => row.startsWith("NEXT_LOCALE="));
return match?.split("=")[1];
}
return undefined;
};

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

View File

@@ -0,0 +1,24 @@
"use client";
import React, {useEffect} from 'react';
import {useRouter} from 'next/navigation';
import {useAuthStore} from '@/shared/store/authStore';
import {isBrowser} from "motion/react";
const PrivateRoute = ({children}: { children: React.ReactNode }) => {
const {user, isAuthenticated} = useAuthStore();
const router = useRouter();
useEffect(() => {
if (isBrowser && (!user || !isAuthenticated)) {
router.push('/auth/login');
}
}, [user, isAuthenticated, router]);
if (!isAuthenticated) {
return null;
}
return <>{children}</>;
};
export default PrivateRoute;

View File

@@ -0,0 +1,21 @@
"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,2 @@
export * from "./PrivateRouteProvider"
export * from "./QueryProvider"

View File

@@ -0,0 +1,41 @@
import {create} from 'zustand';
import {persist} from 'zustand/middleware';
import {isBrowser} from "motion/react";
interface AuthData {
id: number;
phone: string;
access_token: string;
}
interface AuthState {
isAuthenticated: boolean;
user: AuthData | null;
login: (data: AuthData) => void;
logout: () => void;
}
export const useAuthStore = create<AuthState>()(
persist(
(set) => ({
user: isBrowser && typeof window !== "undefined" ? JSON.parse(localStorage.getItem('auth-store') || 'null') : null,
login: (data) => {
set({user: data});
if (isBrowser && typeof window !== "undefined") {
localStorage.setItem('auth-store', JSON.stringify(data));
}
},
logout: () => {
set({user: null});
if (isBrowser && typeof window !== "undefined") {
localStorage.removeItem('auth-store');
}
},
isAuthenticated: isBrowser && typeof window !== "undefined" && localStorage.getItem("auth-store") !== null,
}),
{
name: 'auth-store',
}
)
);

View File

@@ -0,0 +1,23 @@
@layer utilities {
.my-container {
@apply container mx-auto transition-all duration-300;
}
.section-title{
@apply text-2xl max-sm:text-[28px] font-bold mb-4 text-[2.8rem] leading-[1.17];
}
.section-subtitle{
@apply text-base leading-[1.6];
}
.section-wrapper{
@apply py-24
}
.profile-section-wrapper{
@apply bg-white rounded-xl w-full p-4 h-fit
}
.profile-section-title{
@apply text-xl font-semibold
}
.profile-section-subtitle{
@apply text-sm font-semibold text-gray-500
}
}

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