Compare commits

...

13 Commits

Author SHA1 Message Date
nabijonovdavronbek619@gmail.com
8c5499dac1 faq sectin connected to backedn 2026-01-14 19:24:19 +05:00
nabijonovdavronbek619@gmail.com
e94b7678f3 for github 2026-01-13 11:15:53 +05:00
nabijonovdavronbek619@gmail.com
9e9a2f79c4 fix: sitemap changed product from detail 2026-01-13 11:13:51 +05:00
nabijonovdavronbek619@gmail.com
3de1299af8 updated openGraf 2026-01-12 10:36:15 +05:00
nabijonovdavronbek619@gmail.com
f840063827 for gitea 2026-01-09 18:29:51 +05:00
nabijonovdavronbek619@gmail.com
cd50a10539 sitemap added 2026-01-09 18:28:22 +05:00
nabijonovdavronbek619@gmail.com
c2c39d44a0 ceo optimization 2026-01-09 17:38:01 +05:00
nabijonovdavronbek619@gmail.com
a700fdddc6 product detail page responsive updated 2026-01-09 14:50:11 +05:00
nabijonovdavronbek619@gmail.com
5bb9a566ca products page updated for loading 2026-01-09 14:30:30 +05:00
nabijonovdavronbek619@gmail.com
2f3f01f1fc for github deployment 2026-01-09 14:21:01 +05:00
nabijonovdavronbek619@gmail.com
67861ad5c8 favicon.ico file added 2026-01-09 14:19:10 +05:00
nabijonovdavronbek619@gmail.com
733a1a5fc8 detail page updated , product name zustand clear 2026-01-09 11:53:10 +05:00
nabijonovdavronbek619@gmail.com
e53d40bd61 detail page added 2026-01-09 11:39:58 +05:00
22 changed files with 607 additions and 448 deletions

294
README.md
View File

