Compare commits
21 Commits
be7b94f15e
...
003fa5ccce
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
003fa5ccce | ||
|
|
d684198ac4 | ||
|
|
b3cf0c2a49 | ||
|
|
6e41a836a7 | ||
|
|
cf26b778fc | ||
|
|
eec6004b25 | ||
|
|
d8173e1e76 | ||
|
|
6d6a25637a | ||
|
|
45fbeaf77d | ||
|
|
d636a92dee | ||
|
|
40e2b93f1d | ||
|
|
aa2260f212 | ||
|
|
ca8369cc31 | ||
|
|
65e6a248d1 | ||
|
|
e6a1ec8899 | ||
|
|
9609b82bb6 | ||
|
|
ece8c502d1 | ||
|
|
84d174a79d | ||
|
|
93cc3686f7 | ||
|
|
e5cb1be8a2 | ||
|
|
fe08ff7209 |
@@ -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;
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -3,6 +3,7 @@
|
||||
:root {
|
||||
--background: #ffffff;
|
||||
--foreground: #171717;
|
||||
--primary:#468965;
|
||||
}
|
||||
|
||||
@theme inline {
|
||||
@@ -10,6 +11,7 @@
|
||||
--color-foreground: var(--foreground);
|
||||
--font-sans: var(--font-geist-sans);
|
||||
--font-mono: var(--font-geist-mono);
|
||||
--color-primary:var(--primary)
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
|
||||
@@ -1,48 +1,20 @@
|
||||
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 "../i18n/request"; // i18n config faylini import qilamiz
|
||||
import { LanguageProvider } from "@/context/language-context";
|
||||
import "./globals.css";
|
||||
import { Navbar } from "@/components/Navbar";
|
||||
import { Footer } from "@/components/Footer";
|
||||
|
||||
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();
|
||||
|
||||
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>
|
||||
<Navbar />
|
||||
{children}
|
||||
</NextIntlClientProvider>
|
||||
<Footer />
|
||||
</LanguageProvider>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
|
||||
export default RootLayout;
|
||||
|
||||
79
app/page.tsx
79
app/page.tsx
@@ -1,65 +1,24 @@
|
||||
import Image from "next/image";
|
||||
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";
|
||||
|
||||
const HERO_IMAGES = [
|
||||
"/product/product.jpg",
|
||||
"/product/product1.jpg",
|
||||
"/product/product2.jpg",
|
||||
"/product/product3.jpg",
|
||||
];
|
||||
|
||||
export default function Home() {
|
||||
return (
|
||||
<div className="flex min-h-screen items-center justify-center bg-zinc-50 font-sans dark:bg-black">
|
||||
<main className="flex min-h-screen w-full max-w-3xl flex-col items-center justify-between py-32 px-16 bg-white dark:bg-black sm:items-start">
|
||||
<Image
|
||||
className="dark:invert"
|
||||
src="/next.svg"
|
||||
alt="Next.js logo"
|
||||
width={100}
|
||||
height={20}
|
||||
priority
|
||||
/>
|
||||
<div className="flex flex-col items-center gap-6 text-center sm:items-start sm:text-left">
|
||||
<h1 className="max-w-xs text-3xl font-semibold leading-10 tracking-tight text-black dark:text-zinc-50">
|
||||
To get started, edit the page.tsx file.
|
||||
</h1>
|
||||
<p className="max-w-md text-lg leading-8 text-zinc-600 dark:text-zinc-400">
|
||||
Looking for a starting point or more instructions? Head over to{" "}
|
||||
<a
|
||||
href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
||||
className="font-medium text-zinc-950 dark:text-zinc-50"
|
||||
>
|
||||
Templates
|
||||
</a>{" "}
|
||||
or the{" "}
|
||||
<a
|
||||
href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
||||
className="font-medium text-zinc-950 dark:text-zinc-50"
|
||||
>
|
||||
Learning
|
||||
</a>{" "}
|
||||
center.
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex flex-col gap-4 text-base font-medium sm:flex-row">
|
||||
<a
|
||||
className="flex h-12 w-full items-center justify-center gap-2 rounded-full bg-foreground px-5 text-background transition-colors hover:bg-[#383838] dark:hover:bg-[#ccc] md:w-[158px]"
|
||||
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<Image
|
||||
className="dark:invert"
|
||||
src="/vercel.svg"
|
||||
alt="Vercel logomark"
|
||||
width={16}
|
||||
height={16}
|
||||
/>
|
||||
Deploy Now
|
||||
</a>
|
||||
<a
|
||||
className="flex h-12 w-full items-center justify-center rounded-full border border-solid border-black/[.08] px-5 transition-colors hover:border-transparent hover:bg-black/[.04] dark:border-white/[.145] dark:hover:bg-[#1a1a1a] md:w-[158px]"
|
||||
href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Documentation
|
||||
</a>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
<main>
|
||||
<ShowCase images={HERO_IMAGES} />
|
||||
<About />
|
||||
<ProductsGrid />
|
||||
<FAQ />
|
||||
<ContactForm />
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
10
app/product/page.tsx
Normal file
10
app/product/page.tsx
Normal file
@@ -0,0 +1,10 @@
|
||||
import dynamic from "next/dynamic";
|
||||
const Products = dynamic(() => import("@/components/productsPage/products"));
|
||||
|
||||
export default function Page() {
|
||||
return (
|
||||
<div className="min-h-screen bg-white">
|
||||
<Products />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,16 +1,16 @@
|
||||
"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+ лет" },
|
||||
{ icon: Users, labelKey: "Experts", value: "50+" },
|
||||
{ icon: Zap, labelKey: "Reliability", value: "99.9%" },
|
||||
{ icon: Award, labelKey: "experiance", value: "10+ лет" },
|
||||
{ icon: Users, labelKey: "experts", value: "50+" },
|
||||
{ icon: Zap, labelKey: "truth", value: "99.9%" },
|
||||
];
|
||||
|
||||
const containerVariants = {
|
||||
@@ -37,9 +37,9 @@ 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" />
|
||||
<div className="w-20 h-1 bg-primary mx-auto rounded-full" />
|
||||
</motion.div>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-12 items-center">
|
||||
@@ -51,10 +51,10 @@ 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
|
||||
{/* <motion.div
|
||||
variants={containerVariants}
|
||||
initial="hidden"
|
||||
whileInView="visible"
|
||||
@@ -78,7 +78,7 @@ export function About() {
|
||||
</div>
|
||||
</motion.div>
|
||||
))}
|
||||
</motion.div>
|
||||
</motion.div> */}
|
||||
</motion.div>
|
||||
|
||||
{/* Right - Stats */}
|
||||
@@ -95,16 +95,16 @@ export function About() {
|
||||
<motion.div
|
||||
key={idx}
|
||||
whileHover={{ scale: 1.05, y: -5 }}
|
||||
className="bg-linear-to-br from-blue-50 to-blue-100 rounded-lg p-6 shadow-md hover:shadow-lg transition-shadow"
|
||||
className="bg-linear-to-r from-[#dae7e0] to-[#bffcdb] rounded-lg p-6 shadow-md hover:shadow-lg transition-shadow"
|
||||
>
|
||||
<div className="flex items-center gap-4 mb-2">
|
||||
<Icon className="text-blue-600" size={32} />
|
||||
<Icon className="text-primary" size={32} />
|
||||
<h3 className="text-2xl font-bold text-gray-900">
|
||||
{feature.value}
|
||||
</h3>
|
||||
</div>
|
||||
<p className="text-gray-700 font-medium">
|
||||
{feature.labelKey}
|
||||
{t.about[feature.labelKey]}
|
||||
</p>
|
||||
</motion.div>
|
||||
);
|
||||
|
||||
@@ -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,9 +82,9 @@ 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" />
|
||||
<div className="w-20 h-1 bg-primary mx-auto rounded-full" />
|
||||
</motion.div>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-12 items-center">
|
||||
@@ -98,11 +98,10 @@ export function ContactForm() {
|
||||
>
|
||||
<div>
|
||||
<h3 className="text-2xl font-bold text-gray-900 mb-2">
|
||||
Get In Touch
|
||||
{t.contact.title}
|
||||
</h3>
|
||||
<p className="text-gray-600 mb-8">
|
||||
Reach out to us for inquiries, support, or partnership
|
||||
opportunities.
|
||||
{t.contact.desc}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -110,18 +109,18 @@ export function ContactForm() {
|
||||
{[
|
||||
{
|
||||
icon: Phone,
|
||||
title: "Phone",
|
||||
value: "+998 (99) 123-45-67",
|
||||
title: "phone_title",
|
||||
value: "+998 (99) 869-74-70",
|
||||
},
|
||||
{
|
||||
icon: MessageSquare,
|
||||
title: "Telegram",
|
||||
title: "telegram_title",
|
||||
value: "@firma_support",
|
||||
},
|
||||
{
|
||||
icon: MapPin,
|
||||
title: "Address",
|
||||
value: "Tashkent, Uzbekistan",
|
||||
title: "addres_title",
|
||||
value: "Tashkent, Сергели 6 а 179 кв",
|
||||
},
|
||||
].map((item, idx) => {
|
||||
const Icon = item.icon;
|
||||
@@ -131,10 +130,10 @@ export function ContactForm() {
|
||||
whileHover={{ x: 5 }}
|
||||
className="flex gap-4"
|
||||
>
|
||||
<Icon className="text-blue-600 shrink-0" size={24} />
|
||||
<Icon className="text-primary shrink-0" size={24} />
|
||||
<div>
|
||||
<h4 className="font-semibold text-gray-900">
|
||||
{item.title}
|
||||
{t.contact[item.title]}
|
||||
</h4>
|
||||
<p className="text-gray-600">{item.value}</p>
|
||||
</div>
|
||||
@@ -155,14 +154,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 +169,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 +185,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 +200,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"
|
||||
@@ -237,9 +236,9 @@ export function ContactForm() {
|
||||
whileTap={{ scale: 0.95 }}
|
||||
type="submit"
|
||||
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"
|
||||
className="w-full px-6 py-3 bg-primary/80 text-white rounded-lg font-semibold hover:bg-primary hover:cursor-pointer transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
{loading ? "Sending..." : t("contact.send")}
|
||||
{loading ? "Sending..." : t.contact.send}
|
||||
</motion.button>
|
||||
</motion.form>
|
||||
</div>
|
||||
|
||||
@@ -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,9 +60,9 @@ 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" />
|
||||
<div className="w-20 h-1 bg-primary mx-auto rounded-full" />
|
||||
</motion.div>
|
||||
|
||||
{/* FAQ Items */}
|
||||
@@ -81,14 +81,14 @@ 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 }}
|
||||
transition={{ duration: 0.3 }}
|
||||
className="ml-4 shrink-0"
|
||||
>
|
||||
<ChevronDown className="text-blue-600" size={24} />
|
||||
<ChevronDown className="text-primary" size={24} />
|
||||
</motion.div>
|
||||
</div>
|
||||
</motion.button>
|
||||
@@ -102,9 +102,9 @@ export function FAQ({ items }: FaqProps) {
|
||||
transition={{ duration: 0.3 }}
|
||||
className="overflow-hidden"
|
||||
>
|
||||
<div className="bg-blue-50 p-6 rounded-b-lg border-t border-gray-200">
|
||||
<div className="bg-primary/20 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>
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
"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";
|
||||
import Image from "next/image";
|
||||
|
||||
export function Footer() {
|
||||
const t = useTranslations();
|
||||
|
||||
const { t } = useLanguage();
|
||||
const socialLinks = [
|
||||
{ icon: Facebook, href: "#", label: "Facebook" },
|
||||
{ icon: Linkedin, href: "#", label: "LinkedIn" },
|
||||
@@ -27,48 +27,54 @@ export function Footer() {
|
||||
};
|
||||
|
||||
return (
|
||||
<footer className="bg-gray-900 text-white">
|
||||
<footer className="bg-linear-to-br from-primary to-gray-800 text-white">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-16">
|
||||
<motion.div
|
||||
variants={containerVariants}
|
||||
initial="hidden"
|
||||
whileInView="visible"
|
||||
viewport={{ once: true }}
|
||||
className="grid grid-cols-1 md:grid-cols-4 gap-8 mb-12"
|
||||
className="grid grid-cols-1 md:grid-cols-4 max-md:justify-items-center gap-8 mb-12"
|
||||
>
|
||||
{/* Brand */}
|
||||
<motion.div variants={itemVariants}>
|
||||
<h3 className="text-2xl font-bold bg-linear-to-r from-blue-400 to-blue-600 bg-clip-text text-transparent mb-2">
|
||||
FIRMA
|
||||
<motion.div variants={itemVariants} className="max-md:flex flex-col items-center justify-center">
|
||||
<h3 className=" mb-2">
|
||||
<Image
|
||||
src="/logo.jpg"
|
||||
alt="image"
|
||||
width={80}
|
||||
height={50}
|
||||
className="rounded-xl object-cover"
|
||||
/>
|
||||
</h3>
|
||||
<p className="text-gray-400 text-sm">
|
||||
Premium industrial pumps and equipment.
|
||||
{t.footer.common.premium_title}
|
||||
</p>
|
||||
</motion.div>
|
||||
|
||||
{/* Quick Links */}
|
||||
<motion.div variants={itemVariants}>
|
||||
<h4 className="font-semibold mb-4">Quick Links</h4>
|
||||
<ul className="space-y-2 text-gray-400 text-sm">
|
||||
<h4 className="font-semibold mb-4 max-md:text-center">{t.footer.common.quickLinks}</h4>
|
||||
<ul className="space-y-2 text-gray-400 text-sm max-md:text-center">
|
||||
<li>
|
||||
<a href="#about" className="hover:text-white transition-colors">
|
||||
About Us
|
||||
<a href="#about" className="hover:text-white transition-colors text-[15px] ">
|
||||
{t.footer.common.aboutUs}
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
href="#products"
|
||||
className="hover:text-white transition-colors"
|
||||
className="hover:text-white text-[15px] transition-colors"
|
||||
>
|
||||
Products
|
||||
{t.footer.common.products}
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
href="#contact"
|
||||
className="hover:text-white transition-colors"
|
||||
className="hover:text-white text-[15px] transition-colors"
|
||||
>
|
||||
Contact
|
||||
{t.footer.common.contact}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
@@ -76,17 +82,17 @@ export function Footer() {
|
||||
|
||||
{/* Contact Info */}
|
||||
<motion.div variants={itemVariants}>
|
||||
<h4 className="font-semibold mb-4">Contact</h4>
|
||||
<ul className="space-y-2 text-gray-400 text-sm">
|
||||
<li>Email: info@firma.uz</li>
|
||||
<li>Phone: +998 (99) 123-45-67</li>
|
||||
<li>Telegram: @firma_support</li>
|
||||
<h4 className="font-semibold mb-4 max-md:text-center">{t.footer.common.contact}</h4>
|
||||
<ul className="space-y-2 text-gray-400 text-sm max-md:text-center">
|
||||
<li className="text-[15px]">{t.footer.common.email}: info@firma.uz</li>
|
||||
<li className="text-[15px]">{t.footer.common.phone}: +998 (99) 869-74-70</li>
|
||||
<li className="text-[15px]">{t.footer.common.telegram}: @firma_support</li>
|
||||
</ul>
|
||||
</motion.div>
|
||||
|
||||
{/* 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;
|
||||
@@ -96,7 +102,7 @@ export function Footer() {
|
||||
href={link.href}
|
||||
whileHover={{ scale: 1.2 }}
|
||||
whileTap={{ scale: 0.95 }}
|
||||
className="p-2 bg-gray-800 rounded-lg hover:bg-blue-600 transition-colors"
|
||||
className="p-2 bg-gray-800 rounded-lg hover:bg-primary transition-colors"
|
||||
aria-label={link.label}
|
||||
>
|
||||
<Icon size={20} />
|
||||
@@ -116,7 +122,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>
|
||||
|
||||
@@ -2,10 +2,11 @@
|
||||
|
||||
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";
|
||||
import Image from "next/image";
|
||||
|
||||
interface NavLink {
|
||||
id: string;
|
||||
@@ -19,20 +20,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);
|
||||
@@ -44,16 +40,16 @@ export function Navbar({ logoText = "FIRMA" }: NavbarProps) {
|
||||
};
|
||||
|
||||
return (
|
||||
<nav className="sticky top-0 z-50 bg-white/80 backdrop-blur-md border-b border-gray-200">
|
||||
<nav className="sticky top-0 z-50 bg-white/80 backdrop-blur-md p-2 overflow-hidden border-b border-gray-200">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="flex justify-between items-center h-16">
|
||||
{/* Logo */}
|
||||
<motion.div whileHover={{ scale: 1.05 }} whileTap={{ scale: 0.95 }}>
|
||||
<Link
|
||||
href={`/${locale}`}
|
||||
className="text-2xl font-bold bg-linear-to-r from-blue-600 to-blue-800 bg-clip-text text-transparent"
|
||||
href={`/`}
|
||||
className=" relative overflow-hidden"
|
||||
>
|
||||
{logoText}
|
||||
<Image src='/logo.jpg' alt="image" width={80} height={50} className="rounded-xl object-cover" />
|
||||
</Link>
|
||||
</motion.div>
|
||||
|
||||
@@ -64,23 +60,16 @@ export function Navbar({ logoText = "FIRMA" }: NavbarProps) {
|
||||
key={link.id}
|
||||
whileHover={{ color: "#2563eb" }}
|
||||
onClick={() => handleScroll(link.href)}
|
||||
className="text-gray-700 hover:text-blue-600 transition-colors"
|
||||
className="text-[#468965] hover:text-[#468965] transition-colors hover:cursor-pointer"
|
||||
>
|
||||
{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
|
||||
@@ -104,9 +93,9 @@ export function Navbar({ logoText = "FIRMA" }: NavbarProps) {
|
||||
<button
|
||||
key={link.id}
|
||||
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"
|
||||
className="block w-full text-left px-4 py-2 text-gray-700 hover:bg-primary rounded-lg transition-colors"
|
||||
>
|
||||
{t(link.labelKey)}
|
||||
{link.labelKey}
|
||||
</button>
|
||||
))}
|
||||
</motion.div>
|
||||
|
||||
@@ -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,9 +22,9 @@ 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"
|
||||
className="object-contain group-hover:scale-110 transition-transform duration-300"
|
||||
/>
|
||||
<div className="absolute inset-0 bg-black/0 group-hover:bg-black/30 transition-colors" />
|
||||
</div>
|
||||
@@ -33,30 +32,30 @@ 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 */}
|
||||
<div className="mb-4 space-y-2">
|
||||
{/* <div className="mb-4 space-y-2">
|
||||
{product.specs.slice(0, 2).map((spec, idx) => (
|
||||
<div key={idx} className="flex justify-between text-sm">
|
||||
<span className="text-gray-600">{spec.key}:</span>
|
||||
<span className="font-semibold text-gray-900">{spec.value}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div> */}
|
||||
|
||||
{/* CTA Button */}
|
||||
<motion.button
|
||||
whileHover={{ scale: 1.05 }}
|
||||
whileTap={{ scale: 0.95 }}
|
||||
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"
|
||||
className="w-full flex items-center justify-center gap-2 px-4 py-2 bg-primary/80 text-white rounded-lg font-medium hover:bg-primary transition-colors"
|
||||
>
|
||||
{t("products.viewDetails")}
|
||||
{t.details}
|
||||
<ExternalLink size={16} />
|
||||
</motion.button>
|
||||
</div>
|
||||
|
||||
@@ -2,10 +2,11 @@
|
||||
|
||||
import { motion, AnimatePresence } from "framer-motion";
|
||||
import { X, Download } from "lucide-react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import Image from "next/image";
|
||||
import { ProductViewer } from "./ProductViewer";
|
||||
import type { Product } from "@/lib/products";
|
||||
import { useLanguage } from "@/context/language-context";
|
||||
import Link from "next/link";
|
||||
|
||||
interface ProductModalProps {
|
||||
product: Product;
|
||||
@@ -13,7 +14,7 @@ interface ProductModalProps {
|
||||
}
|
||||
|
||||
export function ProductModal({ product, onClose }: ProductModalProps) {
|
||||
const t = useTranslations();
|
||||
const { t } = useLanguage();
|
||||
|
||||
return (
|
||||
<AnimatePresence>
|
||||
@@ -34,7 +35,7 @@ export function ProductModal({ product, onClose }: ProductModalProps) {
|
||||
{/* Header */}
|
||||
<div className="sticky top-0 bg-white border-b border-gray-200 p-6 flex justify-between items-center">
|
||||
<h2 className="text-2xl font-bold text-gray-900">
|
||||
{t(product.nameKey)}
|
||||
{product.nameKey}
|
||||
</h2>
|
||||
<motion.button
|
||||
whileHover={{ scale: 1.1 }}
|
||||
@@ -56,34 +57,14 @@ export function ProductModal({ product, onClose }: ProductModalProps) {
|
||||
images={product.images}
|
||||
autoRotate={true}
|
||||
/>
|
||||
|
||||
{/* Image Thumbnails */}
|
||||
{product.images.length > 1 && (
|
||||
<div className="mt-4 grid grid-cols-4 gap-2">
|
||||
{product.images.map((img, idx) => (
|
||||
<motion.div
|
||||
key={idx}
|
||||
whileHover={{ scale: 1.05 }}
|
||||
className="relative h-20 rounded cursor-pointer overflow-hidden bg-gray-100"
|
||||
>
|
||||
<Image
|
||||
src={img}
|
||||
alt={`${t(product.nameKey)} ${idx + 1}`}
|
||||
fill
|
||||
className="object-cover"
|
||||
/>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Details */}
|
||||
<div>
|
||||
<p className="text-gray-600 mb-6">
|
||||
{product.longDescriptionKey
|
||||
? t(product.longDescriptionKey)
|
||||
: t(product.shortDescriptionKey)}
|
||||
? product.longDescriptionKey
|
||||
: product.shortDescriptionKey}
|
||||
</p>
|
||||
|
||||
{/* Specifications */}
|
||||
@@ -108,21 +89,16 @@ export function ProductModal({ product, onClose }: ProductModalProps) {
|
||||
|
||||
{/* CTA Buttons */}
|
||||
<div className="space-y-3">
|
||||
<motion.button
|
||||
whileHover={{ scale: 1.05 }}
|
||||
whileTap={{ scale: 0.95 }}
|
||||
className="w-full px-6 py-3 bg-blue-600 text-white rounded-lg font-semibold hover:bg-blue-700 transition-colors"
|
||||
>
|
||||
{t("contact.send")}
|
||||
</motion.button>
|
||||
<motion.button
|
||||
whileHover={{ scale: 1.05 }}
|
||||
whileTap={{ scale: 0.95 }}
|
||||
className="w-full px-6 py-3 border border-blue-600 text-blue-600 rounded-lg font-semibold hover:bg-blue-50 transition-colors flex items-center justify-center gap-2"
|
||||
>
|
||||
<Download size={18} />
|
||||
Download Datasheet
|
||||
</motion.button>
|
||||
<Link href="#contact">
|
||||
<motion.button
|
||||
onClick={onClose}
|
||||
whileHover={{ scale: 1.05 }}
|
||||
whileTap={{ scale: 0.95 }}
|
||||
className="w-full px-6 py-3 bg-blue-600 text-white rounded-lg font-semibold hover:bg-blue-700 transition-colors"
|
||||
>
|
||||
{t.contact.send}
|
||||
</motion.button>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -57,7 +57,7 @@ export function ProductViewer({
|
||||
src={primaryImage}
|
||||
alt="Product"
|
||||
fill
|
||||
className="object-cover"
|
||||
className="object-contain"
|
||||
/>
|
||||
</motion.div>
|
||||
) : (
|
||||
|
||||
@@ -2,17 +2,19 @@
|
||||
|
||||
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";
|
||||
import Link from "next/link";
|
||||
import { ChevronsRight } from "lucide-react";
|
||||
|
||||
// hello everyone
|
||||
|
||||
export function ProductsGrid() {
|
||||
const t = useTranslations();
|
||||
const { t } = useLanguage();
|
||||
const products = getAllProducts();
|
||||
const [selectedProduct, setSelectedProduct] = useState<Product | null>(null);
|
||||
|
||||
@@ -47,7 +49,7 @@ export function ProductsGrid() {
|
||||
className="object-cover"
|
||||
/>
|
||||
</div>
|
||||
<div className="absolute w-full h-full top-0 left-0 bg-black opacity-25 -z-40"/>
|
||||
<div className="absolute w-full h-full top-0 left-0 bg-black opacity-25 -z-40" />
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
{/* Header */}
|
||||
<motion.div
|
||||
@@ -58,9 +60,9 @@ 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" />
|
||||
<div className="w-20 h-1 bg-primary mx-auto rounded-full" />
|
||||
</motion.div>
|
||||
|
||||
{/* Product Grid */}
|
||||
@@ -81,6 +83,14 @@ export function ProductsGrid() {
|
||||
))}
|
||||
</motion.div>
|
||||
</div>
|
||||
<div className="mt-10 w-full flex items-center justify-center">
|
||||
<Link
|
||||
href="/product"
|
||||
className="text-primary flex items-center gap-2 text-[18px] hover:bg-primary hover:text-white py-2 px-6 rounded-lg bg-[#ffffffb5] border mx-auto border-white"
|
||||
>
|
||||
{t.more} <ChevronsRight />
|
||||
</Link>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Product Modal */}
|
||||
|
||||
@@ -4,22 +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);
|
||||
|
||||
@@ -51,7 +45,8 @@ export function ShowCase({
|
||||
};
|
||||
|
||||
return (
|
||||
<section className="relative min-h-screen pt-20 pb-20">
|
||||
<section className="relative min-h-screen flex items-center py-20">
|
||||
{/* background image */}
|
||||
<div className="absolute -z-50 top-0 left-0 h-full w-full">
|
||||
<Image
|
||||
src="/images/hero1.jpg"
|
||||
@@ -61,8 +56,9 @@ export function ShowCase({
|
||||
/>
|
||||
</div>
|
||||
<div className="absolute w-full h-full top-0 left-0 bg-black opacity-25 -z-40" />
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-12 items-center">
|
||||
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 w-full h-full flex flex-col justify-center ">
|
||||
<div className="flex flex-1 max-w-xl w-full">
|
||||
{/* Left Content */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, x: -50 }}
|
||||
@@ -71,33 +67,30 @@ 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 }}
|
||||
whileTap={{ scale: 0.95 }}
|
||||
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"
|
||||
className="px-8 py-3 bg-linear-to-r from-primary to-[#7eac93] text-white rounded-lg font-semibold hover:shadow-lg transition-shadow"
|
||||
>
|
||||
{t(ctaLabelKey)}
|
||||
{t.hero.cta}
|
||||
</motion.button>
|
||||
</motion.div>
|
||||
|
||||
{/* Right - Image Carousel */}
|
||||
<motion.div
|
||||
{/* <motion.div
|
||||
initial={{ opacity: 0, x: 50 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
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}
|
||||
@@ -111,15 +104,13 @@ export function ShowCase({
|
||||
src={images[currentImageIndex]}
|
||||
alt={`Pump ${currentImageIndex + 1}`}
|
||||
fill
|
||||
className="object-cover"
|
||||
className="object-contain"
|
||||
priority={currentImageIndex === 0}
|
||||
onMouseEnter={() => setAutoPlay(false)}
|
||||
onMouseLeave={() => setAutoPlay(true)}
|
||||
/>
|
||||
</motion.div>
|
||||
</AnimatePresence>
|
||||
|
||||
{/* Navigation Buttons */}
|
||||
<motion.button
|
||||
whileHover={{ scale: 1.1 }}
|
||||
whileTap={{ scale: 0.95 }}
|
||||
@@ -137,8 +128,6 @@ export function ShowCase({
|
||||
>
|
||||
<ChevronRight className="text-gray-800" />
|
||||
</motion.button>
|
||||
|
||||
{/* Indicators */}
|
||||
<div className="absolute bottom-4 left-1/2 -translate-x-1/2 z-10 flex gap-2">
|
||||
{images.map((_, idx) => (
|
||||
<motion.button
|
||||
@@ -157,7 +146,7 @@ export function ShowCase({
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
</motion.div> */}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
45
components/languageSwitcher.tsx
Normal file
45
components/languageSwitcher.tsx
Normal 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 p-1 border-white border text-black 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>
|
||||
);
|
||||
}
|
||||
70
components/productsPage/emptyData.tsx
Normal file
70
components/productsPage/emptyData.tsx
Normal file
@@ -0,0 +1,70 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import { motion } from "framer-motion";
|
||||
import { FileSearch } from "lucide-react";
|
||||
import { useLanguage } from "@/context/language-context";
|
||||
|
||||
//salomalr
|
||||
|
||||
export default function EmptyState() {
|
||||
const { t } = useLanguage();
|
||||
|
||||
const container = {
|
||||
hidden: { opacity: 0, y: 8 },
|
||||
show: { opacity: 1, y: 0, transition: { staggerChildren: 0.06 } },
|
||||
};
|
||||
|
||||
return (
|
||||
<motion.section
|
||||
aria-labelledby="empty-state-title"
|
||||
className="w-full px-4 py-16 flex items-center justify-center"
|
||||
initial="hidden"
|
||||
animate="show"
|
||||
variants={container}
|
||||
>
|
||||
<div className="max-w-5xl w-full bg-white/60 bg-linear-to-br from-primary to-gray-800 backdrop-blur-md rounded-2xl shadow-lg overflow-hidden">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 items-center p-8 md:p-12">
|
||||
{/* Illustration / Icon */}
|
||||
<motion.div className="flex items-center justify-center">
|
||||
<motion.div
|
||||
className="p-6 bg-linear-to-tr from-blue-50 to-blue-100 rounded-xl shadow-inner"
|
||||
initial={{ scale: 0.96 }}
|
||||
animate={{ scale: [0.96, 1.02, 0.98, 1] }}
|
||||
transition={{
|
||||
duration: 4,
|
||||
repeat: Infinity,
|
||||
repeatType: "mirror",
|
||||
}}
|
||||
>
|
||||
<FileSearch className="w-28 h-28 text-primary" />
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
|
||||
{/* Content */}
|
||||
<motion.div className="text-center md:text-left">
|
||||
<h3
|
||||
id="empty-state-title"
|
||||
className="text-2xl sm:text-3xl font-semibold text-gray-900 dark:text-gray-100"
|
||||
>
|
||||
{t.empty_data.title || "Hech narsa topilmadi"}
|
||||
</h3>
|
||||
|
||||
<p className="mt-3 text-sm sm:text-base text-gray-600 dark:text-gray-300 max-w-xl mx-auto md:mx-0">
|
||||
{t.empty_data.description}
|
||||
</p>
|
||||
|
||||
<div className="mt-6 flex flex-col sm:flex-row items-center sm:items-start gap-3 md:gap-4 justify-center md:justify-start">
|
||||
<motion.a
|
||||
className="inline-flex items-center justify-center px-5 py-2.5 bg-primary/70 hover:bg-primary text-white rounded-lg shadow-md transition-colors text-sm font-medium"
|
||||
// @ts-ignore allow Link-like anchor
|
||||
>
|
||||
<Link href="/">{t.empty_data.back || "Bosh sahifa"}</Link>
|
||||
</motion.a>
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
</div>
|
||||
</motion.section>
|
||||
);
|
||||
}
|
||||
55
components/productsPage/products.tsx
Normal file
55
components/productsPage/products.tsx
Normal file
@@ -0,0 +1,55 @@
|
||||
"use client";
|
||||
import { useEffect, useState } from "react";
|
||||
import EmptyState from "./emptyData";
|
||||
import { GET } from "@/lib/allProducts";
|
||||
import { ProductModal } from "../ProductModal";
|
||||
import { motion } from "framer-motion";
|
||||
import { ProductCard } from "../ProductCard";
|
||||
|
||||
const itemVariants = {
|
||||
hidden: { opacity: 0, y: 20 },
|
||||
visible: { opacity: 1, y: 0 },
|
||||
};
|
||||
|
||||
export default function Products() {
|
||||
const [allProducts, setAllProducts] = useState<any>(null);
|
||||
const [selectedProduct, setSelectedProduct] = useState<any>(null);
|
||||
useEffect(() => {
|
||||
const all = GET();
|
||||
all && Array.isArray(all) && all.length > 0
|
||||
? setAllProducts(all)
|
||||
: setAllProducts([]);
|
||||
setAllProducts;
|
||||
}, []);
|
||||
const handleViewDetails = (slug: string) => {
|
||||
const product = allProducts.find((p: any) => p.slug === slug);
|
||||
if (product) {
|
||||
setSelectedProduct(product);
|
||||
}
|
||||
};
|
||||
return (
|
||||
<div className="">
|
||||
{allProducts && allProducts.length > 0 ? (
|
||||
<div className="grid lg:grid-cols-3 sm:grid-cols-2 grid-cols-1 gap-4">
|
||||
{allProducts.map((product: any) => (
|
||||
<motion.div key={product.id} variants={itemVariants}>
|
||||
<ProductCard
|
||||
product={product}
|
||||
onViewDetails={handleViewDetails}
|
||||
/>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<EmptyState />
|
||||
)}
|
||||
{/* Product Modal */}
|
||||
{selectedProduct && (
|
||||
<ProductModal
|
||||
product={selectedProduct}
|
||||
onClose={() => setSelectedProduct(null)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
257
components/ui/dropdown-menu.tsx
Normal file
257
components/ui/dropdown-menu.tsx
Normal 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,
|
||||
}
|
||||
32
context/language-context.tsx
Normal file
32
context/language-context.tsx
Normal 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: (any)[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
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
export type Locale = "uz" | "ru";
|
||||
|
||||
export const locales: Locale[] = ["uz", "ru"];
|
||||
export const defaultLocale: Locale = "uz";
|
||||
@@ -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 bo‘lsa, 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
|
||||
};
|
||||
});
|
||||
|
||||
14
lib/allProducts.ts
Normal file
14
lib/allProducts.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import axios from "axios";
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
const res = await axios.get("https://api.serenmebel.uz/api/products/");
|
||||
console.log("all products res: ", res?.data);
|
||||
|
||||
return Response.json(res.data);
|
||||
} catch (error: any) {
|
||||
console.log("all products error: ", error);
|
||||
|
||||
return Response.json({ error: error.message });
|
||||
}
|
||||
}
|
||||
23
lib/productZustand.ts
Normal file
23
lib/productZustand.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { create } from "zustand";
|
||||
import { devtools, persist } from "zustand/middleware";
|
||||
|
||||
interface ProductStore {
|
||||
productName: string;
|
||||
setProductName: (name: string) => void;
|
||||
resetProductName: () => void;
|
||||
}
|
||||
|
||||
export const useProductStore = create<ProductStore>()(
|
||||
devtools(
|
||||
persist(
|
||||
(set) => ({
|
||||
productName: "",
|
||||
setProductName: (name: string) => set({ productName: name }),
|
||||
resetProductName: () => set({ productName: "" }),
|
||||
}),
|
||||
{
|
||||
name: "product-storage",
|
||||
}
|
||||
)
|
||||
)
|
||||
);
|
||||
@@ -13,11 +13,10 @@ 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",
|
||||
images: ["/images/pump-1.jpg", "/images/pump-1-alt.jpg"],
|
||||
nameKey: "Schotchik Nasos",
|
||||
slug: "Yuqori sifatli schotchik nasos, benzin, dizel va kerosinni tashishda ishlatiladi.",
|
||||
shortDescriptionKey: "Xavfsiz neft mahsulotlarini tashish uchun",
|
||||
images: ["/product/product.jpg", "/images/pump-1-alt.jpg"],
|
||||
specs: [
|
||||
{ key: "Flow Rate", value: "100 L/min" },
|
||||
{ key: "Pressure", value: "10 bar" },
|
||||
@@ -27,11 +26,10 @@ 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",
|
||||
images: ["/images/pump-2.jpg", "/images/pump-2-alt.jpg"],
|
||||
nameKey: "Agregat Nasos",
|
||||
slug: "Katta volumli neft mahsulotlarini tashishda o'rnatilgan.",
|
||||
shortDescriptionKey: "Kuchli va ishonchli aggregat nasos",
|
||||
images: ["/product/product1.jpg", "/images/pump-2-alt.jpg"],
|
||||
specs: [
|
||||
{ key: "Flow Rate", value: "250 L/min" },
|
||||
{ key: "Pressure", value: "15 bar" },
|
||||
@@ -41,11 +39,10 @@ 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",
|
||||
images: ["/images/pump-3.jpg", "/images/pump-3-alt.jpg"],
|
||||
nameKey: "СЦЛ 20/24",
|
||||
slug:"Chuqurligi 20-24 metrda ishlaydigan professional nasos.",
|
||||
shortDescriptionKey: "Professional kalibrli nasos",
|
||||
images: ["/product/product2.jpg", "/images/pump-3-alt.jpg"],
|
||||
specs: [
|
||||
{ key: "Depth Rating", value: "20-24 m" },
|
||||
{ key: "Flow Rate", value: "150 L/min" },
|
||||
|
||||
199
lib/translations.ts
Normal file
199
lib/translations.ts
Normal file
@@ -0,0 +1,199 @@
|
||||
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.",
|
||||
experiance: "Tajriba",
|
||||
experts: "Mutaxasislar",
|
||||
truth: "Ishonchlilik",
|
||||
},
|
||||
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",
|
||||
desc: "Savollar, qo'llab-quvvatlash yoki hamkorlik imkoniyatlari uchun biz bilan bog'laning.",
|
||||
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)",
|
||||
phone_title: "Telefon",
|
||||
telegram_title: "Telegram",
|
||||
addres_title: "Manzil",
|
||||
},
|
||||
footer: {
|
||||
copyright: "© 2025 Firma. Barcha huquqlar himoyalangan.",
|
||||
followUs: "Bizni kuzatib turing",
|
||||
common: {
|
||||
premium_title: "Premium sanoat nasoslari va uskunalari",
|
||||
quickLinks: "Tezkor havolalar",
|
||||
aboutUs: "Biz haqimizda",
|
||||
products: "Mahsulotlar",
|
||||
contact: "Bog'lanish",
|
||||
email: "Email",
|
||||
phone: "Telefon",
|
||||
telegram: "Telegram",
|
||||
},
|
||||
},
|
||||
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.",
|
||||
},
|
||||
},
|
||||
more: "Ko'proq ko'rish",
|
||||
details: "Batafsil",
|
||||
empty_data:{
|
||||
description:"Mahsulot topilmadi!!!",
|
||||
back:"Asosiy sahifaga qaytish"
|
||||
}
|
||||
},
|
||||
|
||||
ru: {
|
||||
nav: {
|
||||
about: "О нас",
|
||||
products: "Продукты",
|
||||
faq: "FAQ",
|
||||
contact: "Контакт",
|
||||
},
|
||||
hero: {
|
||||
title: "Поставщик промышленного оборудования и насосов",
|
||||
subtitle: "Качественная продукция и услуги с 10+ летним опытом",
|
||||
cta: "Свяжитесь с нами",
|
||||
},
|
||||
about: {
|
||||
title: "О нас",
|
||||
content:
|
||||
"Наша компания имеет 10+ лет опыта в поставке промышленных насосов и измерительного оборудования. Каждый продукт протестирован, качество гарантировано и сопровождается технической поддержкой. Мы предлагаем нашим клиентам полный сервис: техническую консультацию, быструю доставку и установку. Наши специализированные насосы (счетчик, агрегатный насос, СЦЛ 20/24 и др.) используются для безопасной и эффективной транспортировки бензина, дизеля, керосина и других легких нефтепродуктов.",
|
||||
experiance: "Опыт",
|
||||
experts: "Эксперты",
|
||||
truth: "Надёжность",
|
||||
},
|
||||
products: {
|
||||
title: "Продукты",
|
||||
viewDetails: "Подробнее",
|
||||
},
|
||||
faq: {
|
||||
title: "Часто Задаваемые Вопросы",
|
||||
items: [
|
||||
{
|
||||
question: "Гарантия на продукты?",
|
||||
answer:
|
||||
"Да, все наше оборудование поставляется с 12-месячной технической гарантией.",
|
||||
},
|
||||
{
|
||||
question: "Сколько времени займет доставка?",
|
||||
answer:
|
||||
"Обычно 3-14 рабочих дней, в зависимости от наличия и адреса доставки.",
|
||||
},
|
||||
{
|
||||
question: "Есть ли техническая поддержка?",
|
||||
answer:
|
||||
"Да, техническая консультация доступна 24/7 по телефону и Telegram.",
|
||||
},
|
||||
],
|
||||
},
|
||||
contact: {
|
||||
title: "Свяжитесь с нами",
|
||||
desc: "Для вопросов, поддержки или возможностей сотрудничества свяжитесь с нами.",
|
||||
name: "Имя",
|
||||
phone: "Номер телефона",
|
||||
message: "Сообщение",
|
||||
product: "Продукт (опционально)",
|
||||
send: "Отправить",
|
||||
success: "Сообщение успешно отправлено!",
|
||||
error: "Ошибка: не удалось отправить сообщение.",
|
||||
namePlaceholder: "Ваше имя",
|
||||
phonePlaceholder: "+998 XX XXX XX XX",
|
||||
messagePlaceholder: "Ваше сообщение (опционально)",
|
||||
phone_title: "Телефон",
|
||||
telegram_title: "Телеграм",
|
||||
addres_title: "Адрес",
|
||||
},
|
||||
footer: {
|
||||
copyright: "© 2025 Firma. Все права защищены.",
|
||||
followUs: "Следите за нами",
|
||||
common: {
|
||||
premium_title: "Премиальные промышленные насосы и оборудование",
|
||||
quickLinks: "Быстрые ссылки",
|
||||
aboutUs: "О нас",
|
||||
products: "Продукты",
|
||||
contact: "Контакт",
|
||||
email: "Email",
|
||||
phone: "Телефон",
|
||||
telegram: "Телеграм",
|
||||
},
|
||||
},
|
||||
products_list: {
|
||||
pump_1: {
|
||||
name: "Счетчик Насос",
|
||||
shortDescription: "Для безопасной транспортировки нефтепродуктов",
|
||||
description:
|
||||
"Высококачественный счетчиковый насос, используется для транспортировки бензина, дизеля и керосина.",
|
||||
},
|
||||
pump_2: {
|
||||
name: "Агрегатный Насос",
|
||||
shortDescription: "Мощный и надежный агрегатный насос",
|
||||
description:
|
||||
"Установлен для транспортировки больших объемов нефтепродуктов.",
|
||||
},
|
||||
pump_3: {
|
||||
name: "СЦЛ 20/24",
|
||||
shortDescription: "Профессиональный калиброванный насос",
|
||||
description:
|
||||
"Профессиональный насос, работающий на глубине 20-24 метра.",
|
||||
},
|
||||
},
|
||||
more: "Смотреть больше",
|
||||
details: "Подробнее",
|
||||
},
|
||||
};
|
||||
@@ -37,6 +37,7 @@
|
||||
},
|
||||
"contact": {
|
||||
"title": "Свяжитесь с нами",
|
||||
"desc": "Для вопросов, поддержки или возможностей сотрудничества свяжитесь с нами.",
|
||||
"name": "Имя",
|
||||
"phone": "Номер телефона",
|
||||
"message": "Сообщение",
|
||||
|
||||
@@ -37,6 +37,7 @@
|
||||
},
|
||||
"contact": {
|
||||
"title": "Bog'lanish",
|
||||
"desc": "Savollar, qo'llab-quvvatlash yoki hamkorlik imkoniyatlari uchun biz bilan bog'laning.",
|
||||
"name": "Ism",
|
||||
"phone": "Telefon raqami",
|
||||
"message": "Xabar",
|
||||
|
||||
8
next-intl.config.ts
Normal file
8
next-intl.config.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
// next-intl.config.ts
|
||||
import { IntlConfig } from "next-intl";
|
||||
|
||||
const nextIntlConfig: IntlConfig = {
|
||||
locale: "uz", // JSON tarjimalar
|
||||
};
|
||||
|
||||
export default nextIntlConfig;
|
||||
@@ -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);
|
||||
986
package-lock.json
generated
986
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -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": "^16.0.8",
|
||||
"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"
|
||||
@@ -31,6 +35,7 @@
|
||||
"@types/node": "^20",
|
||||
"@types/react": "^19",
|
||||
"@types/react-dom": "^19",
|
||||
"baseline-browser-mapping": "^2.9.5",
|
||||
"eslint": "^9",
|
||||
"eslint-config-next": "16.0.4",
|
||||
"tailwindcss": "^4",
|
||||
|
||||
BIN
public/logo.jpg
Normal file
BIN
public/logo.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 178 KiB |
BIN
public/product/product1.jpg
Normal file
BIN
public/product/product1.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 32 KiB |
BIN
public/product/product2.jpg
Normal file
BIN
public/product/product2.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 115 KiB |
BIN
public/product/product3.jpg
Normal file
BIN
public/product/product3.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 307 KiB |
@@ -29,6 +29,6 @@
|
||||
".next/types/**/*.ts",
|
||||
".next/dev/types/**/*.ts",
|
||||
"**/*.mts"
|
||||
],
|
||||
"exclude": ["node_modules"]
|
||||
, "next.config.ts" ],
|
||||
"exclude": ["node_modules",".next"]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user