Compare commits
18 Commits
2f10fb70a9
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8c5499dac1 | ||
|
|
e94b7678f3 | ||
|
|
9e9a2f79c4 | ||
|
|
3de1299af8 | ||
|
|
f840063827 | ||
|
|
cd50a10539 | ||
|
|
c2c39d44a0 | ||
|
|
a700fdddc6 | ||
|
|
5bb9a566ca | ||
|
|
2f3f01f1fc | ||
|
|
67861ad5c8 | ||
|
|
733a1a5fc8 | ||
|
|
e53d40bd61 | ||
|
|
01a89edfd5 | ||
|
|
260bd8f3c8 | ||
|
|
030fdbc001 | ||
|
|
3dfce04ab0 | ||
|
|
ca80c6920c |
294
README.md
294
README.md
@@ -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
|
||||||
|
|||||||
BIN
app/favicon.ico
BIN
app/favicon.ico
Binary file not shown.
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 15 KiB |
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 mo‘ljallangan nasoslar, datchiklar va texnik qurilmalar. Zamonaviy 3D ko‘rinishda 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 ko‘rinishda ko‘ring 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>
|
||||||
|
|||||||
38
app/product/[slug]/page.tsx
Normal file
38
app/product/[slug]/page.tsx
Normal 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
43
app/sitemap.ts
Normal 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];
|
||||||
|
}
|
||||||
@@ -12,6 +12,7 @@ export function ContactForm() {
|
|||||||
const { t } = useLanguage();
|
const { t } = useLanguage();
|
||||||
|
|
||||||
const productName = useProductStore((state) => state.productName);
|
const productName = useProductStore((state) => state.productName);
|
||||||
|
const reset = useProductStore((state) => state.resetProductName);
|
||||||
|
|
||||||
const [formData, setFormData] = useState({
|
const [formData, setFormData] = useState({
|
||||||
name: "",
|
name: "",
|
||||||
@@ -26,15 +27,29 @@ export function ContactForm() {
|
|||||||
text: string;
|
text: string;
|
||||||
} | null>(null);
|
} | null>(null);
|
||||||
|
|
||||||
|
const phoneRegex = /^\+?\d*$/; // + majburiy ha majburiy
|
||||||
|
|
||||||
const handleChange = (
|
const handleChange = (
|
||||||
e: React.ChangeEvent<
|
e: React.ChangeEvent<
|
||||||
HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement
|
HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement
|
||||||
>
|
>
|
||||||
) => {
|
) => {
|
||||||
|
const { name, value } = e.target;
|
||||||
|
|
||||||
|
if (name === "phone") {
|
||||||
|
// regexga mos kelmasa — state o‘zgarmaydi
|
||||||
|
if (!phoneRegex.test(value)) return;
|
||||||
|
|
||||||
setFormData((prev) => ({
|
setFormData((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
[e.target.name]: e.target.value,
|
phone: value,
|
||||||
}));
|
}));
|
||||||
|
} else {
|
||||||
|
setFormData((prev) => ({
|
||||||
|
...prev,
|
||||||
|
[name]: value,
|
||||||
|
}));
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSubmit = async (e: React.FormEvent) => {
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
@@ -54,14 +69,16 @@ ${formData.message || "—"}
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const token = "8242045471:AAHaECS0psWg1jGBaIk1GxxTG6sBAssK_vw"; // Use environment variable
|
const token = "8242045471:AAHaECS0psWg1jGBaIk1GxxTG6sBAssK_vw"; // Use environment variablesalomalr olsun
|
||||||
const chatId = 6134458285;
|
const chatId = 1045736611;
|
||||||
|
|
||||||
await axios.post(`https://api.telegram.org/bot${token}/sendMessage`, {
|
await axios.post(`https://api.telegram.org/bot${token}/sendMessage`, {
|
||||||
chat_id: chatId,
|
chat_id: chatId,
|
||||||
text: text,
|
text: text,
|
||||||
});
|
});
|
||||||
setMessage({ type: "success", text: t.contact.success });
|
setMessage({ type: "success", text: t.contact.success });
|
||||||
|
reset();
|
||||||
|
setFormData({ name: "", phone: "", message: "", productName: "" });
|
||||||
} catch {
|
} catch {
|
||||||
setMessage({ type: "error", text: t.contact.error });
|
setMessage({ type: "error", text: t.contact.error });
|
||||||
} finally {
|
} finally {
|
||||||
@@ -168,7 +185,7 @@ ${formData.message || "—"}
|
|||||||
value={formData.name}
|
value={formData.name}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
placeholder={t.contact.namePlaceholder}
|
placeholder={t.contact.namePlaceholder}
|
||||||
className="w-full px-4 py-2 border border-gray-600 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
|
className="w-full text-black px-4 py-2 border border-gray-600 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -184,7 +201,7 @@ ${formData.message || "—"}
|
|||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
placeholder={t.contact.phonePlaceholder}
|
placeholder={t.contact.phonePlaceholder}
|
||||||
required
|
required
|
||||||
className="w-full px-4 py-2 border border-gray-600 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
|
className="w-full text-black px-4 py-2 border border-gray-600 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -199,7 +216,7 @@ ${formData.message || "—"}
|
|||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
placeholder={t.contact.messagePlaceholder}
|
placeholder={t.contact.messagePlaceholder}
|
||||||
rows={4}
|
rows={4}
|
||||||
className="w-full px-4 py-2 border border-gray-600 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 resize-none"
|
className="w-full text-black px-4 py-2 border border-gray-600 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 resize-none"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -214,7 +231,7 @@ ${formData.message || "—"}
|
|||||||
value={productName ? productName : formData.productName}
|
value={productName ? productName : formData.productName}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
placeholder={t.contact.product}
|
placeholder={t.contact.product}
|
||||||
className="w-full px-4 py-2 border border-gray-600 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
|
className="w-full text-black px-4 py-2 border border-gray-600 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -1,24 +1,25 @@
|
|||||||
"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();
|
|
||||||
const [openIndex, setOpenIndex] = useState<number | null>(0);
|
|
||||||
|
|
||||||
const defaultItems: FaqItem[] = [
|
|
||||||
{
|
{
|
||||||
questionKey: "faq.items.0.question",
|
questionKey: "faq.items.0.question",
|
||||||
answerKey: "faq.items.0.answer",
|
answerKey: "faq.items.0.answer",
|
||||||
@@ -31,9 +32,24 @@ export function FAQ({ items }: FaqProps) {
|
|||||||
questionKey: "faq.items.2.question",
|
questionKey: "faq.items.2.question",
|
||||||
answerKey: "faq.items.2.answer",
|
answerKey: "faq.items.2.answer",
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const faqItems = items || defaultItems;
|
export function FAQ() {
|
||||||
|
const { t, language } = useLanguage();
|
||||||
|
const [openIndex, setOpenIndex] = useState<number | null>(0);
|
||||||
|
const [faqItems, setFaqItems] = useState<FaqItem[]>([]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
async function fetchFaq() {
|
||||||
|
const allFaq = await getAllFaq();
|
||||||
|
const faqItems: FaqItem[] = allFaq.map((item) => ({
|
||||||
|
questionKey: language === "uz" ? item.question_uz : item.question_ru,
|
||||||
|
answerKey: language === "uz" ? item.answer_uz : item.answer_ru,
|
||||||
|
}));
|
||||||
|
faqItems.length === 0 ? setFaqItems(defaultItems) : setFaqItems(faqItems);
|
||||||
|
}
|
||||||
|
fetchFaq();
|
||||||
|
}, [language]);
|
||||||
|
|
||||||
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>
|
||||||
|
|||||||
@@ -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,15 +49,31 @@ 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) =>
|
||||||
|
link.id === "products" ? (
|
||||||
|
<Link
|
||||||
|
key={link.id}
|
||||||
|
href={link.href}
|
||||||
|
className="text-[#468965] hover:text-[#468965] transition-colors hover:cursor-pointer"
|
||||||
|
>
|
||||||
|
{link.labelKey}
|
||||||
|
</Link>
|
||||||
|
) : (
|
||||||
<motion.button
|
<motion.button
|
||||||
key={link.id}
|
key={link.id}
|
||||||
whileHover={{ color: "#2563eb" }}
|
whileHover={{ color: "#2563eb" }}
|
||||||
@@ -64,7 +82,8 @@ export function Navbar({ logoText = "FIRMA" }: NavbarProps) {
|
|||||||
>
|
>
|
||||||
{link.labelKey}
|
{link.labelKey}
|
||||||
</motion.button>
|
</motion.button>
|
||||||
))}
|
)
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Language & Mobile Menu */}
|
{/* Language & Mobile Menu */}
|
||||||
|
|||||||
99
components/detailPage/detailInfo.tsx
Normal file
99
components/detailPage/detailInfo.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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
16
components/loading.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -5,16 +5,26 @@ 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 (
|
||||||
|
<Link
|
||||||
|
href={`/product/${product.slug}`}
|
||||||
|
onClick={() => {
|
||||||
|
setProductName(languageIndex ? product.name_uz : product.name_ru);
|
||||||
|
setProducts(product);
|
||||||
|
}}
|
||||||
|
>
|
||||||
<motion.div
|
<motion.div
|
||||||
initial={{ opacity: 0, y: 20 }}
|
initial={{ opacity: 0, y: 20 }}
|
||||||
animate={{ opacity: 1, y: 0 }}
|
animate={{ opacity: 1, y: 0 }}
|
||||||
@@ -26,11 +36,12 @@ export function ProductCard({ product, onViewDetails }: ProductCardProps) {
|
|||||||
<motion.div
|
<motion.div
|
||||||
whileHover={{ scale: 1.05 }}
|
whileHover={{ scale: 1.05 }}
|
||||||
transition={{ duration: 0.4 }}
|
transition={{ duration: 0.4 }}
|
||||||
className="w-full h-full"
|
className="w-full h-full relative"
|
||||||
>
|
>
|
||||||
<img
|
<Image
|
||||||
src={`https://api.serenmebel.uz${product.image}`}
|
src={`https://admin.promtechno.uz${product.image}`}
|
||||||
alt={languageIndex?product.name_uz:product.name_ru}
|
alt={languageIndex ? product.name_uz : product.name_ru}
|
||||||
|
fill
|
||||||
className="w-full h-full object-cover"
|
className="w-full h-full object-cover"
|
||||||
/>
|
/>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
@@ -43,28 +54,28 @@ export function ProductCard({ product, onViewDetails }: ProductCardProps) {
|
|||||||
<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.button>
|
</motion.div>
|
||||||
</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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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://api.serenmebel.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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -8,34 +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 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.get("https://admin.promtechno.uz/api/products/").then((res) => {
|
setLoading(true);
|
||||||
console.log("all data main page: ", res?.data);
|
const products = await getAllProducts();
|
||||||
const allData = res?.data || [];
|
setAllProducts(
|
||||||
setAllProducts(allData.slice(0,3));
|
products.map((product: any) => ({
|
||||||
});
|
...product,
|
||||||
|
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">
|
||||||
@@ -63,7 +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 */}
|
||||||
|
{loading || (allProducts && allProducts.length > 0) ? (
|
||||||
<motion.div
|
<motion.div
|
||||||
initial="hidden"
|
initial="hidden"
|
||||||
whileInView="visible"
|
whileInView="visible"
|
||||||
@@ -71,14 +78,14 @@ export function ProductsGrid() {
|
|||||||
className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8"
|
className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8"
|
||||||
>
|
>
|
||||||
{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>
|
||||||
|
) : (
|
||||||
|
<EmptyState page="main" />
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-10 w-full flex items-center justify-center">
|
<div className="mt-10 w-full flex items-center justify-center">
|
||||||
<Link
|
<Link
|
||||||
@@ -89,14 +96,6 @@ export function ProductsGrid() {
|
|||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{/* Product Modal */}
|
|
||||||
{selectedProduct && (
|
|
||||||
<ProductModal
|
|
||||||
product={selectedProduct}
|
|
||||||
onClose={() => setSelectedProduct(null)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { useLanguage } from "@/context/language-context";
|
|||||||
|
|
||||||
//salomalr
|
//salomalr
|
||||||
|
|
||||||
export default function EmptyState() {
|
export default function EmptyState({page}:{page: string}) {
|
||||||
const { t } = useLanguage();
|
const { t } = useLanguage();
|
||||||
|
|
||||||
const container = {
|
const container = {
|
||||||
@@ -23,7 +23,7 @@ export default function EmptyState() {
|
|||||||
animate="show"
|
animate="show"
|
||||||
variants={container}
|
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="max-w-5xl w-full bg-linear-to-br from-primary via-primary/50 to-gray-300 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">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 items-center p-8 md:p-12">
|
||||||
{/* Illustration / Icon */}
|
{/* Illustration / Icon */}
|
||||||
<motion.div className="flex items-center justify-center">
|
<motion.div className="flex items-center justify-center">
|
||||||
@@ -54,6 +54,7 @@ export default function EmptyState() {
|
|||||||
{t.empty_data.description}
|
{t.empty_data.description}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
{page !== "main" && (
|
||||||
<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">
|
<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.div
|
<motion.div
|
||||||
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"
|
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"
|
||||||
@@ -62,6 +63,7 @@ export default function EmptyState() {
|
|||||||
<Link href="/">{t.empty_data.back || "Bosh sahifa"}</Link>
|
<Link href="/">{t.empty_data.back || "Bosh sahifa"}</Link>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
</motion.div>
|
</motion.div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -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,48 +14,41 @@ 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 />
|
<EmptyState page="products" />
|
||||||
)}
|
|
||||||
{/* Product Modal */}
|
|
||||||
{selectedProduct && (
|
|
||||||
<ProductModal
|
|
||||||
product={selectedProduct}
|
|
||||||
onClose={() => setSelectedProduct(null)}
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
26
lib/api.ts
Normal file
26
lib/api.ts
Normal 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();
|
||||||
|
}
|
||||||
17
lib/generateStaticParam.ts
Normal file
17
lib/generateStaticParam.ts
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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(
|
devtools((set) => ({
|
||||||
persist(
|
|
||||||
(set) => ({
|
|
||||||
productName: "",
|
productName: "",
|
||||||
setProductName: (name: string) => set({ productName: name }),
|
setProductName: (name: string) => set({ productName: name }),
|
||||||
resetProductName: () => set({ productName: "" }),
|
resetProductName: () => set({ productName: "" }),
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
|
||||||
|
interface ProductDetail {
|
||||||
|
product: Product | null;
|
||||||
|
setProducts: (product: Product) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useProduct = create<ProductDetail>()(
|
||||||
|
devtools(
|
||||||
|
persist(
|
||||||
|
(set) => ({
|
||||||
|
product: null,
|
||||||
|
setProducts: (product: Product) => set({ product }),
|
||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
name: "product-storage",
|
name: "product-detail-storage",
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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
12
lib/slug.ts
Normal 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
BIN
public/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
BIN
public/logo1.png
Normal file
BIN
public/logo1.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.3 MiB |
Reference in New Issue
Block a user