translation bug fixed

This commit is contained in:
nabijonovdavronbek619@gmail.com
2025-11-29 13:48:55 +05:00
parent ca8369cc31
commit aa2260f212
28 changed files with 1521 additions and 242 deletions

View File

@@ -1,26 +0,0 @@
import { ReactNode } from "react";
import { notFound } from "next/navigation";
import { locales } from "@/i18n.config";
export function generateStaticParams() {
return locales.map((locale) => ({ locale }));
}
interface LocaleLayoutProps {
children: ReactNode;
params: Promise<{
locale: string;
}>;
}
async function LocaleLayout({ children, params }: LocaleLayoutProps) {
const { locale } = await params;
if (!locales.includes(locale as any)) {
notFound();
}
return <>{children}</>;
}
export default LocaleLayout;

View File

@@ -1,36 +0,0 @@
"use client";
import { Navbar } from "@/components/Navbar";
import { ShowCase } from "@/components/ShowCase";
import { About } from "@/components/About";
import { ProductsGrid } from "@/components/ProductsGrid";
import { FAQ } from "@/components/FAQ";
import { ContactForm } from "@/components/ContactForm";
import { Footer } from "@/components/Footer";
const HERO_IMAGES = [
"/product/product.jpg",
"/product/product.jpg",
"/product/product.jpg",
"/product/product.jpg",
"/product/product.jpg",
];
export default function Home() {
return (
<main>
<Navbar />
<ShowCase
titleKey="hero.title"
subtitleKey="hero.subtitle"
ctaLabelKey="hero.cta"
images={HERO_IMAGES}
/>
<About />
<ProductsGrid />
<FAQ />
<ContactForm />
<Footer />
</main>
);
}

View File

