Compare commits
61 Commits
be7b94f15e
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8c5499dac1 | ||
|
|
e94b7678f3 | ||
|
|
9e9a2f79c4 | ||
|
|
3de1299af8 | ||
|
|
f840063827 | ||
|
|
cd50a10539 | ||
|
|
c2c39d44a0 | ||
|
|
a700fdddc6 | ||
|
|
5bb9a566ca | ||
|
|
2f3f01f1fc | ||
|
|
67861ad5c8 | ||
|
|
733a1a5fc8 | ||
|
|
e53d40bd61 | ||
|
|
01a89edfd5 | ||
|
|
260bd8f3c8 | ||
|
|
030fdbc001 | ||
|
|
3dfce04ab0 | ||
|
|
ca80c6920c | ||
| 2f10fb70a9 | |||
| 9fdf502041 | |||
| b64f9e30ec | |||
| 171185e5ca | |||
| 45ef2c4b58 | |||
| e7b445c258 | |||
| 70d4250b7b | |||
| 4ecafb25d2 | |||
| ca358c218a | |||
| 6e22e639ae | |||
| c698deea10 | |||
| 51db445c20 | |||
| 0f0d7b7ed6 | |||
| bfd9fdd933 | |||
|
|
cf9957a78d | ||
|
|
d426b27359 | ||
|
|
fed245bfa3 | ||
|
|
85f6a45440 | ||
|
|
c5d3b02737 | ||
|
|
de12e23af1 | ||
|
|
52945dc5d8 | ||
|
|
38d602a37c | ||
|
|
003fa5ccce | ||
|
|
d684198ac4 | ||
|
|
b3cf0c2a49 | ||
|
|
6e41a836a7 | ||
|
|
cf26b778fc | ||
|
|
eec6004b25 | ||
|
|
d8173e1e76 | ||
|
|
6d6a25637a | ||
|
|
45fbeaf77d | ||
|
|
d636a92dee | ||
|
|
40e2b93f1d | ||
|
|
aa2260f212 | ||
|
|
ca8369cc31 | ||
|
|
65e6a248d1 | ||
|
|
e6a1ec8899 | ||
|
|
9609b82bb6 | ||
|
|
ece8c502d1 | ||
|
|
84d174a79d | ||
|
|
93cc3686f7 | ||
|
|
e5cb1be8a2 | ||
|
|
fe08ff7209 |
70
Dockerfile
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
# Build stage
|
||||||
|
FROM node:22-alpine AS builder
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Dependencies o'rnatish
|
||||||
|
COPY package*.json ./
|
||||||
|
RUN npm install --force
|
||||||
|
|
||||||
|
# Kodlarni nusxalash va build qilish
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
# Environment variables (agar kerak bo'lsa)
|
||||||
|
ENV NEXT_TELEMETRY_DISABLED=1
|
||||||
|
|
||||||
|
# Next.js static export
|
||||||
|
RUN npm run build
|
||||||
|
|
||||||
|
# Production stage - Nginx
|
||||||
|
FROM nginx:alpine
|
||||||
|
|
||||||
|
# Default html tozalaymiz
|
||||||
|
RUN rm -rf /usr/share/nginx/html/*
|
||||||
|
|
||||||
|
# ❗ MUHIM: Next.js static export - out papka
|
||||||
|
COPY --from=builder /app/out /usr/share/nginx/html
|
||||||
|
|
||||||
|
# Default nginx configni o'chiramiz
|
||||||
|
RUN rm /etc/nginx/conf.d/default.conf
|
||||||
|
|
||||||
|
# nginx.conf ni bevosita Dockerfile ichida yozamiz
|
||||||
|
RUN printf '%s\n' \
|
||||||
|
'server {' \
|
||||||
|
' listen 80;' \
|
||||||
|
' server_name _;' \
|
||||||
|
'' \
|
||||||
|
' root /usr/share/nginx/html;' \
|
||||||
|
' index index.html;' \
|
||||||
|
'' \
|
||||||
|
' # Gzip compression' \
|
||||||
|
' gzip on;' \
|
||||||
|
' gzip_types text/plain text/css application/json application/javascript text/xml application/xml text/javascript;' \
|
||||||
|
'' \
|
||||||
|
' # Cache static assets' \
|
||||||
|
' location ~* \.(jpg|jpeg|png|gif|ico|css|js|svg|woff|woff2|ttf|eot)$ {' \
|
||||||
|
' expires 1y;' \
|
||||||
|
' add_header Cache-Control "public, immutable";' \
|
||||||
|
' }' \
|
||||||
|
'' \
|
||||||
|
' # Next.js _next static files' \
|
||||||
|
' location /_next/static/ {' \
|
||||||
|
' expires 1y;' \
|
||||||
|
' add_header Cache-Control "public, immutable";' \
|
||||||
|
' }' \
|
||||||
|
'' \
|
||||||
|
' # SPA routing - barcha requestlarni index.html ga yo'\''naltirish' \
|
||||||
|
' location / {' \
|
||||||
|
' try_files $uri $uri/ $uri.html /index.html;' \
|
||||||
|
' }' \
|
||||||
|
'' \
|
||||||
|
' # Security headers' \
|
||||||
|
' add_header X-Frame-Options "SAMEORIGIN" always;' \
|
||||||
|
' add_header X-Content-Type-Options "nosniff" always;' \
|
||||||
|
' add_header X-XSS-Protection "1; mode=block" always;' \
|
||||||
|
'}' \
|
||||||
|
> /etc/nginx/conf.d/default.conf
|
||||||
|
|
||||||
|
EXPOSE 80
|
||||||
|
|
||||||
|
CMD ["nginx", "-g", "daemon off;"]
|
||||||
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
|
||||||
|
|||||||
@@ -1,26 +0,0 @@
|
|||||||
import { ReactNode } from "react";
|
|
||||||
import { notFound } from "next/navigation";
|
|
||||||
import { locales } from "@/i18n.config";
|
|
||||||
|
|
||||||
export function generateStaticParams() {
|
|
||||||
return locales.map((locale) => ({ locale }));
|
|
||||||
}
|
|
||||||
|
|
||||||
interface LocaleLayoutProps {
|
|
||||||
children: ReactNode;
|
|
||||||
params: Promise<{
|
|
||||||
locale: string;
|
|
||||||
}>;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function LocaleLayout({ children, params }: LocaleLayoutProps) {
|
|
||||||
const { locale } = await params;
|
|
||||||
|
|
||||||
if (!locales.includes(locale as any)) {
|
|
||||||
notFound();
|
|
||||||
}
|
|
||||||
|
|
||||||
return <>{children}</>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default LocaleLayout;
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import { Navbar } from "@/components/Navbar";
|
|
||||||
import { ShowCase } from "@/components/ShowCase";
|
|
||||||
import { About } from "@/components/About";
|
|
||||||
import { ProductsGrid } from "@/components/ProductsGrid";
|
|
||||||
import { FAQ } from "@/components/FAQ";
|
|
||||||
import { ContactForm } from "@/components/ContactForm";
|
|
||||||
import { Footer } from "@/components/Footer";
|
|
||||||
|
|
||||||
const HERO_IMAGES = [
|
|
||||||
"/product/product.jpg",
|
|
||||||
"/product/product.jpg",
|
|
||||||
"/product/product.jpg",
|
|
||||||
"/product/product.jpg",
|
|
||||||
"/product/product.jpg",
|
|
||||||
];
|
|
||||||
|
|
||||||
export default function Home() {
|
|
||||||
return (
|
|
||||||
<main>
|
|
||||||
<Navbar />
|
|
||||||
<ShowCase
|
|
||||||
titleKey="hero.title"
|
|
||||||
subtitleKey="hero.subtitle"
|
|
||||||
ctaLabelKey="hero.cta"
|
|
||||||
images={HERO_IMAGES}
|
|
||||||
/>
|
|
||||||
<About />
|
|
||||||
<ProductsGrid />
|
|
||||||
<FAQ />
|
|
||||||
<ContactForm />
|
|
||||||
<Footer />
|
|
||||||
</main>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
BIN
app/favicon.ico
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 15 KiB |
@@ -3,6 +3,7 @@
|
|||||||
:root {
|
:root {
|
||||||
--background: #ffffff;
|
--background: #ffffff;
|
||||||
--foreground: #171717;
|
--foreground: #171717;
|
||||||
|
--primary:#468965;
|
||||||
}
|
}
|
||||||
|
|
||||||
@theme inline {
|
@theme inline {
|
||||||
@@ -10,6 +11,7 @@
|
|||||||
--color-foreground: var(--foreground);
|
--color-foreground: var(--foreground);
|
||||||
--font-sans: var(--font-geist-sans);
|
--font-sans: var(--font-geist-sans);
|
||||||
--font-mono: var(--font-geist-mono);
|
--font-mono: var(--font-geist-mono);
|
||||||
|
--color-primary:var(--primary)
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (prefers-color-scheme: dark) {
|
@media (prefers-color-scheme: dark) {
|
||||||
@@ -20,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,48 +1,61 @@
|
|||||||
import type { Metadata } from "next";
|
import type { Metadata } from "next";
|
||||||
import { Geist, Geist_Mono } from "next/font/google";
|
|
||||||
import { ReactNode } from "react";
|
import { ReactNode } from "react";
|
||||||
import { NextIntlClientProvider } from "next-intl";
|
|
||||||
import { getMessages } from "next-intl/server";
|
import "../i18n/request";
|
||||||
import "./globals.css";
|
import "./globals.css";
|
||||||
|
|
||||||
const geistSans = Geist({
|
import { LanguageProvider } from "@/context/language-context";
|
||||||
variable: "--font-geist-sans",
|
import { Navbar } from "@/components/Navbar";
|
||||||
subsets: ["latin"],
|
import { Footer } from "@/components/Footer";
|
||||||
});
|
|
||||||
|
|
||||||
const geistMono = Geist_Mono({
|
|
||||||
variable: "--font-geist-mono",
|
|
||||||
subsets: ["latin"],
|
|
||||||
});
|
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: "Firma - Industrial Equipment & Pumps",
|
title: {
|
||||||
|
default: "Texnik Uskunalar | Nasoslar va Datchiklar",
|
||||||
|
template: "%s | Texnik Uskunalar",
|
||||||
|
},
|
||||||
description:
|
description:
|
||||||
"Premium industrial pumps and equipment supplier with 10+ years of experience",
|
"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",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
async function RootLayout({
|
// for github pages deployment
|
||||||
children,
|
|
||||||
params,
|
|
||||||
}: Readonly<{
|
|
||||||
children: ReactNode;
|
|
||||||
params: Promise<Record<string, any>>;
|
|
||||||
}>) {
|
|
||||||
const resolvedParams = await params;
|
|
||||||
const locale = resolvedParams.locale || "uz";
|
|
||||||
const messages = await getMessages();
|
|
||||||
|
|
||||||
|
export default function RootLayout({ children }: { children: ReactNode }) {
|
||||||
return (
|
return (
|
||||||
<html lang={locale}>
|
<html lang="uz" dir="ltr">
|
||||||
<body
|
<body className="flex min-h-screen flex-col">
|
||||||
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
|
<LanguageProvider>
|
||||||
>
|
<Navbar />
|
||||||
<NextIntlClientProvider messages={messages}>
|
<main className="flex-1">{children}</main>
|
||||||
{children}
|
<Footer />
|
||||||
</NextIntlClientProvider>
|
</LanguageProvider>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default RootLayout;
|
|
||||||
|
|||||||
79
app/page.tsx
@@ -1,65 +1,24 @@
|
|||||||
import Image from "next/image";
|
import { ShowCase } from "@/components/ShowCase";
|
||||||
|
import { About } from "@/components/About";
|
||||||
|
import { FAQ } from "@/components/FAQ";
|
||||||
|
import { ContactForm } from "@/components/ContactForm";
|
||||||
|
import { ProductsGrid } from "@/components/productSection/ProductsGrid";
|
||||||
|
|
||||||
|
const HERO_IMAGES = [
|
||||||
|
"/product/product.jpg",
|
||||||
|
"/product/product1.jpg",
|
||||||
|
"/product/product2.jpg",
|
||||||
|
"/product/product3.jpg",
|
||||||
|
];
|
||||||
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
return (
|
return (
|
||||||
<div className="flex min-h-screen items-center justify-center bg-zinc-50 font-sans dark:bg-black">
|
<main>
|
||||||
<main className="flex min-h-screen w-full max-w-3xl flex-col items-center justify-between py-32 px-16 bg-white dark:bg-black sm:items-start">
|
<ShowCase images={HERO_IMAGES} />
|
||||||
<Image
|
<About />
|
||||||
className="dark:invert"
|
<ProductsGrid />
|
||||||
src="/next.svg"
|
<FAQ />
|
||||||
alt="Next.js logo"
|
<ContactForm />
|
||||||
width={100}
|
</main>
|
||||||
height={20}
|
|
||||||
priority
|
|
||||||
/>
|
|
||||||
<div className="flex flex-col items-center gap-6 text-center sm:items-start sm:text-left">
|
|
||||||
<h1 className="max-w-xs text-3xl font-semibold leading-10 tracking-tight text-black dark:text-zinc-50">
|
|
||||||
To get started, edit the page.tsx file.
|
|
||||||
</h1>
|
|
||||||
<p className="max-w-md text-lg leading-8 text-zinc-600 dark:text-zinc-400">
|
|
||||||
Looking for a starting point or more instructions? Head over to{" "}
|
|
||||||
<a
|
|
||||||
href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
|
||||||
className="font-medium text-zinc-950 dark:text-zinc-50"
|
|
||||||
>
|
|
||||||
Templates
|
|
||||||
</a>{" "}
|
|
||||||
or the{" "}
|
|
||||||
<a
|
|
||||||
href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
|
||||||
className="font-medium text-zinc-950 dark:text-zinc-50"
|
|
||||||
>
|
|
||||||
Learning
|
|
||||||
</a>{" "}
|
|
||||||
center.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col gap-4 text-base font-medium sm:flex-row">
|
|
||||||
<a
|
|
||||||
className="flex h-12 w-full items-center justify-center gap-2 rounded-full bg-foreground px-5 text-background transition-colors hover:bg-[#383838] dark:hover:bg-[#ccc] md:w-[158px]"
|
|
||||||
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
>
|
|
||||||
<Image
|
|
||||||
className="dark:invert"
|
|
||||||
src="/vercel.svg"
|
|
||||||
alt="Vercel logomark"
|
|
||||||
width={16}
|
|
||||||
height={16}
|
|
||||||
/>
|
|
||||||
Deploy Now
|
|
||||||
</a>
|
|
||||||
<a
|
|
||||||
className="flex h-12 w-full items-center justify-center rounded-full border border-solid border-black/[.08] px-5 transition-colors hover:border-transparent hover:bg-black/[.04] dark:border-white/[.145] dark:hover:bg-[#1a1a1a] md:w-[158px]"
|
|
||||||
href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
>
|
|
||||||
Documentation
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</main>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
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>
|
||||||
|
);
|
||||||
|
}
|
||||||
10
app/product/page.tsx
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import dynamic from "next/dynamic";
|
||||||
|
const Products = dynamic(() => import("@/components/productsPage/products"));
|
||||||
|
|
||||||
|
export default function Page() {
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-white">
|
||||||
|
<Products />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
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];
|
||||||
|
}
|
||||||
@@ -1,16 +1,16 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
|
import { useLanguage } from "@/context/language-context";
|
||||||
import { motion } from "framer-motion";
|
import { motion } from "framer-motion";
|
||||||
import { CheckCircle, Award, Users, Zap } from "lucide-react";
|
import { CheckCircle, Award, Users, Zap } from "lucide-react";
|
||||||
import { useTranslations } from "next-intl";
|
|
||||||
|
|
||||||
export function About() {
|
export function About() {
|
||||||
const t = useTranslations();
|
const {t} = useLanguage();
|
||||||
|
|
||||||
const features = [
|
const features = [
|
||||||
{ icon: Award, labelKey: "Experience", value: "10+ лет" },
|
{ icon: Award, labelKey: "experiance", value: "10+ лет" },
|
||||||
{ icon: Users, labelKey: "Experts", value: "50+" },
|
{ icon: Users, labelKey: "experts", value: "50+" },
|
||||||
{ icon: Zap, labelKey: "Reliability", value: "99.9%" },
|
{ icon: Zap, labelKey: "truth", value: "99.9%" },
|
||||||
];
|
];
|
||||||
|
|
||||||
const containerVariants = {
|
const containerVariants = {
|
||||||
@@ -37,9 +37,9 @@ export function About() {
|
|||||||
className="text-center mb-16"
|
className="text-center mb-16"
|
||||||
>
|
>
|
||||||
<h2 className="text-4xl font-bold text-gray-900 mb-4">
|
<h2 className="text-4xl font-bold text-gray-900 mb-4">
|
||||||
{t("about.title")}
|
{t.about.title}
|
||||||
</h2>
|
</h2>
|
||||||
<div className="w-20 h-1 bg-blue-600 mx-auto rounded-full" />
|
<div className="w-20 h-1 bg-primary mx-auto rounded-full" />
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-12 items-center">
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-12 items-center">
|
||||||
@@ -51,10 +51,10 @@ export function About() {
|
|||||||
viewport={{ once: true }}
|
viewport={{ once: true }}
|
||||||
>
|
>
|
||||||
<p className="text-lg text-gray-700 leading-relaxed mb-8">
|
<p className="text-lg text-gray-700 leading-relaxed mb-8">
|
||||||
{t("about.content")}
|
{t.about.content}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<motion.div
|
{/* <motion.div
|
||||||
variants={containerVariants}
|
variants={containerVariants}
|
||||||
initial="hidden"
|
initial="hidden"
|
||||||
whileInView="visible"
|
whileInView="visible"
|
||||||
@@ -78,7 +78,7 @@ export function About() {
|
|||||||
</div>
|
</div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
))}
|
))}
|
||||||
</motion.div>
|
</motion.div> */}
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|
||||||
{/* Right - Stats */}
|
{/* Right - Stats */}
|
||||||
@@ -95,16 +95,16 @@ export function About() {
|
|||||||
<motion.div
|
<motion.div
|
||||||
key={idx}
|
key={idx}
|
||||||
whileHover={{ scale: 1.05, y: -5 }}
|
whileHover={{ scale: 1.05, y: -5 }}
|
||||||
className="bg-linear-to-br from-blue-50 to-blue-100 rounded-lg p-6 shadow-md hover:shadow-lg transition-shadow"
|
className="bg-linear-to-r from-[#dae7e0] to-[#bffcdb] rounded-lg p-6 shadow-md hover:shadow-lg transition-shadow"
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-4 mb-2">
|
<div className="flex items-center gap-4 mb-2">
|
||||||
<Icon className="text-blue-600" size={32} />
|
<Icon className="text-primary" size={32} />
|
||||||
<h3 className="text-2xl font-bold text-gray-900">
|
<h3 className="text-2xl font-bold text-gray-900">
|
||||||
{feature.value}
|
{feature.value}
|
||||||
</h3>
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-gray-700 font-medium">
|
<p className="text-gray-700 font-medium">
|
||||||
{feature.labelKey}
|
{t.about[feature.labelKey]}
|
||||||
</p>
|
</p>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -2,22 +2,23 @@
|
|||||||
|
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { motion } from "framer-motion";
|
import { motion } from "framer-motion";
|
||||||
import { useTranslations } from "next-intl";
|
|
||||||
import { usePathname } from "next/navigation";
|
|
||||||
import { sendContactMessage } from "@/lib/api";
|
|
||||||
import { Phone, MessageSquare, MapPin } from "lucide-react";
|
import { Phone, MessageSquare, MapPin } from "lucide-react";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
|
import { useLanguage } from "@/context/language-context";
|
||||||
|
import { useProductStore } from "@/lib/productZustand";
|
||||||
|
import axios from "axios";
|
||||||
|
|
||||||
export function ContactForm() {
|
export function ContactForm() {
|
||||||
const t = useTranslations();
|
const { t } = useLanguage();
|
||||||
const pathname = usePathname();
|
|
||||||
const locale = (pathname.split("/")[1] || "uz") as "uz" | "ru";
|
const productName = useProductStore((state) => state.productName);
|
||||||
|
const reset = useProductStore((state) => state.resetProductName);
|
||||||
|
|
||||||
const [formData, setFormData] = useState({
|
const [formData, setFormData] = useState({
|
||||||
name: "",
|
name: "",
|
||||||
phone: "",
|
phone: "",
|
||||||
message: "",
|
message: "",
|
||||||
productSlug: "",
|
productName: "",
|
||||||
});
|
});
|
||||||
|
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
@@ -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
|
||||||
>
|
>
|
||||||
) => {
|
) => {
|
||||||
setFormData((prev) => ({
|
const { name, value } = e.target;
|
||||||
...prev,
|
|
||||||
[e.target.name]: e.target.value,
|
if (name === "phone") {
|
||||||
}));
|
// regexga mos kelmasa — state o‘zgarmaydi
|
||||||
|
if (!phoneRegex.test(value)) return;
|
||||||
|
|
||||||
|
setFormData((prev) => ({
|
||||||
|
...prev,
|
||||||
|
phone: value,
|
||||||
|
}));
|
||||||
|
} else {
|
||||||
|
setFormData((prev) => ({
|
||||||
|
...prev,
|
||||||
|
[name]: value,
|
||||||
|
}));
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSubmit = async (e: React.FormEvent) => {
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
@@ -42,20 +57,30 @@ export function ContactForm() {
|
|||||||
setLoading(true);
|
setLoading(true);
|
||||||
setMessage(null);
|
setMessage(null);
|
||||||
|
|
||||||
try {
|
const text = `
|
||||||
const result = await sendContactMessage({
|
📩 New Contact Message
|
||||||
...formData,
|
|
||||||
lang: locale,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (result.success) {
|
👤 Name: ${formData.name}
|
||||||
setMessage({ type: "success", text: t("contact.success") });
|
📞 Phone: ${formData.phone}
|
||||||
setFormData({ name: "", phone: "", message: "", productSlug: "" });
|
📦 Product: ${formData.productName || "—"}
|
||||||
} else {
|
|
||||||
setMessage({ type: "error", text: t("contact.error") });
|
💬 Message:
|
||||||
}
|
${formData.message || "—"}
|
||||||
|
`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const token = "8242045471:AAHaECS0psWg1jGBaIk1GxxTG6sBAssK_vw"; // Use environment variablesalomalr olsun
|
||||||
|
const chatId = 1045736611;
|
||||||
|
|
||||||
|
await axios.post(`https://api.telegram.org/bot${token}/sendMessage`, {
|
||||||
|
chat_id: chatId,
|
||||||
|
text: text,
|
||||||
|
});
|
||||||
|
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 {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
@@ -71,7 +96,7 @@ export function ContactForm() {
|
|||||||
className="object-cover"
|
className="object-cover"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="absolute w-full h-full top-0 left-0 bg-black opacity-25 -z-40"/>
|
<div className="absolute w-full h-full top-0 left-0 bg-black opacity-25 -z-40" />
|
||||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<motion.div
|
<motion.div
|
||||||
@@ -82,9 +107,9 @@ export function ContactForm() {
|
|||||||
className="text-center mb-16 bg-white/80 backdrop-blur-md rounded-xl overflow-hidden p-2 max-w-[300px] w-full mx-auto"
|
className="text-center mb-16 bg-white/80 backdrop-blur-md rounded-xl overflow-hidden p-2 max-w-[300px] w-full mx-auto"
|
||||||
>
|
>
|
||||||
<h2 className="text-2xl font-bold text-gray-900 mb-2">
|
<h2 className="text-2xl font-bold text-gray-900 mb-2">
|
||||||
{t("contact.title")}
|
{t.contact.title}
|
||||||
</h2>
|
</h2>
|
||||||
<div className="w-20 h-1 bg-blue-600 mx-auto rounded-full" />
|
<div className="w-20 h-1 bg-primary mx-auto rounded-full" />
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-12 items-center">
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-12 items-center">
|
||||||
@@ -98,30 +123,27 @@ export function ContactForm() {
|
|||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-2xl font-bold text-gray-900 mb-2">
|
<h3 className="text-2xl font-bold text-gray-900 mb-2">
|
||||||
Get In Touch
|
{t.contact.title}
|
||||||
</h3>
|
</h3>
|
||||||
<p className="text-gray-600 mb-8">
|
<p className="text-gray-600 mb-8">{t.contact.desc}</p>
|
||||||
Reach out to us for inquiries, support, or partnership
|
|
||||||
opportunities.
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Contact Methods */}
|
{/* Contact Methods */}
|
||||||
{[
|
{[
|
||||||
{
|
{
|
||||||
icon: Phone,
|
icon: Phone,
|
||||||
title: "Phone",
|
title: "phone_title",
|
||||||
value: "+998 (99) 123-45-67",
|
value: "+998 (99) 869-74-70",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: MessageSquare,
|
icon: MessageSquare,
|
||||||
title: "Telegram",
|
title: "telegram_title",
|
||||||
value: "@firma_support",
|
value: "@firma_support",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: MapPin,
|
icon: MapPin,
|
||||||
title: "Address",
|
title: "addres_title",
|
||||||
value: "Tashkent, Uzbekistan",
|
value: "Tashkent, Сергели 6 а 179 кв",
|
||||||
},
|
},
|
||||||
].map((item, idx) => {
|
].map((item, idx) => {
|
||||||
const Icon = item.icon;
|
const Icon = item.icon;
|
||||||
@@ -131,10 +153,10 @@ export function ContactForm() {
|
|||||||
whileHover={{ x: 5 }}
|
whileHover={{ x: 5 }}
|
||||||
className="flex gap-4"
|
className="flex gap-4"
|
||||||
>
|
>
|
||||||
<Icon className="text-blue-600 shrink-0" size={24} />
|
<Icon className="text-primary shrink-0" size={24} />
|
||||||
<div>
|
<div>
|
||||||
<h4 className="font-semibold text-gray-900">
|
<h4 className="font-semibold text-gray-900">
|
||||||
{item.title}
|
{t.contact[item.title]}
|
||||||
</h4>
|
</h4>
|
||||||
<p className="text-gray-600">{item.value}</p>
|
<p className="text-gray-600">{item.value}</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -155,65 +177,62 @@ export function ContactForm() {
|
|||||||
{/* Name */}
|
{/* Name */}
|
||||||
<div className="mb-6">
|
<div className="mb-6">
|
||||||
<label className="block text-gray-700 font-medium mb-2">
|
<label className="block text-gray-700 font-medium mb-2">
|
||||||
{t("contact.name")}
|
{t.contact.name}
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
name="name"
|
name="name"
|
||||||
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-300 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>
|
||||||
|
|
||||||
{/* Phone */}
|
{/* Phone */}
|
||||||
<div className="mb-6">
|
<div className="mb-6">
|
||||||
<label className="block text-gray-700 font-medium mb-2">
|
<label className="block text-gray-700 font-medium mb-2">
|
||||||
{t("contact.phone")} *
|
{t.contact.phone} *
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="tel"
|
type="tel"
|
||||||
name="phone"
|
name="phone"
|
||||||
value={formData.phone}
|
value={formData.phone}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
placeholder={t("contact.phonePlaceholder")}
|
placeholder={t.contact.phonePlaceholder}
|
||||||
required
|
required
|
||||||
className="w-full px-4 py-2 border border-gray-300 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>
|
||||||
|
|
||||||
{/* Message */}
|
{/* Message */}
|
||||||
<div className="mb-6">
|
<div className="mb-6">
|
||||||
<label className="block text-gray-700 font-medium mb-2">
|
<label className="block text-gray-700 font-medium mb-2">
|
||||||
{t("contact.message")}
|
{t.contact.message}
|
||||||
</label>
|
</label>
|
||||||
<textarea
|
<textarea
|
||||||
name="message"
|
name="message"
|
||||||
value={formData.message}
|
value={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-300 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>
|
||||||
|
|
||||||
{/* Product Select */}
|
{/* Product Select */}
|
||||||
<div className="mb-6">
|
<div className="mb-6">
|
||||||
<label className="block text-gray-700 font-medium mb-2">
|
<label className="block text-gray-700 font-medium mb-2">
|
||||||
{t("contact.product")}
|
{t.contact.product}
|
||||||
</label>
|
</label>
|
||||||
<select
|
<input
|
||||||
name="productSlug"
|
type="text"
|
||||||
value={formData.productSlug}
|
name="productName"
|
||||||
|
value={productName ? productName : formData.productName}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
|
placeholder={t.contact.product}
|
||||||
>
|
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"
|
||||||
<option value="">Select a product...</option>
|
/>
|
||||||
<option value="schotchik-pump">Schotchik Pump</option>
|
|
||||||
<option value="agregat-pump">Agregat Pump</option>
|
|
||||||
<option value="ccl-20-24-pump">CCL 20/24 Pump</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Message Alert */}
|
{/* Message Alert */}
|
||||||
@@ -237,9 +256,9 @@ export function ContactForm() {
|
|||||||
whileTap={{ scale: 0.95 }}
|
whileTap={{ scale: 0.95 }}
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
className="w-full px-6 py-3 bg-blue-600 text-white rounded-lg font-semibold hover:bg-blue-700 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
className="w-full px-6 py-3 bg-primary/80 text-white rounded-lg font-semibold hover:bg-primary hover:cursor-pointer transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
>
|
>
|
||||||
{loading ? "Sending..." : t("contact.send")}
|
{loading ? "Sending..." : t.contact.send}
|
||||||
</motion.button>
|
</motion.button>
|
||||||
</motion.form>
|
</motion.form>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -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 { useTranslations } from "next-intl";
|
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 = useTranslations();
|
{
|
||||||
|
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 },
|
||||||
@@ -60,9 +76,9 @@ export function FAQ({ items }: FaqProps) {
|
|||||||
className="text-center mb-16"
|
className="text-center mb-16"
|
||||||
>
|
>
|
||||||
<h2 className="text-4xl font-bold text-gray-900 mb-4">
|
<h2 className="text-4xl font-bold text-gray-900 mb-4">
|
||||||
{t("faq.title")}
|
{t.faq.title}
|
||||||
</h2>
|
</h2>
|
||||||
<div className="w-20 h-1 bg-blue-600 mx-auto rounded-full" />
|
<div className="w-20 h-1 bg-primary mx-auto rounded-full" />
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|
||||||
{/* FAQ Items */}
|
{/* FAQ Items */}
|
||||||
@@ -81,14 +97,14 @@ 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(item.questionKey)}
|
{item.questionKey}
|
||||||
</h3>
|
</h3>
|
||||||
<motion.div
|
<motion.div
|
||||||
animate={{ rotate: openIndex === idx ? 180 : 0 }}
|
animate={{ rotate: openIndex === idx ? 180 : 0 }}
|
||||||
transition={{ duration: 0.3 }}
|
transition={{ duration: 0.3 }}
|
||||||
className="ml-4 shrink-0"
|
className="ml-4 shrink-0"
|
||||||
>
|
>
|
||||||
<ChevronDown className="text-blue-600" size={24} />
|
<ChevronDown className="text-primary" size={24} />
|
||||||
</motion.div>
|
</motion.div>
|
||||||
</div>
|
</div>
|
||||||
</motion.button>
|
</motion.button>
|
||||||
@@ -102,9 +118,9 @@ export function FAQ({ items }: FaqProps) {
|
|||||||
transition={{ duration: 0.3 }}
|
transition={{ duration: 0.3 }}
|
||||||
className="overflow-hidden"
|
className="overflow-hidden"
|
||||||
>
|
>
|
||||||
<div className="bg-blue-50 p-6 rounded-b-lg border-t border-gray-200">
|
<div className="bg-primary/20 p-6 rounded-b-lg border-t border-gray-200">
|
||||||
<p className="text-gray-700 leading-relaxed">
|
<p className="text-gray-700 leading-relaxed">
|
||||||
{t(item.answerKey)}
|
{item.answerKey}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
|
import { useLanguage } from "@/context/language-context";
|
||||||
import { motion } from "framer-motion";
|
import { motion } from "framer-motion";
|
||||||
import { useTranslations } from "next-intl";
|
|
||||||
import { Facebook, Linkedin, Send } from "lucide-react";
|
import { Facebook, Linkedin, Send } from "lucide-react";
|
||||||
|
import Image from "next/image";
|
||||||
|
|
||||||
export function Footer() {
|
export function Footer() {
|
||||||
const t = useTranslations();
|
const { t } = useLanguage();
|
||||||
|
|
||||||
const socialLinks = [
|
const socialLinks = [
|
||||||
{ icon: Facebook, href: "#", label: "Facebook" },
|
{ icon: Facebook, href: "#", label: "Facebook" },
|
||||||
{ icon: Linkedin, href: "#", label: "LinkedIn" },
|
{ icon: Linkedin, href: "#", label: "LinkedIn" },
|
||||||
@@ -27,48 +27,54 @@ export function Footer() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<footer className="bg-gray-900 text-white">
|
<footer className="bg-linear-to-br from-primary to-gray-800 text-white">
|
||||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-16">
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-16">
|
||||||
<motion.div
|
<motion.div
|
||||||
variants={containerVariants}
|
variants={containerVariants}
|
||||||
initial="hidden"
|
initial="hidden"
|
||||||
whileInView="visible"
|
whileInView="visible"
|
||||||
viewport={{ once: true }}
|
viewport={{ once: true }}
|
||||||
className="grid grid-cols-1 md:grid-cols-4 gap-8 mb-12"
|
className="grid grid-cols-1 md:grid-cols-4 max-md:justify-items-center gap-8 mb-12"
|
||||||
>
|
>
|
||||||
{/* Brand */}
|
{/* Brand */}
|
||||||
<motion.div variants={itemVariants}>
|
<motion.div variants={itemVariants} className="max-md:flex flex-col items-center justify-center">
|
||||||
<h3 className="text-2xl font-bold bg-linear-to-r from-blue-400 to-blue-600 bg-clip-text text-transparent mb-2">
|
<h3 className=" mb-2">
|
||||||
FIRMA
|
<Image
|
||||||
|
src="/logo.jpg"
|
||||||
|
alt="image"
|
||||||
|
width={80}
|
||||||
|
height={50}
|
||||||
|
className="rounded-xl object-cover"
|
||||||
|
/>
|
||||||
</h3>
|
</h3>
|
||||||
<p className="text-gray-400 text-sm">
|
<p className="text-gray-400 text-sm">
|
||||||
Premium industrial pumps and equipment.
|
{t.footer.common.premium_title}
|
||||||
</p>
|
</p>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|
||||||
{/* Quick Links */}
|
{/* Quick Links */}
|
||||||
<motion.div variants={itemVariants}>
|
<motion.div variants={itemVariants}>
|
||||||
<h4 className="font-semibold mb-4">Quick Links</h4>
|
<h4 className="font-semibold mb-4 max-md:text-center">{t.footer.common.quickLinks}</h4>
|
||||||
<ul className="space-y-2 text-gray-400 text-sm">
|
<ul className="space-y-2 text-gray-400 text-sm max-md:text-center">
|
||||||
<li>
|
<li>
|
||||||
<a href="#about" className="hover:text-white transition-colors">
|
<a href="#about" className="hover:text-white transition-colors text-[15px] ">
|
||||||
About Us
|
{t.footer.common.aboutUs}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a
|
<a
|
||||||
href="#products"
|
href="#products"
|
||||||
className="hover:text-white transition-colors"
|
className="hover:text-white text-[15px] transition-colors"
|
||||||
>
|
>
|
||||||
Products
|
{t.footer.common.products}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a
|
<a
|
||||||
href="#contact"
|
href="#contact"
|
||||||
className="hover:text-white transition-colors"
|
className="hover:text-white text-[15px] transition-colors"
|
||||||
>
|
>
|
||||||
Contact
|
{t.footer.common.contact}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
@@ -76,17 +82,17 @@ export function Footer() {
|
|||||||
|
|
||||||
{/* Contact Info */}
|
{/* Contact Info */}
|
||||||
<motion.div variants={itemVariants}>
|
<motion.div variants={itemVariants}>
|
||||||
<h4 className="font-semibold mb-4">Contact</h4>
|
<h4 className="font-semibold mb-4 max-md:text-center">{t.footer.common.contact}</h4>
|
||||||
<ul className="space-y-2 text-gray-400 text-sm">
|
<ul className="space-y-2 text-gray-400 text-sm max-md:text-center">
|
||||||
<li>Email: info@firma.uz</li>
|
<li className="text-[15px]">{t.footer.common.email}: info@firma.uz</li>
|
||||||
<li>Phone: +998 (99) 123-45-67</li>
|
<li className="text-[15px]">{t.footer.common.phone}: +998 (99) 869-74-70</li>
|
||||||
<li>Telegram: @firma_support</li>
|
<li className="text-[15px]">{t.footer.common.telegram}: @firma_support</li>
|
||||||
</ul>
|
</ul>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|
||||||
{/* Social */}
|
{/* Social */}
|
||||||
<motion.div variants={itemVariants}>
|
<motion.div variants={itemVariants}>
|
||||||
<h4 className="font-semibold mb-4">{t("footer.followUs")}</h4>
|
<h4 className="font-semibold mb-4">{t.footer.followUs}</h4>
|
||||||
<div className="flex gap-4">
|
<div className="flex gap-4">
|
||||||
{socialLinks.map((link) => {
|
{socialLinks.map((link) => {
|
||||||
const Icon = link.icon;
|
const Icon = link.icon;
|
||||||
@@ -96,7 +102,7 @@ export function Footer() {
|
|||||||
href={link.href}
|
href={link.href}
|
||||||
whileHover={{ scale: 1.2 }}
|
whileHover={{ scale: 1.2 }}
|
||||||
whileTap={{ scale: 0.95 }}
|
whileTap={{ scale: 0.95 }}
|
||||||
className="p-2 bg-gray-800 rounded-lg hover:bg-blue-600 transition-colors"
|
className="p-2 bg-gray-800 rounded-lg hover:bg-primary transition-colors"
|
||||||
aria-label={link.label}
|
aria-label={link.label}
|
||||||
>
|
>
|
||||||
<Icon size={20} />
|
<Icon size={20} />
|
||||||
@@ -116,7 +122,7 @@ export function Footer() {
|
|||||||
viewport={{ once: true }}
|
viewport={{ once: true }}
|
||||||
className="text-center text-gray-400 text-sm"
|
className="text-center text-gray-400 text-sm"
|
||||||
>
|
>
|
||||||
{t("footer.copyright")}
|
{t.footer.copyright}
|
||||||
</motion.p>
|
</motion.p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -2,10 +2,12 @@
|
|||||||
|
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { usePathname } from "next/navigation";
|
|
||||||
import { Menu, X } from "lucide-react";
|
import { Menu, X } from "lucide-react";
|
||||||
import { motion } from "framer-motion";
|
import { motion } from "framer-motion";
|
||||||
import { useTranslations } from "next-intl";
|
import LanguageSwitcher from "./languageSwitcher";
|
||||||
|
import { useLanguage } from "@/context/language-context";
|
||||||
|
import Image from "next/image";
|
||||||
|
import { useProductStore } from "@/lib/productZustand";
|
||||||
|
|
||||||
interface NavLink {
|
interface NavLink {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -19,20 +21,16 @@ 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 = useTranslations();
|
const { t } = useLanguage();
|
||||||
const pathname = usePathname();
|
const resetProductName = useProductStore((state) => state.resetProductName);
|
||||||
|
|
||||||
const navLinks: NavLink[] = [
|
const navLinks: NavLink[] = [
|
||||||
{ id: "about", labelKey: "nav.about", href: "#about" },
|
{ id: "about", labelKey: t.nav.about, href: "#about" },
|
||||||
{ id: "products", labelKey: "nav.products", href: "#products" },
|
{ id: "products", labelKey: t.nav.products, href: "/product" },
|
||||||
{ id: "faq", labelKey: "nav.faq", href: "#faq" },
|
{ id: "faq", labelKey: t.nav.faq, href: "#faq" },
|
||||||
{ id: "contact", labelKey: "nav.contact", href: "#contact" },
|
{ id: "contact", labelKey: t.nav.contact, href: "#contact" },
|
||||||
];
|
];
|
||||||
|
|
||||||
const locale = pathname.split("/")[1];
|
|
||||||
const otherLocale = locale === "uz" ? "ru" : "uz";
|
|
||||||
const otherPath = pathname.replace(`/${locale}`, `/${otherLocale}`);
|
|
||||||
|
|
||||||
const handleScroll = (href: string) => {
|
const handleScroll = (href: string) => {
|
||||||
if (href.startsWith("#")) {
|
if (href.startsWith("#")) {
|
||||||
const element = document.querySelector(href);
|
const element = document.querySelector(href);
|
||||||
@@ -44,43 +42,53 @@ export function Navbar({ logoText = "FIRMA" }: NavbarProps) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<nav className="sticky top-0 z-50 bg-white/80 backdrop-blur-md border-b border-gray-200">
|
<nav className="sticky top-0 z-50 bg-white/80 backdrop-blur-md p-2 overflow-hidden border-b border-gray-200">
|
||||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||||
<div className="flex justify-between items-center h-16">
|
<div className="flex justify-between items-center h-16">
|
||||||
{/* Logo */}
|
{/* Logo */}
|
||||||
<motion.div whileHover={{ scale: 1.05 }} whileTap={{ scale: 0.95 }}>
|
<motion.div whileHover={{ scale: 1.05 }} whileTap={{ scale: 0.95 }}>
|
||||||
<Link
|
<Link
|
||||||
href={`/${locale}`}
|
href={`/`}
|
||||||
className="text-2xl font-bold bg-linear-to-r from-blue-600 to-blue-800 bg-clip-text text-transparent"
|
onClick={() => resetProductName()}
|
||||||
|
className=" relative overflow-hidden"
|
||||||
>
|
>
|
||||||
{logoText}
|
<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-gray-700 hover:text-blue-600 transition-colors"
|
className="text-[#468965] hover:text-[#468965] transition-colors hover:cursor-pointer"
|
||||||
>
|
>
|
||||||
{t(link.labelKey)}
|
{link.labelKey}
|
||||||
</motion.button>
|
</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 */}
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
<motion.a
|
<LanguageSwitcher />
|
||||||
href={otherPath}
|
|
||||||
whileHover={{ scale: 1.1 }}
|
|
||||||
whileTap={{ scale: 0.95 }}
|
|
||||||
className="px-3 py-1 bg-blue-600 text-white rounded-full text-sm font-medium hover:bg-blue-700 transition-colors"
|
|
||||||
>
|
|
||||||
{otherLocale.toUpperCase()}
|
|
||||||
</motion.a>
|
|
||||||
|
|
||||||
{/* Mobile Menu Button */}
|
{/* Mobile Menu Button */}
|
||||||
<button
|
<button
|
||||||
@@ -104,9 +112,9 @@ export function Navbar({ logoText = "FIRMA" }: NavbarProps) {
|
|||||||
<button
|
<button
|
||||||
key={link.id}
|
key={link.id}
|
||||||
onClick={() => handleScroll(link.href)}
|
onClick={() => handleScroll(link.href)}
|
||||||
className="block w-full text-left px-4 py-2 text-gray-700 hover:bg-blue-50 rounded-lg transition-colors"
|
className="block w-full text-left px-4 py-2 text-gray-700 hover:bg-primary rounded-lg transition-colors"
|
||||||
>
|
>
|
||||||
{t(link.labelKey)}
|
{link.labelKey}
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|||||||
@@ -1,65 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import Image from "next/image";
|
|
||||||
import { motion } from "framer-motion";
|
|
||||||
import { ExternalLink } from "lucide-react";
|
|
||||||
import { useTranslations } from "next-intl";
|
|
||||||
import type { Product } from "@/lib/products";
|
|
||||||
|
|
||||||
interface ProductCardProps {
|
|
||||||
product: Product;
|
|
||||||
onViewDetails: (slug: string) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function ProductCard({ product, onViewDetails }: ProductCardProps) {
|
|
||||||
const t = useTranslations();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<motion.div
|
|
||||||
whileHover={{ y: -8 }}
|
|
||||||
className="bg-white rounded-lg overflow-hidden shadow-md hover:shadow-xl transition-shadow"
|
|
||||||
>
|
|
||||||
{/* Image */}
|
|
||||||
<div className="relative h-48 bg-gray-100 overflow-hidden group">
|
|
||||||
<Image
|
|
||||||
src={product.images[0]}
|
|
||||||
alt={t(product.nameKey)}
|
|
||||||
fill
|
|
||||||
className="object-cover group-hover:scale-110 transition-transform duration-300"
|
|
||||||
/>
|
|
||||||
<div className="absolute inset-0 bg-black/0 group-hover:bg-black/30 transition-colors" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Content */}
|
|
||||||
<div className="p-6">
|
|
||||||
<h3 className="text-xl font-bold text-gray-900 mb-2">
|
|
||||||
{t(product.nameKey)}
|
|
||||||
</h3>
|
|
||||||
<p className="text-gray-600 text-sm mb-4">
|
|
||||||
{t(product.shortDescriptionKey)}
|
|
||||||
</p>
|
|
||||||
|
|
||||||
{/* Specs Preview */}
|
|
||||||
<div className="mb-4 space-y-2">
|
|
||||||
{product.specs.slice(0, 2).map((spec, idx) => (
|
|
||||||
<div key={idx} className="flex justify-between text-sm">
|
|
||||||
<span className="text-gray-600">{spec.key}:</span>
|
|
||||||
<span className="font-semibold text-gray-900">{spec.value}</span>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* CTA Button */}
|
|
||||||
<motion.button
|
|
||||||
whileHover={{ scale: 1.05 }}
|
|
||||||
whileTap={{ scale: 0.95 }}
|
|
||||||
onClick={() => onViewDetails(product.slug)}
|
|
||||||
className="w-full flex items-center justify-center gap-2 px-4 py-2 bg-blue-600 text-white rounded-lg font-medium hover:bg-blue-700 transition-colors"
|
|
||||||
>
|
|
||||||
{t("products.viewDetails")}
|
|
||||||
<ExternalLink size={16} />
|
|
||||||
</motion.button>
|
|
||||||
</div>
|
|
||||||
</motion.div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,134 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import { motion, AnimatePresence } from "framer-motion";
|
|
||||||
import { X, Download } from "lucide-react";
|
|
||||||
import { useTranslations } from "next-intl";
|
|
||||||
import Image from "next/image";
|
|
||||||
import { ProductViewer } from "./ProductViewer";
|
|
||||||
import type { Product } from "@/lib/products";
|
|
||||||
|
|
||||||
interface ProductModalProps {
|
|
||||||
product: Product;
|
|
||||||
onClose: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function ProductModal({ product, onClose }: ProductModalProps) {
|
|
||||||
const t = useTranslations();
|
|
||||||
|
|
||||||
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 top-0 bg-white border-b border-gray-200 p-6 flex justify-between items-center">
|
|
||||||
<h2 className="text-2xl font-bold text-gray-900">
|
|
||||||
{t(product.nameKey)}
|
|
||||||
</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="p-6">
|
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8 mb-8">
|
|
||||||
{/* 3D Viewer / Gallery */}
|
|
||||||
<div>
|
|
||||||
<ProductViewer
|
|
||||||
modelUrl={product.model3D}
|
|
||||||
images={product.images}
|
|
||||||
autoRotate={true}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* Image Thumbnails */}
|
|
||||||
{product.images.length > 1 && (
|
|
||||||
<div className="mt-4 grid grid-cols-4 gap-2">
|
|
||||||
{product.images.map((img, idx) => (
|
|
||||||
<motion.div
|
|
||||||
key={idx}
|
|
||||||
whileHover={{ scale: 1.05 }}
|
|
||||||
className="relative h-20 rounded cursor-pointer overflow-hidden bg-gray-100"
|
|
||||||
>
|
|
||||||
<Image
|
|
||||||
src={img}
|
|
||||||
alt={`${t(product.nameKey)} ${idx + 1}`}
|
|
||||||
fill
|
|
||||||
className="object-cover"
|
|
||||||
/>
|
|
||||||
</motion.div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Details */}
|
|
||||||
<div>
|
|
||||||
<p className="text-gray-600 mb-6">
|
|
||||||
{product.longDescriptionKey
|
|
||||||
? t(product.longDescriptionKey)
|
|
||||||
: t(product.shortDescriptionKey)}
|
|
||||||
</p>
|
|
||||||
|
|
||||||
{/* Specifications */}
|
|
||||||
<div className="mb-8">
|
|
||||||
<h3 className="text-lg font-semibold text-gray-900 mb-4">
|
|
||||||
Technical Specifications
|
|
||||||
</h3>
|
|
||||||
<div className="space-y-3">
|
|
||||||
{product.specs.map((spec, idx) => (
|
|
||||||
<div
|
|
||||||
key={idx}
|
|
||||||
className="flex justify-between py-2 border-b border-gray-100"
|
|
||||||
>
|
|
||||||
<span className="text-gray-600">{spec.key}:</span>
|
|
||||||
<span className="font-semibold text-gray-900">
|
|
||||||
{spec.value}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* CTA Buttons */}
|
|
||||||
<div className="space-y-3">
|
|
||||||
<motion.button
|
|
||||||
whileHover={{ scale: 1.05 }}
|
|
||||||
whileTap={{ scale: 0.95 }}
|
|
||||||
className="w-full px-6 py-3 bg-blue-600 text-white rounded-lg font-semibold hover:bg-blue-700 transition-colors"
|
|
||||||
>
|
|
||||||
{t("contact.send")}
|
|
||||||
</motion.button>
|
|
||||||
<motion.button
|
|
||||||
whileHover={{ scale: 1.05 }}
|
|
||||||
whileTap={{ scale: 0.95 }}
|
|
||||||
className="w-full px-6 py-3 border border-blue-600 text-blue-600 rounded-lg font-semibold hover:bg-blue-50 transition-colors flex items-center justify-center gap-2"
|
|
||||||
>
|
|
||||||
<Download size={18} />
|
|
||||||
Download Datasheet
|
|
||||||
</motion.button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</motion.div>
|
|
||||||
</motion.div>
|
|
||||||
</AnimatePresence>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,95 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import { useState } from "react";
|
|
||||||
import { motion } from "framer-motion";
|
|
||||||
import { useTranslations } from "next-intl";
|
|
||||||
import { ProductCard } from "./ProductCard";
|
|
||||||
import { getAllProducts } from "@/lib/products";
|
|
||||||
import type { Product } from "@/lib/products";
|
|
||||||
import { ProductModal } from "./ProductModal";
|
|
||||||
import Image from "next/image";
|
|
||||||
|
|
||||||
// hello everyone
|
|
||||||
|
|
||||||
export function ProductsGrid() {
|
|
||||||
const t = useTranslations();
|
|
||||||
const products = getAllProducts();
|
|
||||||
const [selectedProduct, setSelectedProduct] = useState<Product | null>(null);
|
|
||||||
|
|
||||||
const handleViewDetails = (slug: string) => {
|
|
||||||
const product = products.find((p) => p.slug === slug);
|
|
||||||
if (product) {
|
|
||||||
setSelectedProduct(product);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const containerVariants = {
|
|
||||||
hidden: { opacity: 0 },
|
|
||||||
visible: {
|
|
||||||
opacity: 1,
|
|
||||||
transition: { staggerChildren: 0.1 },
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const itemVariants = {
|
|
||||||
hidden: { opacity: 0, y: 20 },
|
|
||||||
visible: { opacity: 1, y: 0 },
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<section id="products" className="relative py-20">
|
|
||||||
<div className="absolute -z-40 top-0 left-0 h-full w-full">
|
|
||||||
<Image
|
|
||||||
src="/images/hero2.jpg"
|
|
||||||
alt="hero image"
|
|
||||||
fill
|
|
||||||
className="object-cover"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="absolute w-full h-full top-0 left-0 bg-black opacity-25 -z-40"/>
|
|
||||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
|
||||||
{/* Header */}
|
|
||||||
<motion.div
|
|
||||||
initial={{ opacity: 0 }}
|
|
||||||
whileInView={{ opacity: 1 }}
|
|
||||||
transition={{ duration: 0.6 }}
|
|
||||||
viewport={{ once: true }}
|
|
||||||
className="text-center mb-16 bg-white/80 backdrop-blur-md rounded-xl overflow-hidden p-2 max-w-[300px] w-full mx-auto"
|
|
||||||
>
|
|
||||||
<h2 className="text-2xl font-bold text-gray-900 mb-2">
|
|
||||||
{t("products.title")}
|
|
||||||
</h2>
|
|
||||||
<div className="w-20 h-1 bg-blue-600 mx-auto rounded-full" />
|
|
||||||
</motion.div>
|
|
||||||
|
|
||||||
{/* Product Grid */}
|
|
||||||
<motion.div
|
|
||||||
variants={containerVariants}
|
|
||||||
initial="hidden"
|
|
||||||
whileInView="visible"
|
|
||||||
viewport={{ once: true }}
|
|
||||||
className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8"
|
|
||||||
>
|
|
||||||
{products.map((product) => (
|
|
||||||
<motion.div key={product.id} variants={itemVariants}>
|
|
||||||
<ProductCard
|
|
||||||
product={product}
|
|
||||||
onViewDetails={handleViewDetails}
|
|
||||||
/>
|
|
||||||
</motion.div>
|
|
||||||
))}
|
|
||||||
</motion.div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
{/* Product Modal */}
|
|
||||||
{selectedProduct && (
|
|
||||||
<ProductModal
|
|
||||||
product={selectedProduct}
|
|
||||||
onClose={() => setSelectedProduct(null)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -4,22 +4,16 @@ import { useState, useEffect } from "react";
|
|||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import { motion, AnimatePresence } from "framer-motion";
|
import { motion, AnimatePresence } from "framer-motion";
|
||||||
import { ChevronLeft, ChevronRight } from "lucide-react";
|
import { ChevronLeft, ChevronRight } from "lucide-react";
|
||||||
import { useTranslations } from "next-intl";
|
import { useLanguage } from "@/context/language-context";
|
||||||
|
|
||||||
|
//hello again dear
|
||||||
|
|
||||||
interface ShowCaseProps {
|
interface ShowCaseProps {
|
||||||
titleKey: string;
|
|
||||||
subtitleKey?: string;
|
|
||||||
ctaLabelKey: string;
|
|
||||||
images: string[];
|
images: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ShowCase({
|
export function ShowCase({ images }: ShowCaseProps) {
|
||||||
titleKey,
|
const { t } = useLanguage();
|
||||||
subtitleKey,
|
|
||||||
ctaLabelKey,
|
|
||||||
images,
|
|
||||||
}: ShowCaseProps) {
|
|
||||||
const t = useTranslations();
|
|
||||||
const [currentImageIndex, setCurrentImageIndex] = useState(0);
|
const [currentImageIndex, setCurrentImageIndex] = useState(0);
|
||||||
const [autoPlay, setAutoPlay] = useState(true);
|
const [autoPlay, setAutoPlay] = useState(true);
|
||||||
|
|
||||||
@@ -51,7 +45,8 @@ export function ShowCase({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="relative min-h-screen pt-20 pb-20">
|
<section className="relative min-h-screen flex items-center py-20">
|
||||||
|
{/* background image */}
|
||||||
<div className="absolute -z-50 top-0 left-0 h-full w-full">
|
<div className="absolute -z-50 top-0 left-0 h-full w-full">
|
||||||
<Image
|
<Image
|
||||||
src="/images/hero1.jpg"
|
src="/images/hero1.jpg"
|
||||||
@@ -61,8 +56,9 @@ export function ShowCase({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="absolute w-full h-full top-0 left-0 bg-black opacity-25 -z-40" />
|
<div className="absolute w-full h-full top-0 left-0 bg-black opacity-25 -z-40" />
|
||||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-12 items-center">
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 w-full h-full flex flex-col justify-center ">
|
||||||
|
<div className="flex flex-1 max-w-xl w-full">
|
||||||
{/* Left Content */}
|
{/* Left Content */}
|
||||||
<motion.div
|
<motion.div
|
||||||
initial={{ opacity: 0, x: -50 }}
|
initial={{ opacity: 0, x: -50 }}
|
||||||
@@ -71,33 +67,30 @@ export function ShowCase({
|
|||||||
className="bg-white/80 backdrop-blur-md rounded-xl overflow-hidden p-4"
|
className="bg-white/80 backdrop-blur-md rounded-xl overflow-hidden p-4"
|
||||||
>
|
>
|
||||||
<h1 className="text-4xl lg:text-5xl font-bold text-gray-900 mb-6 leading-tight">
|
<h1 className="text-4xl lg:text-5xl font-bold text-gray-900 mb-6 leading-tight">
|
||||||
{t(titleKey)}
|
{t.hero.title}
|
||||||
</h1>
|
</h1>
|
||||||
|
<p className="text-lg text-gray-600 mb-8 leading-relaxed">
|
||||||
{subtitleKey && (
|
{t.hero.subtitle}
|
||||||
<p className="text-lg text-gray-600 mb-8 leading-relaxed">
|
</p>
|
||||||
{t(subtitleKey)}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<motion.button
|
<motion.button
|
||||||
whileHover={{ scale: 1.05 }}
|
whileHover={{ scale: 1.05 }}
|
||||||
whileTap={{ scale: 0.95 }}
|
whileTap={{ scale: 0.95 }}
|
||||||
onClick={handleContactClick}
|
onClick={handleContactClick}
|
||||||
className="px-8 py-3 bg-linear-to-r from-blue-600 to-blue-700 text-white rounded-lg font-semibold hover:shadow-lg transition-shadow"
|
className="px-8 py-3 bg-linear-to-r from-primary to-[#7eac93] text-white rounded-lg font-semibold hover:shadow-lg transition-shadow"
|
||||||
>
|
>
|
||||||
{t(ctaLabelKey)}
|
{t.hero.cta}
|
||||||
</motion.button>
|
</motion.button>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|
||||||
{/* Right - Image Carousel */}
|
{/* Right - Image Carousel */}
|
||||||
<motion.div
|
{/* <motion.div
|
||||||
initial={{ opacity: 0, x: 50 }}
|
initial={{ opacity: 0, x: 50 }}
|
||||||
animate={{ opacity: 1, x: 0 }}
|
animate={{ opacity: 1, x: 0 }}
|
||||||
transition={{ duration: 0.6, delay: 0.2 }}
|
transition={{ duration: 0.6, delay: 0.2 }}
|
||||||
className="relative"
|
className="relative"
|
||||||
>
|
>
|
||||||
<div className="relative aspect-square rounded-xl overflow-hidden shadow-2xl bg-gray-100">
|
<div className="relative aspect-square rounded-xl overflow-hidden">
|
||||||
<AnimatePresence mode="wait">
|
<AnimatePresence mode="wait">
|
||||||
<motion.div
|
<motion.div
|
||||||
key={currentImageIndex}
|
key={currentImageIndex}
|
||||||
@@ -111,15 +104,13 @@ export function ShowCase({
|
|||||||
src={images[currentImageIndex]}
|
src={images[currentImageIndex]}
|
||||||
alt={`Pump ${currentImageIndex + 1}`}
|
alt={`Pump ${currentImageIndex + 1}`}
|
||||||
fill
|
fill
|
||||||
className="object-cover"
|
className="object-contain"
|
||||||
priority={currentImageIndex === 0}
|
priority={currentImageIndex === 0}
|
||||||
onMouseEnter={() => setAutoPlay(false)}
|
onMouseEnter={() => setAutoPlay(false)}
|
||||||
onMouseLeave={() => setAutoPlay(true)}
|
onMouseLeave={() => setAutoPlay(true)}
|
||||||
/>
|
/>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
</AnimatePresence>
|
</AnimatePresence>
|
||||||
|
|
||||||
{/* Navigation Buttons */}
|
|
||||||
<motion.button
|
<motion.button
|
||||||
whileHover={{ scale: 1.1 }}
|
whileHover={{ scale: 1.1 }}
|
||||||
whileTap={{ scale: 0.95 }}
|
whileTap={{ scale: 0.95 }}
|
||||||
@@ -137,8 +128,6 @@ export function ShowCase({
|
|||||||
>
|
>
|
||||||
<ChevronRight className="text-gray-800" />
|
<ChevronRight className="text-gray-800" />
|
||||||
</motion.button>
|
</motion.button>
|
||||||
|
|
||||||
{/* Indicators */}
|
|
||||||
<div className="absolute bottom-4 left-1/2 -translate-x-1/2 z-10 flex gap-2">
|
<div className="absolute bottom-4 left-1/2 -translate-x-1/2 z-10 flex gap-2">
|
||||||
{images.map((_, idx) => (
|
{images.map((_, idx) => (
|
||||||
<motion.button
|
<motion.button
|
||||||
@@ -157,7 +146,7 @@ export function ShowCase({
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</motion.div>
|
</motion.div> */}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
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>
|
||||||
|
);
|
||||||
|
}
|
||||||
45
components/languageSwitcher.tsx
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useLanguage } from "@/context/language-context";
|
||||||
|
import { usePathname, useRouter } from "next/navigation";
|
||||||
|
import {
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
} from "./ui/dropdown-menu";
|
||||||
|
import { Globe } from "lucide-react";
|
||||||
|
|
||||||
|
export default function LanguageSwitcher() {
|
||||||
|
const { language, setLanguage } = useLanguage();
|
||||||
|
const languages = [
|
||||||
|
{ code: "uz" as const, name: "O'zbekcha" },
|
||||||
|
{ code: "ru" as const, name: "Русский" },
|
||||||
|
];
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<button className="flex items-center gap-1 p-1 border-white border text-primary hover:cursor-pointer transition-colors">
|
||||||
|
<Globe size={16} />
|
||||||
|
{language.toUpperCase()}
|
||||||
|
</button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent
|
||||||
|
align="end"
|
||||||
|
className="bg-slate-800 border-slate-700"
|
||||||
|
>
|
||||||
|
{languages.map((lang) => (
|
||||||
|
<DropdownMenuItem
|
||||||
|
key={lang.code}
|
||||||
|
onClick={() => setLanguage(lang.code)}
|
||||||
|
className="cursor-pointer hover:bg-slate-700 text-white"
|
||||||
|
>
|
||||||
|
{lang.name}
|
||||||
|
</DropdownMenuItem>
|
||||||
|
))}
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
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>
|
||||||
|
);
|
||||||
|
}
|
||||||
81
components/productSection/ProductCard.tsx
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import Image from "next/image";
|
||||||
|
import { motion } from "framer-motion";
|
||||||
|
import { ExternalLink } from "lucide-react";
|
||||||
|
import { type Product } from "@/lib/products";
|
||||||
|
import { useLanguage } from "@/context/language-context";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { useProduct, useProductStore } from "@/lib/productZustand";
|
||||||
|
|
||||||
|
interface ProductCardProps {
|
||||||
|
product: Product;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ProductCard({ product }: ProductCardProps) {
|
||||||
|
const { t, language } = useLanguage();
|
||||||
|
const languageIndex = language === "uz" ? true : false;
|
||||||
|
const setProductName = useProductStore((state) => state.setProductName);
|
||||||
|
const setProducts = useProduct((state) => state.setProducts);
|
||||||
|
return (
|
||||||
|
<Link
|
||||||
|
href={`/product/${product.slug}`}
|
||||||
|
onClick={() => {
|
||||||
|
setProductName(languageIndex ? product.name_uz : product.name_ru);
|
||||||
|
setProducts(product);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ duration: 0.3 }}
|
||||||
|
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"
|
||||||
|
>
|
||||||
|
{/* Image Container - Fixed Height */}
|
||||||
|
<div className="relative w-full h-64 overflow-hidden bg-linear-to-br from-gray-50 to-gray-100">
|
||||||
|
<motion.div
|
||||||
|
whileHover={{ scale: 1.05 }}
|
||||||
|
transition={{ duration: 0.4 }}
|
||||||
|
className="w-full h-full relative"
|
||||||
|
>
|
||||||
|
<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 */}
|
||||||
|
<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>
|
||||||
|
|
||||||
|
{/* Content Container - Flex Grow */}
|
||||||
|
<div className="flex flex-col grow p-6">
|
||||||
|
{/* Product Name */}
|
||||||
|
<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}
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
{/* Short Description - Fixed Height with Line Clamp */}
|
||||||
|
<p className="text-gray-600 text-sm leading-relaxed mb-4 line-clamp-3 grow">
|
||||||
|
{languageIndex ? product.name_uz : product.name_ru}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{/* CTA Button - Always at Bottom */}
|
||||||
|
<motion.div
|
||||||
|
whileHover={{ scale: 1.02 }}
|
||||||
|
whileTap={{ scale: 0.98 }}
|
||||||
|
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>
|
||||||
|
<ExternalLink className="w-4 h-4 group-hover/button:translate-x-1 transition-transform duration-300" />
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 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" />
|
||||||
|
</motion.div>
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -57,7 +57,7 @@ export function ProductViewer({
|
|||||||
src={primaryImage}
|
src={primaryImage}
|
||||||
alt="Product"
|
alt="Product"
|
||||||
fill
|
fill
|
||||||
className="object-cover"
|
className="object-contain"
|
||||||
/>
|
/>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
) : (
|
) : (
|
||||||
101
components/productSection/ProductsGrid.tsx
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { motion } from "framer-motion";
|
||||||
|
import type { Product } from "@/lib/products";
|
||||||
|
import Image from "next/image";
|
||||||
|
import { useLanguage } from "@/context/language-context";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { ChevronsRight } from "lucide-react";
|
||||||
|
import { ProductCard } from "./ProductCard";
|
||||||
|
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
|
||||||
|
|
||||||
|
export function ProductsGrid() {
|
||||||
|
const { t } = useLanguage();
|
||||||
|
const [allProducts, setAllProducts] = useState<any>([]);
|
||||||
|
const [loading, setLoading] = useState<boolean>(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
async function getData() {
|
||||||
|
setLoading(true);
|
||||||
|
const products = await getAllProducts();
|
||||||
|
setAllProducts(
|
||||||
|
products.map((product: any) => ({
|
||||||
|
...product,
|
||||||
|
slug: generateSlug(product.name_uz),
|
||||||
|
})).slice(0, 3)
|
||||||
|
);
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
getData();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<section id="products" className="relative py-20">
|
||||||
|
<div className="absolute -z-40 top-0 left-0 h-full w-full">
|
||||||
|
<Image
|
||||||
|
src="/images/hero2.jpg"
|
||||||
|
alt="hero image"
|
||||||
|
fill
|
||||||
|
className="object-cover"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="absolute w-full h-full top-0 left-0 bg-black opacity-25 -z-40" />
|
||||||
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||||
|
{/* Header */}
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
whileInView={{ opacity: 1 }}
|
||||||
|
transition={{ duration: 0.6 }}
|
||||||
|
viewport={{ once: true }}
|
||||||
|
className="text-center mb-16 bg-white/80 backdrop-blur-md rounded-xl overflow-hidden p-2 max-w-[300px] w-full mx-auto"
|
||||||
|
>
|
||||||
|
<h2 className="text-2xl font-bold text-gray-900 mb-2">
|
||||||
|
{t.products.title}
|
||||||
|
</h2>
|
||||||
|
<div className="w-20 h-1 bg-primary mx-auto rounded-full" />
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
{loading && (
|
||||||
|
<div className="w-full flex items-center justify-center">
|
||||||
|
<Loading />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Product Grid */}
|
||||||
|
{loading || (allProducts && allProducts.length > 0) ? (
|
||||||
|
<motion.div
|
||||||
|
initial="hidden"
|
||||||
|
whileInView="visible"
|
||||||
|
viewport={{ once: true }}
|
||||||
|
className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8"
|
||||||
|
>
|
||||||
|
{allProducts.map((product: any) => (
|
||||||
|
<motion.div key={product.id}>
|
||||||
|
<ProductCard product={product} />
|
||||||
|
</motion.div>
|
||||||
|
))}
|
||||||
|
</motion.div>
|
||||||
|
) : (
|
||||||
|
<EmptyState page="main" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="mt-10 w-full flex items-center justify-center">
|
||||||
|
<Link
|
||||||
|
href="/product"
|
||||||
|
className="text-primary flex items-center gap-2 text-[18px] hover:bg-primary hover:text-white py-2 px-6 rounded-lg bg-[#ffffffb5] border mx-auto border-white"
|
||||||
|
>
|
||||||
|
{t.more} <ChevronsRight />
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
72
components/productsPage/emptyData.tsx
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import Link from "next/link";
|
||||||
|
import { motion } from "framer-motion";
|
||||||
|
import { FileSearch } from "lucide-react";
|
||||||
|
import { useLanguage } from "@/context/language-context";
|
||||||
|
|
||||||
|
//salomalr
|
||||||
|
|
||||||
|
export default function EmptyState({page}:{page: string}) {
|
||||||
|
const { t } = useLanguage();
|
||||||
|
|
||||||
|
const container = {
|
||||||
|
hidden: { opacity: 0, y: 8 },
|
||||||
|
show: { opacity: 1, y: 0, transition: { staggerChildren: 0.06 } },
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<motion.section
|
||||||
|
aria-labelledby="empty-state-title"
|
||||||
|
className="w-full px-4 py-16 flex items-center justify-center"
|
||||||
|
initial="hidden"
|
||||||
|
animate="show"
|
||||||
|
variants={container}
|
||||||
|
>
|
||||||
|
<div className="max-w-5xl w-full bg-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">
|
||||||
|
{/* Illustration / Icon */}
|
||||||
|
<motion.div className="flex items-center justify-center">
|
||||||
|
<motion.div
|
||||||
|
className="p-6 bg-linear-to-tr from-blue-50 to-blue-100 rounded-xl shadow-inner"
|
||||||
|
initial={{ scale: 0.96 }}
|
||||||
|
animate={{ scale: [0.96, 1.02, 0.98, 1] }}
|
||||||
|
transition={{
|
||||||
|
duration: 4,
|
||||||
|
repeat: Infinity,
|
||||||
|
repeatType: "mirror",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<FileSearch className="w-28 h-28 text-primary" />
|
||||||
|
</motion.div>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
{/* Content */}
|
||||||
|
<motion.div className="text-center md:text-left">
|
||||||
|
<h3
|
||||||
|
id="empty-state-title"
|
||||||
|
className="text-2xl sm:text-3xl font-semibold text-gray-900 dark:text-gray-100"
|
||||||
|
>
|
||||||
|
{t.empty_data.title || "Hech narsa topilmadi"}
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<p className="mt-3 text-sm sm:text-base text-gray-600 dark:text-gray-300 max-w-xl mx-auto md:mx-0">
|
||||||
|
{t.empty_data.description}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{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">
|
||||||
|
<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"
|
||||||
|
// @ts-ignore allow Link-like anchor
|
||||||
|
>
|
||||||
|
<Link href="/">{t.empty_data.back || "Bosh sahifa"}</Link>
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</motion.section>
|
||||||
|
);
|
||||||
|
}
|
||||||
55
components/productsPage/products.tsx
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
"use client";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import EmptyState from "./emptyData";
|
||||||
|
import { motion } from "framer-motion";
|
||||||
|
import { ProductCard } from "../productSection/ProductCard";
|
||||||
|
import axios from "axios";
|
||||||
|
import Loading from "../loading";
|
||||||
|
import { generateSlug } from "@/lib/slug";
|
||||||
|
import { getAllProducts } from "@/lib/api";
|
||||||
|
|
||||||
|
const itemVariants = {
|
||||||
|
hidden: { opacity: 0, y: 20 },
|
||||||
|
visible: { opacity: 1, y: 0 },
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function Products() {
|
||||||
|
const [allProducts, setAllProducts] = useState<any>([]);
|
||||||
|
const [loading, setLoading] = useState<boolean>(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
async function getData() {
|
||||||
|
setLoading(true);
|
||||||
|
const products = await getAllProducts();
|
||||||
|
setAllProducts(
|
||||||
|
products.map((product: any) => ({
|
||||||
|
...product,
|
||||||
|
slug: generateSlug(product.name_uz),
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
getData();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="py-20 max-w-[1200px] w-full mx-auto px-2 ">
|
||||||
|
{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">
|
||||||
|
{allProducts.map((product: any) => (
|
||||||
|
<motion.div key={product.id} variants={itemVariants}>
|
||||||
|
<ProductCard product={product} />
|
||||||
|
</motion.div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<EmptyState page="products" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
257
components/ui/dropdown-menu.tsx
Normal file
@@ -0,0 +1,257 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import * as React from 'react'
|
||||||
|
import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu'
|
||||||
|
import { CheckIcon, ChevronRightIcon, CircleIcon } from 'lucide-react'
|
||||||
|
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
|
function DropdownMenu({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.Root>) {
|
||||||
|
return <DropdownMenuPrimitive.Root data-slot="dropdown-menu" {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
|
function DropdownMenuPortal({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.Portal>) {
|
||||||
|
return (
|
||||||
|
<DropdownMenuPrimitive.Portal data-slot="dropdown-menu-portal" {...props} />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function DropdownMenuTrigger({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.Trigger>) {
|
||||||
|
return (
|
||||||
|
<DropdownMenuPrimitive.Trigger
|
||||||
|
data-slot="dropdown-menu-trigger"
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function DropdownMenuContent({
|
||||||
|
className,
|
||||||
|
sideOffset = 4,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.Content>) {
|
||||||
|
return (
|
||||||
|
<DropdownMenuPrimitive.Portal>
|
||||||
|
<DropdownMenuPrimitive.Content
|
||||||
|
data-slot="dropdown-menu-content"
|
||||||
|
sideOffset={sideOffset}
|
||||||
|
className={cn(
|
||||||
|
'bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 max-h-(--radix-dropdown-menu-content-available-height) min-w-32 origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border p-1 shadow-md',
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
</DropdownMenuPrimitive.Portal>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function DropdownMenuGroup({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.Group>) {
|
||||||
|
return (
|
||||||
|
<DropdownMenuPrimitive.Group data-slot="dropdown-menu-group" {...props} />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function DropdownMenuItem({
|
||||||
|
className,
|
||||||
|
inset,
|
||||||
|
variant = 'default',
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.Item> & {
|
||||||
|
inset?: boolean
|
||||||
|
variant?: 'default' | 'destructive'
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<DropdownMenuPrimitive.Item
|
||||||
|
data-slot="dropdown-menu-item"
|
||||||
|
data-inset={inset}
|
||||||
|
data-variant={variant}
|
||||||
|
className={cn(
|
||||||
|
"focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:text-destructive! [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-disabled:pointer-events-none data-disabled:opacity-50 data-inset:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function DropdownMenuCheckboxItem({
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
checked,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.CheckboxItem>) {
|
||||||
|
return (
|
||||||
|
<DropdownMenuPrimitive.CheckboxItem
|
||||||
|
data-slot="dropdown-menu-checkbox-item"
|
||||||
|
className={cn(
|
||||||
|
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
checked={checked}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
|
||||||
|
<DropdownMenuPrimitive.ItemIndicator>
|
||||||
|
<CheckIcon className="size-4" />
|
||||||
|
</DropdownMenuPrimitive.ItemIndicator>
|
||||||
|
</span>
|
||||||
|
{children}
|
||||||
|
</DropdownMenuPrimitive.CheckboxItem>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function DropdownMenuRadioGroup({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioGroup>) {
|
||||||
|
return (
|
||||||
|
<DropdownMenuPrimitive.RadioGroup
|
||||||
|
data-slot="dropdown-menu-radio-group"
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function DropdownMenuRadioItem({
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioItem>) {
|
||||||
|
return (
|
||||||
|
<DropdownMenuPrimitive.RadioItem
|
||||||
|
data-slot="dropdown-menu-radio-item"
|
||||||
|
className={cn(
|
||||||
|
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
|
||||||
|
<DropdownMenuPrimitive.ItemIndicator>
|
||||||
|
<CircleIcon className="size-2 fill-current" />
|
||||||
|
</DropdownMenuPrimitive.ItemIndicator>
|
||||||
|
</span>
|
||||||
|
{children}
|
||||||
|
</DropdownMenuPrimitive.RadioItem>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function DropdownMenuLabel({
|
||||||
|
className,
|
||||||
|
inset,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.Label> & {
|
||||||
|
inset?: boolean
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<DropdownMenuPrimitive.Label
|
||||||
|
data-slot="dropdown-menu-label"
|
||||||
|
data-inset={inset}
|
||||||
|
className={cn(
|
||||||
|
'px-2 py-1.5 text-sm font-medium data-inset:pl-8',
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function DropdownMenuSeparator({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.Separator>) {
|
||||||
|
return (
|
||||||
|
<DropdownMenuPrimitive.Separator
|
||||||
|
data-slot="dropdown-menu-separator"
|
||||||
|
className={cn('bg-border -mx-1 my-1 h-px', className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function DropdownMenuShortcut({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<'span'>) {
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
data-slot="dropdown-menu-shortcut"
|
||||||
|
className={cn(
|
||||||
|
'text-muted-foreground ml-auto text-xs tracking-widest',
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function DropdownMenuSub({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.Sub>) {
|
||||||
|
return <DropdownMenuPrimitive.Sub data-slot="dropdown-menu-sub" {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
|
function DropdownMenuSubTrigger({
|
||||||
|
className,
|
||||||
|
inset,
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.SubTrigger> & {
|
||||||
|
inset?: boolean
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<DropdownMenuPrimitive.SubTrigger
|
||||||
|
data-slot="dropdown-menu-sub-trigger"
|
||||||
|
data-inset={inset}
|
||||||
|
className={cn(
|
||||||
|
"focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-inset:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
<ChevronRightIcon className="ml-auto size-4" />
|
||||||
|
</DropdownMenuPrimitive.SubTrigger>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function DropdownMenuSubContent({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.SubContent>) {
|
||||||
|
return (
|
||||||
|
<DropdownMenuPrimitive.SubContent
|
||||||
|
data-slot="dropdown-menu-sub-content"
|
||||||
|
className={cn(
|
||||||
|
'bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-32 origin-(--radix-dropdown-menu-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-lg',
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuPortal,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuGroup,
|
||||||
|
DropdownMenuLabel,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuCheckboxItem,
|
||||||
|
DropdownMenuRadioGroup,
|
||||||
|
DropdownMenuRadioItem,
|
||||||
|
DropdownMenuSeparator,
|
||||||
|
DropdownMenuShortcut,
|
||||||
|
DropdownMenuSub,
|
||||||
|
DropdownMenuSubTrigger,
|
||||||
|
DropdownMenuSubContent,
|
||||||
|
}
|
||||||
32
context/language-context.tsx
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import { translations } from "@/lib/translations"
|
||||||
|
import { createContext, useContext, useState, type ReactNode } from "react"
|
||||||
|
|
||||||
|
type Language = "uz" | "ru"
|
||||||
|
|
||||||
|
interface LanguageContextType {
|
||||||
|
language: Language
|
||||||
|
setLanguage: (lang: Language) => void
|
||||||
|
t: (any)[Language]
|
||||||
|
}
|
||||||
|
|
||||||
|
const LanguageContext = createContext<LanguageContextType | undefined>(undefined)
|
||||||
|
|
||||||
|
export function LanguageProvider({ children }: { children: ReactNode }) {
|
||||||
|
const [language, setLanguage] = useState<Language>("uz")
|
||||||
|
|
||||||
|
return (
|
||||||
|
<LanguageContext.Provider value={{ language, setLanguage, t: translations[language] }}>
|
||||||
|
{children}
|
||||||
|
</LanguageContext.Provider>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useLanguage() {
|
||||||
|
const context = useContext(LanguageContext)
|
||||||
|
if (!context) {
|
||||||
|
throw new Error("useLanguage must be used within LanguageProvider")
|
||||||
|
}
|
||||||
|
return context
|
||||||
|
}
|
||||||
12
docker-compose.yaml
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
services:
|
||||||
|
frontend:
|
||||||
|
build: .
|
||||||
|
container_name: promtechno-web
|
||||||
|
ports:
|
||||||
|
- "3000:80"
|
||||||
|
volumes:
|
||||||
|
- .:/app
|
||||||
|
- /app/node_modules
|
||||||
|
environment:
|
||||||
|
- CHOKIDAR_USEPOLLING=true
|
||||||
|
- HOST=0.0.0.0
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
export type Locale = "uz" | "ru";
|
|
||||||
|
|
||||||
export const locales: Locale[] = ["uz", "ru"];
|
|
||||||
export const defaultLocale: Locale = "uz";
|
|
||||||
@@ -1,10 +1,18 @@
|
|||||||
import { getRequestConfig } from "next-intl/server";
|
// i18n/request.ts
|
||||||
|
import { getRequestConfig, type GetRequestConfigParams } from "next-intl/server";
|
||||||
|
import { notFound } from "next/navigation";
|
||||||
|
|
||||||
export default getRequestConfig(async ({ requestLocale }) => {
|
export const locales = ['uz','ru'];
|
||||||
const locale = (await requestLocale) || "uz";
|
|
||||||
|
export default getRequestConfig(async ({ locale }: GetRequestConfigParams) => {
|
||||||
|
// Agar locale undefined yoki not supported bo‘lsa, 404
|
||||||
|
if (!locale || !locales.includes(locale)) notFound();
|
||||||
|
|
||||||
|
// endi TypeScript uchun locale string ekanligi aniq
|
||||||
|
const messages = (await import(`../locales/${locale}.json`)).default;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
locale,
|
locale, // string, undefined emas
|
||||||
messages: (await import(`../locales/${locale}.json`)).default,
|
messages, // JSON fayl
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|||||||
48
lib/api.ts
@@ -1,30 +1,26 @@
|
|||||||
import axios from "axios";
|
import { FaqBackendItem } from "@/components/FAQ";
|
||||||
|
import { Product } from "@/lib/products";
|
||||||
|
|
||||||
export const apiClient = axios.create({
|
export async function getAllProducts(): Promise<Product[]> {
|
||||||
baseURL: process.env.NEXT_PUBLIC_SITE_URL || "http://localhost:3000",
|
const res = await fetch("https://admin.promtechno.uz/api/products/", {
|
||||||
timeout: 10000,
|
cache: "force-cache", // build time uchun for gitea
|
||||||
headers: {
|
});
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export async function sendContactMessage(payload: {
|
if (!res.ok) {
|
||||||
name: string;
|
console.log("Failed to fetch products");
|
||||||
phone: string;
|
return [];
|
||||||
message?: string;
|
|
||||||
productSlug?: string;
|
|
||||||
lang?: "uz" | "ru";
|
|
||||||
}) {
|
|
||||||
try {
|
|
||||||
const response = await apiClient.post("/api/contact", payload);
|
|
||||||
return { success: true, data: response.data };
|
|
||||||
} catch (error) {
|
|
||||||
if (axios.isAxiosError(error)) {
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
error: error.response?.data?.error || "Failed to send message",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return { success: false, error: "Network error" };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
36
lib/productZustand.ts
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
import { create } from "zustand";
|
||||||
|
import { devtools, persist } from "zustand/middleware";
|
||||||
|
import type { Product } from "@/lib/products";
|
||||||
|
|
||||||
|
interface ProductStore {
|
||||||
|
productName: string;
|
||||||
|
setProductName: (name: string) => void;
|
||||||
|
resetProductName: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
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(
|
||||||
|
persist(
|
||||||
|
(set) => ({
|
||||||
|
product: null,
|
||||||
|
setProducts: (product: Product) => set({ product }),
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
name: "product-detail-storage",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
@@ -1,64 +1,17 @@
|
|||||||
|
interface features {
|
||||||
|
key_uz: string;
|
||||||
|
key_ru: string;
|
||||||
|
value_uz: string;
|
||||||
|
value_ru: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface Product {
|
export interface Product {
|
||||||
id: string;
|
id: number;
|
||||||
nameKey: string;
|
name_uz: string;
|
||||||
slug: string;
|
name_ru: string;
|
||||||
shortDescriptionKey: string;
|
description_uz: string;
|
||||||
longDescriptionKey?: string;
|
description_ru: string;
|
||||||
images: string[];
|
features:features[];
|
||||||
model3D?: string;
|
image:string;
|
||||||
specs: { key: string; value: string }[];
|
slug?: string;
|
||||||
}
|
|
||||||
|
|
||||||
// Sample products data
|
|
||||||
export const products: Product[] = [
|
|
||||||
{
|
|
||||||
id: "1",
|
|
||||||
nameKey: "products_list.pump_1.name",
|
|
||||||
slug: "schotchik-pump",
|
|
||||||
shortDescriptionKey: "products_list.pump_1.shortDescription",
|
|
||||||
longDescriptionKey: "products_list.pump_1.description",
|
|
||||||
images: ["/images/pump-1.jpg", "/images/pump-1-alt.jpg"],
|
|
||||||
specs: [
|
|
||||||
{ key: "Flow Rate", value: "100 L/min" },
|
|
||||||
{ key: "Pressure", value: "10 bar" },
|
|
||||||
{ key: "Power", value: "5.5 kW" },
|
|
||||||
{ key: "Temperature Range", value: "-10°C to 60°C" },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "2",
|
|
||||||
nameKey: "products_list.pump_2.name",
|
|
||||||
slug: "agregat-pump",
|
|
||||||
shortDescriptionKey: "products_list.pump_2.shortDescription",
|
|
||||||
longDescriptionKey: "products_list.pump_2.description",
|
|
||||||
images: ["/images/pump-2.jpg", "/images/pump-2-alt.jpg"],
|
|
||||||
specs: [
|
|
||||||
{ key: "Flow Rate", value: "250 L/min" },
|
|
||||||
{ key: "Pressure", value: "15 bar" },
|
|
||||||
{ key: "Power", value: "11 kW" },
|
|
||||||
{ key: "Temperature Range", value: "-10°C to 70°C" },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "3",
|
|
||||||
nameKey: "products_list.pump_3.name",
|
|
||||||
slug: "ccl-20-24-pump",
|
|
||||||
shortDescriptionKey: "products_list.pump_3.shortDescription",
|
|
||||||
longDescriptionKey: "products_list.pump_3.description",
|
|
||||||
images: ["/images/pump-3.jpg", "/images/pump-3-alt.jpg"],
|
|
||||||
specs: [
|
|
||||||
{ key: "Depth Rating", value: "20-24 m" },
|
|
||||||
{ key: "Flow Rate", value: "150 L/min" },
|
|
||||||
{ key: "Suction Lift", value: "7 m" },
|
|
||||||
{ key: "Power", value: "7.5 kW" },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
export function getProductBySlug(slug: string): Product | undefined {
|
|
||||||
return products.find((p) => p.slug === slug);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getAllProducts(): Product[] {
|
|
||||||
return products;
|
|
||||||
}
|
}
|
||||||
|
|||||||
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
|
||||||
|
}
|
||||||
205
lib/translations.ts
Normal file
@@ -0,0 +1,205 @@
|
|||||||
|
export const translations = {
|
||||||
|
uz: {
|
||||||
|
nav: {
|
||||||
|
about: "Biz haqimizda",
|
||||||
|
products: "Mahsulotlar",
|
||||||
|
faq: "FAQ",
|
||||||
|
contact: "Bog'lanish",
|
||||||
|
},
|
||||||
|
hero: {
|
||||||
|
title: "Sanoat Uskunalari va Nasoslar Yetkazuvchisi",
|
||||||
|
subtitle: "10+ yil tajribasi bilan sifatli mahsulot va xizmat",
|
||||||
|
cta: "Bog'lanish",
|
||||||
|
},
|
||||||
|
about: {
|
||||||
|
title: "Biz haqimizda",
|
||||||
|
content:
|
||||||
|
"Kompaniyamiz sanoat nasoslari va o'lchov uskunalarini yetkazib berishda 10+ yil tajribaga ega. Har bir mahsulot sinovdan o'tkazilgan, sifat kafolatlangan va texnik xizmat ko'rsatish bilan ta'minlanadi. Biz mijozlarimizga texnik maslahat, tez etkazib berish va o'rnatish bo'yicha to'liq xizmat taklif etamiz. Ixtisoslashgan nasoslarimiz (schotchik, agregat nasos, СЦЛ 20/24 va boshqalar) benzin, dizel, kerosin va boshqa yengil neft mahsulotlarini xavfsiz va samarali tashishda ishlatiladi.",
|
||||||
|
experiance: "Tajriba",
|
||||||
|
experts: "Mutaxasislar",
|
||||||
|
truth: "Ishonchlilik",
|
||||||
|
},
|
||||||
|
products: {
|
||||||
|
title: "Mahsulotlar",
|
||||||
|
viewDetails: "Batafsil",
|
||||||
|
},
|
||||||
|
faq: {
|
||||||
|
title: "Tez-tez So'raladigan Savollar",
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
question: "Mahsulotlar uchun kafolat bormi?",
|
||||||
|
answer:
|
||||||
|
"Ha, barcha uskunalarimizga 12 oylik texnik kafolat beriladi.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
question: "Yetkazib berish muddati qancha?",
|
||||||
|
answer: "Odatda 3-14 ish kuni, mavjudlik va manzilga bog'liq.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
question: "Texnik qo'llab-quvvatlash bormi?",
|
||||||
|
answer: "Ha, telefon va Telegram orqali 24/7 texnik maslahat mavjud.",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
contact: {
|
||||||
|
title: "Bog'lanish",
|
||||||
|
desc: "Savollar, qo'llab-quvvatlash yoki hamkorlik imkoniyatlari uchun biz bilan bog'laning.",
|
||||||
|
name: "Ism",
|
||||||
|
phone: "Telefon raqami",
|
||||||
|
message: "Xabar",
|
||||||
|
product: "Mahsulot (ixtiyoriy)",
|
||||||
|
send: "Yuborish",
|
||||||
|
success: "Xabar muvaffaqiyatli yuborildi!",
|
||||||
|
error: "Xato: Xabarni yuborib bo'lmadi.",
|
||||||
|
namePlaceholder: "Sizning ismingiz",
|
||||||
|
phonePlaceholder: "+998 XX XXX XX XX",
|
||||||
|
messagePlaceholder: "Sizning xabaringiz (ixtiyoriy)",
|
||||||
|
phone_title: "Telefon",
|
||||||
|
telegram_title: "Telegram",
|
||||||
|
addres_title: "Manzil",
|
||||||
|
},
|
||||||
|
footer: {
|
||||||
|
copyright: "© 2025 Firma. Barcha huquqlar himoyalangan.",
|
||||||
|
followUs: "Bizni kuzatib turing",
|
||||||
|
common: {
|
||||||
|
premium_title: "Premium sanoat nasoslari va uskunalari",
|
||||||
|
quickLinks: "Tezkor havolalar",
|
||||||
|
aboutUs: "Biz haqimizda",
|
||||||
|
products: "Mahsulotlar",
|
||||||
|
contact: "Bog'lanish",
|
||||||
|
email: "Email",
|
||||||
|
phone: "Telefon",
|
||||||
|
telegram: "Telegram",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
products_list: {
|
||||||
|
pump_1: {
|
||||||
|
name: "Schotchik Nasos",
|
||||||
|
shortDescription: "Xavfsiz neft mahsulotlarini tashish uchun",
|
||||||
|
description:
|
||||||
|
"Yuqori sifatli schotchik nasos, benzin, dizel va kerosinni tashishda ishlatiladi.",
|
||||||
|
},
|
||||||
|
pump_2: {
|
||||||
|
name: "Agregat Nasos",
|
||||||
|
shortDescription: "Kuchli va ishonchli aggregat nasos",
|
||||||
|
description: "Katta volumli neft mahsulotlarini tashishda o'rnatilgan.",
|
||||||
|
},
|
||||||
|
pump_3: {
|
||||||
|
name: "СЦЛ 20/24",
|
||||||
|
shortDescription: "Professional kalibrli nasos",
|
||||||
|
description: "Chuqurligi 20-24 metrda ishlaydigan professional nasos.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
more: "Ko'proq ko'rish",
|
||||||
|
details: "Batafsil",
|
||||||
|
features: "Texnik tavsiflar",
|
||||||
|
empty_data: {
|
||||||
|
description: "Mahsulot topilmadi!!!",
|
||||||
|
back: "Asosiy sahifaga qaytish",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
ru: {
|
||||||
|
nav: {
|
||||||
|
about: "О нас",
|
||||||
|
products: "Продукты",
|
||||||
|
faq: "FAQ",
|
||||||
|
contact: "Контакт",
|
||||||
|
},
|
||||||
|
hero: {
|
||||||
|
title: "Поставщик промышленного оборудования и насосов",
|
||||||
|
subtitle: "Качественная продукция и услуги с 10+ летним опытом",
|
||||||
|
cta: "Свяжитесь с нами",
|
||||||
|
},
|
||||||
|
about: {
|
||||||
|
title: "О нас",
|
||||||
|
content:
|
||||||
|
"Наша компания имеет 10+ лет опыта в поставке промышленных насосов и измерительного оборудования. Каждый продукт протестирован, качество гарантировано и сопровождается технической поддержкой. Мы предлагаем нашим клиентам полный сервис: техническую консультацию, быструю доставку и установку. Наши специализированные насосы (счетчик, агрегатный насос, СЦЛ 20/24 и др.) используются для безопасной и эффективной транспортировки бензина, дизеля, керосина и других легких нефтепродуктов.",
|
||||||
|
experiance: "Опыт",
|
||||||
|
experts: "Эксперты",
|
||||||
|
truth: "Надёжность",
|
||||||
|
},
|
||||||
|
products: {
|
||||||
|
title: "Продукты",
|
||||||
|
viewDetails: "Подробнее",
|
||||||
|
},
|
||||||
|
faq: {
|
||||||
|
title: "Часто Задаваемые Вопросы",
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
question: "Гарантия на продукты?",
|
||||||
|
answer:
|
||||||
|
"Да, все наше оборудование поставляется с 12-месячной технической гарантией.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
question: "Сколько времени займет доставка?",
|
||||||
|
answer:
|
||||||
|
"Обычно 3-14 рабочих дней, в зависимости от наличия и адреса доставки.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
question: "Есть ли техническая поддержка?",
|
||||||
|
answer:
|
||||||
|
"Да, техническая консультация доступна 24/7 по телефону и Telegram.",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
contact: {
|
||||||
|
title: "Свяжитесь с нами",
|
||||||
|
desc: "Для вопросов, поддержки или возможностей сотрудничества свяжитесь с нами.",
|
||||||
|
name: "Имя",
|
||||||
|
phone: "Номер телефона",
|
||||||
|
message: "Сообщение",
|
||||||
|
product: "Продукт (опционально)",
|
||||||
|
send: "Отправить",
|
||||||
|
success: "Сообщение успешно отправлено!",
|
||||||
|
error: "Ошибка: не удалось отправить сообщение.",
|
||||||
|
namePlaceholder: "Ваше имя",
|
||||||
|
phonePlaceholder: "+998 XX XXX XX XX",
|
||||||
|
messagePlaceholder: "Ваше сообщение (опционально)",
|
||||||
|
phone_title: "Телефон",
|
||||||
|
telegram_title: "Телеграм",
|
||||||
|
addres_title: "Адрес",
|
||||||
|
},
|
||||||
|
footer: {
|
||||||
|
copyright: "© 2025 Firma. Все права защищены.",
|
||||||
|
followUs: "Следите за нами",
|
||||||
|
common: {
|
||||||
|
premium_title: "Премиальные промышленные насосы и оборудование",
|
||||||
|
quickLinks: "Быстрые ссылки",
|
||||||
|
aboutUs: "О нас",
|
||||||
|
products: "Продукты",
|
||||||
|
contact: "Контакт",
|
||||||
|
email: "Email",
|
||||||
|
phone: "Телефон",
|
||||||
|
telegram: "Телеграм",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
products_list: {
|
||||||
|
pump_1: {
|
||||||
|
name: "Счетчик Насос",
|
||||||
|
shortDescription: "Для безопасной транспортировки нефтепродуктов",
|
||||||
|
description:
|
||||||
|
"Высококачественный счетчиковый насос, используется для транспортировки бензина, дизеля и керосина.",
|
||||||
|
},
|
||||||
|
pump_2: {
|
||||||
|
name: "Агрегатный Насос",
|
||||||
|
shortDescription: "Мощный и надежный агрегатный насос",
|
||||||
|
description:
|
||||||
|
"Установлен для транспортировки больших объемов нефтепродуктов.",
|
||||||
|
},
|
||||||
|
pump_3: {
|
||||||
|
name: "СЦЛ 20/24",
|
||||||
|
shortDescription: "Профессиональный калиброванный насос",
|
||||||
|
description:
|
||||||
|
"Профессиональный насос, работающий на глубине 20-24 метра.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
more: "Смотреть больше",
|
||||||
|
details: "Подробнее",
|
||||||
|
features: "Технические характеристики",
|
||||||
|
empty_data: {
|
||||||
|
description: "Товар не найден!!!",
|
||||||
|
back: "Вернуться на главную страницу",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -16,7 +16,8 @@
|
|||||||
},
|
},
|
||||||
"products": {
|
"products": {
|
||||||
"title": "Продукты",
|
"title": "Продукты",
|
||||||
"viewDetails": "Подробнее"
|
"viewDetails": "Подробнее",
|
||||||
|
"features": "Технические характеристики"
|
||||||
},
|
},
|
||||||
"faq": {
|
"faq": {
|
||||||
"title": "Часто Задаваемые Вопросы",
|
"title": "Часто Задаваемые Вопросы",
|
||||||
@@ -37,6 +38,7 @@
|
|||||||
},
|
},
|
||||||
"contact": {
|
"contact": {
|
||||||
"title": "Свяжитесь с нами",
|
"title": "Свяжитесь с нами",
|
||||||
|
"desc": "Для вопросов, поддержки или возможностей сотрудничества свяжитесь с нами.",
|
||||||
"name": "Имя",
|
"name": "Имя",
|
||||||
"phone": "Номер телефона",
|
"phone": "Номер телефона",
|
||||||
"message": "Сообщение",
|
"message": "Сообщение",
|
||||||
|
|||||||
@@ -16,7 +16,8 @@
|
|||||||
},
|
},
|
||||||
"products": {
|
"products": {
|
||||||
"title": "Mahsulotlar",
|
"title": "Mahsulotlar",
|
||||||
"viewDetails": "Batafsil"
|
"viewDetails": "Batafsil",
|
||||||
|
"features": "Texnik tavsiflar"
|
||||||
},
|
},
|
||||||
"faq": {
|
"faq": {
|
||||||
"title": "Tez-tez So'raladigan Savollar",
|
"title": "Tez-tez So'raladigan Savollar",
|
||||||
@@ -37,6 +38,7 @@
|
|||||||
},
|
},
|
||||||
"contact": {
|
"contact": {
|
||||||
"title": "Bog'lanish",
|
"title": "Bog'lanish",
|
||||||
|
"desc": "Savollar, qo'llab-quvvatlash yoki hamkorlik imkoniyatlari uchun biz bilan bog'laning.",
|
||||||
"name": "Ism",
|
"name": "Ism",
|
||||||
"phone": "Telefon raqami",
|
"phone": "Telefon raqami",
|
||||||
"message": "Xabar",
|
"message": "Xabar",
|
||||||
|
|||||||
8
next-intl.config.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
// next-intl.config.ts
|
||||||
|
import { IntlConfig } from "next-intl";
|
||||||
|
|
||||||
|
const nextIntlConfig: IntlConfig = {
|
||||||
|
locale: "uz", // JSON tarjimalar
|
||||||
|
};
|
||||||
|
|
||||||
|
export default nextIntlConfig;
|
||||||
@@ -1,16 +1,20 @@
|
|||||||
import type { NextConfig } from "next";
|
import type { NextConfig } from "next";
|
||||||
import createNextIntlPlugin from "next-intl/plugin";
|
import createNextIntlPlugin from "next-intl/plugin";
|
||||||
|
|
||||||
const withNextIntl = createNextIntlPlugin();
|
|
||||||
|
|
||||||
const nextConfig: NextConfig = {
|
const nextConfig: NextConfig = {
|
||||||
|
output: 'export',
|
||||||
images: {
|
images: {
|
||||||
remotePatterns: [],
|
unoptimized: true,
|
||||||
unoptimized: process.env.NODE_ENV === "development",
|
remotePatterns: [
|
||||||
},
|
{
|
||||||
experimental: {
|
protocol: "https",
|
||||||
optimizePackageImports: ["@react-three/fiber", "@react-three/drei"],
|
hostname: "admin.promtechno.uz",
|
||||||
|
pathname: "/resources/media/**",
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const withNextIntl = createNextIntlPlugin();
|
||||||
|
|
||||||
export default withNextIntl(nextConfig);
|
export default withNextIntl(nextConfig);
|
||||||
|
|||||||
986
package-lock.json
generated
@@ -10,18 +10,22 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@hookform/resolvers": "^5.2.2",
|
"@hookform/resolvers": "^5.2.2",
|
||||||
|
"@radix-ui/react-dropdown-menu": "^2.1.16",
|
||||||
"@react-three/drei": "^10.7.7",
|
"@react-three/drei": "^10.7.7",
|
||||||
"@react-three/fiber": "^9.4.0",
|
"@react-three/fiber": "^9.4.0",
|
||||||
"axios": "^1.13.2",
|
"axios": "^1.13.2",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"framer-motion": "^12.23.24",
|
"framer-motion": "^12.23.24",
|
||||||
|
"i18next": "^25.6.3",
|
||||||
"lucide-react": "^0.554.0",
|
"lucide-react": "^0.554.0",
|
||||||
"next": "16.0.4",
|
"next": "^16.0.8",
|
||||||
"next-intl": "^4.5.5",
|
"next-i18next": "^15.4.2",
|
||||||
|
"next-intl": "^4.5.6",
|
||||||
"react": "19.2.0",
|
"react": "19.2.0",
|
||||||
"react-dom": "19.2.0",
|
"react-dom": "19.2.0",
|
||||||
"react-hook-form": "^7.66.1",
|
"react-hook-form": "^7.66.1",
|
||||||
|
"react-i18next": "^16.3.5",
|
||||||
"tailwind-merge": "^3.4.0",
|
"tailwind-merge": "^3.4.0",
|
||||||
"three": "^0.181.2",
|
"three": "^0.181.2",
|
||||||
"zod": "^4.1.13"
|
"zod": "^4.1.13"
|
||||||
@@ -31,6 +35,7 @@
|
|||||||
"@types/node": "^20",
|
"@types/node": "^20",
|
||||||
"@types/react": "^19",
|
"@types/react": "^19",
|
||||||
"@types/react-dom": "^19",
|
"@types/react-dom": "^19",
|
||||||
|
"baseline-browser-mapping": "^2.9.5",
|
||||||
"eslint": "^9",
|
"eslint": "^9",
|
||||||
"eslint-config-next": "16.0.4",
|
"eslint-config-next": "16.0.4",
|
||||||
"tailwindcss": "^4",
|
"tailwindcss": "^4",
|
||||||
|
|||||||
BIN
public/favicon.ico
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
public/logo.jpg
Normal file
|
After Width: | Height: | Size: 178 KiB |
BIN
public/logo1.png
Normal file
|
After Width: | Height: | Size: 2.3 MiB |
BIN
public/product/product1.jpg
Normal file
|
After Width: | Height: | Size: 32 KiB |
BIN
public/product/product2.jpg
Normal file
|
After Width: | Height: | Size: 115 KiB |
BIN
public/product/product3.jpg
Normal file
|
After Width: | Height: | Size: 307 KiB |
@@ -29,6 +29,6 @@
|
|||||||
".next/types/**/*.ts",
|
".next/types/**/*.ts",
|
||||||
".next/dev/types/**/*.ts",
|
".next/dev/types/**/*.ts",
|
||||||
"**/*.mts"
|
"**/*.mts"
|
||||||
],
|
, "next.config.ts" ],
|
||||||
"exclude": ["node_modules"]
|
"exclude": ["node_modules",".next"]
|
||||||
}
|
}
|
||||||
|
|||||||