@@ -1,242 +1,184 @@
# FIRMA - Industrial Equipment PortfolioThis is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app). # FIRMA - Industrial Equipment & Pump Supplier Website
A modern, responsive Next.js portfolio website for an industrial equipment and pump supplier company. Built with TypeScript, TailwindCSS, Framer Motion, Three.js/R3F, and internationalization support (Uzbek/Russian).## Getting Started A modern, responsive Next.js portfolio website for **Max Di Group** - a company specializing in industrial equipment, pumps, sensors, and technical devices. The website showcases products with 3D visualizations and provides comprehensive information in both Uzbek and Russian languages.
## 🚀 FeaturesFirst, run the development server: ## 🌟 Project Overview
- **Multi-language Support**: Uzbek (uz) and Russian (ru) with `next-intl````bash This project is a professional business website for a company that supplies industrial equipment including:
- **Modern UI/UX**: Built with TailwindCSS and Framer Motion animationsnpm run dev - Pumps and pump aggregates (ASVN-80, SCL 20/24, KM-100, SCN 75/70)
- Liquid flow meters and counters (PPV-100, PPO series)
- Fuel dispensing equipment and modules (MZ-35)
- Safety valves and breathing apparatus (KDM series)
- Filtration systems (FJU series)
- Loading/unloading equipment for rail and road tankers
- **3D Product Visualization**: Three.js/React Three Fiber for GLB/GLTF models# or The website features:
- **Responsive Design**: Mobile-first, fully responsive across all devicesyarn dev - **3D Product Visualization**: Interactive 3D models using Three.js
- **Multi-language Support**: Full internationalization (Uzbek/Russian)
- **Responsive Design**: Mobile-first approach
- **SEO Optimization**: Proper metadata and structured data
- **Contact Integration**: Telegram bot for inquiries
- **Static Export**: Ready for deployment to any hosting
- **Telegram Bot Integration**: Contact form submissions sent via Telegram bot# or ## 🚀 Features
- **SEO Optimized**: Proper metadata, structured data, and semantic HTMLpnpm dev - **Multi-language Support**: Uzbek (uz) and Russian (ru) with `next-intl`
- **Modern UI/UX**: Built with TailwindCSS and Framer Motion animations
- **Performance**: Image optimization, lazy loading, and code splitting# or - **3D Product Visualization**: Three.js/React Three Fiber for GLB/GLTF models
- **Responsive Design**: Mobile-first, fully responsive across all devices
- **Accessibility**: WCAG compliant with keyboard navigation and ARIA labelsbun dev - **Telegram Bot Integration**: Contact form submissions sent via Telegram bot
- **SEO Optimized**: Proper metadata, structured data, and semantic HTML
```` - **Performance**: Image optimization, lazy loading, and code splitting
- **Accessibility**: WCAG compliant with keyboard navigation and ARIA labels
- **Static Export**: Ready for deployment without server requirements
## 🛠️ Tech Stack ## 🛠️ Tech Stack
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
- **Framework**: Next.js 15+ (App Router) - **Framework**: Next.js 15+ (App Router)
- **Language**: TypeScript
- **Language**: TypeScriptYou can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
- **Styling**: TailwindCSS - **Styling**: TailwindCSS
- **Animations**: Framer Motion
- **Animations**: Framer MotionThis project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.
- **3D Graphics**: Three.js + React Three Fiber + Drei - **3D Graphics**: Three.js + React Three Fiber + Drei
- **HTTP Client**: Axios
- **HTTP Client**: Axios## Learn More
- **i18n**: next-intl - **i18n**: next-intl
- **Icons**: lucide-react
- **Icons**: lucide-reactTo learn more about Next.js, take a look at the following resources:
- **Forms**: React Hook Form + Zod - **Forms**: React Hook Form + Zod
- **State Management**: Zustand
- **Build**: Static export for optimal performance
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. ## 📦 Quick Start
## 📦 Quick Start- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. ### Prerequisites
- Node.js 18+
- npm, yarn, or pnpm
### Installation
### PrerequisitesYou can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome! 1. Clone the repository:
- Node.js 18.0+ ```bash
git clone <repository-url>
cd firma
```
- npm or yarn## Deploy on Vercel 2. Install dependencies:
```bash
npm install
# or
yarn install
# or
pnpm install
```
3. Run the development server:
### SetupThe easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
1. **Navigate to project** (already done):Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.
```bash
cd /home/zero/Projects/felix/firma
````
2. **Dependencies installed** ✓
3. **Configure environment variables**:
```bash
# Edit .env.local
TELEGRAM_BOT_TOKEN=your_token_here
TELEGRAM_CHAT_ID=your_chat_id_here
NEXT_PUBLIC_SITE_URL=http://localhost:3000
```
4. **Add product images to `public/images/`**:
- `hero-pump-1.jpg` through `hero-pump-5.jpg`
- `pump-1.jpg`, `pump-1-alt.jpg`
- `pump-2.jpg`, `pump-2-alt.jpg`
- `pump-3.jpg`, `pump-3-alt.jpg`
## 🚀 Running the Project
### Development:
```bash ```bash
npm run dev npm run dev
# or
yarn dev
# or
pnpm dev
``` ```
Visit: 4. Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
- Uzbek: http://localhost:3000/uz ### Build for Production
- Russian: http://localhost:3000/ru
### Build & Production:
```bash ```bash
npm run build npm run build
npm run start npm run start
``` ```
## 📁 Key Files For static export (recommended):
| File | Purpose | ```bash
| ----------------------------- | --------------------------------- | npm run build
| `app/[locale]/page.tsx` | Home page template |
| `components/Navbar.tsx` | Navigation with language switcher |
| `components/ShowCase.tsx` | Hero section with image carousel |
| `components/ProductsGrid.tsx` | Product grid and modal |
| `components/ContactForm.tsx` | Contact form with Telegram |
| `lib/products.ts` | Product data and types |
| `locales/uz.json` | Uzbek translations |
| `locales/ru.json` | Russian translations |
| `app/api/contact/route.ts` | Telegram API endpoint |
| `middleware.ts` | i18n routing middleware |
## 🔧 Configuration
### Add/Edit Products
Edit `lib/products.ts`:
```typescript
export const products: Product[] = [
{
id: "1",
nameKey: "products_list.pump_1.name",
slug: "schotchik-pump",
shortDescriptionKey: "products_list.pump_1.shortDescription",
images: ["/images/pump-1.jpg"],
specs: [{ key: "Flow Rate", value: "100 L/min" }],
},
];
``` ```
### Update Translations The static files will be generated in the `out` directory.
Edit `locales/uz.json` and `locales/ru.json`: ## 📁 Project Structure
```json ```
{ firma/
"nav": { ├── app/ # Next.js App Router
"about": "Biz haqimizda" ├── api/ # API routes
} │ ├── detail/[slug]/ # Dynamic product detail pages
} │ ├── globals.css # Global styles
│ ├── layout.tsx # Root layout
│ ├── page.tsx # Homepage
│ └── sitemap.ts # Sitemap generation
├── components/ # React components
│ ├── detailPage/ # Product detail components
│ ├── productSection/ # Product listing components
│ ├── ui/ # Reusable UI components
│ └── ...
├── context/ # React context providers
├── i18n/ # Internationalization
├── lib/ # Utility functions and types
├── locales/ # Translation files
├── public/ # Static assets
│ ├── images/ # Image assets
│ ├── logo.jpg # Company logo
│ └── ...
└── ...
``` ```
### Telegram Bot Setup ## 🌐 Internationalization
1. Message [@BotFather](https://t.me/BotFather) on Telegram The website supports two languages:
2. Create a bot and get token
3. Get your chat ID (use [@userinfobot](https://t.me/userinfobot))
4. Add to `.env.local`
## 📱 Sections - **Uzbek (uz)**: Default language
- **Russian (ru)**: Secondary language
- **Navbar**: Logo, navigation links, language switcher, mobile menu Language switching is available in the navigation bar. All content including product descriptions, features, and UI text is fully translated.
- **Hero**: Title, subtitle, CTA, image carousel with autoplay
- **About**: Company info, features, stats
- **Products**: Grid of product cards with modal details
- **FAQ**: 3 collapsible Q&A items
- **Contact**: Form with name, phone, message, product selection
- **Footer**: Links, social media, copyright
## 🎨 Customization ## 📞 Contact Integration
### Colors The contact form integrates with a Telegram bot to send inquiries directly to the company's support team. Configuration requires:
Edit `tailwind.config.ts` - currently uses blue theme - Telegram Bot Token
- Chat ID for message delivery
### Animations ## 🔍 SEO & Performance
Framer Motion used throughout - edit individual component files - **Sitemap**: Automatically generated at `/sitemap.xml`
- **Meta Tags**: Comprehensive metadata for each page
### Fonts - **Image Optimization**: Next.js automatic image optimization
- **Static Export**: No server required, fast loading times
Edit `app/layout.tsx` - currently using Geist font family - **Core Web Vitals**: Optimized for performance metrics
## 🚀 Deployment ## 🚀 Deployment
### Vercel (Recommended) The project is configured for static export, making it easy to deploy to any static hosting service:
```bash 1. Build the project: `npm run build`
git push origin main 2. Deploy the `out` directory to your hosting provider
# Connect repo on vercel.com 3. Configure domain and SSL as needed
# Add .env variables in project settings
```
### Other Hosting Recommended hosting platforms:
Works on any Node.js platform (Railway, Heroku, AWS, DigitalOcean, etc.) - Vercel
- Netlify
- GitHub Pages
- Any static hosting service
## 📝 API Routes ## 📝 License
### POST `/api/contact` This project is proprietary software for Max Di Group.
```json ## 📞 Support
{
"name": "John Doe",
"phone": "+998991234567",
"message": "Contact message",
"productSlug": "schotchik-pump",
"lang": "uz"
}
```
Sends message to Telegram via bot. For technical support or inquiries:
## 🧪 Testing - Email: [company email]
- Telegram: @firma_support
```bash - Phone: +998 (99) 869-74-70
# Check for build errors
npm run build
# Check TypeScript
npm run typecheck
# Run linting
npm run lint
```
## 📚 Resources
- [Next.js Docs](https://nextjs.org/docs)
- [next-intl](https://next-intl-docs.vercel.app/)
- [Framer Motion](https://www.framer.com/motion/)
- [Three.js](https://threejs.org/)
- [TailwindCSS](https://tailwindcss.com/)
## 📄 License
MIT License - feel free to use for your projects
--- ---
**Created**: November 2025 | **Status**: Production Ready Built with ❤️ for Max Di Group - Industrial Equipment Excellence

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -22,7 +22,7 @@
} }
body { body {
background: var(--background); background: white;
color: var(--foreground); color: var(--foreground);
font-family: Arial, Helvetica, sans-serif; font-family: Arial, Helvetica, sans-serif;
} }

View File

@@ -1,17 +1,58 @@
import type { Metadata } from "next";
import { ReactNode } from "react"; import { ReactNode } from "react";
import "../i18n/request"; // i18n config faylini import qilamiz
import { LanguageProvider } from "@/context/language-context"; import "../i18n/request";
import "./globals.css"; import "./globals.css";
import { LanguageProvider } from "@/context/language-context";
import { Navbar } from "@/components/Navbar"; import { Navbar } from "@/components/Navbar";
import { Footer } from "@/components/Footer"; import { Footer } from "@/components/Footer";
export const metadata: Metadata = {
title: {
default: "Texnik Uskunalar | Nasoslar va Datchiklar",
template: "%s | Texnik Uskunalar",
},
description:
"Sanoat uchun moljallangan nasoslar, datchiklar va texnik qurilmalar. Zamonaviy 3D korinishda mahsulotlar katalogi.",
keywords: [
"nasos",
"datchik",
"texnik uskuna",
"industrial equipment",
"sensor",
"pump",
],
authors: [{ name: "Max Di Group" }],
metadataBase: new URL("https://promtechno.uz/"),
openGraph: {
title: "Texnik Uskunalar",
description:
"Nasoslar va datchiklarni 3D korinishda koring va solishtiring",
url: "https://promtechno.uz/",
siteName: "Texnik Uskunalar",
locale: "uz_UZ",
type: "website",
images: [
{
url: "/logo.jpg",
width: 1200,
height: 630,
alt: "Texnik Uskunalar Logo",
},
],
},
};
// for github pages deployment
export default function RootLayout({ children }: { children: ReactNode }) { export default function RootLayout({ children }: { children: ReactNode }) {
return ( return (
<html lang="uz"> <html lang="uz" dir="ltr">
<body> <body className="flex min-h-screen flex-col">
<LanguageProvider> <LanguageProvider>
<Navbar /> <Navbar />
{children} <main className="flex-1">{children}</main>
<Footer /> <Footer />
</LanguageProvider> </LanguageProvider>
</body> </body>

View File

@@ -0,0 +1,38 @@
import { ContactForm } from "@/components/ContactForm";
import DetailInfo from "@/components/detailPage/detailInfo";
import { Product } from "@/lib/products";
import { generateSlug } from "@/lib/slug";
import { getAllProducts } from "@/lib/api";
import { notFound } from "next/navigation";
export async function generateStaticParams() {
const products = await getAllProducts();
return products.map((product) => ({
slug: generateSlug(product.name_uz),
}));
}
async function getProduct(slug: string): Promise<Product | undefined> {
const products = await getAllProducts();
return products.find((product) => generateSlug(product.name_uz) === slug);
}
export default async function Page({ params }: { params: any }) {
const { slug } = await params;
const product = await getProduct(slug);
if (!product) {
notFound();
}
return (
<div>
<DetailInfo product={product} />
<section id="contact">
<ContactForm />
</section>
</div>
);
}

43
app/sitemap.ts Normal file
View File

@@ -0,0 +1,43 @@
export const dynamic = "force-static";
import { MetadataRoute } from "next";
export default function sitemap(): MetadataRoute.Sitemap {
const baseUrl = "https://promtechno.uz";
// Static pages
const staticPages = [
{
url: baseUrl,
lastModified: new Date(),
changeFrequency: "weekly" as const,
priority: 1,
},
];
// Product detail pages - based on actual product slugs from API github
const productSlugs = [
"ppv-100-1-6-su-6-0-60-0-5foiz-mexanik-suyuqlik-hisoblagichi",
"ppo-40-0-6-su-suyuqlik-hisoblagichi",
"ppo-25-1-6-su-oval-tishli-suyuqlik-hisoblagichi",
"scl-20-24-nasosi",
"km-100-80-170e-elektro-nasosi",
"scn-75-70-nasosi",
"usn-150-04-pastdan-tushirish-quyi-oqizish-qurilmasi-ta-sir-zonasi-4-m",
"asn-80-02-yuqoridan-quyish-qurilmasi",
"usn-100-usn-76-neft-mahsulotlari-va-maxsus-suyuqliklarni-pastdan-oqizish-qurilmalari",
"mexanik-nafas-olish-klapani-kdm-100-150-200-250-ichki-yong-inga-qarshi-himoyali",
"fju-100-1-6-suyuqlik-filtri",
"yoqilg-i-quyish-moduli-mz-35",
"asvn-80-nasos-agregati",
];
const productPages = productSlugs.map((slug) => ({
url: `${baseUrl}/product/${slug}`,
lastModified: new Date(),
changeFrequency: "monthly" as const,
priority: 0.8,
}));
return [...staticPages, ...productPages];
}

View File

@@ -1,39 +1,55 @@
"use client"; "use client";
import { useState } from "react"; import { useEffect, useState } from "react";
import { motion, AnimatePresence } from "framer-motion"; import { motion, AnimatePresence } from "framer-motion";
import { ChevronDown } from "lucide-react"; import { ChevronDown } from "lucide-react";
import { useLanguage } from "@/context/language-context"; import { useLanguage } from "@/context/language-context";
import { getAllFaq } from "@/lib/api";
interface FaqItem { interface FaqItem {
questionKey: string; questionKey: string;
answerKey: string; answerKey: string;
} }
interface FaqProps { export interface FaqBackendItem {
items?: FaqItem[]; id: number;
question_ru: string;
question_uz: string;
answer_ru: string;
answer_uz: string;
} }
export function FAQ({ items }: FaqProps) { const defaultItems: FaqItem[] = [
const {t} = useLanguage(); {
questionKey: "faq.items.0.question",
answerKey: "faq.items.0.answer",
},
{
questionKey: "faq.items.1.question",
answerKey: "faq.items.1.answer",
},
{
questionKey: "faq.items.2.question",
answerKey: "faq.items.2.answer",
},
];
export function FAQ() {
const { t, language } = useLanguage();
const [openIndex, setOpenIndex] = useState<number | null>(0); const [openIndex, setOpenIndex] = useState<number | null>(0);
const [faqItems, setFaqItems] = useState<FaqItem[]>([]);
const defaultItems: FaqItem[] = [ useEffect(() => {
{ async function fetchFaq() {
questionKey: "faq.items.0.question", const allFaq = await getAllFaq();
answerKey: "faq.items.0.answer", const faqItems: FaqItem[] = allFaq.map((item) => ({
}, questionKey: language === "uz" ? item.question_uz : item.question_ru,
{ answerKey: language === "uz" ? item.answer_uz : item.answer_ru,
questionKey: "faq.items.1.question", }));
answerKey: "faq.items.1.answer", faqItems.length === 0 ? setFaqItems(defaultItems) : setFaqItems(faqItems);
}, }
{ fetchFaq();
questionKey: "faq.items.2.question", }, [language]);
answerKey: "faq.items.2.answer",
},
];
const faqItems = items || defaultItems;
const containerVariants = { const containerVariants = {
hidden: { opacity: 0 }, hidden: { opacity: 0 },
@@ -81,7 +97,7 @@ export function FAQ({ items }: FaqProps) {
> >
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<h3 className="text-lg font-semibold text-gray-900 flex-1"> <h3 className="text-lg font-semibold text-gray-900 flex-1">
{t.faq.items[idx].question} {item.questionKey}
</h3> </h3>
<motion.div <motion.div
animate={{ rotate: openIndex === idx ? 180 : 0 }} animate={{ rotate: openIndex === idx ? 180 : 0 }}
@@ -104,7 +120,7 @@ export function FAQ({ items }: FaqProps) {
> >
<div className="bg-primary/20 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"> <p className="text-gray-700 leading-relaxed">
{t.faq.items[idx].answer} {item.answerKey}
</p> </p>
</div> </div>
</motion.div> </motion.div>

View File

@@ -7,6 +7,7 @@ import { motion } from "framer-motion";
import LanguageSwitcher from "./languageSwitcher"; import LanguageSwitcher from "./languageSwitcher";
import { useLanguage } from "@/context/language-context"; import { useLanguage } from "@/context/language-context";
import Image from "next/image"; import Image from "next/image";
import { useProductStore } from "@/lib/productZustand";
interface NavLink { interface NavLink {
id: string; id: string;
@@ -21,12 +22,13 @@ interface NavbarProps {
export function Navbar({ logoText = "FIRMA" }: NavbarProps) { export function Navbar({ logoText = "FIRMA" }: NavbarProps) {
const [isOpen, setIsOpen] = useState(false); const [isOpen, setIsOpen] = useState(false);
const { t } = useLanguage(); const { t } = useLanguage();
const resetProductName = useProductStore((state) => state.resetProductName);
const navLinks: NavLink[] = [ const navLinks: NavLink[] = [
{ id: "about", labelKey: t.nav.about, href: "#about" }, { id: "about", labelKey: t.nav.about, href: "#about" },
{ id: "products", labelKey: t.nav.products, href: "#products" }, { id: "products", labelKey: t.nav.products, href: "/product" },
{ id: "faq", labelKey: t.nav.faq, href: "#faq" }, { id: "faq", labelKey: t.nav.faq, href: "#faq" },
{ id: "contact", labelKey: t.nav.contact , href: "#contact" }, { id: "contact", labelKey: t.nav.contact, href: "#contact" },
]; ];
const handleScroll = (href: string) => { const handleScroll = (href: string) => {
@@ -47,24 +49,41 @@ export function Navbar({ logoText = "FIRMA" }: NavbarProps) {
<motion.div whileHover={{ scale: 1.05 }} whileTap={{ scale: 0.95 }}> <motion.div whileHover={{ scale: 1.05 }} whileTap={{ scale: 0.95 }}>
<Link <Link
href={`/`} href={`/`}
onClick={() => resetProductName()}
className=" relative overflow-hidden" className=" relative overflow-hidden"
> >
<Image src='/logo.jpg' alt="image" width={80} height={50} className="rounded-xl object-cover" /> <Image
src="/logo.jpg"
alt="image"
width={80}
height={50}
className="rounded-xl object-cover"
/>
</Link> </Link>
</motion.div> </motion.div>
{/* Desktop Menu */} {/* Desktop Menu */}
<div className="hidden md:flex items-center gap-8"> <div className="hidden md:flex items-center gap-8">
{navLinks.map((link) => ( {navLinks.map((link) =>
<motion.button link.id === "products" ? (
key={link.id} <Link
whileHover={{ color: "#2563eb" }} key={link.id}
onClick={() => handleScroll(link.href)} href={link.href}
className="text-[#468965] hover:text-[#468965] transition-colors hover:cursor-pointer" className="text-[#468965] hover:text-[#468965] transition-colors hover:cursor-pointer"
> >
{link.labelKey} {link.labelKey}
</motion.button> </Link>
))} ) : (
<motion.button
key={link.id}
whileHover={{ color: "#2563eb" }}
onClick={() => handleScroll(link.href)}
className="text-[#468965] hover:text-[#468965] transition-colors hover:cursor-pointer"
>
{link.labelKey}
</motion.button>
)
)}
</div> </div>
{/* Language & Mobile Menu */} {/* Language & Mobile Menu */}

View File

@@ -0,0 +1,99 @@
"use client";
import { motion } from "framer-motion";
import { useLanguage } from "@/context/language-context";
import Link from "next/link";
import Image from "next/image";
import { Product } from "@/lib/products";
interface DetailInfoProps {
product: Product;
}
export default function DetailInfo({ product }: DetailInfoProps) {
const { t, language } = useLanguage();
const languageIndex = language === "uz" ? true : false;
const handleScroll = (href: string) => {
const element = document.querySelector(href);
if (element) {
element.scrollIntoView({ behavior: "smooth" });
}
};
return (
<div className="bg-white">
<div className="max-w-[1200px] mx-auto">
{/* Header */}
<div className="sticky z-10 top-0 md:p-6 p-2 flex justify-center items-center">
<h2 className="md:text-2xl text-lg font-bold text-gray-900">
{languageIndex ? product.name_uz : product.name_ru}
</h2>
</div>
{/* Content */}
<div className="sm:p-6 p-2">
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4 lg:gap-8 mb-8">
{/* Image */}
<div className="relative w-full h-64 sm:h-80 lg:h-96 rounded-lg overflow-hidden bg-gray-50">
<Image
src={`https://admin.promtechno.uz${product.image}`}
alt={languageIndex ? product.name_uz : product.name_ru}
fill
sizes="(max-width: 768px) 100vw, (max-width: 1024px) 50vw, 600px"
className="object-contain p-4"
priority
/>
</div>
{/* Content */}
<div className="space-y-4 lg:space-y-6 lg:py-4">
{/* Description */}
<div className="text-gray-800 text-sm sm:text-base leading-relaxed">
{languageIndex
? product.description_uz
: product.description_ru}
</div>
{/* CTA Buttons */}
<div className="pt-2">
<Link href="#contact" onClick={() => handleScroll("#contact")}>
<motion.button
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
className="w-full sm:w-auto px-8 py-3 bg-primary/80 text-white rounded-lg font-semibold hover:bg-primary transition-all duration-300 shadow-md hover:shadow-lg"
>
{t.contact.send}
</motion.button>
</Link>
</div>
</div>
</div>
{/* Specifications */}
<div>
<div className="mb-8">
<h3 className="text-lg font-semibold text-gray-900 mb-4">
{t.features}
</h3>
<div className="space-y-3 flex items-start justify-start gap-4 flex-wrap ">
{product.features?.map((spec, idx) => (
<div
key={idx}
className="flex flex-col justify-between py-2 border border-gray-100 rounded-lg p-2"
>
<span className="text-gray-600">
{languageIndex ? spec.key_uz : spec.key_ru}:
</span>
<span className="text-gray-900">
{languageIndex ? spec.value_uz : spec.value_ru}
</span>
</div>
))}
</div>
</div>
</div>
</div>
</div>
</div>
);
}

View File

@@ -20,7 +20,7 @@ export default function LanguageSwitcher() {
<div> <div>
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger asChild> <DropdownMenuTrigger asChild>
<button className="flex items-center gap-1 p-1 border-white border text-black hover:text-primary transition-colors"> <button className="flex items-center gap-1 p-1 border-white border text-primary hover:cursor-pointer transition-colors">
<Globe size={16} /> <Globe size={16} />
{language.toUpperCase()} {language.toUpperCase()}
</button> </button>

16
components/loading.tsx Normal file
View File

@@ -0,0 +1,16 @@
import { useLanguage } from "@/context/language-context";
export default function Loading() {
const { language } = useLanguage();
const languageIndex = language === "uz" ? true : false;
return (
<div className="h-52 w-52 rounded-xl bg-linear-to-br from-primary via-primary/60 to-gray-300 backdrop-blur-md flex flex-col items-center justify-center gap-3">
<div className="w-8 h-8 border-4 border-gray-300 border-t-white rounded-full animate-spin" />
<span className="text-white text-lg text-center">
{languageIndex ? "Yuklanmoqda..." : "Загрузка..."}
</span>
</div>
);
}

View File

@@ -5,66 +5,77 @@ import { motion } from "framer-motion";
import { ExternalLink } from "lucide-react"; import { ExternalLink } from "lucide-react";
import { type Product } from "@/lib/products"; import { type Product } from "@/lib/products";
import { useLanguage } from "@/context/language-context"; import { useLanguage } from "@/context/language-context";
import Link from "next/link";
import { useProduct, useProductStore } from "@/lib/productZustand";
interface ProductCardProps { interface ProductCardProps {
product: Product; product: Product;
onViewDetails: (slug: number) => void;
} }
export function ProductCard({ product, onViewDetails }: ProductCardProps) { export function ProductCard({ product }: ProductCardProps) {
const { t, language } = useLanguage(); const { t, language } = useLanguage();
const languageIndex = language === "uz" ? true : false; const languageIndex = language === "uz" ? true : false;
const setProductName = useProductStore((state) => state.setProductName);
const setProducts = useProduct((state) => state.setProducts);
return ( return (
<motion.div <Link
initial={{ opacity: 0, y: 20 }} href={`/product/${product.slug}`}
animate={{ opacity: 1, y: 0 }} onClick={() => {
transition={{ duration: 0.3 }} setProductName(languageIndex ? product.name_uz : product.name_ru);
className="group relative h-full flex flex-col bg-white rounded-2xl overflow-hidden shadow-md hover:shadow-2xl transition-all duration-300 border border-gray-100" setProducts(product);
}}
> >
{/* Image Container - Fixed Height */} <motion.div
<div className="relative w-full h-64 overflow-hidden bg-linear-to-br from-gray-50 to-gray-100"> initial={{ opacity: 0, y: 20 }}
<motion.div animate={{ opacity: 1, y: 0 }}
whileHover={{ scale: 1.05 }} transition={{ duration: 0.3 }}
transition={{ duration: 0.4 }} className="group relative h-full flex flex-col bg-white rounded-2xl overflow-hidden shadow-md hover:shadow-2xl transition-all duration-300 border border-gray-100"
className="w-full h-full" >
> {/* Image Container - Fixed Height */}
<img <div className="relative w-full h-64 overflow-hidden bg-linear-to-br from-gray-50 to-gray-100">
src={`https://admin.promtechno.uz${product.image}`} <motion.div
alt={languageIndex?product.name_uz:product.name_ru} whileHover={{ scale: 1.05 }}
className="w-full h-full object-cover" transition={{ duration: 0.4 }}
/> className="w-full h-full relative"
</motion.div> >
<Image
src={`https://admin.promtechno.uz${product.image}`}
alt={languageIndex ? product.name_uz : product.name_ru}
fill
className="w-full h-full object-cover"
/>
</motion.div>
{/* Gradient Overlay */} {/* Gradient Overlay */}
<div className="absolute inset-0 bg-linear-to-t from-black/20 via-transparent to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-300" /> <div className="absolute inset-0 bg-linear-to-t from-black/20 via-transparent to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-300" />
</div> </div>
{/* Content Container - Flex Grow */} {/* Content Container - Flex Grow */}
<div className="flex flex-col grow p-6"> <div className="flex flex-col grow p-6">
{/* Product Name */} {/* Product Name */}
<h3 className="text-xl font-bold text-gray-900 mb-3 line-clamp-2 group-hover:text-primary transition-colors duration-300"> <h3 className="text-xl font-bold text-gray-900 mb-3 line-clamp-2 group-hover:text-primary transition-colors duration-300">
{languageIndex?product.name_uz:product.name_ru} {languageIndex ? product.name_uz : product.name_ru}
</h3> </h3>
{/* Short Description - Fixed Height with Line Clamp */} {/* Short Description - Fixed Height with Line Clamp */}
<p className="text-gray-600 text-sm leading-relaxed mb-4 line-clamp-3 grow"> <p className="text-gray-600 text-sm leading-relaxed mb-4 line-clamp-3 grow">
{languageIndex?product.name_uz:product.name_ru} {languageIndex ? product.name_uz : product.name_ru}
</p> </p>
{/* CTA Button - Always at Bottom */} {/* CTA Button - Always at Bottom */}
<motion.button <motion.div
whileHover={{ scale: 1.02 }} whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }} whileTap={{ scale: 0.98 }}
onClick={() => onViewDetails(product.id)} className="w-full flex items-center justify-center gap-2 px-6 py-3 bg-primary text-white rounded-xl font-semibold shadow-lg shadow-blue-500/30 hover:shadow-xl hover:shadow-primary/40 transition-all duration-300 group/button"
className="w-full flex items-center justify-center gap-2 px-6 py-3 bg-primary text-white rounded-xl font-semibold shadow-lg shadow-blue-500/30 hover:shadow-xl hover:shadow-primary/40 transition-all duration-300 group/button" >
> <span>{t.details}</span>
<span>{t.details}</span> <ExternalLink className="w-4 h-4 group-hover/button:translate-x-1 transition-transform duration-300" />
<ExternalLink className="w-4 h-4 group-hover/button:translate-x-1 transition-transform duration-300" /> </motion.div>
</motion.button> </div>
</div>
{/* Decorative Corner */} {/* Decorative Corner */}
<div className="absolute top-0 right-0 w-20 h-20 bg-linear-to-br from-blue-500/10 to-transparent rounded-bl-full opacity-0 group-hover:opacity-100 transition-opacity duration-300" /> <div className="absolute top-0 right-0 w-20 h-20 bg-linear-to-br from-blue-500/10 to-transparent rounded-bl-full opacity-0 group-hover:opacity-100 transition-opacity duration-300" />
</motion.div> </motion.div>
</Link>
); );
} }

View File

@@ -1,122 +0,0 @@
"use client";
import { motion, AnimatePresence } from "framer-motion";
import { X } from "lucide-react";
import type { Product } from "@/lib/products";
import { useLanguage } from "@/context/language-context";
import Link from "next/link";
import { useProductStore } from "@/lib/productZustand";
import Image from "next/image";
import { usePathname } from "next/navigation";
interface ProductModalProps {
product: Product;
onClose: () => void;
}
// for github firma repo
export function ProductModal({ product, onClose }: ProductModalProps) {
const { t, language } = useLanguage();
const setProductName = useProductStore((state) => state.setProductName);
const languageIndex = language === "uz" ? true : false;
const pathName = usePathname();
const isPathName = pathName === "/product";
return (
<AnimatePresence>
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
onClick={onClose}
className="fixed inset-0 bg-black/50 z-50 flex items-center justify-center p-4"
>
<motion.div
initial={{ scale: 0.95, opacity: 0 }}
animate={{ scale: 1, opacity: 1 }}
exit={{ scale: 0.95, opacity: 0 }}
onClick={(e) => e.stopPropagation()}
className="bg-white rounded-lg max-w-4xl w-full max-h-[90vh] overflow-y-auto"
>
{/* Header */}
<div className="sticky z-10 top-0 bg-white border-b border-gray-200 sm:p-6 p-2 flex justify-between items-center">
<h2 className="md:text-2xl text-lg font-bold text-gray-900">
{languageIndex ? product.name_uz : product.name_ru}
</h2>
<motion.button
whileHover={{ scale: 1.1 }}
whileTap={{ scale: 0.95 }}
onClick={onClose}
className="p-2 hover:bg-gray-100 rounded-lg transition-colors"
>
<X size={24} />
</motion.button>
</div>
{/* Content */}
<div className="sm:p-6 p-2">
<div className="grid grid-cols-1 md:grid-cols-2 gap-8 mb-8">
{/* Image */}
<div className="relative max-sm:w-full max-md:h-50">
<Image
src={`https://admin.promtechno.uz${product.image}`}
alt="image"
fill
className="object-contain max-md:h-50"
/>
</div>
{/* Specifications */}
<div>
<div className="mb-8">
<h3 className="text-lg font-semibold text-gray-900 mb-4">
{t.features}
</h3>
<div className="space-y-3">
{product.features.map((spec, idx) => (
<div
key={idx}
className="flex max-sm:flex-col justify-between py-2 border-b border-gray-100"
>
<span className="text-gray-600">
{languageIndex ? spec.key_uz : spec.key_ru}:
</span>
<span className="text-gray-600">
{languageIndex ? spec.value_uz : spec.value_ru}
</span>
</div>
))}
</div>
</div>
{/* CTA Buttons */}
<div className="space-y-3">
<Link href={isPathName ? "/#contact" : "#contact"} >
<motion.button
onClick={() => {
onClose();
setProductName(
languageIndex ? product.name_uz : product.name_ru
);
}}
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
className="w-full px-6 py-3 bg-primary/80 text-white rounded-lg font-semibold hover:bg-primary transition-colors"
>
{t.contact.send}
</motion.button>
</Link>
</div>
</div>
</div>
<div className="text-gray-800 max-sm:text-[14px]">
{languageIndex ? product.description_uz : product.description_ru}
</div>
</div>
</motion.div>
</motion.div>
</AnimatePresence>
);
}

View File

@@ -8,37 +8,34 @@ import { useLanguage } from "@/context/language-context";
import Link from "next/link"; import Link from "next/link";
import { ChevronsRight } from "lucide-react"; import { ChevronsRight } from "lucide-react";
import { ProductCard } from "./ProductCard"; import { ProductCard } from "./ProductCard";
import { ProductModal } from "./ProductModal";
import axios from "axios"; import axios from "axios";
import EmptyState from "../productsPage/emptyData"; import EmptyState from "../productsPage/emptyData";
import Loading from "../loading";
import { getAllProducts } from "@/lib/api";
import { generateSlug } from "@/lib/slug";
// hello everyone // hello everyone
export function ProductsGrid() { export function ProductsGrid() {
const { t } = useLanguage(); const { t } = useLanguage();
const [selectedProduct, setSelectedProduct] = useState<Product | null>(null);
const [allProducts, setAllProducts] = useState<any>([]); const [allProducts, setAllProducts] = useState<any>([]);
const [loading, setLoading] = useState<boolean>(false);
useEffect(() => { useEffect(() => {
async function getData() { async function getData() {
await axios setLoading(true);
.get("https://admin.promtechno.uz/api/products/") const products = await getAllProducts();
.then((res) => { setAllProducts(
console.log("all data main page: ", res?.data); products.map((product: any) => ({
const allData = res?.data || []; ...product,
setAllProducts(allData.slice(0, 3)); slug: generateSlug(product.name_uz),
}); })).slice(0, 3)
);
setLoading(false);
} }
getData(); getData();
}, []); }, []);
const handleViewDetails = (slug: number) => {
const product = allProducts.find((p: any) => p.id === slug);
if (product) {
setSelectedProduct(product);
}
};
return ( return (
<> <>
<section id="products" className="relative py-20"> <section id="products" className="relative py-20">
@@ -66,8 +63,14 @@ export function ProductsGrid() {
<div className="w-20 h-1 bg-primary mx-auto rounded-full" /> <div className="w-20 h-1 bg-primary mx-auto rounded-full" />
</motion.div> </motion.div>
{loading && (
<div className="w-full flex items-center justify-center">
<Loading />
</div>
)}
{/* Product Grid */} {/* Product Grid */}
{allProducts && allProducts.length > 0 ? ( {loading || (allProducts && allProducts.length > 0) ? (
<motion.div <motion.div
initial="hidden" initial="hidden"
whileInView="visible" whileInView="visible"
@@ -76,10 +79,7 @@ export function ProductsGrid() {
> >
{allProducts.map((product: any) => ( {allProducts.map((product: any) => (
<motion.div key={product.id}> <motion.div key={product.id}>
<ProductCard <ProductCard product={product} />
product={product}
onViewDetails={handleViewDetails}
/>
</motion.div> </motion.div>
))} ))}
</motion.div> </motion.div>
@@ -96,14 +96,6 @@ export function ProductsGrid() {
</Link> </Link>
</div> </div>
</section> </section>
{/* Product Modalll */}
{selectedProduct && (
<ProductModal
product={selectedProduct}
onClose={() => setSelectedProduct(null)}
/>
)}
</> </>
); );
} }

View File

@@ -1,10 +1,12 @@
"use client"; "use client";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import EmptyState from "./emptyData"; import EmptyState from "./emptyData";
import { ProductModal } from "../productSection/ProductModal";
import { motion } from "framer-motion"; import { motion } from "framer-motion";
import { ProductCard } from "../productSection/ProductCard"; import { ProductCard } from "../productSection/ProductCard";
import axios from "axios"; import axios from "axios";
import Loading from "../loading";
import { generateSlug } from "@/lib/slug";
import { getAllProducts } from "@/lib/api";
const itemVariants = { const itemVariants = {
hidden: { opacity: 0, y: 20 }, hidden: { opacity: 0, y: 20 },
@@ -12,49 +14,42 @@ const itemVariants = {
}; };
export default function Products() { export default function Products() {
const [allProducts, setAllProducts] = useState<any>(null); const [allProducts, setAllProducts] = useState<any>([]);
const [selectedProduct, setSelectedProduct] = useState<any>(null); const [loading, setLoading] = useState<boolean>(false);
useEffect(() => { useEffect(() => {
async function getData() { async function getData() {
await axios.get("https://admin.promtechno.uz/api/products/").then((res) => { setLoading(true);
console.log("all data: ", res?.data); const products = await getAllProducts();
const allData = res?.data || []; setAllProducts(
setAllProducts(allData); products.map((product: any) => ({
}); ...product,
slug: generateSlug(product.name_uz),
}))
);
setLoading(false);
} }
getData(); getData();
}, []); }, []);
const handleViewDetails = (id: number) => {
const product = allProducts.find((p: any) => p.id === id);
if (product) {
setSelectedProduct(product);
}
};
return ( return (
<div className="py-20 max-w-[1200px] w-full mx-auto px-2 "> <div className="py-20 max-w-[1200px] w-full mx-auto px-2 ">
{allProducts && allProducts.length > 0 ? ( {loading && (
<div className="w-full flex items-center justify-center">
<Loading />
</div>
)}
{loading || (allProducts && allProducts.length > 0) ? (
<div className="grid lg:grid-cols-3 sm:grid-cols-2 grid-cols-1 gap-4"> <div className="grid lg:grid-cols-3 sm:grid-cols-2 grid-cols-1 gap-4">
{allProducts.map((product: any) => ( {allProducts.map((product: any) => (
<motion.div key={product.id} variants={itemVariants}> <motion.div key={product.id} variants={itemVariants}>
<ProductCard <ProductCard product={product} />
product={product}
onViewDetails={handleViewDetails}
/>
</motion.div> </motion.div>
))} ))}
</div> </div>
) : ( ) : (
<EmptyState page="products" /> <EmptyState page="products" />
)} )}
{/* Product Modal */}
{selectedProduct && (
<ProductModal
product={selectedProduct}
onClose={() => setSelectedProduct(null)}
/>
)}
</div> </div>
); );
} }

26
lib/api.ts Normal file
View File

@@ -0,0 +1,26 @@
import { FaqBackendItem } from "@/components/FAQ";
import { Product } from "@/lib/products";
export async function getAllProducts(): Promise<Product[]> {
const res = await fetch("https://admin.promtechno.uz/api/products/", {
cache: "force-cache", // build time uchun for gitea
});
if (!res.ok) {
console.log("Failed to fetch products");
return [];
}
return res.json();
}
export async function getAllFaq(): Promise<FaqBackendItem[]> {
const res = await fetch("https://admin.promtechno.uz/api/faqs/");
if (!res.ok) {
console.log("Failed to fetch faqs");
return [];
}
return res.json();
}

View File

@@ -0,0 +1,17 @@
import axios from "axios";
import { generateSlug } from "./slug";
export async function generateStaticParams() {
try {
await axios.get("https://admin.promtechno.uz/api/products/").then((res) => {
console.log("all data: ", res?.data);
const allData = res?.data || [];
return allData.map((product: any) => ({
slug: generateSlug(product.name_uz),
}));
});
} catch (error) {
console.error("Error in generateStaticParams:", error);
return []; // Xato bo'lsa bo'sh array qaytarish
}
}

View File

@@ -1,5 +1,6 @@
import { create } from "zustand"; import { create } from "zustand";
import { devtools, persist } from "zustand/middleware"; import { devtools, persist } from "zustand/middleware";
import type { Product } from "@/lib/products";
interface ProductStore { interface ProductStore {
productName: string; productName: string;
@@ -8,15 +9,27 @@ interface ProductStore {
} }
export const useProductStore = create<ProductStore>()( export const useProductStore = create<ProductStore>()(
devtools((set) => ({
productName: "",
setProductName: (name: string) => set({ productName: name }),
resetProductName: () => set({ productName: "" }),
}))
);
interface ProductDetail {
product: Product | null;
setProducts: (product: Product) => void;
}
export const useProduct = create<ProductDetail>()(
devtools( devtools(
persist( persist(
(set) => ({ (set) => ({
productName: "", product: null,
setProductName: (name: string) => set({ productName: name }), setProducts: (product: Product) => set({ product }),
resetProductName: () => set({ productName: "" }),
}), }),
{ {
name: "product-storage", name: "product-detail-storage",
} }
) )
) )

View File

@@ -13,4 +13,5 @@ export interface Product {
description_ru: string; description_ru: string;
features:features[]; features:features[];
image:string; image:string;
slug?: string;
} }

12
lib/slug.ts Normal file
View File

@@ -0,0 +1,12 @@
export function generateSlug(productName: string): string {
return productName
.toLowerCase()
.replace(/\s+/g, "-") // Bo'shliqlarni tire bilan almashtirish
.replace(/[()]/g, "") // Qavslarni olib tashlash
.replace(/[–—]/g, "-") // Maxsus tire'larni oddiy tire bilan
.replace(/%/g, "foiz") // % ni foiz deb yozish
.replace(/,/g, "-") // Vergullarni tire bilan
.replace(/\.+/g, "-") // Nuqtalarni tire bilan
.replace(/-+/g, "-") // Bir nechta tire'ni bitta tire bilan
.replace(/^-|-$/g, ""); // Boshi va oxiridagi tire'larni olib tashlash
}

BIN
public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

BIN
public/logo1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 MiB