@@ -1,48 +1,18 @@
import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google";
import { ReactNode } from "react";
import { NextIntlClientProvider } from "next-intl";
import { getMessages } from "next-intl/server";
import "./globals.css";
const geistSans = Geist({
variable: "--font-geist-sans",
subsets: ["latin"],
});
const geistMono = Geist_Mono({
variable: "--font-geist-mono",
subsets: ["latin"],
});
export const metadata: Metadata = {
title: "Firma - Industrial Equipment & Pumps",
description:
"Premium industrial pumps and equipment supplier with 10+ years of experience",
};
async function RootLayout({
children,
params,
}: Readonly<{
children: ReactNode;
params: Promise<Record<string, any>>;
}>) {
const resolvedParams = await params;
const locale = resolvedParams.locale || "uz";
const messages = await getMessages();
import "../i18n/request"; // i18n config faylini import qilamiz
import { LanguageProvider } from "@/context/language-context";
import './globals.css'
export default function RootLayout({ children }: { children: ReactNode }) {
return (
<html lang={locale}>
<body
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
>
<NextIntlClientProvider messages={messages}>
<html lang="uz">
<body>
<LanguageProvider>
{children}
</NextIntlClientProvider>
</LanguageProvider>
</body>
</html>
);
}
export default RootLayout;

View File

@@ -1,5 +1,32 @@
import { redirect } from "next/navigation";
"use client";
import { Navbar } from "@/components/Navbar";
import { ShowCase } from "@/components/ShowCase";
import { About } from "@/components/About";
import { ProductsGrid } from "@/components/ProductsGrid";
import { FAQ } from "@/components/FAQ";
import { ContactForm } from "@/components/ContactForm";
import { Footer } from "@/components/Footer";
const HERO_IMAGES = [
"/product/product.jpg",
"/product/product1.jpg",
"/product/product2.jpg",
"/product/product3.jpg",
];
export default function Home() {
redirect('/uz')
return (
<main>
<Navbar />
<ShowCase
images={HERO_IMAGES}
/>
<About />
<ProductsGrid />
<FAQ />
<ContactForm />
<Footer />
</main>
);
}

View File

@@ -1,11 +1,11 @@
"use client";
import { useLanguage } from "@/context/language-context";
import { motion } from "framer-motion";
import { CheckCircle, Award, Users, Zap } from "lucide-react";
import { useTranslations } from "next-intl";
export function About() {
const t = useTranslations();
const {t} = useLanguage();
const features = [
{ icon: Award, labelKey: "Experience", value: "10+ лет" },
@@ -37,7 +37,7 @@ export function About() {
className="text-center mb-16"
>
<h2 className="text-4xl font-bold text-gray-900 mb-4">
{t("about.title")}
{t.about.title}
</h2>
<div className="w-20 h-1 bg-blue-600 mx-auto rounded-full" />
</motion.div>
@@ -51,7 +51,7 @@ export function About() {
viewport={{ once: true }}
>
<p className="text-lg text-gray-700 leading-relaxed mb-8">
{t("about.content")}
{t.about.content}
</p>
<motion.div

View File

@@ -2,14 +2,14 @@
import { useState } from "react";
import { motion } from "framer-motion";
import { useTranslations } from "next-intl";
import { usePathname } from "next/navigation";
import { sendContactMessage } from "@/lib/api";
import { Phone, MessageSquare, MapPin } from "lucide-react";
import Image from "next/image";
import { useLanguage } from "@/context/language-context";
export function ContactForm() {
const t = useTranslations();
const {t} = useLanguage();
const pathname = usePathname();
const locale = (pathname.split("/")[1] || "uz") as "uz" | "ru";
@@ -49,13 +49,13 @@ export function ContactForm() {
});
if (result.success) {
setMessage({ type: "success", text: t("contact.success") });
setMessage({ type: "success", text: t.contact.success });
setFormData({ name: "", phone: "", message: "", productSlug: "" });
} else {
setMessage({ type: "error", text: t("contact.error") });
setMessage({ type: "error", text: t.contact.error });
}
} catch {
setMessage({ type: "error", text: t("contact.error") });
setMessage({ type: "error", text: t.contact.error });
} finally {
setLoading(false);
}
@@ -82,7 +82,7 @@ export function ContactForm() {
className="text-center mb-16 bg-white/80 backdrop-blur-md rounded-xl overflow-hidden p-2 max-w-[300px] w-full mx-auto"
>
<h2 className="text-2xl font-bold text-gray-900 mb-2">
{t("contact.title")}
{t.contact.title}
</h2>
<div className="w-20 h-1 bg-blue-600 mx-auto rounded-full" />
</motion.div>
@@ -155,14 +155,14 @@ export function ContactForm() {
{/* Name */}
<div className="mb-6">
<label className="block text-gray-700 font-medium mb-2">
{t("contact.name")}
{t.contact.name}
</label>
<input
type="text"
name="name"
value={formData.name}
onChange={handleChange}
placeholder={t("contact.namePlaceholder")}
placeholder={t.contact.namePlaceholder}
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
</div>
@@ -170,14 +170,14 @@ export function ContactForm() {
{/* Phone */}
<div className="mb-6">
<label className="block text-gray-700 font-medium mb-2">
{t("contact.phone")} *
{t.contact.phone} *
</label>
<input
type="tel"
name="phone"
value={formData.phone}
onChange={handleChange}
placeholder={t("contact.phonePlaceholder")}
placeholder={t.contact.phonePlaceholder}
required
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
@@ -186,13 +186,13 @@ export function ContactForm() {
{/* Message */}
<div className="mb-6">
<label className="block text-gray-700 font-medium mb-2">
{t("contact.message")}
{t.contact.message}
</label>
<textarea
name="message"
value={formData.message}
onChange={handleChange}
placeholder={t("contact.messagePlaceholder")}
placeholder={t.contact.messagePlaceholder}
rows={4}
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 resize-none"
/>
@@ -201,7 +201,7 @@ export function ContactForm() {
{/* Product Select */}
<div className="mb-6">
<label className="block text-gray-700 font-medium mb-2">
{t("contact.product")}
{t.contact.product}
</label>
<select
name="productSlug"
@@ -239,7 +239,7 @@ export function ContactForm() {
disabled={loading}
className="w-full px-6 py-3 bg-blue-600 text-white rounded-lg font-semibold hover:bg-blue-700 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
>
{loading ? "Sending..." : t("contact.send")}
{loading ? "Sending..." : t.contact.send}
</motion.button>
</motion.form>
</div>

View File

@@ -3,7 +3,7 @@
import { useState } from "react";
import { motion, AnimatePresence } from "framer-motion";
import { ChevronDown } from "lucide-react";
import { useTranslations } from "next-intl";
import { useLanguage } from "@/context/language-context";
interface FaqItem {
questionKey: string;
@@ -15,7 +15,7 @@ interface FaqProps {
}
export function FAQ({ items }: FaqProps) {
const t = useTranslations();
const {t} = useLanguage();
const [openIndex, setOpenIndex] = useState<number | null>(0);
const defaultItems: FaqItem[] = [
@@ -60,7 +60,7 @@ export function FAQ({ items }: FaqProps) {
className="text-center mb-16"
>
<h2 className="text-4xl font-bold text-gray-900 mb-4">
{t("faq.title")}
{t.faq.title}
</h2>
<div className="w-20 h-1 bg-blue-600 mx-auto rounded-full" />
</motion.div>
@@ -81,7 +81,7 @@ export function FAQ({ items }: FaqProps) {
>
<div className="flex items-center justify-between">
<h3 className="text-lg font-semibold text-gray-900 flex-1">
{t(item.questionKey)}
{t.faq.items[idx].question}
</h3>
<motion.div
animate={{ rotate: openIndex === idx ? 180 : 0 }}
@@ -104,7 +104,7 @@ export function FAQ({ items }: FaqProps) {
>
<div className="bg-blue-50 p-6 rounded-b-lg border-t border-gray-200">
<p className="text-gray-700 leading-relaxed">
{t(item.answerKey)}
{t.faq.items[idx].answer}
</p>
</div>
</motion.div>

View File

@@ -1,12 +1,11 @@
"use client";
import { useLanguage } from "@/context/language-context";
import { motion } from "framer-motion";
import { useTranslations } from "next-intl";
import { Facebook, Linkedin, Send } from "lucide-react";
export function Footer() {
const t = useTranslations();
const {t} = useLanguage();
const socialLinks = [
{ icon: Facebook, href: "#", label: "Facebook" },
{ icon: Linkedin, href: "#", label: "LinkedIn" },
@@ -86,7 +85,7 @@ export function Footer() {
{/* Social */}
<motion.div variants={itemVariants}>
<h4 className="font-semibold mb-4">{t("footer.followUs")}</h4>
<h4 className="font-semibold mb-4">{t.footer.followUs}</h4>
<div className="flex gap-4">
{socialLinks.map((link) => {
const Icon = link.icon;
@@ -116,7 +115,7 @@ export function Footer() {
viewport={{ once: true }}
className="text-center text-gray-400 text-sm"
>
{t("footer.copyright")}
{t.footer.copyright}
</motion.p>
</div>
</div>

View File

@@ -2,10 +2,10 @@
import { useState } from "react";
import Link from "next/link";
import { usePathname } from "next/navigation";
import { Menu, X } from "lucide-react";
import { motion } from "framer-motion";
import { useTranslations } from "next-intl";
import LanguageSwitcher from "./languageSwitcher";
import { useLanguage } from "@/context/language-context";
interface NavLink {
id: string;
@@ -19,20 +19,15 @@ interface NavbarProps {
export function Navbar({ logoText = "FIRMA" }: NavbarProps) {
const [isOpen, setIsOpen] = useState(false);
const t = useTranslations();
const pathname = usePathname();
const { t } = useLanguage();
const navLinks: NavLink[] = [
{ id: "about", labelKey: "nav.about", href: "#about" },
{ id: "products", labelKey: "nav.products", href: "#products" },
{ id: "faq", labelKey: "nav.faq", href: "#faq" },
{ id: "contact", labelKey: "nav.contact", href: "#contact" },
{ id: "about", labelKey: t.nav.about, href: "#about" },
{ id: "products", labelKey: t.nav.products, href: "#products" },
{ id: "faq", labelKey: t.nav.faq, href: "#faq" },
{ id: "contact", labelKey: t.nav.contact , href: "#contact" },
];
const locale = pathname.split("/")[1];
const otherLocale = locale === "uz" ? "ru" : "uz";
const otherPath = pathname.replace(`/${locale}`, `/${otherLocale}`);
const handleScroll = (href: string) => {
if (href.startsWith("#")) {
const element = document.querySelector(href);
@@ -50,7 +45,7 @@ export function Navbar({ logoText = "FIRMA" }: NavbarProps) {
{/* Logo */}
<motion.div whileHover={{ scale: 1.05 }} whileTap={{ scale: 0.95 }}>
<Link
href={`/${locale}`}
href={`/`}
className="text-2xl font-bold bg-linear-to-r from-blue-600 to-blue-800 bg-clip-text text-transparent"
>
{logoText}
@@ -66,21 +61,14 @@ export function Navbar({ logoText = "FIRMA" }: NavbarProps) {
onClick={() => handleScroll(link.href)}
className="text-gray-700 hover:text-blue-600 transition-colors"
>
{t(link.labelKey)}
{link.labelKey}
</motion.button>
))}
</div>
{/* Language & Mobile Menu */}
<div className="flex items-center gap-4">
<motion.a
href={otherPath}
whileHover={{ scale: 1.1 }}
whileTap={{ scale: 0.95 }}
className="px-3 py-1 bg-blue-600 text-white rounded-full text-sm font-medium hover:bg-blue-700 transition-colors"
>
{otherLocale.toUpperCase()}
</motion.a>
<LanguageSwitcher />
{/* Mobile Menu Button */}
<button
@@ -106,7 +94,7 @@ export function Navbar({ logoText = "FIRMA" }: NavbarProps) {
onClick={() => handleScroll(link.href)}
className="block w-full text-left px-4 py-2 text-gray-700 hover:bg-blue-50 rounded-lg transition-colors"
>
{t(link.labelKey)}
{link.labelKey}
</button>
))}
</motion.div>

View File

@@ -3,8 +3,8 @@
import Image from "next/image";
import { motion } from "framer-motion";
import { ExternalLink } from "lucide-react";
import { useTranslations } from "next-intl";
import type { Product } from "@/lib/products";
import { useLanguage } from "@/context/language-context";
interface ProductCardProps {
product: Product;
@@ -12,8 +12,7 @@ interface ProductCardProps {
}
export function ProductCard({ product, onViewDetails }: ProductCardProps) {
const t = useTranslations();
const {t} = useLanguage();
return (
<motion.div
whileHover={{ y: -8 }}
@@ -23,7 +22,7 @@ export function ProductCard({ product, onViewDetails }: ProductCardProps) {
<div className="relative h-48 bg-gray-100 overflow-hidden group">
<Image
src={product.images[0]}
alt={t(product.nameKey)}
alt={product.nameKey}
fill
className="object-cover group-hover:scale-110 transition-transform duration-300"
/>
@@ -33,10 +32,10 @@ export function ProductCard({ product, onViewDetails }: ProductCardProps) {
{/* Content */}
<div className="p-6">
<h3 className="text-xl font-bold text-gray-900 mb-2">
{t(product.nameKey)}
{product.nameKey}
</h3>
<p className="text-gray-600 text-sm mb-4">
{t(product.shortDescriptionKey)}
{product.shortDescriptionKey}
</p>
{/* Specs Preview */}
@@ -56,7 +55,7 @@ export function ProductCard({ product, onViewDetails }: ProductCardProps) {
onClick={() => onViewDetails(product.slug)}
className="w-full flex items-center justify-center gap-2 px-4 py-2 bg-blue-600 text-white rounded-lg font-medium hover:bg-blue-700 transition-colors"
>
{t("products.viewDetails")}
Batafsil
<ExternalLink size={16} />
</motion.button>
</div>

View File

@@ -2,17 +2,17 @@
import { useState } from "react";
import { motion } from "framer-motion";
import { useTranslations } from "next-intl";
import { ProductCard } from "./ProductCard";
import { getAllProducts } from "@/lib/products";
import type { Product } from "@/lib/products";
import { ProductModal } from "./ProductModal";
import Image from "next/image";
import { useLanguage } from "@/context/language-context";
// hello everyone
export function ProductsGrid() {
const t = useTranslations();
const {t} = useLanguage();
const products = getAllProducts();
const [selectedProduct, setSelectedProduct] = useState<Product | null>(null);
@@ -58,7 +58,7 @@ export function ProductsGrid() {
className="text-center mb-16 bg-white/80 backdrop-blur-md rounded-xl overflow-hidden p-2 max-w-[300px] w-full mx-auto"
>
<h2 className="text-2xl font-bold text-gray-900 mb-2">
{t("products.title")}
{t.products.title}
</h2>
<div className="w-20 h-1 bg-blue-600 mx-auto rounded-full" />
</motion.div>

View File

@@ -4,24 +4,16 @@ import { useState, useEffect } from "react";
import Image from "next/image";
import { motion, AnimatePresence } from "framer-motion";
import { ChevronLeft, ChevronRight } from "lucide-react";
import { useTranslations } from "next-intl";
import { useLanguage } from "@/context/language-context";
//hello again dear
interface ShowCaseProps {
titleKey: string;
subtitleKey?: string;
ctaLabelKey: string;
images: string[];
}
export function ShowCase({
titleKey,
subtitleKey,
ctaLabelKey,
images,
}: ShowCaseProps) {
const t = useTranslations();
export function ShowCase({ images }: ShowCaseProps) {
const { t } = useLanguage();
const [currentImageIndex, setCurrentImageIndex] = useState(0);
const [autoPlay, setAutoPlay] = useState(true);
@@ -54,7 +46,6 @@ export function ShowCase({
return (
<section className="relative min-h-screen pt-20 pb-20">
{/* background image */}
<div className="absolute -z-50 top-0 left-0 h-full w-full">
<Image
@@ -76,14 +67,11 @@ export function ShowCase({
className="bg-white/80 backdrop-blur-md rounded-xl overflow-hidden p-4"
>
<h1 className="text-4xl lg:text-5xl font-bold text-gray-900 mb-6 leading-tight">
{t(titleKey)}
{t.hero.title}
</h1>
{subtitleKey && (
<p className="text-lg text-gray-600 mb-8 leading-relaxed">
{t(subtitleKey)}
</p>
)}
<p className="text-lg text-gray-600 mb-8 leading-relaxed">
{t.hero.subtitle}
</p>
<motion.button
whileHover={{ scale: 1.05 }}
@@ -91,7 +79,7 @@ export function ShowCase({
onClick={handleContactClick}
className="px-8 py-3 bg-linear-to-r from-blue-600 to-blue-700 text-white rounded-lg font-semibold hover:shadow-lg transition-shadow"
>
{t(ctaLabelKey)}
{t.hero.cta}
</motion.button>
</motion.div>
@@ -102,7 +90,7 @@ export function ShowCase({
transition={{ duration: 0.6, delay: 0.2 }}
className="relative"
>
<div className="relative aspect-square rounded-xl overflow-hidden shadow-2xl bg-gray-100">
<div className="relative aspect-square rounded-xl overflow-hidden">
<AnimatePresence mode="wait">
<motion.div
key={currentImageIndex}

View File

@@ -0,0 +1,45 @@
"use client";
import { useLanguage } from "@/context/language-context";
import { usePathname, useRouter } from "next/navigation";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "./ui/dropdown-menu";
import { Globe } from "lucide-react";
export default function LanguageSwitcher() {
const { language, setLanguage } = useLanguage();
const languages = [
{ code: "uz" as const, name: "O'zbekcha" },
{ code: "ru" as const, name: "Русский" },
];
return (
<div>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<button className="flex items-center gap-1 hover:text-primary transition-colors">
<Globe size={16} />
{language.toUpperCase()}
</button>
</DropdownMenuTrigger>
<DropdownMenuContent
align="end"
className="bg-slate-800 border-slate-700"
>
{languages.map((lang) => (
<DropdownMenuItem
key={lang.code}
onClick={() => setLanguage(lang.code)}
className="cursor-pointer hover:bg-slate-700 text-white"
>
{lang.name}
</DropdownMenuItem>
))}
</DropdownMenuContent>
</DropdownMenu>
</div>
);
}

View File

@@ -0,0 +1,257 @@
'use client'
import * as React from 'react'
import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu'
import { CheckIcon, ChevronRightIcon, CircleIcon } from 'lucide-react'
import { cn } from '@/lib/utils'
function DropdownMenu({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Root>) {
return <DropdownMenuPrimitive.Root data-slot="dropdown-menu" {...props} />
}
function DropdownMenuPortal({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Portal>) {
return (
<DropdownMenuPrimitive.Portal data-slot="dropdown-menu-portal" {...props} />
)
}
function DropdownMenuTrigger({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Trigger>) {
return (
<DropdownMenuPrimitive.Trigger
data-slot="dropdown-menu-trigger"
{...props}
/>
)
}
function DropdownMenuContent({
className,
sideOffset = 4,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Content>) {
return (
<DropdownMenuPrimitive.Portal>
<DropdownMenuPrimitive.Content
data-slot="dropdown-menu-content"
sideOffset={sideOffset}
className={cn(
'bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 max-h-(--radix-dropdown-menu-content-available-height) min-w-32 origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border p-1 shadow-md',
className,
)}
{...props}
/>
</DropdownMenuPrimitive.Portal>
)
}
function DropdownMenuGroup({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Group>) {
return (
<DropdownMenuPrimitive.Group data-slot="dropdown-menu-group" {...props} />
)
}
function DropdownMenuItem({
className,
inset,
variant = 'default',
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Item> & {
inset?: boolean
variant?: 'default' | 'destructive'
}) {
return (
<DropdownMenuPrimitive.Item
data-slot="dropdown-menu-item"
data-inset={inset}
data-variant={variant}
className={cn(
"focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:text-destructive! [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-disabled:pointer-events-none data-disabled:opacity-50 data-inset:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className,
)}
{...props}
/>
)
}
function DropdownMenuCheckboxItem({
className,
children,
checked,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.CheckboxItem>) {
return (
<DropdownMenuPrimitive.CheckboxItem
data-slot="dropdown-menu-checkbox-item"
className={cn(
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className,
)}
checked={checked}
{...props}
>
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<CheckIcon className="size-4" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.CheckboxItem>
)
}
function DropdownMenuRadioGroup({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioGroup>) {
return (
<DropdownMenuPrimitive.RadioGroup
data-slot="dropdown-menu-radio-group"
{...props}
/>
)
}
function DropdownMenuRadioItem({
className,
children,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioItem>) {
return (
<DropdownMenuPrimitive.RadioItem
data-slot="dropdown-menu-radio-item"
className={cn(
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className,
)}
{...props}
>
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<CircleIcon className="size-2 fill-current" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.RadioItem>
)
}
function DropdownMenuLabel({
className,
inset,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Label> & {
inset?: boolean
}) {
return (
<DropdownMenuPrimitive.Label
data-slot="dropdown-menu-label"
data-inset={inset}
className={cn(
'px-2 py-1.5 text-sm font-medium data-inset:pl-8',
className,
)}
{...props}
/>
)
}
function DropdownMenuSeparator({
className,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Separator>) {
return (
<DropdownMenuPrimitive.Separator
data-slot="dropdown-menu-separator"
className={cn('bg-border -mx-1 my-1 h-px', className)}
{...props}
/>
)
}
function DropdownMenuShortcut({
className,
...props
}: React.ComponentProps<'span'>) {
return (
<span
data-slot="dropdown-menu-shortcut"
className={cn(
'text-muted-foreground ml-auto text-xs tracking-widest',
className,
)}
{...props}
/>
)
}
function DropdownMenuSub({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Sub>) {
return <DropdownMenuPrimitive.Sub data-slot="dropdown-menu-sub" {...props} />
}
function DropdownMenuSubTrigger({
className,
inset,
children,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.SubTrigger> & {
inset?: boolean
}) {
return (
<DropdownMenuPrimitive.SubTrigger
data-slot="dropdown-menu-sub-trigger"
data-inset={inset}
className={cn(
"focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-inset:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className,
)}
{...props}
>
{children}
<ChevronRightIcon className="ml-auto size-4" />
</DropdownMenuPrimitive.SubTrigger>
)
}
function DropdownMenuSubContent({
className,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.SubContent>) {
return (
<DropdownMenuPrimitive.SubContent
data-slot="dropdown-menu-sub-content"
className={cn(
'bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-32 origin-(--radix-dropdown-menu-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-lg',
className,
)}
{...props}
/>
)
}
export {
DropdownMenu,
DropdownMenuPortal,
DropdownMenuTrigger,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuLabel,
DropdownMenuItem,
DropdownMenuCheckboxItem,
DropdownMenuRadioGroup,
DropdownMenuRadioItem,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuSub,
DropdownMenuSubTrigger,
DropdownMenuSubContent,
}

View File

@@ -0,0 +1,32 @@
"use client"
import { translations } from "@/lib/translations"
import { createContext, useContext, useState, type ReactNode } from "react"
type Language = "uz" | "ru"
interface LanguageContextType {
language: Language
setLanguage: (lang: Language) => void
t: (typeof translations)[Language]
}
const LanguageContext = createContext<LanguageContextType | undefined>(undefined)
export function LanguageProvider({ children }: { children: ReactNode }) {
const [language, setLanguage] = useState<Language>("uz")
return (
<LanguageContext.Provider value={{ language, setLanguage, t: translations[language] }}>
{children}
</LanguageContext.Provider>
)
}
export function useLanguage() {
const context = useContext(LanguageContext)
if (!context) {
throw new Error("useLanguage must be used within LanguageProvider")
}
return context
}

View File

@@ -1,4 +0,0 @@
export type Locale = "uz" | "ru";
export const locales: Locale[] = ["uz", "ru"];
export const defaultLocale: Locale = "uz";

View File

@@ -1,10 +1,18 @@
import { getRequestConfig } from "next-intl/server";
// i18n/request.ts
import { getRequestConfig, type GetRequestConfigParams } from "next-intl/server";
import { notFound } from "next/navigation";
export default getRequestConfig(async ({ requestLocale }) => {
const locale = (await requestLocale) || "uz";
export const locales = ['uz','ru'];
export default getRequestConfig(async ({ locale }: GetRequestConfigParams) => {
// Agar locale undefined yoki not supported bolsa, 404
if (!locale || !locales.includes(locale)) notFound();
// endi TypeScript uchun locale string ekanligi aniq
const messages = (await import(`../locales/${locale}.json`)).default;
return {
locale,
messages: (await import(`../locales/${locale}.json`)).default,
locale, // string, undefined emas
messages, // JSON fayl
};
});

View File

@@ -13,10 +13,9 @@ export interface Product {
export const products: Product[] = [
{
id: "1",
nameKey: "products_list.pump_1.name",
slug: "schotchik-pump",
shortDescriptionKey: "products_list.pump_1.shortDescription",
longDescriptionKey: "products_list.pump_1.description",
nameKey: "Schotchik Nasos",
slug: "Yuqori sifatli schotchik nasos, benzin, dizel va kerosinni tashishda ishlatiladi.",
shortDescriptionKey: "Xavfsiz neft mahsulotlarini tashish uchun",
images: ["/images/pump-1.jpg", "/images/pump-1-alt.jpg"],
specs: [
{ key: "Flow Rate", value: "100 L/min" },
@@ -27,10 +26,9 @@ export const products: Product[] = [
},
{
id: "2",
nameKey: "products_list.pump_2.name",
slug: "agregat-pump",
shortDescriptionKey: "products_list.pump_2.shortDescription",
longDescriptionKey: "products_list.pump_2.description",
nameKey: "Agregat Nasos",
slug: "Katta volumli neft mahsulotlarini tashishda o'rnatilgan.",
shortDescriptionKey: "Kuchli va ishonchli aggregat nasos",
images: ["/images/pump-2.jpg", "/images/pump-2-alt.jpg"],
specs: [
{ key: "Flow Rate", value: "250 L/min" },
@@ -41,10 +39,9 @@ export const products: Product[] = [
},
{
id: "3",
nameKey: "products_list.pump_3.name",
slug: "ccl-20-24-pump",
shortDescriptionKey: "products_list.pump_3.shortDescription",
longDescriptionKey: "products_list.pump_3.description",
nameKey: "СЦЛ 20/24",
slug:"Chuqurligi 20-24 metrda ishlaydigan professional nasos.",
shortDescriptionKey: "Professional kalibrli nasos",
images: ["/images/pump-3.jpg", "/images/pump-3-alt.jpg"],
specs: [
{ key: "Depth Rating", value: "20-24 m" },

159
lib/translations.ts Normal file
View File

@@ -0,0 +1,159 @@
export const translations = {
uz: {
nav: {
about: "Biz haqimizda",
products: "Mahsulotlar",
faq: "FAQ",
contact: "Bog'lanish",
},
hero: {
title: "Sanoat Uskunalari va Nasoslar Yetkazuvchisi",
subtitle: "10+ yil tajribasi bilan sifatli mahsulot va xizmat",
cta: "Bog'lanish",
},
about: {
title: "Biz haqimizda",
content:
"Kompaniyamiz sanoat nasoslari va o'lchov uskunalarini yetkazib berishda 10+ yil tajribaga ega. Har bir mahsulot sinovdan o'tkazilgan, sifat kafolatlangan va texnik xizmat ko'rsatish bilan ta'minlanadi. Biz mijozlarimizga texnik maslahat, tez etkazib berish va o'rnatish bo'yicha to'liq xizmat taklif etamiz. Ixtisoslashgan nasoslarimiz (schotchik, agregat nasos, СЦЛ 20/24 va boshqalar) benzin, dizel, kerosin va boshqa yengil neft mahsulotlarini xavfsiz va samarali tashishda ishlatiladi.",
},
products: {
title: "Mahsulotlar",
viewDetails: "Batafsil",
},
faq: {
title: "Tez-tez So'raladigan Savollar",
items: [
{
question: "Mahsulotlar uchun kafolat bormi?",
answer: "Ha, barcha uskunalarimizga 12 oylik texnik kafolat beriladi.",
},
{
question: "Yetkazib berish muddati qancha?",
answer: "Odatda 3-14 ish kuni, mavjudlik va manzilga bog'liq.",
},
{
question: "Texnik qo'llab-quvvatlash bormi?",
answer: "Ha, telefon va Telegram orqali 24/7 texnik maslahat mavjud.",
},
],
},
contact: {
title: "Bog'lanish",
name: "Ism",
phone: "Telefon raqami",
message: "Xabar",
product: "Mahsulot (ixtiyoriy)",
send: "Yuborish",
success: "Xabar muvaffaqiyatli yuborildi!",
error: "Xato: Xabarni yuborib bo'lmadi.",
namePlaceholder: "Sizning ismingiz",
phonePlaceholder: "+998 XX XXX XX XX",
messagePlaceholder: "Sizning xabaringiz (ixtiyoriy)",
},
footer: {
copyright: "© 2025 Firma. Barcha huquqlar himoyalangan.",
followUs: "Bizni kuzatib turing",
},
products_list: {
pump_1: {
name: "Schotchik Nasos",
shortDescription: "Xavfsiz neft mahsulotlarini tashish uchun",
description:
"Yuqori sifatli schotchik nasos, benzin, dizel va kerosinni tashishda ishlatiladi.",
},
pump_2: {
name: "Agregat Nasos",
shortDescription: "Kuchli va ishonchli aggregat nasos",
description:
"Katta volumli neft mahsulotlarini tashishda o'rnatilgan.",
},
pump_3: {
name: "СЦЛ 20/24",
shortDescription: "Professional kalibrli nasos",
description:
"Chuqurligi 20-24 metrda ishlaydigan professional nasos.",
},
},
},
ru: {
nav: {
about: "О нас",
products: "Продукты",
faq: "FAQ",
contact: "Контакт",
},
hero: {
title: "Поставщик промышленного оборудования и насосов",
subtitle: "Качественная продукция и услуги с 10+ летним опытом",
cta: "Свяжитесь с нами",
},
about: {
title: "О нас",
content:
"Наша компания имеет 10+ лет опыта в поставке промышленных насосов и измерительного оборудования. Каждый продукт протестирован, качество гарантировано и сопровождается технической поддержкой. Мы предлагаем нашим клиентам полный сервис: техническую консультацию, быструю доставку и установку. Наши специализированные насосы (счетчик, агрегатный насос, СЦЛ 20/24 и др.) используются для безопасной и эффективной транспортировки бензина, дизеля, керосина и других легких нефтепродуктов.",
},
products: {
title: "Продукты",
viewDetails: "Подробнее",
},
faq: {
title: "Часто Задаваемые Вопросы",
items: [
{
question: "Гарантия на продукты?",
answer:
"Да, все наше оборудование поставляется с 12-месячной технической гарантией.",
},
{
question: "Сколько времени займет доставка?",
answer:
"Обычно 3-14 рабочих дней, в зависимости от наличия и адреса доставки.",
},
{
question: "Есть ли техническая поддержка?",
answer:
"Да, техническая консультация доступна 24/7 по телефону и Telegram.",
},
],
},
contact: {
title: "Свяжитесь с нами",
name: "Имя",
phone: "Номер телефона",
message: "Сообщение",
product: "Продукт (опционально)",
send: "Отправить",
success: "Сообщение успешно отправлено!",
error: "Ошибка: не удалось отправить сообщение.",
namePlaceholder: "Ваше имя",
phonePlaceholder: "+998 XX XXX XX XX",
messagePlaceholder: "Ваше сообщение (опционально)",
},
footer: {
copyright: "© 2025 Firma. Все права защищены.",
followUs: "Следите за нами",
},
products_list: {
pump_1: {
name: "Счетчик Насос",
shortDescription:
"Для безопасной транспортировки нефтепродуктов",
description:
"Высококачественный счетчиковый насос, используется для транспортировки бензина, дизеля и керосина.",
},
pump_2: {
name: "Агрегатный Насос",
shortDescription: "Мощный и надежный агрегатный насос",
description:
"Установлен для транспортировки больших объемов нефтепродуктов.",
},
pump_3: {
name: "СЦЛ 20/24",
shortDescription: "Профессиональный калиброванный насос",
description:
"Профессиональный насос, работающий на глубине 20-24 метра.",
},
},
},
};

10
middlewere.ts Normal file
View File

@@ -0,0 +1,10 @@
import createMiddleware from 'next-intl/middleware';
export default createMiddleware({
locales: ['uz', 'ru'],
defaultLocale: 'uz'
});
export const config = {
matcher: ['/','/(de|en)/:path*']
};

8
next-intl.config.ts Normal file
View File

@@ -0,0 +1,8 @@
// next-intl.config.ts
import { IntlConfig } from "next-intl";
const nextIntlConfig: IntlConfig = {
locale: "uz", // JSON tarjimalar
};
export default nextIntlConfig;

View File

@@ -1,16 +1,7 @@
import type { NextConfig } from "next";
import createNextIntlPlugin from "next-intl/plugin";
import createNextIntlPlugin from 'next-intl/plugin';
const nextConfig = {};
const withNextIntl = createNextIntlPlugin();
const nextConfig: NextConfig = {
images: {
remotePatterns: [],
unoptimized: process.env.NODE_ENV === "development",
},
experimental: {
optimizePackageImports: ["@react-three/fiber", "@react-three/drei"],
},
};
export default withNextIntl(nextConfig);

899
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -10,18 +10,22 @@
},
"dependencies": {
"@hookform/resolvers": "^5.2.2",
"@radix-ui/react-dropdown-menu": "^2.1.16",
"@react-three/drei": "^10.7.7",
"@react-three/fiber": "^9.4.0",
"axios": "^1.13.2",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"framer-motion": "^12.23.24",
"i18next": "^25.6.3",
"lucide-react": "^0.554.0",
"next": "16.0.4",
"next-intl": "^4.5.5",
"next-i18next": "^15.4.2",
"next-intl": "^4.5.6",
"react": "19.2.0",
"react-dom": "19.2.0",
"react-hook-form": "^7.66.1",
"react-i18next": "^16.3.5",
"tailwind-merge": "^3.4.0",
"three": "^0.181.2",
"zod": "^4.1.13"

BIN
public/product/product1.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

BIN
public/product/product2.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 115 KiB

BIN
public/product/product3.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 307 KiB

View File

@@ -29,6 +29,6 @@
".next/types/**/*.ts",
".next/dev/types/**/*.ts",
"**/*.mts"
],
"exclude": ["node_modules"]
, "next.config.ts" ],
"exclude": ["node_modules",".next"]
}