api ulandi
This commit is contained in:
12
.npmrc
Normal file
12
.npmrc
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
# pnpm configuration
|
||||||
|
|
||||||
|
# auto audit installing
|
||||||
|
audit=true
|
||||||
|
|
||||||
|
# allow running install scripts (needed for husky)
|
||||||
|
ignore-scripts=false
|
||||||
|
|
||||||
|
# turn on npm registry SSL checking
|
||||||
|
strict-ssl=true
|
||||||
|
|
||||||
|
minimum-release-age=262974
|
||||||
43
.vscode/i18n-ally-reviews.yml
vendored
Normal file
43
.vscode/i18n-ally-reviews.yml
vendored
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
# Review comments generated by i18n-ally. Please commit this file.
|
||||||
|
|
||||||
|
reviews:
|
||||||
|
Telefon:
|
||||||
|
locales:
|
||||||
|
ru:
|
||||||
|
comments:
|
||||||
|
- user:
|
||||||
|
name: Samandar Turgunboyev
|
||||||
|
email: turgunboyevsamandar4@gmail.com
|
||||||
|
id: QUEUy9CGkD4khcw6527AN
|
||||||
|
type: approve
|
||||||
|
comment: Telefon
|
||||||
|
time: '2025-12-19T09:09:29.279Z'
|
||||||
|
- user:
|
||||||
|
name: Samandar Turgunboyev
|
||||||
|
email: turgunboyevsamandar4@gmail.com
|
||||||
|
id: FW_Vq54ZIAcVtvWPj0VQY
|
||||||
|
type: approve
|
||||||
|
comment: Telefon
|
||||||
|
time: '2025-12-19T09:09:52.956Z'
|
||||||
|
- user:
|
||||||
|
name: Samandar Turgunboyev
|
||||||
|
email: turgunboyevsamandar4@gmail.com
|
||||||
|
id: Gn-IGHEPPkGFsdkA0mTfO
|
||||||
|
type: request_change
|
||||||
|
comment: Telefon ru
|
||||||
|
time: '2025-12-19T09:10:00.049Z'
|
||||||
|
resolved: true
|
||||||
|
- user:
|
||||||
|
name: Samandar Turgunboyev
|
||||||
|
email: turgunboyevsamandar4@gmail.com
|
||||||
|
id: m7aQaQ0oRVJDQWYboYseK
|
||||||
|
type: approve
|
||||||
|
comment: Telefon
|
||||||
|
time: '2025-12-19T09:10:21.133Z'
|
||||||
|
- user:
|
||||||
|
name: Samandar Turgunboyev
|
||||||
|
email: turgunboyevsamandar4@gmail.com
|
||||||
|
id: 42WzrKnBoWRNzCHFiFbWR
|
||||||
|
type: request_change
|
||||||
|
comment: ''
|
||||||
|
time: '2025-12-19T09:10:26.214Z'
|
||||||
7
.vscode/settings.json
vendored
Normal file
7
.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"i18n-ally.localesPaths": [
|
||||||
|
"src/shared/config/i18n",
|
||||||
|
"src/shared/config/i18n/messages"
|
||||||
|
],
|
||||||
|
"i18n-ally.sourceLanguage": "ru"
|
||||||
|
}
|
||||||
@@ -8,7 +8,10 @@ const nextConfig: NextConfig = {
|
|||||||
// ignoreDuringBuilds: true,
|
// ignoreDuringBuilds: true,
|
||||||
// },
|
// },
|
||||||
images: {
|
images: {
|
||||||
remotePatterns: [{ protocol: 'http', hostname: '**' }],
|
remotePatterns: [
|
||||||
|
{ protocol: 'http', hostname: '**' },
|
||||||
|
{ protocol: 'https', hostname: '**' },
|
||||||
|
],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
const withNextIntl = createNextIntlPlugin({
|
const withNextIntl = createNextIntlPlugin({
|
||||||
|
|||||||
9688
package-lock.json
generated
9688
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -10,6 +10,7 @@
|
|||||||
"lint": "eslint src --fix",
|
"lint": "eslint src --fix",
|
||||||
"prepare": "husky"
|
"prepare": "husky"
|
||||||
},
|
},
|
||||||
|
"packageManager": "pnpm@9.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@dnd-kit/core": "^6.3.1",
|
"@dnd-kit/core": "^6.3.1",
|
||||||
"@dnd-kit/modifiers": "^9.0.0",
|
"@dnd-kit/modifiers": "^9.0.0",
|
||||||
@@ -43,6 +44,8 @@
|
|||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"dayjs": "^1.11.13",
|
"dayjs": "^1.11.13",
|
||||||
"embla-carousel-react": "^8.6.0",
|
"embla-carousel-react": "^8.6.0",
|
||||||
|
"framer-motion": "^12.23.26",
|
||||||
|
"js-cookie": "^3.0.5",
|
||||||
"lucide-react": "^0.503.0",
|
"lucide-react": "^0.503.0",
|
||||||
"next": "^16.0.10",
|
"next": "^16.0.10",
|
||||||
"next-intl": "^4.3.9",
|
"next-intl": "^4.3.9",
|
||||||
@@ -61,6 +64,7 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/eslintrc": "^3",
|
"@eslint/eslintrc": "^3",
|
||||||
"@tailwindcss/postcss": "^4",
|
"@tailwindcss/postcss": "^4",
|
||||||
|
"@types/js-cookie": "^3.0.6",
|
||||||
"@types/node": "^20",
|
"@types/node": "^20",
|
||||||
"@types/react": "^19",
|
"@types/react": "^19",
|
||||||
"@types/react-dom": "^19",
|
"@types/react-dom": "^19",
|
||||||
|
|||||||
6491
pnpm-lock.yaml
generated
Normal file
6491
pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,10 +1,10 @@
|
|||||||
import SubCategory from '@/features/category/ui/SubCategory';
|
import Product from '@/features/category/ui/Product';
|
||||||
import { Suspense } from 'react';
|
import { Suspense } from 'react';
|
||||||
|
|
||||||
const page = () => {
|
const page = () => {
|
||||||
return (
|
return (
|
||||||
<Suspense>
|
<Suspense>
|
||||||
<SubCategory />
|
<Product />
|
||||||
</Suspense>
|
</Suspense>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -20,9 +20,11 @@ export default function LayoutShell({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Suspense>
|
<Suspense>
|
||||||
<Navbar />
|
<div className="flex min-h-screen flex-col">
|
||||||
{children}
|
<Navbar />
|
||||||
{!hideFooter && <Footer />}
|
<main className="flex-1 max-lg:mb-20">{children}</main>
|
||||||
|
{!hideFooter && <Footer />}
|
||||||
|
</div>
|
||||||
</Suspense>
|
</Suspense>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,9 @@
|
|||||||
import { subCategoriesData } from '@/features/category/lib/data';
|
|
||||||
import { CategoryCarousel } from '@/widgets/categories/ui/category-carousel';
|
|
||||||
import Welcome from '@/widgets/welcome/ui';
|
import Welcome from '@/widgets/welcome/ui';
|
||||||
|
|
||||||
export default async function Home() {
|
export default async function Home() {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Welcome />
|
<Welcome />
|
||||||
{subCategoriesData.slice(0, 6).map((e) => (
|
|
||||||
<CategoryCarousel category={e} key={e.id} />
|
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
BIN
src/assets/delivery-service-groceries-fresh-food.jpg
Normal file
BIN
src/assets/delivery-service-groceries-fresh-food.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 124 KiB |
BIN
src/assets/fresh-vegetables-and-fruits-in-supermarket.jpg
Normal file
BIN
src/assets/fresh-vegetables-and-fruits-in-supermarket.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 261 KiB |
BIN
src/assets/premium-quality-organic-food-products-display.jpg
Normal file
BIN
src/assets/premium-quality-organic-food-products-display.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 84 KiB |
9
src/features/about/lib/api.ts
Normal file
9
src/features/about/lib/api.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import httpClient from '@/shared/config/api/httpClient';
|
||||||
|
import { API_URLS } from '@/shared/config/api/URLs';
|
||||||
|
|
||||||
|
export const partner_api = {
|
||||||
|
async send(body: FormData) {
|
||||||
|
const res = httpClient.post(API_URLS.Partners, body);
|
||||||
|
return res;
|
||||||
|
},
|
||||||
|
};
|
||||||
6
src/features/about/lib/type.ts
Normal file
6
src/features/about/lib/type.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
export interface PartnerSendBody {
|
||||||
|
company_name: string;
|
||||||
|
full_name: string;
|
||||||
|
phone_number: string;
|
||||||
|
file: File;
|
||||||
|
}
|
||||||
@@ -1,7 +1,9 @@
|
|||||||
import { Card } from '@/shared/ui/card';
|
import { Card } from '@/shared/ui/card';
|
||||||
|
import { useTranslations } from 'next-intl';
|
||||||
import Image from 'next/image';
|
import Image from 'next/image';
|
||||||
|
|
||||||
export function AboutContent() {
|
export function AboutContent() {
|
||||||
|
const t = useTranslations();
|
||||||
const features = [
|
const features = [
|
||||||
{
|
{
|
||||||
number: '1',
|
number: '1',
|
||||||
@@ -44,7 +46,7 @@ export function AboutContent() {
|
|||||||
{/* Mission Section */}
|
{/* Mission Section */}
|
||||||
<div className="mb-20">
|
<div className="mb-20">
|
||||||
<h2 className="text-4xl md:text-5xl font-bold text-center mb-16 text-balance">
|
<h2 className="text-4xl md:text-5xl font-bold text-center mb-16 text-balance">
|
||||||
Bizning maqsadimiz
|
{t('Bizning maqsadimiz')}
|
||||||
</h2>
|
</h2>
|
||||||
<div className="grid md:grid-cols-3 gap-8">
|
<div className="grid md:grid-cols-3 gap-8">
|
||||||
{features.map((feature) => (
|
{features.map((feature) => (
|
||||||
@@ -55,9 +57,11 @@ export function AboutContent() {
|
|||||||
<div className="text-6xl font-bold text-primary mb-4">
|
<div className="text-6xl font-bold text-primary mb-4">
|
||||||
{feature.number}
|
{feature.number}
|
||||||
</div>
|
</div>
|
||||||
<h3 className="text-2xl font-semibold mb-4">{feature.title}</h3>
|
<h3 className="text-2xl font-semibold mb-4">
|
||||||
|
{t(feature.title)}
|
||||||
|
</h3>
|
||||||
<p className="text-muted-foreground leading-relaxed">
|
<p className="text-muted-foreground leading-relaxed">
|
||||||
{feature.description}
|
{t(feature.description)}
|
||||||
</p>
|
</p>
|
||||||
</Card>
|
</Card>
|
||||||
))}
|
))}
|
||||||
@@ -67,25 +71,22 @@ export function AboutContent() {
|
|||||||
{/* About Text */}
|
{/* About Text */}
|
||||||
<div className="mb-20 max-w-4xl mx-auto text-center">
|
<div className="mb-20 max-w-4xl mx-auto text-center">
|
||||||
<h2 className="text-3xl md:text-4xl font-bold mb-8 text-balance">
|
<h2 className="text-3xl md:text-4xl font-bold mb-8 text-balance">
|
||||||
Innovatsiya, sifat va professionallik
|
{t('Innovatsiya, sifat va professionallik')}
|
||||||
</h2>
|
</h2>
|
||||||
<p className="text-lg text-muted-foreground leading-relaxed mb-6">
|
<p className="text-lg text-muted-foreground leading-relaxed mb-6">
|
||||||
{`Gastro Market – bu gastronomiya dunyosidagi eng so'nggi
|
{t(
|
||||||
yangiliklarni, retseptlarni va tendentsiyalarni taqdim etuvchi
|
`Gastro Market – bu gastronomiya dunyosidagi eng so'nggi yangiliklarni`,
|
||||||
onlayn platforma. Biz o'quvchilarimizga sifatli va qiziqarli kontent
|
)}
|
||||||
taqdim etishga intilamiz.`}
|
|
||||||
</p>
|
</p>
|
||||||
<p className="text-lg text-muted-foreground leading-relaxed">
|
<p className="text-lg text-muted-foreground leading-relaxed">
|
||||||
{`Bizning jamoamiz tajribali kulinariya mutaxassislari, oshpazlar va
|
{t(`Bizning jamoamiz tajribali kulinariya mutaxassislari`)}
|
||||||
gastronomiya sohasidagi ekspertlardan iborat. Biz har bir maqolada
|
|
||||||
sifat va professionallikka e'tibor qaratamiz.`}
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Image Gallery */}
|
{/* Image Gallery */}
|
||||||
<div className="mb-20">
|
<div className="mb-20">
|
||||||
<h3 className="text-3xl font-bold text-center mb-12 text-balance">
|
<h3 className="text-3xl font-bold text-center mb-12 text-balance">
|
||||||
Bizning dunyo
|
{t('Bizning dunyo')}
|
||||||
</h3>
|
</h3>
|
||||||
<div className="grid md:grid-cols-3 gap-6">
|
<div className="grid md:grid-cols-3 gap-6">
|
||||||
{images.map((image, idx) => (
|
{images.map((image, idx) => (
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
|
import { useTranslations } from 'next-intl';
|
||||||
import Image from 'next/image';
|
import Image from 'next/image';
|
||||||
|
|
||||||
export function AboutHero() {
|
export function AboutHero() {
|
||||||
|
const t = useTranslations();
|
||||||
return (
|
return (
|
||||||
<section className="relative h-[60vh] min-h-[500px] flex items-center justify-center overflow-hidden">
|
<section className="relative h-[60vh] min-h-[500px] flex items-center justify-center overflow-hidden">
|
||||||
<div className="absolute inset-0 z-0">
|
<div className="absolute inset-0 z-0">
|
||||||
@@ -17,9 +19,9 @@ export function AboutHero() {
|
|||||||
Gastro Market
|
Gastro Market
|
||||||
</h1>
|
</h1>
|
||||||
<p className="text-xl md:text-2xl text-white/90 font-light leading-relaxed text-balance">
|
<p className="text-xl md:text-2xl text-white/90 font-light leading-relaxed text-balance">
|
||||||
{
|
{t(
|
||||||
"Gastronomiya va kulinariya san'ati haqidagi yetakchi onlayn magazin"
|
"Gastronomiya va kulinariya san'ati haqidagi yetakchi onlayn magazin",
|
||||||
}
|
)}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import formatPhone from '@/shared/lib/formatPhone';
|
import formatPhone from '@/shared/lib/formatPhone';
|
||||||
|
import onlyNumber from '@/shared/lib/onlyNumber';
|
||||||
import { Button } from '@/shared/ui/button';
|
import { Button } from '@/shared/ui/button';
|
||||||
import { Card } from '@/shared/ui/card';
|
import { Card } from '@/shared/ui/card';
|
||||||
import {
|
import {
|
||||||
@@ -15,11 +16,13 @@ import {
|
|||||||
import { Input } from '@/shared/ui/input';
|
import { Input } from '@/shared/ui/input';
|
||||||
import { Label } from '@/shared/ui/label';
|
import { Label } from '@/shared/ui/label';
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import { Upload } from 'lucide-react';
|
import { useMutation } from '@tanstack/react-query';
|
||||||
import { useState } from 'react';
|
import { Loader2, Upload } from 'lucide-react';
|
||||||
|
import { useTranslations } from 'next-intl';
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import * as z from 'zod';
|
import * as z from 'zod';
|
||||||
|
import { partner_api } from '../lib/api';
|
||||||
|
|
||||||
const MAX_FILE_SIZE = 5 * 1024 * 1024; // 5MB
|
const MAX_FILE_SIZE = 5 * 1024 * 1024; // 5MB
|
||||||
const ACCEPTED_FILE_TYPES = [
|
const ACCEPTED_FILE_TYPES = [
|
||||||
@@ -32,18 +35,18 @@ const partnershipFormSchema = z.object({
|
|||||||
companyName: z.string().min(2, {
|
companyName: z.string().min(2, {
|
||||||
message: "Kompaniya nomi kamida 2 ta belgidan iborat bo'lishi kerak",
|
message: "Kompaniya nomi kamida 2 ta belgidan iborat bo'lishi kerak",
|
||||||
}),
|
}),
|
||||||
website: z
|
// website: z
|
||||||
.string()
|
// .string()
|
||||||
.url({ message: "To'g'ri website manzilini kiriting" })
|
// .url({ message: "To'g'ri website manzilini kiriting" })
|
||||||
.optional()
|
// .optional()
|
||||||
.or(z.literal('')),
|
// .or(z.literal('')),
|
||||||
contactPerson: z
|
contactPerson: z
|
||||||
.string()
|
.string()
|
||||||
.min(2, { message: "Ism kamida 2 ta belgidan iborat bo'lishi kerak" }),
|
.min(2, { message: "Ism kamida 2 ta belgidan iborat bo'lishi kerak" }),
|
||||||
email: z
|
// email: z
|
||||||
.string()
|
// .string()
|
||||||
.email({ message: "To'g'ri email manzilini kiriting" })
|
// .email({ message: "To'g'ri email manzilini kiriting" })
|
||||||
.optional(),
|
// .optional(),
|
||||||
phone: z
|
phone: z
|
||||||
.string()
|
.string()
|
||||||
.min(9, { message: "To'g'ri telefon raqamini kiriting" })
|
.min(9, { message: "To'g'ri telefon raqamini kiriting" })
|
||||||
@@ -66,53 +69,56 @@ const partnershipFormSchema = z.object({
|
|||||||
type PartnershipFormValues = z.infer<typeof partnershipFormSchema>;
|
type PartnershipFormValues = z.infer<typeof partnershipFormSchema>;
|
||||||
|
|
||||||
export function PartnershipForm() {
|
export function PartnershipForm() {
|
||||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
const t = useTranslations();
|
||||||
|
|
||||||
const form = useForm<PartnershipFormValues>({
|
const form = useForm<PartnershipFormValues>({
|
||||||
resolver: zodResolver(partnershipFormSchema),
|
resolver: zodResolver(partnershipFormSchema),
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
companyName: '',
|
companyName: '',
|
||||||
website: '',
|
|
||||||
contactPerson: '',
|
contactPerson: '',
|
||||||
email: '',
|
|
||||||
phone: '+998',
|
phone: '+998',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
async function onSubmit(data: PartnershipFormValues) {
|
const { mutate, isPending } = useMutation({
|
||||||
console.log(data);
|
mutationFn: (body: FormData) => partner_api.send(body),
|
||||||
|
onSuccess: () => {
|
||||||
setIsSubmitting(true);
|
toast.success(t("So'rov yuborildi!"), {
|
||||||
|
|
||||||
try {
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
||||||
|
|
||||||
toast.success("So'rov yuborildi!", {
|
|
||||||
richColors: true,
|
richColors: true,
|
||||||
position: 'top-center',
|
position: 'top-center',
|
||||||
});
|
});
|
||||||
|
|
||||||
form.reset();
|
form.reset();
|
||||||
} catch {
|
},
|
||||||
|
onError: () => {
|
||||||
toast.error('Xatolik yuz berdi', {
|
toast.error('Xatolik yuz berdi', {
|
||||||
richColors: true,
|
richColors: true,
|
||||||
position: 'top-center',
|
position: 'top-center',
|
||||||
});
|
});
|
||||||
} finally {
|
},
|
||||||
setIsSubmitting(false);
|
});
|
||||||
|
|
||||||
|
async function onSubmit(data: PartnershipFormValues) {
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('company_name', data.companyName);
|
||||||
|
formData.append('full_name', data.contactPerson);
|
||||||
|
formData.append('phone_number', onlyNumber(data.phone));
|
||||||
|
|
||||||
|
if (data.companyFile && data.companyFile.length > 0) {
|
||||||
|
formData.append('file', data.companyFile[0]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
mutate(formData);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="px-4 mb-5">
|
<section className="px-4 mb-5" id="contact">
|
||||||
<div className="max-w-3xl mx-auto">
|
<div className="max-w-3xl mx-auto">
|
||||||
<div className="text-center mb-5">
|
<div className="text-center mb-5">
|
||||||
<h2 className="text-2xl md:text-5xl font-bold mb-2 text-balance">
|
<h2 className="text-2xl md:text-5xl font-bold mb-2 text-balance">
|
||||||
{`Hamkor bo'ling`}
|
{t(`Hamkor bo'ling`)}
|
||||||
</h2>
|
</h2>
|
||||||
<p className="text-md text-muted-foreground leading-relaxed">
|
<p className="text-md text-muted-foreground leading-relaxed">
|
||||||
{`Gastro Market bilan hamkorlik qilishni xohlaysizmi? Quyidagi formani
|
{t(`Gastro Market bilan hamkorlik qilishni xohlaysizmi?`)}
|
||||||
to'ldiring va biz siz bilan tez orada bog'lanamiz.`}
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -124,7 +130,7 @@ export function PartnershipForm() {
|
|||||||
name="companyName"
|
name="companyName"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<Label>Kompaniya nomi</Label>
|
<Label>{t('Kompaniya nomi')}</Label>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input
|
<Input
|
||||||
placeholder="Kompaniyangiz nomini kiriting"
|
placeholder="Kompaniyangiz nomini kiriting"
|
||||||
@@ -137,12 +143,12 @@ export function PartnershipForm() {
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<FormField
|
{/* <FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="website"
|
name="website"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<Label>Website</Label>
|
<Label>{t('Website')}</Label>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input
|
<Input
|
||||||
placeholder="https://example.com"
|
placeholder="https://example.com"
|
||||||
@@ -154,14 +160,14 @@ export function PartnershipForm() {
|
|||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/> */}
|
||||||
|
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="contactPerson"
|
name="contactPerson"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<Label>Ism Familiya</Label>
|
<Label>{t('Ism Familiya')}</Label>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input
|
<Input
|
||||||
placeholder="Ism va familiya"
|
placeholder="Ism va familiya"
|
||||||
@@ -174,13 +180,13 @@ export function PartnershipForm() {
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="grid md:grid-cols-2 gap-6">
|
<div className="grid md:grid-cols-1 gap-6">
|
||||||
<FormField
|
{/* <FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="email"
|
name="email"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem className="flex flex-col justify-start">
|
<FormItem className="flex flex-col justify-start">
|
||||||
<Label>Email</Label>
|
<Label>{t('Email')}</Label>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input
|
<Input
|
||||||
placeholder="example@email.com"
|
placeholder="example@email.com"
|
||||||
@@ -192,14 +198,14 @@ export function PartnershipForm() {
|
|||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/> */}
|
||||||
|
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="phone"
|
name="phone"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem className="flex flex-col justify-start">
|
<FormItem className="flex flex-col justify-start">
|
||||||
<Label>Telefon raqami</Label>
|
<Label>{t('Telefon raqami')}</Label>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input
|
<Input
|
||||||
placeholder="+998 90 123 45 67"
|
placeholder="+998 90 123 45 67"
|
||||||
@@ -219,7 +225,7 @@ export function PartnershipForm() {
|
|||||||
name="companyFile"
|
name="companyFile"
|
||||||
render={({ field: { onChange, value, ...field } }) => (
|
render={({ field: { onChange, value, ...field } }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<Label>Kompaniya hujjati</Label>
|
<Label>{t('Kompaniya hujjati')}</Label>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<div>
|
<div>
|
||||||
<Input
|
<Input
|
||||||
@@ -239,18 +245,18 @@ export function PartnershipForm() {
|
|||||||
>
|
>
|
||||||
<Upload className="size-10 text-muted-foreground" />
|
<Upload className="size-10 text-muted-foreground" />
|
||||||
<p className="text-muted-foreground text-lg">
|
<p className="text-muted-foreground text-lg">
|
||||||
Faylni tanlang
|
{t('Faylni tanlang')}
|
||||||
</p>
|
</p>
|
||||||
{value && value.length > 0 && (
|
{value && value.length > 0 && (
|
||||||
<p className="mt-2 text-sm text-gray-500">
|
<p className="mt-2 text-sm text-gray-500">
|
||||||
Tanlangan fayl: {value[0].name}
|
{t('Tanlangan fayl')}: {value[0].name}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
</FormLabel>
|
</FormLabel>
|
||||||
</div>
|
</div>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormDescription>
|
<FormDescription>
|
||||||
PDF yoki Word formatida (maksimal 5MB)
|
{t('PDF yoki Word formatida (maksimal 5MB)')}
|
||||||
</FormDescription>
|
</FormDescription>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
@@ -260,10 +266,14 @@ export function PartnershipForm() {
|
|||||||
<Button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
size="lg"
|
size="lg"
|
||||||
className="w-full"
|
className="w-full cursor-pointer"
|
||||||
disabled={isSubmitting}
|
disabled={isPending}
|
||||||
>
|
>
|
||||||
{isSubmitting ? 'Yuborilmoqda...' : "So'rov yuborish"}
|
{isPending ? (
|
||||||
|
<Loader2 className="animate-spin" />
|
||||||
|
) : (
|
||||||
|
t("So'rov yuborish")
|
||||||
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
</form>
|
</form>
|
||||||
</Form>
|
</Form>
|
||||||
|
|||||||
9
src/features/auth/lib/api.ts
Normal file
9
src/features/auth/lib/api.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import httpClient from '@/shared/config/api/httpClient';
|
||||||
|
import { API_URLS } from '@/shared/config/api/URLs';
|
||||||
|
|
||||||
|
export const auth_api = {
|
||||||
|
async login(body: { username: string; password: string; tg_id?: string }) {
|
||||||
|
const res = await httpClient.post(API_URLS.Login, body);
|
||||||
|
return res;
|
||||||
|
},
|
||||||
|
};
|
||||||
6
src/features/auth/lib/form.ts
Normal file
6
src/features/auth/lib/form.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
import z from 'zod';
|
||||||
|
|
||||||
|
export const authForm = z.object({
|
||||||
|
username: z.string().min(1, { message: 'Majburiy maydon' }),
|
||||||
|
password: z.string().min(1, { message: 'Majburiy maydon' }),
|
||||||
|
});
|
||||||
@@ -1,150 +1,65 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useRouter } from '@/shared/config/i18n/navigation';
|
import { Link, useRouter } from '@/shared/config/i18n/navigation';
|
||||||
import formatPhone from '@/shared/lib/formatPhone';
|
import { setToken, setUser } from '@/shared/lib/token';
|
||||||
|
import { Button } from '@/shared/ui/button';
|
||||||
|
import {
|
||||||
|
Form,
|
||||||
|
FormControl,
|
||||||
|
FormField,
|
||||||
|
FormItem,
|
||||||
|
FormMessage,
|
||||||
|
} from '@/shared/ui/form';
|
||||||
import { Input } from '@/shared/ui/input';
|
import { Input } from '@/shared/ui/input';
|
||||||
import { ArrowRight, Check, Lock, Phone } from 'lucide-react';
|
import { Label } from '@/shared/ui/label';
|
||||||
import { useEffect, useRef, useState } from 'react';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
|
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
type Step = 'phone' | 'otp';
|
import { Loader2, User } from 'lucide-react';
|
||||||
|
import { useTranslations } from 'next-intl';
|
||||||
|
import { useForm } from 'react-hook-form';
|
||||||
|
import { toast } from 'sonner';
|
||||||
|
import z from 'zod';
|
||||||
|
import { auth_api } from '../lib/api';
|
||||||
|
import { authForm } from '../lib/form';
|
||||||
|
|
||||||
const Login = () => {
|
const Login = () => {
|
||||||
const [step, setStep] = useState<Step>('phone');
|
|
||||||
const [phoneNumber, setPhoneNumber] = useState<string>('+998');
|
|
||||||
const [otp, setOtp] = useState<string[]>(['', '', '', '', '', '']);
|
|
||||||
const [isLoading, setIsLoading] = useState<boolean>(false);
|
|
||||||
const [error, setError] = useState<string>('');
|
|
||||||
const [countdown, setCountdown] = useState<number>(60);
|
|
||||||
const [canResend, setCanResend] = useState<boolean>(false);
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const t = useTranslations();
|
||||||
|
const form = useForm<z.infer<typeof authForm>>({
|
||||||
|
resolver: zodResolver(authForm),
|
||||||
|
defaultValues: {
|
||||||
|
password: '',
|
||||||
|
username: '',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
const otpInputs = useRef<Array<HTMLInputElement | null>>([]);
|
const { mutate, isPending } = useMutation({
|
||||||
|
mutationFn: (body: {
|
||||||
|
username: string;
|
||||||
|
password: string;
|
||||||
|
tg_id?: string;
|
||||||
|
}) => auth_api.login(body),
|
||||||
|
onSuccess: (res) => {
|
||||||
|
setToken(res.data.access);
|
||||||
|
setUser(form.getValues('username'));
|
||||||
|
router.push('/');
|
||||||
|
queryClient.refetchQueries({ queryKey: ['product_list'] });
|
||||||
|
},
|
||||||
|
onError: () => {
|
||||||
|
toast.error(t('Username yoki parol xato kiritildi'), {
|
||||||
|
richColors: true,
|
||||||
|
position: 'top-center',
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
/* Countdown */
|
function onSubmit(values: z.infer<typeof authForm>) {
|
||||||
useEffect(() => {
|
mutate({
|
||||||
if (step === 'otp' && countdown > 0) {
|
password: values.password,
|
||||||
const timer = setTimeout(() => setCountdown((c) => c - 1), 1000);
|
username: values.username,
|
||||||
return () => clearTimeout(timer);
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (countdown === 0) {
|
|
||||||
setCanResend(true);
|
|
||||||
}
|
|
||||||
}, [countdown, step]);
|
|
||||||
|
|
||||||
/* Phone submit */
|
|
||||||
const handlePhoneSubmit = (): void => {
|
|
||||||
setError('');
|
|
||||||
|
|
||||||
if (phoneNumber.length < 9) {
|
|
||||||
setError("Telefon raqamni to'liq kiriting");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setIsLoading(true);
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
setIsLoading(false);
|
|
||||||
setStep('otp');
|
|
||||||
setCountdown(60);
|
|
||||||
setCanResend(false);
|
|
||||||
}, 1500);
|
|
||||||
};
|
|
||||||
|
|
||||||
/* OTP change */
|
|
||||||
const handleOtpChange = (index: number, value: string): void => {
|
|
||||||
if (value && !/^\d$/.test(value)) return;
|
|
||||||
|
|
||||||
const newOtp = [...otp];
|
|
||||||
newOtp[index] = value;
|
|
||||||
setOtp(newOtp);
|
|
||||||
|
|
||||||
if (value && index < 5) {
|
|
||||||
otpInputs.current[index + 1]?.focus();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (newOtp.every((d) => d !== '') && index === 5) {
|
|
||||||
handleOtpSubmit(newOtp);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/* OTP keydown */
|
|
||||||
const handleOtpKeyDown = (
|
|
||||||
index: number,
|
|
||||||
e: React.KeyboardEvent<HTMLInputElement>,
|
|
||||||
): void => {
|
|
||||||
if (e.key === 'Backspace' && !otp[index] && index > 0) {
|
|
||||||
otpInputs.current[index - 1]?.focus();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/* OTP paste */
|
|
||||||
const handleOtpPaste = (e: React.ClipboardEvent<HTMLDivElement>): void => {
|
|
||||||
e.preventDefault();
|
|
||||||
const pasted = e.clipboardData.getData('text').slice(0, 6);
|
|
||||||
|
|
||||||
if (!/^\d+$/.test(pasted)) return;
|
|
||||||
|
|
||||||
const newOtp = pasted.split('');
|
|
||||||
setOtp([...newOtp, ...Array(6 - newOtp.length).fill('')]);
|
|
||||||
|
|
||||||
const lastIndex = Math.min(newOtp.length - 1, 5);
|
|
||||||
otpInputs.current[lastIndex]?.focus();
|
|
||||||
|
|
||||||
if (pasted.length === 6) {
|
|
||||||
setTimeout(() => handleOtpSubmit(newOtp), 100);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleOtpSubmit = (otpArray: string[] = otp): void => {
|
|
||||||
setError('');
|
|
||||||
const otpCode = otpArray.join('');
|
|
||||||
|
|
||||||
if (otpCode.length < 6) {
|
|
||||||
setError("Kodni to'liq kiriting");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setIsLoading(true);
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
setIsLoading(false);
|
|
||||||
|
|
||||||
if (otpCode === '123456') {
|
|
||||||
localStorage.setItem('user', 'true');
|
|
||||||
router.push('/');
|
|
||||||
} else {
|
|
||||||
setError("Noto'g'ri kod. Qayta urinib ko'ring.");
|
|
||||||
setOtp(['', '', '', '', '', '']);
|
|
||||||
otpInputs.current[0]?.focus();
|
|
||||||
}
|
|
||||||
}, 1500);
|
|
||||||
};
|
|
||||||
|
|
||||||
/* Resend */
|
|
||||||
const handleResendOtp = (): void => {
|
|
||||||
if (!canResend) return;
|
|
||||||
|
|
||||||
setIsLoading(true);
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
setIsLoading(false);
|
|
||||||
setCountdown(60);
|
|
||||||
setCanResend(false);
|
|
||||||
setOtp(['', '', '', '', '', '']);
|
|
||||||
otpInputs.current[0]?.focus();
|
|
||||||
alert('Yangi kod yuborildi!');
|
|
||||||
}, 1000);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleChangeNumber = (): void => {
|
|
||||||
setStep('phone');
|
|
||||||
setPhoneNumber('');
|
|
||||||
setOtp(['', '', '', '', '', '']);
|
|
||||||
setError('');
|
|
||||||
setCountdown(60);
|
|
||||||
setCanResend(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="custom-container flex justify-center items-center h-[85vh]">
|
<div className="custom-container flex justify-center items-center h-[85vh]">
|
||||||
@@ -152,167 +67,71 @@ const Login = () => {
|
|||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="bg-gradient-to-r from-blue-600 to-indigo-600 p-8 text-white text-center">
|
<div className="bg-gradient-to-r from-blue-600 to-indigo-600 p-8 text-white text-center">
|
||||||
<div className="w-20 h-20 bg-white bg-opacity-20 rounded-full flex items-center justify-center mx-auto mb-4">
|
<div className="w-20 h-20 bg-white bg-opacity-20 rounded-full flex items-center justify-center mx-auto mb-4">
|
||||||
{step === 'phone' ? (
|
<User className="w-10 h-10 text-blue-500" />
|
||||||
<Phone className="w-10 h-10 text-blue-500" />
|
|
||||||
) : (
|
|
||||||
<Lock className="w-10 h-10 text-blue-500" />
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
<h1 className="text-2xl font-bold mb-2">
|
<p className="text-blue-100 text-2xl font-semibold">
|
||||||
{step === 'phone' ? 'Xush kelibsiz!' : 'Kodni tasdiqlang'}
|
{t('Tizimga kirish')}
|
||||||
</h1>
|
|
||||||
<p className="text-blue-100">
|
|
||||||
{step === 'phone'
|
|
||||||
? 'Telefon raqamingizni kiriting'
|
|
||||||
: `${phoneNumber} raqamiga yuborilgan kodni kiriting`}
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Form */}
|
{/* Form */}
|
||||||
<div className="p-8">
|
<Form {...form}>
|
||||||
{step === 'phone' ? (
|
<form
|
||||||
// Phone Number Step
|
className="p-8 space-y-4"
|
||||||
<div>
|
onSubmit={form.handleSubmit(onSubmit)}
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
>
|
||||||
Telefon raqam
|
<FormField
|
||||||
</label>
|
control={form.control}
|
||||||
<div className="relative">
|
name="username"
|
||||||
<div className="absolute inset-y-0 left-0 pl-4 flex items-center pointer-events-none">
|
render={({ field }) => (
|
||||||
<Phone className="w-5 h-5 text-gray-400" />
|
<FormItem>
|
||||||
</div>
|
<Label>{t('Username')}</Label>
|
||||||
<Input
|
<FormControl>
|
||||||
type="tel"
|
<Input
|
||||||
value={formatPhone(phoneNumber)}
|
placeholder={t('Username')}
|
||||||
onChange={(e) => {
|
className="h-12"
|
||||||
const value = e.target.value.replace(/\D/g, '');
|
{...field}
|
||||||
setPhoneNumber(value);
|
/>
|
||||||
setError('');
|
</FormControl>
|
||||||
}}
|
<FormMessage />
|
||||||
placeholder="+998 90 123-45-67"
|
</FormItem>
|
||||||
maxLength={17}
|
|
||||||
className="w-full pl-12 pr-4 py-4 h-12 border-2 border-gray-300 rounded-xl focus:outline-none focus:border-blue-500 transition text-lg"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{error && <p className="text-red-500 text-sm mt-2">{error}</p>}
|
|
||||||
|
|
||||||
<button
|
|
||||||
onClick={handlePhoneSubmit}
|
|
||||||
disabled={isLoading || phoneNumber.length < 9}
|
|
||||||
className="w-full mt-6 bg-gradient-to-r from-blue-600 to-indigo-600 text-white py-4 rounded-xl font-semibold hover:from-blue-700 hover:to-indigo-700 transition disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center gap-2"
|
|
||||||
>
|
|
||||||
{isLoading ? (
|
|
||||||
<>
|
|
||||||
<div className="w-5 h-5 border-2 border-white border-t-transparent rounded-full animate-spin" />
|
|
||||||
Yuborilmoqda...
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
Kodni olish
|
|
||||||
<ArrowRight className="w-5 h-5" />
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<p className="text-center text-sm text-gray-500 mt-6">
|
|
||||||
Davom etish orqali siz bizning{' '}
|
|
||||||
<a href="#" className="text-blue-600 hover:underline">
|
|
||||||
Foydalanish shartlari
|
|
||||||
</a>{' '}
|
|
||||||
va{' '}
|
|
||||||
<a href="#" className="text-blue-600 hover:underline">
|
|
||||||
Maxfiylik siyosati
|
|
||||||
</a>
|
|
||||||
ga rozilik bildirasiz
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
// OTP Step
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-4 text-center">
|
|
||||||
6 raqamli kodni kiriting
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<div
|
|
||||||
className="flex gap-2 justify-center mb-6"
|
|
||||||
onPaste={handleOtpPaste}
|
|
||||||
>
|
|
||||||
{otp.map((digit, index) => (
|
|
||||||
<Input
|
|
||||||
key={index}
|
|
||||||
ref={(el) => {
|
|
||||||
otpInputs.current[index] = el;
|
|
||||||
}}
|
|
||||||
type="text"
|
|
||||||
inputMode="numeric"
|
|
||||||
maxLength={1}
|
|
||||||
value={digit}
|
|
||||||
onChange={(e) => handleOtpChange(index, e.target.value)}
|
|
||||||
onKeyDown={(e) => handleOtpKeyDown(index, e)}
|
|
||||||
className="w-12 h-14 text-center text-2xl font-bold border-2 border-gray-300 rounded-lg focus:outline-none focus:border-blue-500 transition"
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{error && (
|
|
||||||
<div className="bg-red-50 border border-red-200 text-red-600 px-4 py-3 rounded-lg mb-4 text-sm text-center">
|
|
||||||
{error}
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
<button
|
<FormField
|
||||||
onClick={() => handleOtpSubmit()}
|
control={form.control}
|
||||||
disabled={isLoading || otp.some((digit) => digit === '')}
|
name="password"
|
||||||
className="w-full bg-gradient-to-r from-blue-600 to-indigo-600 text-white py-4 rounded-xl font-semibold hover:from-blue-700 hover:to-indigo-700 transition disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center gap-2"
|
render={({ field }) => (
|
||||||
>
|
<FormItem>
|
||||||
{isLoading ? (
|
<Label>{t('Parol')}</Label>
|
||||||
<>
|
<FormControl>
|
||||||
<div className="w-5 h-5 border-2 border-white border-t-transparent rounded-full animate-spin" />
|
<Input
|
||||||
Tekshirilmoqda...
|
placeholder={t('Parol')}
|
||||||
</>
|
className="h-12"
|
||||||
) : (
|
{...field}
|
||||||
<>
|
/>
|
||||||
Tasdiqlash
|
</FormControl>
|
||||||
<Check className="w-5 h-5" />
|
<FormMessage />
|
||||||
</>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
</button>
|
/>
|
||||||
|
<p className="text-muted-foreground font-semibold mt-5 text-sm">
|
||||||
{/* Resend OTP */}
|
{t(
|
||||||
<div className="mt-6 text-center">
|
"Agarda sizda kirish uchun login va parol yo'q bolsa iltimos bizga murojat qiling",
|
||||||
{canResend ? (
|
)}{' '}
|
||||||
<button
|
<Link href={'/about/#contact'} className="text-blue-500">
|
||||||
onClick={handleResendOtp}
|
{t('Murojat qilish')}
|
||||||
disabled={isLoading}
|
</Link>
|
||||||
className="text-blue-600 hover:text-blue-700 font-semibold hover:underline"
|
</p>
|
||||||
>
|
<Button
|
||||||
Kodni qayta yuborish
|
disabled={isPending}
|
||||||
</button>
|
type="submit"
|
||||||
) : (
|
className="w-full h-12 text-md"
|
||||||
<p className="text-gray-500 text-sm">
|
>
|
||||||
Kodni qayta yuborish ({countdown}s)
|
{isPending ? <Loader2 className="animate-spin" /> : t('Kirish')}
|
||||||
</p>
|
</Button>
|
||||||
)}
|
</form>
|
||||||
</div>
|
</Form>
|
||||||
|
|
||||||
{/* Change Number */}
|
|
||||||
<button
|
|
||||||
onClick={handleChangeNumber}
|
|
||||||
className="w-full mt-4 text-gray-600 hover:text-gray-800 font-medium"
|
|
||||||
>
|
|
||||||
{"Raqamni o'zgartirish"}
|
|
||||||
</button>
|
|
||||||
|
|
||||||
{/* Demo info */}
|
|
||||||
<div className="mt-6 bg-blue-50 border border-blue-200 rounded-lg p-4">
|
|
||||||
<p className="text-sm text-blue-800 text-center">
|
|
||||||
<strong>Demo uchun:</strong>
|
|
||||||
{`Kod sifatida "123456" kiriting`}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
75
src/features/cart/lib/api.ts
Normal file
75
src/features/cart/lib/api.ts
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
import httpClient from '@/shared/config/api/httpClient';
|
||||||
|
import { API_URLS } from '@/shared/config/api/URLs';
|
||||||
|
import { AxiosResponse } from 'axios';
|
||||||
|
|
||||||
|
interface CartItem {
|
||||||
|
id: string;
|
||||||
|
cart_item: {
|
||||||
|
id: string;
|
||||||
|
product_name: string;
|
||||||
|
product_id: string;
|
||||||
|
product_image: string;
|
||||||
|
quantity: number;
|
||||||
|
product_price: number;
|
||||||
|
}[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface OrderCreateBody {
|
||||||
|
items: {
|
||||||
|
product_id: string;
|
||||||
|
quantity: number;
|
||||||
|
}[];
|
||||||
|
payment_type: 'CASH' | 'ACCOUNT_NUMBER';
|
||||||
|
delivery_type: 'YandexGo' | 'DELIVERY_COURIES' | 'PICKUP';
|
||||||
|
contact_number: string;
|
||||||
|
comment: string;
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const cart_api = {
|
||||||
|
async create(): Promise<AxiosResponse<{ cart_id: string }>> {
|
||||||
|
const res = await httpClient.post(API_URLS.CartCrate);
|
||||||
|
return res;
|
||||||
|
},
|
||||||
|
|
||||||
|
async cart_item(body: { product: string; quantity: number; cart: string }) {
|
||||||
|
const res = await httpClient.post(API_URLS.CartItem, body);
|
||||||
|
return res;
|
||||||
|
},
|
||||||
|
|
||||||
|
async get_cart_items(cart_id: string): Promise<AxiosResponse<CartItem>> {
|
||||||
|
const res = await httpClient.get(`${API_URLS.CartItemList(cart_id)}`);
|
||||||
|
return res;
|
||||||
|
},
|
||||||
|
|
||||||
|
async update_cart_item({
|
||||||
|
body,
|
||||||
|
cart_item_id,
|
||||||
|
}: {
|
||||||
|
body: { quantity: number };
|
||||||
|
cart_item_id: string;
|
||||||
|
}): Promise<AxiosResponse> {
|
||||||
|
const res = await httpClient.patch(
|
||||||
|
`${API_URLS.CartItemUpdate(cart_item_id)}`,
|
||||||
|
body,
|
||||||
|
);
|
||||||
|
return res;
|
||||||
|
},
|
||||||
|
|
||||||
|
async delete_cart_item(cart_item_id: string): Promise<AxiosResponse> {
|
||||||
|
const res = await httpClient.delete(
|
||||||
|
`${API_URLS.CartItemDelete(cart_item_id)}`,
|
||||||
|
);
|
||||||
|
return res;
|
||||||
|
},
|
||||||
|
|
||||||
|
async createOrder(body: OrderCreateBody) {
|
||||||
|
const res = await httpClient.post(`${API_URLS.CreateOrder}`, body);
|
||||||
|
return res;
|
||||||
|
},
|
||||||
|
|
||||||
|
async clear_cart(id: number | string) {
|
||||||
|
const res = await httpClient.get(API_URLS.CartClear(id));
|
||||||
|
return res;
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -6,6 +6,6 @@ export const orderForm = z.object({
|
|||||||
phone: z.string().min(12, { message: 'Xato raqam kiritildi' }),
|
phone: z.string().min(12, { message: 'Xato raqam kiritildi' }),
|
||||||
long: z.string().min(1, { message: 'Majburiy maydon' }),
|
long: z.string().min(1, { message: 'Majburiy maydon' }),
|
||||||
lat: z.string().min(1, { message: 'Majburiy maydon' }),
|
lat: z.string().min(1, { message: 'Majburiy maydon' }),
|
||||||
|
comment: z.string().min(1, { message: 'Majburiy maydon' }),
|
||||||
city: z.string().optional(),
|
city: z.string().optional(),
|
||||||
});
|
});
|
||||||
// 998901234567
|
|
||||||
|
|||||||
@@ -1,8 +1,13 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
import { cart_api } from '@/features/cart/lib/api';
|
||||||
|
import { BASE_URL } from '@/shared/config/api/URLs';
|
||||||
import { useRouter } from '@/shared/config/i18n/navigation';
|
import { useRouter } from '@/shared/config/i18n/navigation';
|
||||||
|
import { useCartId } from '@/shared/hooks/cartId';
|
||||||
|
import formatPrice from '@/shared/lib/formatPrice';
|
||||||
import { Button } from '@/shared/ui/button';
|
import { Button } from '@/shared/ui/button';
|
||||||
import { Input } from '@/shared/ui/input';
|
import { Input } from '@/shared/ui/input';
|
||||||
|
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||||
|
import { AxiosError } from 'axios';
|
||||||
import {
|
import {
|
||||||
ArrowLeft,
|
ArrowLeft,
|
||||||
CreditCard,
|
CreditCard,
|
||||||
@@ -12,305 +17,258 @@ import {
|
|||||||
Trash,
|
Trash,
|
||||||
Truck,
|
Truck,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
|
import { useTranslations } from 'next-intl';
|
||||||
import Image from 'next/image';
|
import Image from 'next/image';
|
||||||
import { useState } from 'react';
|
import { useEffect, useRef, useState } from 'react';
|
||||||
|
import { toast } from 'sonner';
|
||||||
interface CartItem {
|
|
||||||
id: number;
|
|
||||||
name: string;
|
|
||||||
price: number;
|
|
||||||
oldPrice: number;
|
|
||||||
image: string;
|
|
||||||
quantity: number | string;
|
|
||||||
inStock: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
const CartPage = () => {
|
const CartPage = () => {
|
||||||
|
const { cart_id } = useCartId();
|
||||||
|
const queryClient = useQueryClient();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const [cartItems, setCartItems] = useState<CartItem[]>([
|
const t = useTranslations();
|
||||||
{
|
|
||||||
id: 5,
|
|
||||||
name: 'Coca-Cola 1.5L',
|
|
||||||
price: 12000,
|
|
||||||
oldPrice: 14000,
|
|
||||||
image: '/classic-coca-cola.png',
|
|
||||||
quantity: 2,
|
|
||||||
inStock: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 6,
|
|
||||||
name: 'Pepsi 2L',
|
|
||||||
price: 11000,
|
|
||||||
oldPrice: 13000,
|
|
||||||
image: '/pepsi-bottle.jpg',
|
|
||||||
quantity: 1,
|
|
||||||
inStock: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 8,
|
|
||||||
name: 'Sprite 1.5L',
|
|
||||||
price: 10000,
|
|
||||||
oldPrice: 12000,
|
|
||||||
image: '/clear-soda-bottle.png',
|
|
||||||
quantity: 3,
|
|
||||||
inStock: true,
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
|
|
||||||
const subtotal = cartItems.reduce(
|
const { data: cartItems, isLoading } = useQuery({
|
||||||
(sum, item) => sum + item.price * Number(item.quantity),
|
queryKey: ['cart_items', cart_id],
|
||||||
0,
|
queryFn: () => cart_api.get_cart_items(cart_id!),
|
||||||
);
|
enabled: !!cart_id,
|
||||||
const discount = cartItems.reduce((sum, item) => {
|
select: (data) => data.data.cart_item,
|
||||||
if (item.oldPrice) {
|
});
|
||||||
return sum + (item.oldPrice - item.price) * Number(item.quantity);
|
|
||||||
}
|
|
||||||
return sum;
|
|
||||||
}, 0);
|
|
||||||
const deliveryFee = subtotal > 50000 ? 0 : 15000;
|
|
||||||
const total = subtotal - discount + deliveryFee;
|
|
||||||
|
|
||||||
const handleQuantityChange = (id: number, type: 'increase' | 'decrease') => {
|
const [quantities, setQuantities] = useState<Record<string, string>>({});
|
||||||
setCartItems((prev) =>
|
const debounceRef = useRef<Record<string, NodeJS.Timeout | null>>({});
|
||||||
prev.map((item) => {
|
|
||||||
if (item.id === id) {
|
useEffect(() => {
|
||||||
if (type === 'increase')
|
if (!cartItems) return;
|
||||||
return { ...item, quantity: Number(item.quantity) + 1 };
|
const initialQuantities: Record<string, string> = {};
|
||||||
if (type === 'decrease' && Number(item.quantity) > 1)
|
cartItems.forEach((item) => {
|
||||||
return { ...item, quantity: Number(item.quantity) - 1 };
|
initialQuantities[item.id] = String(item.quantity);
|
||||||
}
|
debounceRef.current[item.id] = null;
|
||||||
return item;
|
});
|
||||||
}),
|
setQuantities(initialQuantities);
|
||||||
|
}, [cartItems]);
|
||||||
|
|
||||||
|
const { mutate: updateCartItem } = useMutation({
|
||||||
|
mutationFn: ({
|
||||||
|
body,
|
||||||
|
cart_item_id,
|
||||||
|
}: {
|
||||||
|
body: { quantity: number };
|
||||||
|
cart_item_id: string;
|
||||||
|
}) => cart_api.update_cart_item({ body, cart_item_id }),
|
||||||
|
onSuccess: () =>
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['cart_items', cart_id] }),
|
||||||
|
onError: (err: AxiosError) =>
|
||||||
|
toast.error(err.message, { richColors: true, position: 'top-center' }),
|
||||||
|
});
|
||||||
|
|
||||||
|
const { mutate: deleteCartItem } = useMutation({
|
||||||
|
mutationFn: ({ cart_item_id }: { cart_item_id: string }) =>
|
||||||
|
cart_api.delete_cart_item(cart_item_id),
|
||||||
|
onSuccess: () =>
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['cart_items', cart_id] }),
|
||||||
|
onError: (err: AxiosError) =>
|
||||||
|
toast.error(err.message, { richColors: true, position: 'top-center' }),
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleCheckout = () => router.push('/cart/order');
|
||||||
|
|
||||||
|
if (isLoading)
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen flex items-center justify-center">
|
||||||
|
Loading...
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
};
|
|
||||||
|
|
||||||
// Remove item from cart
|
if (!cartItems || cartItems.length === 0)
|
||||||
const handleRemoveItem = (id: number) => {
|
|
||||||
setCartItems((prev) => prev.filter((item) => item.id !== id));
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleCheckout = () => {
|
|
||||||
router.push('/cart/order');
|
|
||||||
};
|
|
||||||
|
|
||||||
if (cartItems.length === 0) {
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen flex items-center justify-center">
|
<div className="min-h-screen flex items-center justify-center">
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<ShoppingBag className="w-24 h-24 text-gray-300 mx-auto mb-4" />
|
<ShoppingBag className="w-24 h-24 text-gray-300 mx-auto mb-4" />
|
||||||
<h2 className="text-2xl font-bold text-gray-800 mb-2">
|
<h2 className="text-2xl font-bold text-gray-800 mb-2">
|
||||||
{"Savatingiz bo'sh"}
|
{t("Savatingiz bo'sh")}
|
||||||
</h2>
|
</h2>
|
||||||
<p className="text-gray-600 mb-6">
|
<p className="text-gray-600 mb-6">
|
||||||
{"Mahsulotlar qo'shish uchun katalogga o'ting"}
|
{t("Mahsulotlar qo'shish uchun katalogga o'ting")}
|
||||||
</p>
|
</p>
|
||||||
<button
|
<button
|
||||||
onClick={() => router.push('/')}
|
onClick={() => router.push('/')}
|
||||||
className="bg-blue-600 text-white px-6 py-3 rounded-lg hover:bg-blue-700 transition flex items-center gap-2 mx-auto"
|
className="bg-blue-600 text-white px-6 py-3 rounded-lg hover:bg-blue-700 transition flex items-center gap-2 mx-auto"
|
||||||
>
|
>
|
||||||
<ArrowLeft className="w-5 h-5" /> Xarid qilishni boshlash
|
<ArrowLeft className="w-5 h-5" /> {t('Xarid qilishni boshlash')}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
|
||||||
|
const subtotal = cartItems.reduce(
|
||||||
|
(sum, item) => sum + item.product_price * Number(item.quantity),
|
||||||
|
0,
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleQuantityChange = (itemId: string, value: number) => {
|
||||||
|
setQuantities((prev) => ({
|
||||||
|
...prev,
|
||||||
|
[itemId]: String(value),
|
||||||
|
}));
|
||||||
|
|
||||||
|
if (debounceRef.current[itemId]) clearTimeout(debounceRef.current[itemId]!);
|
||||||
|
|
||||||
|
debounceRef.current[itemId] = setTimeout(() => {
|
||||||
|
if (value <= 0) {
|
||||||
|
deleteCartItem({ cart_item_id: itemId });
|
||||||
|
} else {
|
||||||
|
updateCartItem({ body: { quantity: value }, cart_item_id: itemId });
|
||||||
|
}
|
||||||
|
}, 500);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="custom-container mb-6">
|
<div className="custom-container mb-6">
|
||||||
<>
|
<div className="mb-6">
|
||||||
{/* Header */}
|
<h1 className="text-3xl font-bold text-gray-800 mb-2">{t('Savat')}</h1>
|
||||||
<div className="mb-6">
|
<p className="text-gray-600">
|
||||||
<h1 className="text-3xl font-bold text-gray-800 mb-2">Savat</h1>
|
{cartItems.length} {t('ta mahsulot')}
|
||||||
<p className="text-gray-600">{cartItems.length} ta mahsulot</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||||
{/* Cart Items */}
|
<div className="lg:col-span-2">
|
||||||
<div className="lg:col-span-2">
|
<div className="bg-white rounded-lg shadow-md overflow-hidden">
|
||||||
<div className="bg-white rounded-lg shadow-md overflow-hidden">
|
{cartItems.map((item, index) => (
|
||||||
{cartItems.map((item, index) => (
|
<div
|
||||||
<div
|
key={item.id}
|
||||||
key={item.id}
|
className={`p-6 flex relative gap-4 ${
|
||||||
className={`p-6 flex relative gap-4 ${index !== cartItems.length - 1 ? 'border-b' : ''}`}
|
index !== cartItems.length - 1 ? 'border-b' : ''
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
variant="destructive"
|
||||||
|
size="icon"
|
||||||
|
onClick={() => deleteCartItem({ cart_item_id: item.id })}
|
||||||
|
className="absolute right-2 w-7 h-7 top-2 cursor-pointer"
|
||||||
>
|
>
|
||||||
{/* Product Image */}
|
<Trash className="size-4" />
|
||||||
<Button
|
</Button>
|
||||||
variant={'destructive'}
|
|
||||||
size={'icon'}
|
|
||||||
onClick={() => handleRemoveItem(item.id)}
|
|
||||||
className="absolute right-2 w-7 h-7 top-2 cursor-pointer"
|
|
||||||
>
|
|
||||||
<Trash className="size-4" />
|
|
||||||
</Button>
|
|
||||||
<div className="w-24 h-40 bg-gray-100 rounded-lg flex-shrink-0 overflow-hidden">
|
|
||||||
<Image
|
|
||||||
width={500}
|
|
||||||
height={500}
|
|
||||||
src={item.image}
|
|
||||||
alt={item.name}
|
|
||||||
className="w-full h-full object-cover"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Product Info */}
|
<div className="w-24 h-40 bg-gray-100 rounded-lg flex-shrink-0 overflow-hidden">
|
||||||
<div className="flex-1">
|
<Image
|
||||||
<h3 className="font-semibold text-lg mb-1">{item.name}</h3>
|
src={BASE_URL + item.product_image}
|
||||||
<div className="flex items-center gap-2 mb-3 max-lg:flex-col max-lg:items-start max-lg:gap-1">
|
alt={item.product_name}
|
||||||
<span className="text-blue-600 font-bold text-xl">
|
width={500}
|
||||||
{item.price.toLocaleString()} {"so'm"}
|
height={500}
|
||||||
</span>
|
className="object-cover"
|
||||||
{item.oldPrice && (
|
style={{ width: '100%', height: 'auto' }}
|
||||||
<span className="text-gray-400 line-through text-sm">
|
/>
|
||||||
{item.oldPrice.toLocaleString()} {"so'm"}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-center justify-between max-lg:flex-col max-lg:items-start max-lg:gap-1">
|
|
||||||
{/* Quantity Controls */}
|
|
||||||
<div className="flex items-center border border-gray-300 rounded-lg">
|
|
||||||
<button
|
|
||||||
onClick={() =>
|
|
||||||
handleQuantityChange(item.id, 'decrease')
|
|
||||||
}
|
|
||||||
className="p-2 cursor-pointer transition rounded-lg"
|
|
||||||
disabled={Number(item.quantity) <= 1}
|
|
||||||
>
|
|
||||||
<Minus className="w-4 h-4" />
|
|
||||||
</button>
|
|
||||||
<Input
|
|
||||||
type="text"
|
|
||||||
min={1}
|
|
||||||
value={item.quantity}
|
|
||||||
onChange={(e) => {
|
|
||||||
const value = e.target.value;
|
|
||||||
// Bo'sh qiymatga ruxsat berish
|
|
||||||
if (value === '') {
|
|
||||||
setCartItems((prev) =>
|
|
||||||
prev.map((cartItem) =>
|
|
||||||
cartItem.id === item.id
|
|
||||||
? { ...cartItem, quantity: '' }
|
|
||||||
: cartItem,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const number = parseInt(value, 10);
|
|
||||||
if (!isNaN(number) && number > 0) {
|
|
||||||
setCartItems((prev) =>
|
|
||||||
prev.map((cartItem) =>
|
|
||||||
cartItem.id === item.id
|
|
||||||
? { ...cartItem, quantity: number }
|
|
||||||
: cartItem,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
className="w-16 text-center outline-none ring-0 focus-visible:ring-0 border-none font-semibold"
|
|
||||||
/>
|
|
||||||
<button
|
|
||||||
onClick={() =>
|
|
||||||
handleQuantityChange(item.id, 'increase')
|
|
||||||
}
|
|
||||||
className="p-2 cursor-pointer transition rounded-lg"
|
|
||||||
>
|
|
||||||
<Plus className="w-4 h-4" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Order Summary */}
|
|
||||||
<div className="lg:col-span-1">
|
|
||||||
<div className="bg-white rounded-lg shadow-md p-6 sticky top-4">
|
|
||||||
<h3 className="text-xl font-bold mb-4">Buyurtma xulasasi</h3>
|
|
||||||
|
|
||||||
<div className="space-y-3 mb-4">
|
|
||||||
<div className="flex justify-between text-gray-600">
|
|
||||||
<span>Mahsulotlar narxi:</span>
|
|
||||||
<span>
|
|
||||||
{subtotal.toLocaleString()} {"so'm"}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{discount > 0 && (
|
<div className="flex-1">
|
||||||
<div className="flex justify-between text-green-600">
|
<h3 className="font-semibold text-lg mb-1">
|
||||||
<span>Chegirma:</span>
|
{item.product_name}
|
||||||
<span>
|
</h3>
|
||||||
-{discount.toLocaleString()} {"so'm"}
|
<div className="flex items-center gap-2 mb-3 max-lg:flex-col max-lg:items-start max-lg:gap-1">
|
||||||
|
<span className="text-blue-600 font-bold text-xl">
|
||||||
|
{formatPrice(item.product_price, true)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="flex justify-between text-gray-600">
|
<div className="flex items-center border border-gray-300 rounded-lg w-max">
|
||||||
<span className="flex items-center gap-1">
|
<button
|
||||||
<Truck className="w-4 h-4" />
|
onClick={() =>
|
||||||
Yetkazib berish:
|
handleQuantityChange(
|
||||||
</span>
|
item.id,
|
||||||
<span>
|
Number(quantities[item.id]) - 1,
|
||||||
{deliveryFee === 0 ? (
|
)
|
||||||
<span className="text-green-600 font-semibold">
|
}
|
||||||
Bepul
|
className="p-2 cursor-pointer transition rounded-lg"
|
||||||
</span>
|
>
|
||||||
) : (
|
<Minus className="w-4 h-4" />
|
||||||
`${deliveryFee.toLocaleString()} so'm`
|
</button>
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{deliveryFee > 0 && (
|
<Input
|
||||||
<p className="text-sm text-gray-500 bg-blue-50 p-2 rounded">
|
value={quantities[item.id]}
|
||||||
{
|
onChange={(e) => {
|
||||||
"50,000 so'mdan ortiq xarid qiling va yetkazib berishni bepul oling!"
|
const val = e.target.value.replace(/\D/g, ''); // faqat raqam
|
||||||
}
|
setQuantities((prev) => ({
|
||||||
</p>
|
...prev,
|
||||||
)}
|
[item.id]: val,
|
||||||
</div>
|
}));
|
||||||
|
|
||||||
<div className="border-t pt-4 mb-6">
|
// Debounce bilan update
|
||||||
<div className="flex justify-between items-center">
|
const valNum = Number(val);
|
||||||
<span className="text-lg font-semibold">Jami:</span>
|
if (!isNaN(valNum))
|
||||||
<span className="text-2xl font-bold text-blue-600">
|
handleQuantityChange(item.id, valNum);
|
||||||
{total.toLocaleString()} {"so'm"}
|
}}
|
||||||
</span>
|
type="text"
|
||||||
|
className="w-16 text-center"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={() =>
|
||||||
|
handleQuantityChange(
|
||||||
|
item.id,
|
||||||
|
Number(quantities[item.id]) + 1,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
className="p-2 cursor-pointer transition rounded-lg"
|
||||||
|
>
|
||||||
|
<Plus className="w-4 h-4" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
))}
|
||||||
<button
|
|
||||||
onClick={handleCheckout}
|
|
||||||
className="w-full bg-blue-600 text-white py-4 rounded-lg font-semibold hover:bg-blue-700 transition flex items-center justify-center gap-2 mb-4"
|
|
||||||
>
|
|
||||||
<CreditCard className="w-5 h-5" />
|
|
||||||
Buyurtmani rasmiylashtirish
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button
|
|
||||||
onClick={() => router.push('/')}
|
|
||||||
className="w-full border-2 border-gray-300 cursor-pointer text-gray-700 py-3 rounded-lg font-semibold hover:bg-gray-50 transition flex items-center justify-center gap-2"
|
|
||||||
>
|
|
||||||
<ArrowLeft className="w-5 h-5" />
|
|
||||||
Xaridni davom ettirish
|
|
||||||
</button>
|
|
||||||
|
|
||||||
{/* Additional Info */}
|
|
||||||
<div className="mt-6 space-y-3 text-sm text-gray-600">
|
|
||||||
<div className="flex items-start gap-2">
|
|
||||||
<Truck className="w-5 h-5 text-blue-600 flex-shrink-0 mt-0.5" />
|
|
||||||
<span>Tez yetkazib berish 1-2 kun ichida</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-start gap-2">
|
|
||||||
<CreditCard className="w-5 h-5 text-blue-600 flex-shrink-0 mt-0.5" />
|
|
||||||
<span>{"Xavfsiz to'lov usullari"}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</>
|
|
||||||
|
<div className="lg:col-span-1">
|
||||||
|
<div className="bg-white rounded-lg shadow-md p-6 sticky top-4">
|
||||||
|
<h3 className="text-xl font-bold mb-4">{t('Buyurtma haqida')}</h3>
|
||||||
|
|
||||||
|
<div className="space-y-3 mb-4">
|
||||||
|
<div className="flex justify-between text-gray-600">
|
||||||
|
<span>{t('Mahsulotlar narxi')}:</span>
|
||||||
|
<span>{formatPrice(subtotal, true)}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex justify-between text-gray-600">
|
||||||
|
<span className="flex items-center gap-1">
|
||||||
|
<Truck className="w-4 h-4" /> {t('Yetkazib berish')}:
|
||||||
|
</span>
|
||||||
|
<span>
|
||||||
|
<span className="text-green-600 font-semibold">
|
||||||
|
{t('Bepul')}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="border-t pt-4 mb-6">
|
||||||
|
<div className="flex justify-between items-center">
|
||||||
|
<span className="text-lg font-semibold">{t('Jami')}:</span>
|
||||||
|
<span className="text-2xl font-bold text-blue-600">
|
||||||
|
{formatPrice(subtotal, true)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={handleCheckout}
|
||||||
|
className="w-full bg-blue-600 text-white py-4 rounded-lg font-semibold hover:bg-blue-700 transition flex items-center justify-center gap-2 mb-4"
|
||||||
|
>
|
||||||
|
<CreditCard className="w-5 h-5" />{' '}
|
||||||
|
{t('Buyurtmani rasmiylashtirish')}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={() => router.push('/')}
|
||||||
|
className="w-full border-2 border-gray-300 cursor-pointer text-gray-700 py-3 rounded-lg font-semibold hover:bg-gray-50 transition flex items-center justify-center gap-2"
|
||||||
|
>
|
||||||
|
<ArrowLeft className="w-5 h-5" /> {t('Xaridni davom ettirish')}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
|
import { BASE_URL } from '@/shared/config/api/URLs';
|
||||||
|
import { useCartId } from '@/shared/hooks/cartId';
|
||||||
import formatPhone from '@/shared/lib/formatPhone';
|
import formatPhone from '@/shared/lib/formatPhone';
|
||||||
|
import formatPrice from '@/shared/lib/formatPrice';
|
||||||
|
import onlyNumber from '@/shared/lib/onlyNumber';
|
||||||
import { Button } from '@/shared/ui/button';
|
import { Button } from '@/shared/ui/button';
|
||||||
import {
|
import {
|
||||||
Form,
|
Form,
|
||||||
@@ -11,6 +15,7 @@ import {
|
|||||||
} from '@/shared/ui/form';
|
} from '@/shared/ui/form';
|
||||||
import { Input } from '@/shared/ui/input';
|
import { Input } from '@/shared/ui/input';
|
||||||
import { Label } from '@/shared/ui/label';
|
import { Label } from '@/shared/ui/label';
|
||||||
|
import { Textarea } from '@/shared/ui/textarea';
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import {
|
import {
|
||||||
Map,
|
Map,
|
||||||
@@ -19,11 +24,12 @@ import {
|
|||||||
YMaps,
|
YMaps,
|
||||||
ZoomControl,
|
ZoomControl,
|
||||||
} from '@pbe/react-yandex-maps';
|
} from '@pbe/react-yandex-maps';
|
||||||
|
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||||
import {
|
import {
|
||||||
Building2,
|
|
||||||
CheckCircle2,
|
CheckCircle2,
|
||||||
Clock,
|
Clock,
|
||||||
CreditCard,
|
CreditCard,
|
||||||
|
Loader2,
|
||||||
LocateFixed,
|
LocateFixed,
|
||||||
MapPin,
|
MapPin,
|
||||||
Package,
|
Package,
|
||||||
@@ -31,10 +37,13 @@ import {
|
|||||||
User,
|
User,
|
||||||
Wallet,
|
Wallet,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
|
import { useTranslations } from 'next-intl';
|
||||||
import Image from 'next/image';
|
import Image from 'next/image';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
|
import { toast } from 'sonner';
|
||||||
import z from 'zod';
|
import z from 'zod';
|
||||||
|
import { cart_api, OrderCreateBody } from '../lib/api';
|
||||||
import { orderForm } from '../lib/form';
|
import { orderForm } from '../lib/form';
|
||||||
|
|
||||||
interface CoordsData {
|
interface CoordsData {
|
||||||
@@ -44,53 +53,72 @@ interface CoordsData {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const OrderPage = () => {
|
const OrderPage = () => {
|
||||||
|
const t = useTranslations();
|
||||||
const form = useForm<z.infer<typeof orderForm>>({
|
const form = useForm<z.infer<typeof orderForm>>({
|
||||||
resolver: zodResolver(orderForm),
|
resolver: zodResolver(orderForm),
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
firstName: '',
|
firstName: '',
|
||||||
|
comment: '',
|
||||||
lastName: '',
|
lastName: '',
|
||||||
lat: '',
|
lat: '',
|
||||||
long: '',
|
long: '',
|
||||||
phone: '+998',
|
phone: '+998',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
const [cart, setCart] = useState<number | string | null>(null);
|
||||||
const [paymentMethod, setPaymentMethod] = useState('cash');
|
const { cart_id } = useCartId();
|
||||||
const [deliveryMethod, setDeliveryMethod] = useState('standard');
|
|
||||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
|
||||||
const [orderSuccess, setOrderSuccess] = useState(false);
|
const [orderSuccess, setOrderSuccess] = useState(false);
|
||||||
|
const queryClinet = useQueryClient();
|
||||||
|
|
||||||
const cartItems = [
|
const { data } = useQuery({
|
||||||
{
|
queryKey: ['clear_cart', cart],
|
||||||
id: 5,
|
queryFn: () => cart_api.clear_cart(cart!),
|
||||||
name: 'Coca-Cola 1.5L',
|
enabled: !!cart,
|
||||||
price: 12000,
|
});
|
||||||
quantity: 2,
|
|
||||||
image: '/classic-coca-cola.png',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 6,
|
|
||||||
name: 'Pepsi 2L',
|
|
||||||
price: 11000,
|
|
||||||
quantity: 1,
|
|
||||||
image: '/pepsi-bottle.jpg',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 8,
|
|
||||||
name: 'Sprite 1.5L',
|
|
||||||
price: 10000,
|
|
||||||
quantity: 3,
|
|
||||||
image: '/clear-soda-bottle.png',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const subtotal = cartItems.reduce(
|
console.log(data);
|
||||||
(sum, item) => sum + item.price * item.quantity,
|
|
||||||
|
const { mutate, isPending } = useMutation({
|
||||||
|
mutationFn: (body: OrderCreateBody) => cart_api.createOrder(body),
|
||||||
|
onSuccess: () => {
|
||||||
|
setOrderSuccess(true);
|
||||||
|
setCart(cart_id);
|
||||||
|
queryClinet.refetchQueries({ queryKey: ['cart_items'] });
|
||||||
|
},
|
||||||
|
onError: () => {
|
||||||
|
toast.error('Xatolik yuz berdi', {
|
||||||
|
richColors: true,
|
||||||
|
position: 'top-center',
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const { data: cartItems } = useQuery({
|
||||||
|
queryKey: ['cart_items', cart_id],
|
||||||
|
queryFn: () => cart_api.get_cart_items(cart_id!),
|
||||||
|
enabled: !!cart_id,
|
||||||
|
select: (data) => data.data.cart_item,
|
||||||
|
});
|
||||||
|
|
||||||
|
const [paymentMethod, setPaymentMethod] = useState<'CASH' | 'ACCOUNT_NUMBER'>(
|
||||||
|
'CASH',
|
||||||
|
);
|
||||||
|
const [deliveryMethod, setDeliveryMethod] = useState<
|
||||||
|
'YandexGo' | 'DELIVERY_COURIES' | 'PICKUP'
|
||||||
|
>('DELIVERY_COURIES');
|
||||||
|
|
||||||
|
const subtotal = cartItems?.reduce(
|
||||||
|
(sum, item) => sum + item.product_price * item.quantity,
|
||||||
0,
|
0,
|
||||||
);
|
);
|
||||||
|
|
||||||
const deliveryFee =
|
const deliveryFee =
|
||||||
deliveryMethod === 'express' ? 25000 : subtotal > 50000 ? 0 : 15000;
|
deliveryMethod === 'DELIVERY_COURIES'
|
||||||
const total = subtotal + deliveryFee;
|
? 25000
|
||||||
|
: subtotal && subtotal > 50000
|
||||||
|
? 0
|
||||||
|
: 15000;
|
||||||
|
const total = subtotal;
|
||||||
|
|
||||||
const [coords, setCoords] = useState({
|
const [coords, setCoords] = useState({
|
||||||
latitude: 41.311081,
|
latitude: 41.311081,
|
||||||
@@ -187,13 +215,27 @@ const OrderPage = () => {
|
|||||||
}, [cityValue]);
|
}, [cityValue]);
|
||||||
|
|
||||||
function onSubmit(value: z.infer<typeof orderForm>) {
|
function onSubmit(value: z.infer<typeof orderForm>) {
|
||||||
setIsSubmitting(true);
|
if (!cartItems || cartItems.length === 0) {
|
||||||
console.log(value);
|
toast.error('Savatcha bo‘sh', {
|
||||||
|
richColors: true,
|
||||||
|
position: 'top-center',
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
setTimeout(() => {
|
const items = cartItems.map((item) => ({
|
||||||
setIsSubmitting(false);
|
product_id: item.product_id,
|
||||||
setOrderSuccess(true);
|
quantity: item.quantity,
|
||||||
}, 2000);
|
}));
|
||||||
|
|
||||||
|
mutate({
|
||||||
|
comment: value.comment,
|
||||||
|
contact_number: onlyNumber(value.phone),
|
||||||
|
delivery_type: deliveryMethod,
|
||||||
|
name: value.firstName + ' ' + value.lastName,
|
||||||
|
payment_type: paymentMethod,
|
||||||
|
items: items,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (orderSuccess) {
|
if (orderSuccess) {
|
||||||
@@ -204,28 +246,16 @@ const OrderPage = () => {
|
|||||||
<CheckCircle2 className="w-12 h-12 text-green-600" />
|
<CheckCircle2 className="w-12 h-12 text-green-600" />
|
||||||
</div>
|
</div>
|
||||||
<h2 className="text-2xl font-bold text-gray-800 mb-2">
|
<h2 className="text-2xl font-bold text-gray-800 mb-2">
|
||||||
Buyurtma qabul qilindi!
|
{t('Buyurtma qabul qilindi!')}
|
||||||
</h2>
|
</h2>
|
||||||
<p className="text-gray-600 mb-4">
|
|
||||||
Buyurtma raqami:{' '}
|
|
||||||
<span className="font-bold">
|
|
||||||
#ORD-{Math.floor(Math.random() * 10000)}
|
|
||||||
</span>
|
|
||||||
</p>
|
|
||||||
<p className="text-gray-500 mb-6">
|
<p className="text-gray-500 mb-6">
|
||||||
Buyurtmangiz muvaffaqiyatli qabul qilindi. Tez orada sizga aloqaga
|
{t('Buyurtmangiz muvaffaqiyatli qabul qilindi')}
|
||||||
chiqamiz.
|
|
||||||
</p>
|
</p>
|
||||||
<div className="bg-blue-50 p-4 rounded-lg mb-6">
|
|
||||||
<p className="text-sm text-gray-700">
|
|
||||||
Buyurtma holati haqida SMS orqali xabardor qilinasiz
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<button
|
<button
|
||||||
onClick={() => (window.location.href = '/')}
|
onClick={() => (window.location.href = '/')}
|
||||||
className="w-full bg-blue-600 text-white py-3 rounded-lg font-semibold hover:bg-blue-700 transition"
|
className="w-full bg-blue-600 text-white py-3 rounded-lg font-semibold hover:bg-blue-700 transition"
|
||||||
>
|
>
|
||||||
Bosh sahifaga qaytish
|
{t('Bosh sahifaga qaytish')}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -238,9 +268,9 @@ const OrderPage = () => {
|
|||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="mb-6">
|
<div className="mb-6">
|
||||||
<h1 className="text-3xl font-bold text-gray-800 mb-2">
|
<h1 className="text-3xl font-bold text-gray-800 mb-2">
|
||||||
Buyurtmani rasmiylashtirish
|
{t('Buyurtmani rasmiylashtirish')}
|
||||||
</h1>
|
</h1>
|
||||||
<p className="text-gray-600">{"Ma'lumotlaringizni to'ldiring"}</p>
|
<p className="text-gray-600">{t("Ma'lumotlaringizni to'ldiring")}</p>
|
||||||
</div>
|
</div>
|
||||||
<Form {...form}>
|
<Form {...form}>
|
||||||
<form onSubmit={form.handleSubmit(onSubmit)}>
|
<form onSubmit={form.handleSubmit(onSubmit)}>
|
||||||
@@ -252,7 +282,7 @@ const OrderPage = () => {
|
|||||||
<div className="flex items-center gap-2 mb-4">
|
<div className="flex items-center gap-2 mb-4">
|
||||||
<User className="w-5 h-5 text-blue-600" />
|
<User className="w-5 h-5 text-blue-600" />
|
||||||
<h2 className="text-xl font-semibold">
|
<h2 className="text-xl font-semibold">
|
||||||
{"Shaxsiy ma'lumotlar"}
|
{t("Shaxsiy ma'lumotlar")}
|
||||||
</h2>
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
@@ -262,13 +292,13 @@ const OrderPage = () => {
|
|||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem className="flex justify-start flex-col">
|
<FormItem className="flex justify-start flex-col">
|
||||||
<Label className="block text-sm font-medium text-gray-700">
|
<Label className="block text-sm font-medium text-gray-700">
|
||||||
{'Ism'}
|
{t('Ism')}
|
||||||
</Label>
|
</Label>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input
|
<Input
|
||||||
{...field}
|
{...field}
|
||||||
className="w-full border h-12 border-gray-300 rounded-lg px-4 py-3 focus:outline-none focus:border-blue-500"
|
className="w-full border h-12 border-gray-300 rounded-lg px-4 py-3 focus:outline-none focus:border-blue-500"
|
||||||
placeholder="Ismingiz"
|
placeholder={t('Ismingiz')}
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
@@ -282,13 +312,13 @@ const OrderPage = () => {
|
|||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem className="flex justify-start flex-col">
|
<FormItem className="flex justify-start flex-col">
|
||||||
<Label className="block text-sm font-medium text-gray-700">
|
<Label className="block text-sm font-medium text-gray-700">
|
||||||
{'Familiya'}
|
{t('Familiya')}
|
||||||
</Label>
|
</Label>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input
|
<Input
|
||||||
{...field}
|
{...field}
|
||||||
className="w-full border h-12 border-gray-300 rounded-lg px-4 py-3 focus:outline-none focus:border-blue-500"
|
className="w-full border h-12 border-gray-300 rounded-lg px-4 py-3 focus:outline-none focus:border-blue-500"
|
||||||
placeholder="Familiyangiz"
|
placeholder={t('Familiyangiz')}
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
@@ -302,7 +332,7 @@ const OrderPage = () => {
|
|||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<Label className="block text-sm font-medium text-gray-700">
|
<Label className="block text-sm font-medium text-gray-700">
|
||||||
Telefon raqam
|
{t('Telefon raqam')}
|
||||||
</Label>
|
</Label>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input
|
<Input
|
||||||
@@ -319,6 +349,25 @@ const OrderPage = () => {
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="comment"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<Label className="block mt-3 text-sm font-medium text-gray-700">
|
||||||
|
{t('Izoh')}
|
||||||
|
</Label>
|
||||||
|
<FormControl>
|
||||||
|
<Textarea
|
||||||
|
{...field}
|
||||||
|
className="w-full min-h-42 max-h-64 border border-gray-300 rounded-lg px-4 py-3 focus:outline-none focus:border-blue-500"
|
||||||
|
placeholder={t('Izoh')}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Delivery Address */}
|
{/* Delivery Address */}
|
||||||
@@ -326,7 +375,7 @@ const OrderPage = () => {
|
|||||||
<div className="flex items-center gap-2 mb-4">
|
<div className="flex items-center gap-2 mb-4">
|
||||||
<MapPin className="w-5 h-5 text-blue-600" />
|
<MapPin className="w-5 h-5 text-blue-600" />
|
||||||
<h2 className="text-xl font-semibold">
|
<h2 className="text-xl font-semibold">
|
||||||
Yetkazib berish manzili
|
{t('Yetkazib berish manzili')}
|
||||||
</h2>
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
@@ -336,14 +385,14 @@ const OrderPage = () => {
|
|||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<Label className="block text-sm font-medium text-gray-700">
|
<Label className="block text-sm font-medium text-gray-700">
|
||||||
Manzilni qidirish
|
{t('Manzilni qidirish')}
|
||||||
</Label>
|
</Label>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input
|
<Input
|
||||||
{...field}
|
{...field}
|
||||||
type="text"
|
type="text"
|
||||||
className="w-full border h-12 border-gray-300 rounded-lg px-4 py-3 focus:outline-none focus:border-blue-500"
|
className="w-full border h-12 border-gray-300 rounded-lg px-4 py-3 focus:outline-none focus:border-blue-500"
|
||||||
placeholder="Toshkent"
|
placeholder={t('Toshkent')}
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
@@ -392,7 +441,7 @@ const OrderPage = () => {
|
|||||||
className="absolute bottom-3 right-2.5 shadow-md bg-white text-black hover:bg-gray-100"
|
className="absolute bottom-3 right-2.5 shadow-md bg-white text-black hover:bg-gray-100"
|
||||||
>
|
>
|
||||||
<LocateFixed className="w-4 h-4 mr-1" />
|
<LocateFixed className="w-4 h-4 mr-1" />
|
||||||
Mening joylashuvim
|
{t('Mening joylashuvim')}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -403,7 +452,7 @@ const OrderPage = () => {
|
|||||||
<div className="flex items-center gap-2 mb-4">
|
<div className="flex items-center gap-2 mb-4">
|
||||||
<Truck className="w-5 h-5 text-blue-600" />
|
<Truck className="w-5 h-5 text-blue-600" />
|
||||||
<h2 className="text-xl font-semibold">
|
<h2 className="text-xl font-semibold">
|
||||||
Yetkazib berish usuli
|
{t('Yetkazib berish usuli')}
|
||||||
</h2>
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
@@ -412,8 +461,8 @@ const OrderPage = () => {
|
|||||||
type="radio"
|
type="radio"
|
||||||
name="delivery"
|
name="delivery"
|
||||||
value="standard"
|
value="standard"
|
||||||
checked={deliveryMethod === 'standard'}
|
checked={deliveryMethod === 'DELIVERY_COURIES'}
|
||||||
onChange={(e) => setDeliveryMethod(e.target.value)}
|
onChange={() => setDeliveryMethod('DELIVERY_COURIES')}
|
||||||
className="w-4 h-4 text-blue-600"
|
className="w-4 h-4 text-blue-600"
|
||||||
/>
|
/>
|
||||||
<div className="ml-4 flex-1">
|
<div className="ml-4 flex-1">
|
||||||
@@ -421,15 +470,17 @@ const OrderPage = () => {
|
|||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Clock className="w-5 h-5 text-gray-600" />
|
<Clock className="w-5 h-5 text-gray-600" />
|
||||||
<span className="font-semibold">
|
<span className="font-semibold">
|
||||||
Standart yetkazib berish
|
{t('Standart yetkazib berish')}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<span className="font-bold text-blue-600">
|
<span className="font-bold text-blue-600">
|
||||||
{subtotal > 50000 ? 'Bepul' : "15,000 so'm"}
|
{subtotal && subtotal > 50000
|
||||||
|
? 'Bepul'
|
||||||
|
: "15,000 so'm"}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-sm text-gray-500 mt-1">
|
<p className="text-sm text-gray-500 mt-1">
|
||||||
2-3 kun ichida
|
{t('2-3 kun ichida')}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</label>
|
</label>
|
||||||
@@ -439,8 +490,8 @@ const OrderPage = () => {
|
|||||||
type="radio"
|
type="radio"
|
||||||
name="delivery"
|
name="delivery"
|
||||||
value="express"
|
value="express"
|
||||||
checked={deliveryMethod === 'express'}
|
checked={deliveryMethod === 'YandexGo'}
|
||||||
onChange={(e) => setDeliveryMethod(e.target.value)}
|
onChange={() => setDeliveryMethod('YandexGo')}
|
||||||
className="w-4 h-4 text-blue-600"
|
className="w-4 h-4 text-blue-600"
|
||||||
/>
|
/>
|
||||||
<div className="ml-4 flex-1">
|
<div className="ml-4 flex-1">
|
||||||
@@ -448,7 +499,7 @@ const OrderPage = () => {
|
|||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Package className="w-5 h-5 text-gray-600" />
|
<Package className="w-5 h-5 text-gray-600" />
|
||||||
<span className="font-semibold">
|
<span className="font-semibold">
|
||||||
Tez yetkazib berish
|
{t('Tez yetkazib berish')}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<span className="font-bold text-blue-600">
|
<span className="font-bold text-blue-600">
|
||||||
@@ -456,7 +507,7 @@ const OrderPage = () => {
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-sm text-gray-500 mt-1">
|
<p className="text-sm text-gray-500 mt-1">
|
||||||
1 kun ichida
|
{t('1 kun ichida')}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</label>
|
</label>
|
||||||
@@ -466,7 +517,9 @@ const OrderPage = () => {
|
|||||||
<div className="bg-white rounded-lg shadow-md p-6">
|
<div className="bg-white rounded-lg shadow-md p-6">
|
||||||
<div className="flex items-center gap-2 mb-4">
|
<div className="flex items-center gap-2 mb-4">
|
||||||
<CreditCard className="w-5 h-5 text-blue-600" />
|
<CreditCard className="w-5 h-5 text-blue-600" />
|
||||||
<h2 className="text-xl font-semibold">{"To'lov usuli"}</h2>
|
<h2 className="text-xl font-semibold">
|
||||||
|
{t("To'lov usuli")}
|
||||||
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<Label className="flex items-center p-4 border-2 rounded-lg cursor-pointer transition hover:bg-gray-50">
|
<Label className="flex items-center p-4 border-2 rounded-lg cursor-pointer transition hover:bg-gray-50">
|
||||||
@@ -474,16 +527,16 @@ const OrderPage = () => {
|
|||||||
type="radio"
|
type="radio"
|
||||||
name="payment"
|
name="payment"
|
||||||
value="cash"
|
value="cash"
|
||||||
checked={paymentMethod === 'cash'}
|
checked={paymentMethod === 'CASH'}
|
||||||
onChange={(e) => setPaymentMethod(e.target.value)}
|
onChange={() => setPaymentMethod('CASH')}
|
||||||
className="w-4 h-4 text-blue-600"
|
className="w-4 h-4 text-blue-600"
|
||||||
/>
|
/>
|
||||||
<div className="ml-4 flex items-center gap-3">
|
<div className="ml-4 flex items-center gap-3">
|
||||||
<Wallet className="w-6 h-6 text-green-600" />
|
<Wallet className="w-6 h-6 text-green-600" />
|
||||||
<div>
|
<div>
|
||||||
<span className="font-semibold">Naqd pul</span>
|
<span className="font-semibold">{t('Naqd pul')}</span>
|
||||||
<p className="text-sm text-gray-500">
|
<p className="text-sm text-gray-500">
|
||||||
{"Yetkazib berishda to'lash"}
|
{t("Yetkazib berishda to'lash")}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -494,36 +547,18 @@ const OrderPage = () => {
|
|||||||
type="radio"
|
type="radio"
|
||||||
name="payment"
|
name="payment"
|
||||||
value="card"
|
value="card"
|
||||||
checked={paymentMethod === 'card'}
|
checked={paymentMethod === 'ACCOUNT_NUMBER'}
|
||||||
onChange={(e) => setPaymentMethod(e.target.value)}
|
onChange={() => setPaymentMethod('ACCOUNT_NUMBER')}
|
||||||
className="w-4 h-4 text-blue-600"
|
className="w-4 h-4 text-blue-600"
|
||||||
/>
|
/>
|
||||||
<div className="ml-4 flex items-center gap-3">
|
<div className="ml-4 flex items-center gap-3">
|
||||||
<CreditCard className="w-6 h-6 text-blue-600" />
|
<CreditCard className="w-6 h-6 text-blue-600" />
|
||||||
<div>
|
<div>
|
||||||
<span className="font-semibold">Plastik karta</span>
|
<span className="font-semibold">
|
||||||
|
{t('Plastik karta')}
|
||||||
|
</span>
|
||||||
<p className="text-sm text-gray-500">
|
<p className="text-sm text-gray-500">
|
||||||
{"Online to'lov"}
|
{t("Online to'lov")}
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Label>
|
|
||||||
|
|
||||||
<Label className="flex items-center p-4 border-2 rounded-lg cursor-pointer transition hover:bg-gray-50">
|
|
||||||
<Input
|
|
||||||
type="radio"
|
|
||||||
name="payment"
|
|
||||||
value="terminal"
|
|
||||||
checked={paymentMethod === 'terminal'}
|
|
||||||
onChange={(e) => setPaymentMethod(e.target.value)}
|
|
||||||
className="w-4 h-4 text-blue-600"
|
|
||||||
/>
|
|
||||||
<div className="ml-4 flex items-center gap-3">
|
|
||||||
<Building2 className="w-6 h-6 text-purple-600" />
|
|
||||||
<div>
|
|
||||||
<span className="font-semibold">Terminal orqali</span>
|
|
||||||
<p className="text-sm text-gray-500">
|
|
||||||
Yetkazib berishda terminal
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -535,28 +570,32 @@ const OrderPage = () => {
|
|||||||
{/* Right Column - Order Summary */}
|
{/* Right Column - Order Summary */}
|
||||||
<div className="lg:col-span-1">
|
<div className="lg:col-span-1">
|
||||||
<div className="bg-white rounded-lg shadow-md p-6 sticky top-4">
|
<div className="bg-white rounded-lg shadow-md p-6 sticky top-4">
|
||||||
<h3 className="text-xl font-bold mb-4">Mahsulotlar</h3>
|
<h3 className="text-xl font-bold mb-4">{t('Mahsulotlar')}</h3>
|
||||||
|
|
||||||
{/* Cart Items */}
|
{/* Cart Items */}
|
||||||
<div className="space-y-3 mb-4 max-h-60 overflow-y-auto">
|
<div className="space-y-3 mb-4 max-h-60 overflow-y-auto">
|
||||||
{cartItems.map((item) => (
|
{cartItems?.map((item) => (
|
||||||
<div key={item.id} className="flex gap-3 pb-3 border-b">
|
<div key={item.id} className="flex gap-3 pb-3 border-b">
|
||||||
<Image
|
<Image
|
||||||
width={500}
|
width={500}
|
||||||
height={500}
|
height={500}
|
||||||
src={item.image}
|
src={BASE_URL + item.product_image}
|
||||||
alt={item.name}
|
alt={item.product_name}
|
||||||
className="w-16 h-16 object-contain bg-gray-100 rounded"
|
className="w-16 h-16 object-contain bg-gray-100 rounded"
|
||||||
/>
|
/>
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<h4 className="font-medium text-sm">{item.name}</h4>
|
<h4 className="font-medium text-sm">
|
||||||
|
{item.product_name}
|
||||||
|
</h4>
|
||||||
<p className="text-sm text-gray-500">
|
<p className="text-sm text-gray-500">
|
||||||
{item.quantity} x {item.price.toLocaleString()}{' '}
|
{item.quantity} x{' '}
|
||||||
{"so'm"}
|
{formatPrice(item.product_price, true)}
|
||||||
</p>
|
</p>
|
||||||
<p className="font-semibold text-sm">
|
<p className="font-semibold text-sm">
|
||||||
{(item.price * item.quantity).toLocaleString()}{' '}
|
{formatPrice(
|
||||||
{"so'm"}
|
item.product_price * item.quantity,
|
||||||
|
true,
|
||||||
|
)}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -566,17 +605,15 @@ const OrderPage = () => {
|
|||||||
{/* Pricing */}
|
{/* Pricing */}
|
||||||
<div className="space-y-2 mb-4 pt-4 border-t">
|
<div className="space-y-2 mb-4 pt-4 border-t">
|
||||||
<div className="flex justify-between text-gray-600">
|
<div className="flex justify-between text-gray-600">
|
||||||
<span>Mahsulotlar:</span>
|
<span>{t('Mahsulotlar')}:</span>
|
||||||
<span>
|
<span>{subtotal && formatPrice(subtotal, true)}</span>
|
||||||
{subtotal.toLocaleString()} {"so'm"}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-between text-gray-600">
|
<div className="flex justify-between text-gray-600">
|
||||||
<span>Yetkazib berish:</span>
|
<span>{t('Yetkazib berish')}:</span>
|
||||||
<span>
|
<span>
|
||||||
{deliveryFee === 0 ? (
|
{deliveryFee === 0 ? (
|
||||||
<span className="text-green-600 font-semibold">
|
<span className="text-green-600 font-semibold">
|
||||||
Bepul
|
{t('Bepul')}
|
||||||
</span>
|
</span>
|
||||||
) : (
|
) : (
|
||||||
`${deliveryFee.toLocaleString()} so'm`
|
`${deliveryFee.toLocaleString()} so'm`
|
||||||
@@ -587,25 +624,26 @@ const OrderPage = () => {
|
|||||||
|
|
||||||
<div className="border-t pt-4 mb-6">
|
<div className="border-t pt-4 mb-6">
|
||||||
<div className="flex justify-between items-center">
|
<div className="flex justify-between items-center">
|
||||||
<span className="text-lg font-semibold">Jami:</span>
|
<span className="text-lg font-semibold">
|
||||||
|
{t('Jami')}:
|
||||||
|
</span>
|
||||||
<span className="text-2xl font-bold text-blue-600">
|
<span className="text-2xl font-bold text-blue-600">
|
||||||
{total.toLocaleString()} {"so'm"}
|
{total && formatPrice(total, true)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={isSubmitting}
|
disabled={isPending}
|
||||||
className="w-full bg-blue-600 text-white py-4 rounded-lg font-semibold hover:bg-blue-700 transition disabled:bg-gray-400 disabled:cursor-not-allowed"
|
className="w-full bg-blue-600 text-white py-4 rounded-lg font-semibold hover:bg-blue-700 transition disabled:bg-gray-400 disabled:cursor-not-allowed"
|
||||||
>
|
>
|
||||||
{isSubmitting ? (
|
{isPending ? (
|
||||||
<span className="flex items-center justify-center gap-2">
|
<span className="flex items-center justify-center gap-2">
|
||||||
<div className="w-5 h-5 border-2 border-white border-t-transparent rounded-full animate-spin" />
|
<Loader2 className="animate-spin" />
|
||||||
Yuborilmoqda...
|
|
||||||
</span>
|
</span>
|
||||||
) : (
|
) : (
|
||||||
'Buyurtmani tasdiqlash'
|
t('Buyurtmani tasdiqlash')
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,34 +1,53 @@
|
|||||||
'use client';
|
'use client';
|
||||||
import { useRouter } from '@/shared/config/i18n/navigation';
|
import { category_api } from '@/shared/config/api/category/api';
|
||||||
import { categoryList, CategoryType } from '@/widgets/welcome/lib/data';
|
import { BASE_URL } from '@/shared/config/api/URLs';
|
||||||
|
import { Link } from '@/shared/config/i18n/navigation';
|
||||||
|
import { useQuery } from '@tanstack/react-query';
|
||||||
import { ChevronRight } from 'lucide-react';
|
import { ChevronRight } from 'lucide-react';
|
||||||
|
import { useTranslations } from 'next-intl';
|
||||||
|
import Image from 'next/image';
|
||||||
|
|
||||||
const Category = () => {
|
const Category = () => {
|
||||||
const router = useRouter();
|
const t = useTranslations();
|
||||||
const handleCategoryClick = (category: CategoryType) => {
|
const { data: category } = useQuery({
|
||||||
router.push(`/category/${category.name}`);
|
queryKey: ['category_list'],
|
||||||
};
|
queryFn: () => category_api.getCategory({ page: 1, page_size: 99 }),
|
||||||
|
select(data) {
|
||||||
|
return data.data.results;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-gray-50 p-4">
|
<div className="custom-container">
|
||||||
<div className="max-w-6xl mx-auto">
|
<>
|
||||||
<h1 className="text-2xl font-semibold text-gray-900 mb-6">
|
<h1 className="text-2xl font-semibold text-gray-900 mb-6">
|
||||||
Kategoriyalar
|
{t('Kategoriyalar')}
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-3">
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-3">
|
||||||
{categoryList.map((category, index) => (
|
{category &&
|
||||||
<button
|
category.map((category, index) => (
|
||||||
key={index}
|
<Link
|
||||||
onClick={() => handleCategoryClick(category)}
|
key={index}
|
||||||
className="bg-white border border-gray-200 rounded-lg p-4 flex items-center justify-between hover:border-gray-300 transition-colors"
|
href={`/category/${category.id}`}
|
||||||
>
|
className="bg-white border border-gray-200 rounded-lg p-4 flex items-center justify-between hover:border-gray-300 transition-colors"
|
||||||
<span className="text-gray-900 font-medium">{category.name}</span>
|
>
|
||||||
<ChevronRight className="w-5 h-5 text-gray-400" />
|
<div className="flex items-center gap-4">
|
||||||
</button>
|
<Image
|
||||||
))}
|
src={BASE_URL + category.image}
|
||||||
|
alt={category.name}
|
||||||
|
width={70}
|
||||||
|
height={70}
|
||||||
|
/>
|
||||||
|
<span className="text-gray-900 font-medium">
|
||||||
|
{category.name}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<ChevronRight className="w-5 h-5 text-gray-400" />
|
||||||
|
</Link>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,45 +1,67 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useRouter } from '@/shared/config/i18n/navigation';
|
import { product_api } from '@/shared/config/api/product/api';
|
||||||
|
import { usePathname, useRouter } from '@/shared/config/i18n/navigation';
|
||||||
|
import { Card } from '@/shared/ui/card';
|
||||||
|
import { GlobalPagination } from '@/shared/ui/global-pagination';
|
||||||
|
import { Skeleton } from '@/shared/ui/skeleton';
|
||||||
import { ProductCard } from '@/widgets/categories/ui/product-card';
|
import { ProductCard } from '@/widgets/categories/ui/product-card';
|
||||||
|
import { useQuery } from '@tanstack/react-query';
|
||||||
import { ArrowLeft } from 'lucide-react';
|
import { ArrowLeft } from 'lucide-react';
|
||||||
import { useParams } from 'next/navigation';
|
import { useTranslations } from 'next-intl';
|
||||||
import { useState } from 'react';
|
import { useParams, useSearchParams } from 'next/navigation';
|
||||||
import { subCategoriesData } from '../lib/data';
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
const PAGE_SIZE = 36;
|
||||||
|
|
||||||
const Product = () => {
|
const Product = () => {
|
||||||
const { subId } = useParams();
|
const { categoryId } = useParams();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const pathname = usePathname();
|
||||||
|
const searchParams = useSearchParams();
|
||||||
|
const [page, setPage] = useState(1);
|
||||||
|
const t = useTranslations();
|
||||||
|
|
||||||
const decodedSubId = decodeURIComponent(subId as string);
|
useEffect(() => {
|
||||||
|
const urlPage = Number(searchParams.get('page')) || 1;
|
||||||
|
setPage(urlPage);
|
||||||
|
}, [searchParams]);
|
||||||
|
|
||||||
|
const {
|
||||||
|
data: product,
|
||||||
|
isLoading,
|
||||||
|
isError,
|
||||||
|
} = useQuery({
|
||||||
|
queryKey: ['product_list', categoryId, page],
|
||||||
|
queryFn: () => {
|
||||||
|
if (!categoryId) throw new Error('Category ID is required');
|
||||||
|
return product_api.listGetCategoryId({
|
||||||
|
category_id: categoryId.toString(),
|
||||||
|
params: { page, page_size: PAGE_SIZE },
|
||||||
|
});
|
||||||
|
},
|
||||||
|
select(data) {
|
||||||
|
return data.data;
|
||||||
|
},
|
||||||
|
enabled: !!categoryId,
|
||||||
|
});
|
||||||
|
|
||||||
const subCategory =
|
|
||||||
subCategoriesData.find((cat) => cat.name === decodedSubId) ||
|
|
||||||
subCategoriesData[0];
|
|
||||||
const [products, setProducts] = useState(subCategory.products);
|
|
||||||
const handleBack = () => {
|
const handleBack = () => {
|
||||||
router.back();
|
router.back();
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleRemove = (id: number) => {
|
const handlePageChange = (newPage: number) => {
|
||||||
setProducts((prev) =>
|
const params = new URLSearchParams(searchParams.toString());
|
||||||
prev.map((product) =>
|
params.set('page', newPage.toString());
|
||||||
product.id === id ? { ...product, liked: false } : product,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleLiked = (id: number) => {
|
router.push(`${pathname}?${params.toString()}`, {
|
||||||
setProducts((prev) =>
|
scroll: true,
|
||||||
prev.map((product) =>
|
});
|
||||||
product.id === id ? { ...product, liked: true } : product,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="custom-container p-4 mb-5">
|
<div className="custom-container p-4 mb-5 flex flex-col min-h-[calc(85vh)]">
|
||||||
<div>
|
<div className="flex-1">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="mb-6">
|
<div className="mb-6">
|
||||||
<button
|
<button
|
||||||
@@ -47,28 +69,46 @@ const Product = () => {
|
|||||||
className="flex items-center gap-2 text-gray-600 hover:text-gray-900 mb-4"
|
className="flex items-center gap-2 text-gray-600 hover:text-gray-900 mb-4"
|
||||||
>
|
>
|
||||||
<ArrowLeft className="w-5 h-5" />
|
<ArrowLeft className="w-5 h-5" />
|
||||||
<span>Orqaga</span>
|
<span>{t('Orqaga')}</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<h1 className="text-2xl font-semibold text-gray-900">
|
|
||||||
{decodedSubId}
|
|
||||||
</h1>
|
|
||||||
<p className="text-gray-600 text-sm mt-1">
|
<p className="text-gray-600 text-sm mt-1">
|
||||||
{subCategory.products.length} ta mahsulot
|
{product?.total} {t('ta mahsulot')}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-6 gap-3">
|
{/* Products grid */}
|
||||||
{products.map((product) => (
|
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-5 xl:grid-cols-6 gap-3">
|
||||||
<ProductCard
|
{isLoading &&
|
||||||
key={product.id}
|
Array.from({ length: 6 }).map((_, index) => (
|
||||||
product={product}
|
<Card className="p-3 space-y-3 rounded-xl" key={index}>
|
||||||
handleRemove={handleRemove}
|
<Skeleton className="h-40 sm:h-48 md:h-56 w-full rounded-lg" />
|
||||||
handleLiked={handleLiked}
|
<Skeleton className="h-4 w-3/4" />
|
||||||
/>
|
<Skeleton className="h-4 w-1/2" />
|
||||||
))}
|
<Skeleton className="h-10 w-full rounded-lg" />
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
{product &&
|
||||||
|
!isLoading &&
|
||||||
|
product.results
|
||||||
|
.filter((product) => product.is_active)
|
||||||
|
.map((item) => (
|
||||||
|
<ProductCard key={item.id} product={item} error={isError} />
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Pagination at the bottom */}
|
||||||
|
{product && (
|
||||||
|
<div className="w-full mt-5 flex justify-end">
|
||||||
|
<GlobalPagination
|
||||||
|
page={page}
|
||||||
|
total={product.total ?? 0}
|
||||||
|
pageSize={PAGE_SIZE}
|
||||||
|
onChange={handlePageChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -17,8 +17,8 @@ const SubCategory = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-gray-50 p-4">
|
<div className="custom-container">
|
||||||
<div className="max-w-6xl mx-auto">
|
<>
|
||||||
<h1 className="text-2xl font-semibold text-gray-900 mb-6">
|
<h1 className="text-2xl font-semibold text-gray-900 mb-6">
|
||||||
{category.name}
|
{category.name}
|
||||||
</h1>
|
</h1>
|
||||||
@@ -37,7 +37,7 @@ const SubCategory = () => {
|
|||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
11
src/features/faq/lib/api.ts
Normal file
11
src/features/faq/lib/api.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import httpClient from '@/shared/config/api/httpClient';
|
||||||
|
import { API_URLS } from '@/shared/config/api/URLs';
|
||||||
|
import { AxiosResponse } from 'axios';
|
||||||
|
import { FaqList } from './types';
|
||||||
|
|
||||||
|
export const faq_api = {
|
||||||
|
async list(): Promise<AxiosResponse<FaqList[]>> {
|
||||||
|
const res = await httpClient.get(API_URLS.Faq);
|
||||||
|
return res;
|
||||||
|
},
|
||||||
|
};
|
||||||
5
src/features/faq/lib/types.ts
Normal file
5
src/features/faq/lib/types.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
export interface FaqList {
|
||||||
|
id: string;
|
||||||
|
question: string;
|
||||||
|
answer: string;
|
||||||
|
}
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Accordion,
|
Accordion,
|
||||||
AccordionContent,
|
AccordionContent,
|
||||||
@@ -5,158 +7,62 @@ import {
|
|||||||
AccordionTrigger,
|
AccordionTrigger,
|
||||||
} from '@/shared/ui/accordion';
|
} from '@/shared/ui/accordion';
|
||||||
import { Card, CardContent } from '@/shared/ui/card';
|
import { Card, CardContent } from '@/shared/ui/card';
|
||||||
|
import { useQuery } from '@tanstack/react-query';
|
||||||
|
import { useTranslations } from 'next-intl';
|
||||||
|
import { faq_api } from '../lib/api';
|
||||||
|
|
||||||
const Faq = () => {
|
const Faq = () => {
|
||||||
const faqCategories = [
|
const t = useTranslations();
|
||||||
{
|
const { data } = useQuery({
|
||||||
category: 'Umumiy Savollar',
|
queryKey: ['faq_list'],
|
||||||
questions: [
|
queryFn: () => faq_api.list(),
|
||||||
{
|
select(data) {
|
||||||
question: 'Gastro Market nima?',
|
return data.data;
|
||||||
answer:
|
|
||||||
"Gastro Market - bu gastronomiya dunyosidagi eng so'nggi yangiliklarni, retseptlarni va tendentsiyalarni taqdim etuvchi professional onlayn platforma. Biz o'quvchilarimizga sifatli va qiziqarli kontent taqdim etamiz.",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
question: 'Kontent qanday chastotada yangilanadi?',
|
|
||||||
answer:
|
|
||||||
"Biz haftada bir necha marta yangi maqolalar, retseptlar va gastronomiya sohasidagi yangiliklarni nashr qilamiz. Eng so'nggi yangilanishlardan xabardor bo'lish uchun bizning ijtimoiy tarmoqlarimizga obuna bo'ling.",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
question: 'Sizning kontentingiz bepulmi?',
|
|
||||||
answer:
|
|
||||||
"Ha, bizning barcha asosiy kontentimiz mutlaqo bepul. Ba'zi maxsus kontent va premium retseptlar premium obuna talab qilishi mumkin, lekin asosiy maqolalar va yangiliklarni hamma o'qiy oladi.",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
question: "Qanday qilib mualliflaringiz bilan bog'lanish mumkin?",
|
|
||||||
answer:
|
|
||||||
"Har bir maqola ostida muallif haqida ma'lumot va bog'lanish uchun email manzili ko'rsatilgan. Shuningdek, siz umumiy savollar uchun info@gastromarket.uz manziliga yozishingiz mumkin.",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
{
|
});
|
||||||
category: 'Hamkorlik',
|
|
||||||
questions: [
|
|
||||||
{
|
|
||||||
question: 'Qanday qilib hamkorlik qilish mumkin?',
|
|
||||||
answer:
|
|
||||||
"Hamkorlik uchun bizning About sahifamizdagi formani to'ldiring yoki to'g'ridan-to'g'ri partnership@gastromarket.uz manziliga yozing. Biz sizning taklifingizni ko'rib chiqamiz va tez orada javob beramiz.",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
question: 'Qanday turdagi hamkorlikni qabul qilasiz?',
|
|
||||||
answer:
|
|
||||||
"Biz turli xil hamkorlik variantlarini ko'rib chiqamiz: reklama joylashtirish, sponsored content, mahsulot sharhlari, tadbirlar hamkorligi va boshqa formatlar. Har bir taklifni individual ko'rib chiqamiz.",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
question: "Hamkorlik so'roviga javob olish uchun qancha vaqt kerak?",
|
|
||||||
answer:
|
|
||||||
"Odatda biz 3-5 ish kuni ichida barcha hamkorlik so'rovlariga javob beramiz. Agar sizning taklifingiz tezkor javob talab qilsa, iltimos so'rovnomada buni ko'rsating.",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
question: 'Hamkorlik uchun minimal talablar bormi?',
|
|
||||||
answer:
|
|
||||||
"Biz har qanday o'lchamdagi kompaniyalar bilan hamkorlik qilishga tayyormiz. Asosiy talabimiz - bu gastronomiya sohasiga aloqadorlik va sifatli mahsulot/xizmat taklifi.",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
category: 'Kontent va Maqolalar',
|
|
||||||
questions: [
|
|
||||||
{
|
|
||||||
question: "O'z retseptimni qanday qilib taklif qilishim mumkin?",
|
|
||||||
answer:
|
|
||||||
"Agar sizda qiziqarli retsept yoki maqola g'oyasi bo'lsa, content@gastromarket.uz manziliga yozing. Biz sizning taklifingizni ko'rib chiqamiz va agar u bizning standartlarimizga mos kelsa, nashr qilish imkoniyatini muhokama qilamiz.",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
question: 'Maqolalarni qayta nashr qilish mumkinmi?',
|
|
||||||
answer:
|
|
||||||
'Bizning maqolalarimizni qayta nashr qilish uchun oldindan ruxsat olishingiz kerak. Iltimos, permissions@gastromarket.uz manziliga murojaat qiling va qaysi maqolani qanday maqsadda ishlatmoqchi ekanligingizni yozing.',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
question:
|
|
||||||
"Maqolalardagi ma'lumotlar ishonchli bo'lishi qanday kafolatlanadi?",
|
|
||||||
answer:
|
|
||||||
"Barcha maqolalarimiz professional oshpazlar va gastronomiya ekspertlari tomonidan tayyorlanadi va ko'rib chiqiladi. Biz faqat tekshirilgan manba va ma'lumotlardan foydalanamiz.",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
question: "Maxsus mavzu bo'yicha maqola yozishni so'rash mumkinmi?",
|
|
||||||
answer:
|
|
||||||
"Ha, albatta! Agar sizni qiziqtirgan maxsus mavzu bo'lsa, request@gastromarket.uz manziliga yozing. Biz sizning taklifingizni ko'rib chiqamiz va mumkin bo'lsa, kelajakdagi nashrlarga kiritamiz.",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
category: 'Texnik Savollar',
|
|
||||||
questions: [
|
|
||||||
{
|
|
||||||
question:
|
|
||||||
'Saytdan foydalanishda muammo yuzaga kelsa nima qilish kerak?',
|
|
||||||
answer:
|
|
||||||
"Agar texnik muammo yuzaga kelsa, iltimos support@gastromarket.uz manziliga yozing va muammoni batafsil tasvirlab bering. Qaysi brauzer va qurilmadan foydalanayotganingizni ham ko'rsating.",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
question: 'Mobil ilova bormi?',
|
|
||||||
answer:
|
|
||||||
"Hozircha bizda maxsus mobil ilova yo'q, lekin saytimiz barcha qurilmalarda yaxshi ishlaydi. Mobil ilovani kelajakda ishlab chiqishni rejalashtirmoqdamiz.",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
question: "Newsletter ga qanday obuna bo'lish mumkin?",
|
|
||||||
answer:
|
|
||||||
"Sayt pastki qismida newsletter obuna formasi mavjud. Email manzilingizni kiriting va bizning haftalik yangiliklardan xabardor bo'ling.",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
question: "Hisobimni qanday o'chirish mumkin?",
|
|
||||||
answer:
|
|
||||||
"Agar hisobingizni o'chirmoqchi bo'lsangiz, support@gastromarket.uz manziliga yozing. Biz sizning so'rovingizni 7 ish kuni ichida bajaramiz va barcha ma'lumotlaringiz o'chiriladi.",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main className="custom-container">
|
<main className="custom-container">
|
||||||
<section className="relative py-5 from-accent/5 to-background">
|
<section className="relative py-5 from-accent/5 to-background">
|
||||||
<div className="container mx-auto max-w-4xl text-center">
|
<div className="container mx-auto max-w-4xl text-center">
|
||||||
<h1 className="text-2xl md:text-5xl font-bold mb-4 text-balance">
|
<h1 className="text-2xl md:text-5xl font-bold mb-4 text-balance">
|
||||||
{"Tez-tez So'raladigan Savollar"}
|
{t("Tez-tez So'raladigan Savollar")}
|
||||||
</h1>
|
</h1>
|
||||||
<p className="text-lg text-muted-foreground max-w-2xl mx-auto">
|
<p className="text-lg text-muted-foreground max-w-2xl mx-auto">
|
||||||
{"Gastro Market haqida eng ko'p so'raladigan savollarga javoblar"}
|
{t(
|
||||||
|
"Gastro Market haqida eng ko'p so'raladigan savollarga javoblar",
|
||||||
|
)}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section className="py-5">
|
<section className="py-5">
|
||||||
<>
|
<>
|
||||||
<div className="space-y-8">
|
<div className="space-y-2">
|
||||||
{faqCategories.map((category, idx) => (
|
{data &&
|
||||||
<div key={idx}>
|
data.map((category, idx) => (
|
||||||
<h2 className="text-xl font-bold mb-4 text-balance">
|
<div key={idx}>
|
||||||
{category.category}
|
<Card className="p-0">
|
||||||
</h2>
|
<CardContent>
|
||||||
<Card>
|
<Accordion type="single" collapsible className="w-full">
|
||||||
<CardContent>
|
|
||||||
<Accordion type="single" collapsible className="w-full">
|
|
||||||
{category.questions.map((faq, qIdx) => (
|
|
||||||
<AccordionItem
|
<AccordionItem
|
||||||
key={qIdx}
|
value={`item-${idx}`}
|
||||||
value={`item-${idx}-${qIdx}`}
|
|
||||||
className="border-b last:border-b-0"
|
className="border-b last:border-b-0"
|
||||||
>
|
>
|
||||||
<AccordionTrigger className="text-left hover:no-underline py-4">
|
<AccordionTrigger className="text-left hover:no-underline py-4">
|
||||||
<span className="font-semibold text-lg">
|
<span className="font-semibold text-lg">
|
||||||
{faq.question}
|
{category.question}
|
||||||
</span>
|
</span>
|
||||||
</AccordionTrigger>
|
</AccordionTrigger>
|
||||||
<AccordionContent className="text-muted-foreground leading-relaxed pb-4">
|
<AccordionContent className="text-muted-foreground leading-relaxed pb-4">
|
||||||
{faq.answer}
|
{category.answer}
|
||||||
</AccordionContent>
|
</AccordionContent>
|
||||||
</AccordionItem>
|
</AccordionItem>
|
||||||
))}
|
</Accordion>
|
||||||
</Accordion>
|
</CardContent>
|
||||||
</CardContent>
|
</Card>
|
||||||
</Card>
|
</div>
|
||||||
</div>
|
))}
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -1,96 +1,41 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
|
import { product_api } from '@/shared/config/api/product/api';
|
||||||
import { useRouter } from '@/shared/config/i18n/navigation';
|
import { useRouter } from '@/shared/config/i18n/navigation';
|
||||||
import { Button } from '@/shared/ui/button';
|
import { Button } from '@/shared/ui/button';
|
||||||
|
import { Card } from '@/shared/ui/card';
|
||||||
|
import { Skeleton } from '@/shared/ui/skeleton';
|
||||||
import { ProductCard } from '@/widgets/categories/ui/product-card';
|
import { ProductCard } from '@/widgets/categories/ui/product-card';
|
||||||
|
import { useQuery } from '@tanstack/react-query';
|
||||||
|
import { AxiosError } from 'axios';
|
||||||
import { Heart } from 'lucide-react';
|
import { Heart } from 'lucide-react';
|
||||||
import { useState } from 'react';
|
import { useTranslations } from 'next-intl';
|
||||||
|
import { useEffect } from 'react';
|
||||||
// Fake data
|
|
||||||
const LIKED_PRODUCTS = [
|
|
||||||
{
|
|
||||||
id: 1,
|
|
||||||
name: 'Samsung Galaxy S23 Ultra 256GB, Phantom Black',
|
|
||||||
price: 12500000,
|
|
||||||
oldPrice: 15000000,
|
|
||||||
image: '/samsung-galaxy-s24-smartphone.jpg',
|
|
||||||
rating: 4.8,
|
|
||||||
reviews: 342,
|
|
||||||
discount: 17,
|
|
||||||
inStock: true,
|
|
||||||
liked: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 2,
|
|
||||||
name: 'Apple Ipad Pro Tablet',
|
|
||||||
price: 2850000,
|
|
||||||
oldPrice: 3200000,
|
|
||||||
image: '/apple-ipad-pro-tablet.jpg',
|
|
||||||
rating: 4.9,
|
|
||||||
reviews: 567,
|
|
||||||
discount: 11,
|
|
||||||
liked: true,
|
|
||||||
inStock: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 3,
|
|
||||||
name: 'Apple Watch Series 9',
|
|
||||||
price: 7500000,
|
|
||||||
oldPrice: 8500000,
|
|
||||||
image: '/apple-watch-series-9-smartwatch.jpg',
|
|
||||||
rating: 4.7,
|
|
||||||
reviews: 234,
|
|
||||||
discount: 12,
|
|
||||||
inStock: true,
|
|
||||||
liked: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 4,
|
|
||||||
name: 'MacBook Air 13 M2 chip, 8GB RAM, 256GB SSD',
|
|
||||||
price: 14200000,
|
|
||||||
oldPrice: 16000000,
|
|
||||||
image: '/apple-macbook-pro-laptop.jpg',
|
|
||||||
rating: 4.9,
|
|
||||||
reviews: 891,
|
|
||||||
liked: true,
|
|
||||||
discount: 11,
|
|
||||||
inStock: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 5,
|
|
||||||
name: 'Dyson V15 Detect Simsiz Changyutgich',
|
|
||||||
price: 6800000,
|
|
||||||
oldPrice: 7800000,
|
|
||||||
image: '/dyson-v15-detect-vacuum-cleaner.jpg',
|
|
||||||
rating: 4.6,
|
|
||||||
reviews: 178,
|
|
||||||
discount: 13,
|
|
||||||
liked: true,
|
|
||||||
inStock: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 6,
|
|
||||||
name: 'Coca-Cola',
|
|
||||||
price: 1250000,
|
|
||||||
oldPrice: 1650000,
|
|
||||||
image: '/classic-coca-cola.png',
|
|
||||||
rating: 4.5,
|
|
||||||
liked: true,
|
|
||||||
reviews: 423,
|
|
||||||
discount: 24,
|
|
||||||
inStock: true,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
export default function Favourite() {
|
export default function Favourite() {
|
||||||
const [likedProducts, setLikedProducts] = useState(LIKED_PRODUCTS);
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const t = useTranslations();
|
||||||
|
const {
|
||||||
|
data: favourite,
|
||||||
|
isLoading,
|
||||||
|
error,
|
||||||
|
} = useQuery({
|
||||||
|
queryKey: ['favourite_product'],
|
||||||
|
queryFn: () => product_api.favouuriteProduct(),
|
||||||
|
select(data) {
|
||||||
|
return data.data;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const handleRemove = (id: number) => {
|
useEffect(() => {
|
||||||
setLikedProducts((prev) => prev.filter((product) => product.id !== id));
|
if ((error as AxiosError)?.status === 403) {
|
||||||
};
|
router.replace('/auth');
|
||||||
|
} else if ((error as AxiosError)?.status === 401) {
|
||||||
|
router.replace('/auth');
|
||||||
|
}
|
||||||
|
}, [error]);
|
||||||
|
|
||||||
if (likedProducts.length === 0) {
|
if (favourite && favourite.results.length === 0) {
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen py-12">
|
<div className="min-h-screen py-12">
|
||||||
<div className="container mx-auto px-4">
|
<div className="container mx-auto px-4">
|
||||||
@@ -99,18 +44,16 @@ export default function Favourite() {
|
|||||||
<Heart className="w-16 h-16 text-slate-300" />
|
<Heart className="w-16 h-16 text-slate-300" />
|
||||||
</div>
|
</div>
|
||||||
<h2 className="text-2xl font-bold text-slate-800 mb-2">
|
<h2 className="text-2xl font-bold text-slate-800 mb-2">
|
||||||
{"Sevimlilar bo'sh"}
|
{t("Sevimlilar bo'sh")}
|
||||||
</h2>
|
</h2>
|
||||||
<p className="text-slate-500 text-center max-w-md mb-8">
|
<p className="text-slate-500 text-center max-w-md mb-8">
|
||||||
{`Hali hech qanday mahsulotni sevimlilarga qo'shmadingiz.
|
{t(`Hali hech qanday mahsulotni sevimlilarga qo'shmadingiz`)}
|
||||||
Mahsulotlar ro'yxatiga o'ting va yoqqan mahsulotlaringizni
|
|
||||||
saqlang.`}
|
|
||||||
</p>
|
</p>
|
||||||
<Button
|
<Button
|
||||||
className="bg-blue-600 text-white px-8 py-3 rounded-xl hover:bg-blue-700"
|
className="bg-blue-600 text-white px-8 py-3 rounded-xl hover:bg-blue-700"
|
||||||
onClick={() => router.push('/')}
|
onClick={() => router.push('/')}
|
||||||
>
|
>
|
||||||
Xarid qilishni boshlash
|
{t('Xarid qilishni boshlash')}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -118,26 +61,59 @@ export default function Favourite() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return (
|
||||||
|
<div className="custom-container">
|
||||||
|
<div className="mb-8">
|
||||||
|
<Skeleton className="h-8 w-64 mb-2" />
|
||||||
|
<Skeleton className="h-4 w-32" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Grid */}
|
||||||
|
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 gap-4">
|
||||||
|
{Array.from({ length: 10 }).map((_, i) => (
|
||||||
|
<div key={i} className="rounded-xl border p-3 space-y-3">
|
||||||
|
<Skeleton className="h-40 w-full rounded-lg" />
|
||||||
|
<Skeleton className="h-4 w-[80%]" />
|
||||||
|
<Skeleton className="h-4 w-[60%]" />
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="custom-container">
|
<div className="custom-container">
|
||||||
<>
|
<>
|
||||||
<div className="mb-8">
|
<div className="mb-8">
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-3xl font-bold text-slate-800 mb-2">
|
<h1 className="text-3xl font-bold text-slate-800 mb-2">
|
||||||
Sevimli mahsulotlar
|
{t('Sevimli mahsulotlar')}
|
||||||
</h1>
|
</h1>
|
||||||
<p className="text-slate-500">{likedProducts.length} ta mahsulot</p>
|
<p className="text-slate-500">
|
||||||
|
{favourite && favourite.total} {t('ta mahsulot')}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 gap-4 mb-30">
|
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 gap-4 mb-30">
|
||||||
{likedProducts.map((product) => (
|
{isLoading &&
|
||||||
<ProductCard
|
Array.from({ length: 6 }).map((_, index) => (
|
||||||
key={product.id}
|
<Card className="p-3 space-y-3 rounded-xl" key={index}>
|
||||||
product={product}
|
<Skeleton className="h-40 sm:h-48 md:h-56 w-full rounded-lg" />
|
||||||
handleRemove={handleRemove}
|
<Skeleton className="h-4 w-3/4" />
|
||||||
/>
|
<Skeleton className="h-4 w-1/2" />
|
||||||
))}
|
<Skeleton className="h-10 w-full rounded-lg" />
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
{favourite &&
|
||||||
|
!isLoading &&
|
||||||
|
favourite?.results
|
||||||
|
.filter((product) => product.is_active)
|
||||||
|
.map((product) => (
|
||||||
|
<ProductCard key={product.id} product={product} />
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,24 +1,27 @@
|
|||||||
import { Card, CardContent } from '@/shared/ui/card';
|
import { Card, CardContent } from '@/shared/ui/card';
|
||||||
import { Database, Eye, FileText, Lock, Shield, UserCheck } from 'lucide-react';
|
import { Database, Eye, FileText, Lock, Shield } from 'lucide-react';
|
||||||
|
import { useTranslations } from 'next-intl';
|
||||||
|
|
||||||
const PrivacyPolicy = () => {
|
const PrivacyPolicy = () => {
|
||||||
|
const t = useTranslations();
|
||||||
return (
|
return (
|
||||||
<main className="custom-container">
|
<main className="custom-container">
|
||||||
{/* Hero Section */}
|
{/* Hero Section */}
|
||||||
<section>
|
<section>
|
||||||
<div className="container mx-auto max-w-4xl text-center">
|
<div className="text-center">
|
||||||
<div className="inline-flex items-center justify-center w-16 h-16 rounded-full bg-primary/10 mb-6">
|
<div className="inline-flex items-center justify-center w-16 h-16 rounded-full bg-primary/10 mb-6">
|
||||||
<Shield className="w-8 h-8 text-primary" />
|
<Shield className="w-8 h-8 text-primary" />
|
||||||
</div>
|
</div>
|
||||||
<h1 className="text-4xl md:text-5xl font-bold mb-4 text-balance">
|
<p className="text-2xl md:text-5xl font-bold mb-4">
|
||||||
Maxfiylik Siyosati
|
{t('Maxfiylik Siyosati')}
|
||||||
</h1>
|
</p>
|
||||||
<p className="text-lg text-muted-foreground max-w-2xl mx-auto">
|
<p className="text-lg text-muted-foreground">
|
||||||
{`Gastro Market sizning ma'lumotlaringiz xavfsizligini jiddiy qabul
|
{t(
|
||||||
qiladi`}
|
`Gastro Market sizning ma'lumotlaringiz xavfsizligini jiddiy qabul qiladi`,
|
||||||
|
)}
|
||||||
</p>
|
</p>
|
||||||
<p className="text-sm text-muted-foreground mt-4">
|
<p className="text-sm text-muted-foreground mt-4">
|
||||||
Oxirgi yangilanish: 16 Dekabr 2025
|
{t('Oxirgi yangilanish: 16 Dekabr 2025')}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
@@ -29,11 +32,9 @@ const PrivacyPolicy = () => {
|
|||||||
{/* Introduction */}
|
{/* Introduction */}
|
||||||
<div className="prose prose-lg max-w-none mb-12">
|
<div className="prose prose-lg max-w-none mb-12">
|
||||||
<p className="text-muted-foreground leading-relaxed">
|
<p className="text-muted-foreground leading-relaxed">
|
||||||
{`Ushbu Maxfiylik Siyosati Gastro Market online magazinida siz
|
{t(
|
||||||
tomonidan taqdim etilgan shaxsiy ma'lumotlarni qanday to'playmiz,
|
`Ushbu Maxfiylik Siyosati Gastro Market online magazinida siz tomonidan taqdim etilgan shaxsiy ma'lumotlarni qanday to'playmiz`,
|
||||||
ishlatamiz va himoya qilishimizni tushuntiradi. Xizmatlarimizdan
|
)}
|
||||||
foydalanish orqali siz ushbu siyosatda tasvirlangan amaliyotlarga
|
|
||||||
rozilik bildirasiz.`}
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -50,29 +51,30 @@ const PrivacyPolicy = () => {
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<h2 className="text-2xl font-bold mb-4">
|
<h2 className="text-2xl font-bold mb-4">
|
||||||
{"1. Biz To'playdigan Ma'lumotlar"}
|
{t("Biz To'playdigan Ma'lumotlar")}
|
||||||
</h2>
|
</h2>
|
||||||
<div className="space-y-3 text-muted-foreground">
|
<div className="space-y-3 text-muted-foreground">
|
||||||
<p className="leading-relaxed">
|
<p className="leading-relaxed">
|
||||||
{`Sizning tajribangizni yaxshilash uchun biz quyidagi
|
{t(
|
||||||
ma'lumotlarni to'playmiz:`}
|
`Sizning tajribangizni yaxshilash uchun biz quyidagi ma'lumotlarni to'playmiz`,
|
||||||
|
)}
|
||||||
</p>
|
</p>
|
||||||
<ul className="space-y-2 ml-6 list-disc">
|
<ul className="space-y-2 ml-6 list-disc">
|
||||||
<li>
|
<li>
|
||||||
<strong>{`Shaxsiy Ma'lumotlar`}:</strong> Ism, email
|
<strong>{t(`Shaxsiy Ma'lumotlar`)}:</strong>{' '}
|
||||||
manzil, telefon raqami
|
{t('Ism email manzil telefon raqami')}
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<strong>{"Kompaniya Ma'lumotlari"}:</strong> Kompaniya
|
<strong>{"Kompaniya Ma'lumotlari"}:</strong>{' '}
|
||||||
nomi, website, hamkorlik {"so'rovlari"}
|
{t("Kompaniya nomi, website, hamkorlik so'rovlari")}
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<strong>Fayllar:</strong> Hamkorlik uchun yuklangan
|
<strong>{t('Fayllar:')}</strong>{' '}
|
||||||
hujjatlar
|
{t('Hamkorlik uchun yuklangan hujjatlar')}
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<strong>{"Texnik Ma'lumotlar"}:</strong> IP manzil,
|
<strong>{t("Texnik Ma'lumotlar")}:</strong>{' '}
|
||||||
brauzer turi, qurilma {"ma'lumotlari"}
|
{t("IP manzil, brauzer turi, qurilma ma'lumotlari")}
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
@@ -92,31 +94,36 @@ const PrivacyPolicy = () => {
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<h2 className="text-2xl font-bold mb-4">
|
<h2 className="text-2xl font-bold mb-4">
|
||||||
2. {"Ma'lumotlardan Foydalanish"}
|
{t("Ma'lumotlardan Foydalanish")}
|
||||||
</h2>
|
</h2>
|
||||||
<div className="space-y-3 text-muted-foreground">
|
<div className="space-y-3 text-muted-foreground">
|
||||||
<p className="leading-relaxed">
|
<p className="leading-relaxed">
|
||||||
{`To'plangan ma'lumotlaringizdan quyidagi maqsadlarda
|
{t(
|
||||||
foydalanamiz:`}
|
`To'plangan ma'lumotlaringizdan quyidagi maqsadlarda foydalanamiz:`,
|
||||||
|
)}
|
||||||
</p>
|
</p>
|
||||||
<ul className="space-y-2 ml-6 list-disc">
|
<ul className="space-y-2 ml-6 list-disc">
|
||||||
<li>
|
<li>
|
||||||
{
|
{t(
|
||||||
"Hamkorlik so'rovlarini qayta ishlash va javob berish"
|
"Hamkorlik so'rovlarini qayta ishlash va javob berish",
|
||||||
}
|
)}
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
{`Sizga xizmatlarimiz va yangiliklar haqida ma'lumot
|
{t(
|
||||||
berish`}
|
`Sizga xizmatlarimiz va yangiliklar haqida ma'lumot berish`,
|
||||||
|
)}
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
{`Sayt xavfsizligini ta'minlash va firibgarlikka qarshi
|
{t(
|
||||||
kurashish`}
|
`Sayt xavfsizligini ta'minlash va firibgarlikka qarshi kurashish`,
|
||||||
|
)}
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
Foydalanuvchi tajribasini tahlil qilish va yaxshilash
|
{t(
|
||||||
|
'Foydalanuvchi tajribasini tahlil qilish va yaxshilash',
|
||||||
|
)}
|
||||||
</li>
|
</li>
|
||||||
<li>Qonuniy talablarni bajarish</li>
|
<li>{t('Qonuniy talablarni bajarish')}</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -135,22 +142,29 @@ const PrivacyPolicy = () => {
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<h2 className="text-2xl font-bold mb-4">
|
<h2 className="text-2xl font-bold mb-4">
|
||||||
3. {"Ma'lumotlar Xavfsizligi"}
|
{t("Ma'lumotlar Xavfsizligi")}
|
||||||
</h2>
|
</h2>
|
||||||
<div className="space-y-3 text-muted-foreground">
|
<div className="space-y-3 text-muted-foreground">
|
||||||
<p className="leading-relaxed">
|
<p className="leading-relaxed">
|
||||||
{`Biz sizning shaxsiy ma'lumotlaringizni himoya qilish
|
{t(
|
||||||
uchun zamonaviy xavfsizlik choralarini qo'llaymiz:`}
|
`Biz sizning shaxsiy ma'lumotlaringizni himoya qilish uchun zamonaviy xavfsizlik choralarini qo'llaymiz:`,
|
||||||
|
)}
|
||||||
</p>
|
</p>
|
||||||
<ul className="space-y-2 ml-6 list-disc">
|
<ul className="space-y-2 ml-6 list-disc">
|
||||||
<li>
|
<li>
|
||||||
{"SSL/TLS shifrlash orqali ma'lumotlar uzatish"}
|
{t("SSL/TLS shifrlash orqali ma'lumotlar uzatish")}
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
{"Xavfsiz serverlar va ma'lumotlar bazasida saqlash"}
|
{t(
|
||||||
|
"Xavfsiz serverlar va ma'lumotlar bazasida saqlash",
|
||||||
|
)}
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
{t('Cheklangan kirish huquqlari va autentifikatsiya')}
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
{t('Doimiy xavfsizlik monitoringi va yangilanishlar')}
|
||||||
</li>
|
</li>
|
||||||
<li>Cheklangan kirish huquqlari va autentifikatsiya</li>
|
|
||||||
<li>Doimiy xavfsizlik monitoringi va yangilanishlar</li>
|
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -169,22 +183,25 @@ const PrivacyPolicy = () => {
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<h2 className="text-2xl font-bold mb-4">
|
<h2 className="text-2xl font-bold mb-4">
|
||||||
{"4. Ma'lumotlarni Ulashish"}
|
{t("Ma'lumotlarni Ulashish")}
|
||||||
</h2>
|
</h2>
|
||||||
<div className="space-y-3 text-muted-foreground">
|
<div className="space-y-3 text-muted-foreground">
|
||||||
<p className="leading-relaxed">
|
<p className="leading-relaxed">
|
||||||
{`Biz sizning shaxsiy ma'lumotlaringizni uchinchi
|
{t(
|
||||||
shaxslarga sotmaymiz. Ma'lumotlaringiz faqat quyidagi
|
`Biz sizning shaxsiy ma'lumotlaringizni uchinchi shaxslarga sotmaymiz`,
|
||||||
hollarda ulashilishi mumkin:`}
|
)}
|
||||||
</p>
|
</p>
|
||||||
<ul className="space-y-2 ml-6 list-disc">
|
<ul className="space-y-2 ml-6 list-disc">
|
||||||
<li>Sizning roziligingiz bilan</li>
|
<li>{t('Sizning roziligingiz bilan')}</li>
|
||||||
<li>{"Qonuniy talablar bo'yicha"}</li>
|
<li>{t("Qonuniy talablar bo'yicha")}</li>
|
||||||
<li>
|
<li>
|
||||||
{`Xizmat ko'rsatuvchi ishonchli hamkorlar bilan
|
{t(
|
||||||
(maxfiylik shartnomalari ostida)`}
|
`Xizmat ko'rsatuvchi ishonchli hamkorlar bilan (maxfiylik shartnomalari ostida)`,
|
||||||
|
)}
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
{t('Kompaniya birlashuvi yoki sotilishi holatida')}
|
||||||
</li>
|
</li>
|
||||||
<li>Kompaniya birlashuvi yoki sotilishi holatida</li>
|
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -192,131 +209,16 @@ const PrivacyPolicy = () => {
|
|||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
{/* Section 5 */}
|
|
||||||
<Card className="border-l-4 border-l-chart-4">
|
|
||||||
<CardContent className="pt-6">
|
|
||||||
<div className="flex items-start gap-4">
|
|
||||||
<div className="flex-shrink-0">
|
|
||||||
<div className="w-12 h-12 rounded-lg bg-chart-4/10 flex items-center justify-center">
|
|
||||||
<UserCheck className="w-6 h-6 text-chart-4" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex-1">
|
|
||||||
<h2 className="text-2xl font-bold mb-4">
|
|
||||||
5. Sizning Huquqlaringiz
|
|
||||||
</h2>
|
|
||||||
<div className="space-y-3 text-muted-foreground">
|
|
||||||
<p className="leading-relaxed">
|
|
||||||
Sizda quyidagi huquqlar mavjud:
|
|
||||||
</p>
|
|
||||||
<ul className="space-y-2 ml-6 list-disc">
|
|
||||||
<li>
|
|
||||||
<strong>Kirish Huquqi:</strong> {"O'zingiz haqidagi"}
|
|
||||||
{"ma'lumotlarni ko'rish"}
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<strong>Tuzatish Huquqi:</strong>{' '}
|
|
||||||
{`Noto'g'ri
|
|
||||||
ma'lumotlarni tuzatish`}
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<strong>{"O'chirish Huquqi"}:</strong>{' '}
|
|
||||||
{`Ma'lumotlaringizni
|
|
||||||
o'chirishni so'rash`}
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<strong>Rad Etish Huquqi:</strong> Marketing
|
|
||||||
xabarlaridan voz kechish
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<strong>Portativlik:</strong>{' '}
|
|
||||||
{`Ma'lumotlaringizni
|
|
||||||
boshqa joyga ko'chirish`}
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
<p className="mt-4 leading-relaxed">
|
|
||||||
Ushbu huquqlardan foydalanish uchun biz bilan{' '}
|
|
||||||
<a
|
|
||||||
href="mailto:info@gastromarket.uz"
|
|
||||||
className="text-primary hover:underline"
|
|
||||||
>
|
|
||||||
info@gastromarket.uz
|
|
||||||
</a>{' '}
|
|
||||||
{"orqali bog'laning."}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
{/* Section 6 - Cookies */}
|
|
||||||
<Card>
|
|
||||||
<CardContent className="pt-6">
|
|
||||||
<h2 className="text-2xl font-bold mb-4">
|
|
||||||
6. Cookies va Kuzatish Texnologiyalari
|
|
||||||
</h2>
|
|
||||||
<div className="space-y-3 text-muted-foreground">
|
|
||||||
<p className="leading-relaxed">
|
|
||||||
{`Saytimiz cookies va shunga o'xshash texnologiyalardan
|
|
||||||
foydalanadi. Ulardan foydalanish maqsadi:`}
|
|
||||||
</p>
|
|
||||||
<ul className="space-y-2 ml-6 list-disc">
|
|
||||||
<li>Siz tanlagan parametrlarni eslab qolish</li>
|
|
||||||
<li>
|
|
||||||
Sayt traffic va foydalanuvchi xatti-harakatlarini tahlil
|
|
||||||
qilish
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
{"Marketing kampaniyalarining samaradorligini o'lchash"}
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
<p className="mt-4 leading-relaxed">
|
|
||||||
Brauzer sozlamalaridan cookies-ni boshqarishingiz mumkin.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
{/* Section 7 - Children */}
|
|
||||||
<Card>
|
|
||||||
<CardContent className="pt-6">
|
|
||||||
<h2 className="text-2xl font-bold mb-4">
|
|
||||||
7. Bolalar Maxfiyligi
|
|
||||||
</h2>
|
|
||||||
<p className="text-muted-foreground leading-relaxed">
|
|
||||||
{`Bizning xizmatlarimiz 16 yoshdan kichik bolalarga
|
|
||||||
mo'ljallanmagan. Agar siz 16 yoshdan kichik bo'lsangiz,
|
|
||||||
iltimos, shaxsiy ma'lumotlaringizni taqdim etishdan oldin
|
|
||||||
ota-onangiz yoki vasiyingizning roziligini oling.`}
|
|
||||||
</p>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
{/* Section 8 - Changes */}
|
|
||||||
<Card>
|
|
||||||
<CardContent className="pt-6">
|
|
||||||
<h2 className="text-2xl font-bold mb-4">
|
|
||||||
8. {"Siyosatdagi O'zgarishlar"}
|
|
||||||
</h2>
|
|
||||||
<p className="text-muted-foreground leading-relaxed">
|
|
||||||
{`Biz vaqti-vaqti bilan ushbu Maxfiylik Siyosatini yangilashimiz
|
|
||||||
mumkin. Barcha o'zgarishlar ushbu sahifada e'lon qilinadi va
|
|
||||||
yuqorida "Oxirgi yangilanish" sanasi ko'rsatiladi. Muhim
|
|
||||||
o'zgarishlar bo'lsa, sizni email orqali xabardor qilamiz.`}
|
|
||||||
</p>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
{/* Contact Section */}
|
{/* Contact Section */}
|
||||||
<Card className="bg-primary/5 border-primary/20">
|
<Card className="bg-primary/5 border-primary/20">
|
||||||
<CardContent className="pt-6">
|
<CardContent className="pt-6">
|
||||||
<h2 className="text-2xl font-bold mb-4">
|
<h2 className="text-2xl font-bold mb-4">
|
||||||
9. {"Biz Bilan Bog'lanish"}
|
{t("Biz Bilan Bog'lanish")}
|
||||||
</h2>
|
</h2>
|
||||||
<p className="text-muted-foreground leading-relaxed mb-4">
|
<p className="text-muted-foreground leading-relaxed mb-4">
|
||||||
{`Ushbu Maxfiylik Siyosati yoki shaxsiy ma'lumotlaringiz haqida
|
{t(
|
||||||
savollaringiz bo'lsa, biz bilan bog'laning:`}
|
`Ushbu Maxfiylik Siyosati yoki shaxsiy ma'lumotlaringiz haqida savollaringiz bo'lsa, biz bilan bog'laning:`,
|
||||||
|
)}
|
||||||
</p>
|
</p>
|
||||||
<div className="space-y-2 text-muted-foreground">
|
<div className="space-y-2 text-muted-foreground">
|
||||||
<p>
|
<p>
|
||||||
@@ -329,7 +231,7 @@ const PrivacyPolicy = () => {
|
|||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
<strong>Telefon:</strong>{' '}
|
<strong>{t('Telefon')}:</strong>{' '}
|
||||||
<a
|
<a
|
||||||
href="tel:+998901234567"
|
href="tel:+998901234567"
|
||||||
className="text-primary hover:underline"
|
className="text-primary hover:underline"
|
||||||
@@ -338,7 +240,7 @@ const PrivacyPolicy = () => {
|
|||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
<strong>Manzil:</strong> {"Toshkent, O'zbekiston"}
|
<strong>{t('Manzil')}:</strong> {t("Toshkent, O'zbekiston")}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
@@ -346,23 +248,6 @@ const PrivacyPolicy = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{/* Bottom CTA */}
|
|
||||||
<section className="py-16 px-4 bg-muted/30">
|
|
||||||
<div className="container mx-auto max-w-4xl text-center">
|
|
||||||
<h2 className="text-3xl font-bold mb-4">Savollaringiz Bormi?</h2>
|
|
||||||
<p className="text-muted-foreground mb-6 max-w-2xl mx-auto">
|
|
||||||
{`Maxfiylik siyosati yoki ma'lumotlaringiz xavfsizligi haqida
|
|
||||||
qo'shimcha ma'lumot olish uchun bizga murojaat qiling.`}
|
|
||||||
</p>
|
|
||||||
<a
|
|
||||||
href="mailto:privacy@gastromarket.uz"
|
|
||||||
className="inline-flex items-center justify-center px-6 py-3 bg-primary text-primary-foreground rounded-lg hover:bg-primary/90 transition-colors font-medium"
|
|
||||||
>
|
|
||||||
{"Biz Bilan Bog'laning"}
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
</main>
|
</main>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
0
src/features/product/lib/api.ts
Normal file
0
src/features/product/lib/api.ts
Normal file
@@ -1,5 +1,11 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
|
import { cart_api } from '@/features/cart/lib/api';
|
||||||
|
import { product_api } from '@/shared/config/api/product/api';
|
||||||
|
import { BASE_URL } from '@/shared/config/api/URLs';
|
||||||
|
import { useCartId } from '@/shared/hooks/cartId';
|
||||||
|
import formatPrice from '@/shared/lib/formatPrice';
|
||||||
|
import { Card } from '@/shared/ui/card';
|
||||||
import {
|
import {
|
||||||
Carousel,
|
Carousel,
|
||||||
CarouselContent,
|
CarouselContent,
|
||||||
@@ -7,107 +13,112 @@ import {
|
|||||||
CarouselNext,
|
CarouselNext,
|
||||||
CarouselPrevious,
|
CarouselPrevious,
|
||||||
} from '@/shared/ui/carousel';
|
} from '@/shared/ui/carousel';
|
||||||
|
import { Input } from '@/shared/ui/input';
|
||||||
|
import { Skeleton } from '@/shared/ui/skeleton';
|
||||||
import { ProductCard } from '@/widgets/categories/ui/product-card';
|
import { ProductCard } from '@/widgets/categories/ui/product-card';
|
||||||
import {
|
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||||
Heart,
|
import { AxiosError } from 'axios';
|
||||||
Minus,
|
import { Heart, Minus, Plus, Shield, ShoppingCart, Truck } from 'lucide-react';
|
||||||
Plus,
|
import { useTranslations } from 'next-intl';
|
||||||
RotateCcw,
|
|
||||||
Shield,
|
|
||||||
ShoppingCart,
|
|
||||||
Star,
|
|
||||||
Truck,
|
|
||||||
} from 'lucide-react';
|
|
||||||
import Image from 'next/image';
|
import Image from 'next/image';
|
||||||
import { useState } from 'react';
|
import { useParams } from 'next/navigation';
|
||||||
|
import { useEffect, useRef, useState } from 'react';
|
||||||
|
import { toast } from 'sonner';
|
||||||
|
|
||||||
const ProductDetail = () => {
|
const ProductDetail = () => {
|
||||||
|
const t = useTranslations();
|
||||||
const [quantity, setQuantity] = useState(1);
|
const [quantity, setQuantity] = useState(1);
|
||||||
const [selectedImage, setSelectedImage] = useState(0);
|
const { product } = useParams();
|
||||||
const [liked, setLiked] = useState(false);
|
const queryClient = useQueryClient();
|
||||||
|
const [selectedImage, setSelectedImage] = useState<number>(0);
|
||||||
|
const debounceRef = useRef<NodeJS.Timeout | null>(null);
|
||||||
|
const { cart_id } = useCartId();
|
||||||
|
|
||||||
// Fake product data
|
const { data: cartItems } = useQuery({
|
||||||
const product = {
|
queryKey: ['cart_items', cart_id],
|
||||||
id: 5,
|
queryFn: () => cart_api.get_cart_items(cart_id!),
|
||||||
name: 'Coca-Cola 1.5L',
|
enabled: !!cart_id,
|
||||||
price: 12000,
|
});
|
||||||
oldPrice: 14000,
|
|
||||||
image: '/classic-coca-cola.png',
|
|
||||||
rating: 4.8,
|
|
||||||
reviews: 342,
|
|
||||||
discount: 14,
|
|
||||||
inStock: true,
|
|
||||||
description:
|
|
||||||
"Coca-Cola klassik ta'mi bilan ajoyib gazlangan ichimlik. 1.5 litrlik shisha butilkada. Sovuq holda iste'mol qilish tavsiya etiladi.",
|
|
||||||
category: 'Ichimliklar',
|
|
||||||
brand: 'Coca-Cola',
|
|
||||||
volume: '1.5L',
|
|
||||||
supplier: {
|
|
||||||
name: 'Global Trade LLC',
|
|
||||||
logo: '/generic-company-logo.png',
|
|
||||||
phone: '+998 90 123 45 67',
|
|
||||||
email: 'info@globaltrade.uz',
|
|
||||||
},
|
|
||||||
images: [
|
|
||||||
'/classic-coca-cola.png',
|
|
||||||
'/clear-soda-bottle.png',
|
|
||||||
'/classic-coca-cola.png',
|
|
||||||
'/classic-coca-cola.png',
|
|
||||||
'/classic-coca-cola.png',
|
|
||||||
'/classic-coca-cola.png',
|
|
||||||
'/classic-coca-cola.png',
|
|
||||||
'/classic-coca-cola.png',
|
|
||||||
'/classic-coca-cola.png',
|
|
||||||
'/classic-coca-cola.png',
|
|
||||||
'/classic-coca-cola.png',
|
|
||||||
],
|
|
||||||
specifications: {
|
|
||||||
Hajmi: '1.5 litr',
|
|
||||||
'Qadoq turi': 'Plastik butilka',
|
|
||||||
'Ishlab chiqaruvchi': 'Coca-Cola Company',
|
|
||||||
'Saqlash muddati': '12 oy',
|
|
||||||
'Energiya qiymati': '180 kJ / 43 kcal',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const [relatedProducts, setRelatedProducts] = useState([
|
const { data, isLoading } = useQuery({
|
||||||
{
|
queryKey: ['product_detail', product],
|
||||||
id: 6,
|
queryFn: () => {
|
||||||
name: 'Pepsi 2L',
|
if (product) return product_api.detail(product.toString());
|
||||||
price: 11000,
|
|
||||||
reviews: 342,
|
|
||||||
liked: false,
|
|
||||||
inStock: true,
|
|
||||||
oldPrice: 13000,
|
|
||||||
image: '/pepsi-bottle.jpg',
|
|
||||||
rating: 4.6,
|
|
||||||
discount: 15,
|
|
||||||
},
|
},
|
||||||
{
|
select(data) {
|
||||||
id: 8,
|
return data?.data;
|
||||||
name: 'Sprite 1.5L',
|
|
||||||
price: 10000,
|
|
||||||
inStock: true,
|
|
||||||
oldPrice: 12000,
|
|
||||||
image: '/clear-soda-bottle.png',
|
|
||||||
rating: 4.5,
|
|
||||||
reviews: 342,
|
|
||||||
liked: false,
|
|
||||||
discount: 17,
|
|
||||||
},
|
},
|
||||||
{
|
enabled: !!product,
|
||||||
id: 7,
|
});
|
||||||
name: 'Fanta Orange 1L',
|
|
||||||
price: 9000,
|
const { data: recomendation, isLoading: proLoad } = useQuery({
|
||||||
oldPrice: 10000,
|
queryKey: ['product_list'],
|
||||||
inStock: true,
|
queryFn: () => product_api.list({ page: 1, page_size: 12 }),
|
||||||
image: '/fanta-orange-bottle.png',
|
select(data) {
|
||||||
rating: 4.4,
|
return data.data.results;
|
||||||
reviews: 342,
|
|
||||||
liked: true,
|
|
||||||
discount: 10,
|
|
||||||
},
|
},
|
||||||
]);
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!cart_id) return;
|
||||||
|
if (quantity <= 1) return;
|
||||||
|
|
||||||
|
const cartItemId = cartItems?.data?.cart_item.find(
|
||||||
|
(item) => item.product_id === data?.id,
|
||||||
|
)?.id;
|
||||||
|
|
||||||
|
if (!cartItemId) return;
|
||||||
|
|
||||||
|
if (debounceRef.current) clearTimeout(debounceRef.current);
|
||||||
|
|
||||||
|
debounceRef.current = setTimeout(() => {
|
||||||
|
updateCartItem({ body: { quantity }, cart_item_id: cartItemId });
|
||||||
|
}, 500);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (debounceRef.current) clearTimeout(debounceRef.current);
|
||||||
|
};
|
||||||
|
}, [quantity, cart_id]);
|
||||||
|
|
||||||
|
const { mutate } = useMutation({
|
||||||
|
mutationFn: (body: { product: string; quantity: number; cart: string }) =>
|
||||||
|
cart_api.cart_item(body),
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.refetchQueries({ queryKey: ['cart_items'] });
|
||||||
|
},
|
||||||
|
onError: (err: AxiosError) => {
|
||||||
|
const detail = (err.response?.data as { detail: string }).detail;
|
||||||
|
toast.error(detail || err.message, {
|
||||||
|
richColors: true,
|
||||||
|
position: 'top-center',
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const { mutate: updateCartItem } = useMutation({
|
||||||
|
mutationFn: ({
|
||||||
|
body,
|
||||||
|
cart_item_id,
|
||||||
|
}: {
|
||||||
|
body: { quantity: number };
|
||||||
|
cart_item_id: string;
|
||||||
|
}) => cart_api.update_cart_item({ body, cart_item_id }),
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.refetchQueries({ queryKey: ['cart_items'] });
|
||||||
|
},
|
||||||
|
onError: (err: AxiosError) => {
|
||||||
|
toast.error(err.message, { richColors: true, position: 'top-center' });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const favouriteMutation = useMutation({
|
||||||
|
mutationFn: (productId: string) => product_api.favourite(productId),
|
||||||
|
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.refetchQueries({ queryKey: ['product_list'] });
|
||||||
|
queryClient.refetchQueries({ queryKey: ['product_detail', product] });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const handleQuantityChange = (type: string) => {
|
const handleQuantityChange = (type: string) => {
|
||||||
if (type === 'increase') {
|
if (type === 'increase') {
|
||||||
@@ -117,25 +128,23 @@ const ProductDetail = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const addToCart = () => {
|
if (isLoading) {
|
||||||
alert(`${quantity} ta ${product.name} savatchaga qo'shildi!`);
|
return (
|
||||||
};
|
<div className="custom-container pb-5">
|
||||||
|
<div className="w-full max-w-4xl space-y-6">
|
||||||
const handleRemove = (id: number) => {
|
<div className="h-[400px] bg-gray-100 animate-pulse rounded-lg"></div>
|
||||||
setRelatedProducts((prev) =>
|
<div className="h-8 bg-gray-100 animate-pulse rounded w-3/4"></div>
|
||||||
prev.map((product) =>
|
<div className="h-6 bg-gray-100 animate-pulse rounded w-1/4"></div>
|
||||||
product.id === id ? { ...product, liked: false } : product,
|
<div className="h-4 bg-gray-100 animate-pulse rounded w-full"></div>
|
||||||
),
|
<div className="h-4 bg-gray-100 animate-pulse rounded w-5/6"></div>
|
||||||
|
<div className="flex gap-4 mt-4">
|
||||||
|
<div className="h-10 bg-gray-100 animate-pulse rounded flex-1"></div>
|
||||||
|
<div className="h-10 bg-gray-100 animate-pulse rounded w-12"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
};
|
}
|
||||||
|
|
||||||
const handleLiked = (id: number) => {
|
|
||||||
setRelatedProducts((prev) =>
|
|
||||||
prev.map((product) =>
|
|
||||||
product.id === id ? { ...product, liked: true } : product,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="custom-container pb-5">
|
<div className="custom-container pb-5">
|
||||||
@@ -143,26 +152,32 @@ const ProductDetail = () => {
|
|||||||
<div className="bg-white rounded-lg shadow-md p-6 mb-8">
|
<div className="bg-white rounded-lg shadow-md p-6 mb-8">
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
|
||||||
<div>
|
<div>
|
||||||
<div className="relative bg-gray-100 rounded-lg overflow-hidden mb-4">
|
<div className="relative rounded-lg overflow-hidden mb-4">
|
||||||
<Image
|
{data && (
|
||||||
width={500}
|
<Image
|
||||||
height={500}
|
width={500}
|
||||||
src={product.images[selectedImage] || '/placeholder.svg'}
|
height={500}
|
||||||
alt={product.name}
|
src={
|
||||||
className="w-full h-full object-cover"
|
data.images.length > 0
|
||||||
/>
|
? data.images[selectedImage].image
|
||||||
{product.discount > 0 && (
|
: data.image || '/placeholder.svg'
|
||||||
<div className="absolute top-4 left-4 bg-red-500 text-white px-3 py-1 rounded-full text-sm font-semibold">
|
}
|
||||||
-{product.discount}%
|
alt={data.name}
|
||||||
</div>
|
className="w-full h-[400px] object-contain"
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
{!product.inStock && (
|
{/* {products.discount > 0 && (
|
||||||
|
<div className="absolute top-4 left-4 bg-red-500 text-white px-3 py-1 rounded-full text-sm font-semibold">
|
||||||
|
-{products.discount}%
|
||||||
|
</div>
|
||||||
|
)} */}
|
||||||
|
{/* {!products.inStock && (
|
||||||
<div className="absolute inset-0 bg-black bg-opacity-50 flex items-center justify-center">
|
<div className="absolute inset-0 bg-black bg-opacity-50 flex items-center justify-center">
|
||||||
<span className="text-white text-xl font-bold">
|
<span className="text-white text-xl font-bold">
|
||||||
Mavjud emas
|
Mavjud emas
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)} */}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Carousel
|
<Carousel
|
||||||
@@ -173,29 +188,53 @@ const ProductDetail = () => {
|
|||||||
className="w-full"
|
className="w-full"
|
||||||
>
|
>
|
||||||
<CarouselContent className="-ml-2 pr-[15%] sm:pr-0">
|
<CarouselContent className="-ml-2 pr-[15%] sm:pr-0">
|
||||||
{product.images.map((img, index) => (
|
{data && data.images.length > 0 ? (
|
||||||
<CarouselItem
|
data.images.map((img, index) => (
|
||||||
key={index}
|
<CarouselItem
|
||||||
className="pl-2 basis-1/3 sm:basis-1/4 md:basis-1/5 lg:basis-1/6"
|
key={img.id}
|
||||||
>
|
className="pl-2 basis-1/3 sm:basis-1/4 md:basis-1/5 lg:basis-1/6"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
onClick={() => setSelectedImage(index)}
|
||||||
|
className={`aspect-square w-full rounded-lg border-2 overflow-hidden transition ${
|
||||||
|
selectedImage === index
|
||||||
|
? 'border-blue-500'
|
||||||
|
: 'border-gray-200 hover:border-blue-300'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<Image
|
||||||
|
src={
|
||||||
|
img.image.includes(BASE_URL)
|
||||||
|
? img.image
|
||||||
|
: BASE_URL + img.image || '/placeholder.svg'
|
||||||
|
}
|
||||||
|
alt={BASE_URL + data?.image}
|
||||||
|
width={150}
|
||||||
|
height={150}
|
||||||
|
className="w-full h-full object-contain"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</CarouselItem>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<CarouselItem className="pl-2 basis-1/3 sm:basis-1/4 md:basis-1/5 lg:basis-1/6">
|
||||||
<button
|
<button
|
||||||
onClick={() => setSelectedImage(index)}
|
className={`aspect-square w-full rounded-lg border-2 overflow-hidden transition ${'border-blue-500'}`}
|
||||||
className={`aspect-square w-full rounded-lg border-2 overflow-hidden transition ${
|
|
||||||
selectedImage === index
|
|
||||||
? 'border-blue-500'
|
|
||||||
: 'border-gray-200 hover:border-blue-300'
|
|
||||||
}`}
|
|
||||||
>
|
>
|
||||||
<Image
|
<Image
|
||||||
src={img || '/placeholder.svg'}
|
src={
|
||||||
alt={`thumb-${index}`}
|
data?.image.includes(BASE_URL)
|
||||||
|
? data.image
|
||||||
|
: BASE_URL + data?.image || '/placeholder.svg'
|
||||||
|
}
|
||||||
|
alt={BASE_URL + data?.image}
|
||||||
width={150}
|
width={150}
|
||||||
height={150}
|
height={150}
|
||||||
className="w-full h-full object-contain"
|
className="w-full h-full object-contain"
|
||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
</CarouselItem>
|
</CarouselItem>
|
||||||
))}
|
)}
|
||||||
</CarouselContent>
|
</CarouselContent>
|
||||||
</Carousel>
|
</Carousel>
|
||||||
</div>
|
</div>
|
||||||
@@ -203,83 +242,94 @@ const ProductDetail = () => {
|
|||||||
{/* Product Info */}
|
{/* Product Info */}
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-3xl font-bold text-gray-800 mb-2">
|
<h1 className="text-3xl font-bold text-gray-800 mb-2">
|
||||||
{product.name}
|
{data?.name}
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
{/* Rating */}
|
{/* Rating */}
|
||||||
<div className="flex items-center gap-2 mb-4">
|
{/* <div className="flex items-center gap-2 mb-4">
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
{[...Array(5)].map((_, i) => (
|
{[...Array(5)].map((_, i) => (
|
||||||
<Star
|
<Star
|
||||||
key={i}
|
key={i}
|
||||||
className={`w-5 h-5 ${
|
className={`w-5 h-5 ${
|
||||||
i < Math.floor(product.rating)
|
i < Math.floor(products.rating)
|
||||||
? 'fill-yellow-400 text-yellow-400'
|
? 'fill-yellow-400 text-yellow-400'
|
||||||
: 'text-gray-300'
|
: 'text-gray-300'
|
||||||
}`}
|
}`}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
<span className="text-gray-600">{product.rating}</span>
|
<span className="text-gray-600">{products.rating}</span>
|
||||||
</div>
|
</div> */}
|
||||||
|
|
||||||
{/* Price */}
|
{/* Price */}
|
||||||
<div className="mb-6">
|
<div className="mb-6">
|
||||||
<div className="flex items-center gap-3 max-lg:flex-col max-lg:items-start">
|
<div className="flex items-center gap-3 max-lg:flex-col max-lg:items-start">
|
||||||
<span className="text-4xl font-bold text-blue-600">
|
<span className="text-4xl font-bold text-blue-600">
|
||||||
{product.price.toLocaleString()} {"so'm"}
|
{data && formatPrice(data.price, true)}
|
||||||
</span>
|
</span>
|
||||||
{product.oldPrice && (
|
{/* {products.oldPrice && (
|
||||||
<span className="text-xl text-gray-400 line-through">
|
<span className="text-xl text-gray-400 line-through">
|
||||||
{product.oldPrice.toLocaleString()} {"so'm"}
|
{products.oldPrice.toLocaleString()} {"so'm"}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)} */}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Description */}
|
{/* Description */}
|
||||||
<p className="text-gray-600 mb-6">{product.description}</p>
|
<p className="text-gray-600 mb-6">{data?.description}</p>
|
||||||
|
|
||||||
{/* Brand and Category */}
|
{/* Brand and Category */}
|
||||||
<div className="grid grid-cols-2 gap-4 mb-6 max-md:grid-cols-1">
|
{/* <div className="grid grid-cols-2 gap-4 mb-6 max-md:grid-cols-1">
|
||||||
<div>
|
<div>
|
||||||
<span className="text-gray-500">Brand:</span>
|
<span className="text-gray-500">Brand:</span>
|
||||||
<p className="font-semibold">{product.brand}</p>
|
<p className="font-semibold">{products.brand}</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<span className="text-gray-500">Kategoriya:</span>
|
<span className="text-gray-500">Kategoriya:</span>
|
||||||
<p className="font-semibold">{product.category}</p>
|
<p className="font-semibold">{products.category}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div> */}
|
||||||
|
|
||||||
{/* Quantity Selector */}
|
{/* Quantity Selector */}
|
||||||
<div className="mb-6">
|
<div className="mb-6">
|
||||||
<label className="text-gray-700 font-medium mb-2 block">
|
<label className="text-gray-700 font-medium mb-2 block">
|
||||||
Miqdor:
|
{t('Miqdor')}:
|
||||||
</label>
|
</label>
|
||||||
<div className="flex items-center gap-4 max-lg:flex-col max-lg:items-start">
|
<div className="flex items-center gap-4 max-lg:flex-col max-lg:items-start">
|
||||||
<div className="flex items-center border border-gray-300 rounded-lg">
|
<div className="flex items-center border border-gray-300 rounded-lg">
|
||||||
<button
|
<button
|
||||||
onClick={() => handleQuantityChange('decrease')}
|
onClick={() => handleQuantityChange('decrease')}
|
||||||
className="p-3 hover:bg-gray-100 transition"
|
className="p-3 hover:bg-gray-100 transition rounded-lg"
|
||||||
disabled={quantity <= 1}
|
disabled={quantity <= 1}
|
||||||
>
|
>
|
||||||
<Minus className="w-5 h-5" />
|
<Minus className="w-5 h-5" />
|
||||||
</button>
|
</button>
|
||||||
<span className="px-6 font-semibold text-lg">
|
<Input
|
||||||
{quantity}
|
value={quantity}
|
||||||
</span>
|
onChange={(e) => {
|
||||||
|
const v = e.target.value;
|
||||||
|
|
||||||
|
if (!/^\d*$/.test(v)) return;
|
||||||
|
|
||||||
|
const num = Number(v);
|
||||||
|
|
||||||
|
setQuantity(num);
|
||||||
|
}}
|
||||||
|
inputMode="numeric"
|
||||||
|
className="w-14 h-12 border-none text-center text-sm !p-0 focus-visible:ring-0"
|
||||||
|
/>
|
||||||
<button
|
<button
|
||||||
onClick={() => handleQuantityChange('increase')}
|
onClick={() => handleQuantityChange('increase')}
|
||||||
className="p-3 hover:bg-gray-100 transition"
|
className="p-3 hover:bg-gray-100 transition rounded-lg"
|
||||||
>
|
>
|
||||||
<Plus className="w-5 h-5" />
|
<Plus className="w-5 h-5" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<span className="text-gray-600">
|
<span className="text-gray-600">
|
||||||
Jami:{' '}
|
{t('Jami')}:{' '}
|
||||||
<span className="font-bold text-lg">
|
<span className="font-bold text-lg">
|
||||||
{(product.price * quantity).toLocaleString()} {"so'm"}
|
{data && formatPrice(data.price * quantity, true)}
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -288,49 +338,74 @@ const ProductDetail = () => {
|
|||||||
{/* Action Buttons */}
|
{/* Action Buttons */}
|
||||||
<div className="flex gap-4 mb-6">
|
<div className="flex gap-4 mb-6">
|
||||||
<button
|
<button
|
||||||
onClick={addToCart}
|
onClick={(e) => {
|
||||||
disabled={!product.inStock}
|
e.stopPropagation();
|
||||||
className={`flex-1 py-4 rounded-lg cursor-pointer font-semibold text-white flex items-center justify-center gap-2 transition ${
|
const cart = cartItems?.data.cart_item.find(
|
||||||
product.inStock
|
(e) => e.product_id === data?.id,
|
||||||
? 'bg-green-600 hover:bg-green-700'
|
);
|
||||||
: 'bg-gray-400 cursor-not-allowed'
|
console.log(cart);
|
||||||
}`}
|
|
||||||
|
if (cart && data) {
|
||||||
|
updateCartItem({
|
||||||
|
body: {
|
||||||
|
quantity: quantity,
|
||||||
|
},
|
||||||
|
cart_item_id: cart.id,
|
||||||
|
});
|
||||||
|
} else if (!cart && data) {
|
||||||
|
mutate({
|
||||||
|
product: data.id,
|
||||||
|
cart: cart_id!,
|
||||||
|
quantity: quantity,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className={`flex-1 py-4 rounded-lg cursor-pointer font-semibold text-white flex items-center justify-center gap-2 transition ${'bg-green-600 hover:bg-green-700'}`}
|
||||||
>
|
>
|
||||||
<ShoppingCart className="w-5 h-5" />
|
<ShoppingCart className="w-5 h-5" />
|
||||||
{'Savatga'}
|
{t('Savatga')}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => setLiked(!liked)}
|
onClick={() => {
|
||||||
|
if (product) {
|
||||||
|
favouriteMutation.mutate(product.toString());
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
disabled={favouriteMutation.isPending}
|
||||||
className={`p-4 rounded-lg border-2 transition ${
|
className={`p-4 rounded-lg border-2 transition ${
|
||||||
liked
|
data?.liked
|
||||||
? 'border-red-500 bg-red-50'
|
? 'border-red-500 bg-red-50'
|
||||||
: 'border-gray-300 hover:border-red-500'
|
: 'border-gray-300 hover:border-red-500'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<Heart
|
<Heart
|
||||||
className={`w-6 h-6 ${liked ? 'fill-red-500 text-red-500' : 'text-gray-600'}`}
|
className={`w-6 h-6 ${
|
||||||
|
data?.liked
|
||||||
|
? 'fill-red-500 text-red-500'
|
||||||
|
: 'text-gray-600'
|
||||||
|
}`}
|
||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Features */}
|
{/* Features */}
|
||||||
<div className="grid grid-cols-3 gap-4 border-t pt-6">
|
<div className="grid grid-cols-2 gap-4 border-t pt-6">
|
||||||
<div className="flex flex-col items-center text-center">
|
<div className="flex flex-col items-center text-center">
|
||||||
<Truck className="w-8 h-8 text-blue-600 mb-2" />
|
<Truck className="w-8 h-8 text-blue-600 mb-2" />
|
||||||
<span className="text-sm text-gray-600">
|
<span className="text-sm text-gray-600">
|
||||||
Bepul yetkazib berish
|
{t('Bepul yetkazib berish')}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col items-center text-center">
|
<div className="flex flex-col items-center text-center">
|
||||||
<Shield className="w-8 h-8 text-blue-600 mb-2" />
|
<Shield className="w-8 h-8 text-blue-600 mb-2" />
|
||||||
<span className="text-sm text-gray-600">Kafolat</span>
|
<span className="text-sm text-gray-600">{t('Kafolat')}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col items-center text-center">
|
{/* <div className="flex flex-col items-center text-center">
|
||||||
<RotateCcw className="w-8 h-8 text-blue-600 mb-2" />
|
<RotateCcw className="w-8 h-8 text-blue-600 mb-2" />
|
||||||
<span className="text-sm text-gray-600">
|
<span className="text-sm text-gray-600">
|
||||||
14 kun qaytarish
|
14 kun qaytarish
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div> */}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -338,37 +413,72 @@ const ProductDetail = () => {
|
|||||||
|
|
||||||
{/* Specifications */}
|
{/* Specifications */}
|
||||||
<div className="bg-white rounded-lg shadow-md p-6 mb-8">
|
<div className="bg-white rounded-lg shadow-md p-6 mb-8">
|
||||||
<h2 className="text-2xl font-bold mb-4">Xususiyatlari</h2>
|
<h2 className="text-2xl font-bold mb-4">{t('Xususiyatlari')}</h2>
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
{Object.entries(product.specifications).map(([key, value]) => (
|
<div className="flex justify-between border-b pb-2 gap-4">
|
||||||
<div
|
<span className="text-gray-600">{t('Qadoq turi')}:</span>
|
||||||
key={key}
|
<span className="font-semibold text-right">
|
||||||
className="flex justify-between border-b pb-2 gap-4"
|
{data?.unity.name}
|
||||||
>
|
</span>
|
||||||
<span className="text-gray-600">{key}:</span>
|
</div>
|
||||||
<span className="font-semibold text-right">{value}</span>
|
{data?.brand && (
|
||||||
|
<div className="flex justify-between border-b pb-2 gap-4">
|
||||||
|
<span className="text-gray-600">{t('Brandi')}:</span>
|
||||||
|
<span className="font-semibold text-right">{data?.brand}</span>
|
||||||
</div>
|
</div>
|
||||||
))}
|
)}
|
||||||
|
{data?.manufacturer && (
|
||||||
|
<div className="flex justify-between border-b pb-2 gap-4">
|
||||||
|
<span className="text-gray-600">
|
||||||
|
{t('Ishlab chiqaruvchi')}:
|
||||||
|
</span>
|
||||||
|
<span className="font-semibold text-right">
|
||||||
|
{data?.manufacturer}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{data?.volume && (
|
||||||
|
<div className="flex justify-between border-b pb-2 gap-4">
|
||||||
|
<span className="text-gray-600">{t('Hajmi')}:</span>
|
||||||
|
<span className="font-semibold text-right">{data?.volume}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Related Products */}
|
{/* Related Products */}
|
||||||
<div className="bg-white rounded-lg shadow-md p-6">
|
<div className="bg-white rounded-lg shadow-md p-6">
|
||||||
<h2 className="text-2xl font-bold mb-6">{"O'xshash mahsulotlar"}</h2>
|
<h2 className="text-2xl font-bold mb-6">
|
||||||
|
{t("O'xshash mahsulotlar")}
|
||||||
|
</h2>
|
||||||
<Carousel className="w-full">
|
<Carousel className="w-full">
|
||||||
<CarouselContent className="pr-[12%] sm:pr-0">
|
<CarouselContent className="pr-[12%] sm:pr-0">
|
||||||
{relatedProducts.slice(0, 12).map((product) => (
|
{proLoad &&
|
||||||
<CarouselItem
|
Array.from({ length: 6 }).map((__, index) => (
|
||||||
key={product.id}
|
<CarouselItem
|
||||||
className="basis-1/2 sm:basis-1/3 md:basis-1/4 lg:basis-1/6 pb-2"
|
key={index}
|
||||||
>
|
className="basis-1/2 sm:basis-1/3 md:basis-1/4 lg:basis-1/5 xl:basis-1/6 pb-2"
|
||||||
<ProductCard
|
>
|
||||||
product={product}
|
<Card className="p-3 space-y-3 rounded-xl">
|
||||||
handleRemove={handleRemove}
|
<Skeleton className="h-40 sm:h-48 md:h-56 w-full rounded-lg" />
|
||||||
handleLiked={handleLiked}
|
<Skeleton className="h-4 w-3/4" />
|
||||||
/>
|
<Skeleton className="h-4 w-1/2" />
|
||||||
</CarouselItem>
|
<Skeleton className="h-10 w-full rounded-lg" />
|
||||||
))}
|
</Card>
|
||||||
|
</CarouselItem>
|
||||||
|
))}
|
||||||
|
{recomendation &&
|
||||||
|
!proLoad &&
|
||||||
|
recomendation
|
||||||
|
.filter((product) => product.is_active)
|
||||||
|
.map((product) => (
|
||||||
|
<CarouselItem
|
||||||
|
key={product.id}
|
||||||
|
className="basis-1/2 sm:basis-1/3 md:basis-1/3 lg:basis-1/5 xl:basis-1/6 pb-2"
|
||||||
|
>
|
||||||
|
<ProductCard product={product} />
|
||||||
|
</CarouselItem>
|
||||||
|
))}
|
||||||
</CarouselContent>
|
</CarouselContent>
|
||||||
|
|
||||||
<CarouselPrevious className="hidden lg:flex -top-12 right-12 w-9 h-9 bg-blue-600 text-white border-0" />
|
<CarouselPrevious className="hidden lg:flex -top-12 right-12 w-9 h-9 bg-blue-600 text-white border-0" />
|
||||||
|
|||||||
64
src/features/profile/lib/api.ts
Normal file
64
src/features/profile/lib/api.ts
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
import httpClient from '@/shared/config/api/httpClient';
|
||||||
|
import { API_URLS } from '@/shared/config/api/URLs';
|
||||||
|
import { AxiosResponse } from 'axios';
|
||||||
|
|
||||||
|
export interface OrderList {
|
||||||
|
count: number;
|
||||||
|
next: string;
|
||||||
|
previous: string;
|
||||||
|
results: OrderListRes[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface OrderListRes {
|
||||||
|
id: string;
|
||||||
|
order_number: number;
|
||||||
|
status: 'NEW' | 'DONE';
|
||||||
|
total_price: number;
|
||||||
|
payment_type: 'CASH' | 'ACCOUNT_NUMBER';
|
||||||
|
delivery_type: 'YandexGo' | 'DELIVERY_COURIES' | 'PICKUP';
|
||||||
|
delivery_price: number;
|
||||||
|
contact_number: string;
|
||||||
|
comment: string;
|
||||||
|
name: string;
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
id: string;
|
||||||
|
product: {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
image: string;
|
||||||
|
price: number;
|
||||||
|
description: string;
|
||||||
|
unity: string;
|
||||||
|
min_quantity: number;
|
||||||
|
is_active: true;
|
||||||
|
liked: string;
|
||||||
|
brand: string;
|
||||||
|
return_date: string;
|
||||||
|
expires_date: string;
|
||||||
|
manufacturer: string;
|
||||||
|
volume: string;
|
||||||
|
images: [
|
||||||
|
{
|
||||||
|
id: string;
|
||||||
|
image: string;
|
||||||
|
},
|
||||||
|
];
|
||||||
|
};
|
||||||
|
price: number;
|
||||||
|
quantity: number;
|
||||||
|
created_at: string;
|
||||||
|
},
|
||||||
|
];
|
||||||
|
created_at: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const order_api = {
|
||||||
|
async list(params: {
|
||||||
|
page: number;
|
||||||
|
page_size: number;
|
||||||
|
}): Promise<AxiosResponse<OrderList>> {
|
||||||
|
const res = await httpClient.get(API_URLS.OrderList, { params });
|
||||||
|
return res;
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -1,46 +1,29 @@
|
|||||||
import { Button } from '@/shared/ui/button';
|
import { Button } from '@/shared/ui/button';
|
||||||
import { Card, CardContent } from '@/shared/ui/card';
|
import { Card, CardContent } from '@/shared/ui/card';
|
||||||
import { Tabs, TabsList, TabsTrigger } from '@/shared/ui/tabs';
|
import { useQuery } from '@tanstack/react-query';
|
||||||
import { Calendar, CheckCircle, Clock, RefreshCw } from 'lucide-react';
|
import { Calendar, CheckCircle, Clock, RefreshCw } from 'lucide-react';
|
||||||
|
import { useTranslations } from 'next-intl';
|
||||||
import Image from 'next/image';
|
import Image from 'next/image';
|
||||||
import { useState } from 'react';
|
import { order_api } from '../lib/api';
|
||||||
import { orders } from '../lib/data';
|
import { orders } from '../lib/data';
|
||||||
|
|
||||||
const HistoryTabs = () => {
|
const HistoryTabs = () => {
|
||||||
const [historyTab, setHistoryTab] = useState('all');
|
const t = useTranslations();
|
||||||
|
|
||||||
|
const { data } = useQuery({
|
||||||
|
queryKey: ['order_list'],
|
||||||
|
queryFn: () => order_api.list({ page: 1, page_size: 1 }),
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(data);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="flex items-center justify-between mb-4 md:mb-6">
|
<div className="flex items-center justify-between mb-4 md:mb-6">
|
||||||
<h2 className="text-xl md:text-2xl font-bold text-foreground">Tarix</h2>
|
<h2 className="text-xl md:text-2xl font-bold text-foreground">
|
||||||
|
{t('Tarix')}
|
||||||
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
<Tabs
|
|
||||||
value={historyTab}
|
|
||||||
onValueChange={setHistoryTab}
|
|
||||||
className="mb-4 md:mb-6"
|
|
||||||
>
|
|
||||||
<TabsList className="bg-slate-100 w-full grid grid-cols-3 h-auto p-1">
|
|
||||||
<TabsTrigger
|
|
||||||
value="all"
|
|
||||||
className="data-[state=active]:bg-white text-xs md:text-sm py-2"
|
|
||||||
>
|
|
||||||
Barchasi
|
|
||||||
</TabsTrigger>
|
|
||||||
<TabsTrigger
|
|
||||||
value="week"
|
|
||||||
className="data-[state=active]:bg-white text-xs md:text-sm py-2"
|
|
||||||
>
|
|
||||||
Bu hafta
|
|
||||||
</TabsTrigger>
|
|
||||||
<TabsTrigger
|
|
||||||
value="month"
|
|
||||||
className="data-[state=active]:bg-white text-xs md:text-sm py-2"
|
|
||||||
>
|
|
||||||
Bu oy
|
|
||||||
</TabsTrigger>
|
|
||||||
</TabsList>
|
|
||||||
</Tabs>
|
|
||||||
|
|
||||||
<div className="space-y-3 md:space-y-4">
|
<div className="space-y-3 md:space-y-4">
|
||||||
{orders
|
{orders
|
||||||
.filter((o) => o.status === 'delivered')
|
.filter((o) => o.status === 'delivered')
|
||||||
@@ -100,7 +83,7 @@ const HistoryTabs = () => {
|
|||||||
className="bg-transparent gap-1 md:gap-2 text-xs md:text-sm h-8 md:h-9 px-2 md:px-3"
|
className="bg-transparent gap-1 md:gap-2 text-xs md:text-sm h-8 md:h-9 px-2 md:px-3"
|
||||||
>
|
>
|
||||||
<RefreshCw className="w-3 h-3 md:w-4 md:h-4" />
|
<RefreshCw className="w-3 h-3 md:w-4 md:h-4" />
|
||||||
Qayta
|
{t('Qayta')}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
|
|||||||
@@ -2,12 +2,14 @@ import { Badge } from '@/shared/ui/badge';
|
|||||||
import { Card, CardContent } from '@/shared/ui/card';
|
import { Card, CardContent } from '@/shared/ui/card';
|
||||||
import { Tabs, TabsList, TabsTrigger } from '@/shared/ui/tabs';
|
import { Tabs, TabsList, TabsTrigger } from '@/shared/ui/tabs';
|
||||||
import { CheckCircle, MapPin, Package, Truck } from 'lucide-react';
|
import { CheckCircle, MapPin, Package, Truck } from 'lucide-react';
|
||||||
|
import { useTranslations } from 'next-intl';
|
||||||
import Image from 'next/image';
|
import Image from 'next/image';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { orders } from '../lib/data';
|
import { orders } from '../lib/data';
|
||||||
|
|
||||||
const Orders = () => {
|
const Orders = () => {
|
||||||
const [ordersTab, setOrdersTab] = useState('active');
|
const [ordersTab, setOrdersTab] = useState('active');
|
||||||
|
const t = useTranslations();
|
||||||
|
|
||||||
const getStatusInfo = (status: string) => {
|
const getStatusInfo = (status: string) => {
|
||||||
const statusMap: Record<
|
const statusMap: Record<
|
||||||
@@ -61,7 +63,7 @@ const Orders = () => {
|
|||||||
<div>
|
<div>
|
||||||
<div className="flex items-center justify-between mb-4 md:mb-6">
|
<div className="flex items-center justify-between mb-4 md:mb-6">
|
||||||
<h2 className="text-xl md:text-2xl font-bold text-foreground">
|
<h2 className="text-xl md:text-2xl font-bold text-foreground">
|
||||||
Buyurtmalar
|
{t('Buyurtmalar')}
|
||||||
</h2>
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -75,19 +77,21 @@ const Orders = () => {
|
|||||||
value="active"
|
value="active"
|
||||||
className="data-[state=active]:bg-white text-xs md:text-sm py-2"
|
className="data-[state=active]:bg-white text-xs md:text-sm py-2"
|
||||||
>
|
>
|
||||||
Faol ({orders.filter((o) => o.status !== 'delivered').length})
|
{t('Faol')} ({orders.filter((o) => o.status !== 'delivered').length}
|
||||||
|
)
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
<TabsTrigger
|
<TabsTrigger
|
||||||
value="completed"
|
value="completed"
|
||||||
className="data-[state=active]:bg-white text-xs md:text-sm py-2"
|
className="data-[state=active]:bg-white text-xs md:text-sm py-2"
|
||||||
>
|
>
|
||||||
Tugadi ({orders.filter((o) => o.status === 'delivered').length})
|
{t('Tugadi')} (
|
||||||
|
{orders.filter((o) => o.status === 'delivered').length})
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
<TabsTrigger
|
<TabsTrigger
|
||||||
value="all"
|
value="all"
|
||||||
className="data-[state=active]:bg-white text-xs md:text-sm py-2"
|
className="data-[state=active]:bg-white text-xs md:text-sm py-2"
|
||||||
>
|
>
|
||||||
Barcha ({orders.length})
|
{t('Barchasi')} ({orders.length})
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
</TabsList>
|
</TabsList>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
@@ -128,7 +132,7 @@ const Orders = () => {
|
|||||||
<Badge
|
<Badge
|
||||||
className={`${statusInfo.bgColor} ${statusInfo.color} border-0 text-xs`}
|
className={`${statusInfo.bgColor} ${statusInfo.color} border-0 text-xs`}
|
||||||
>
|
>
|
||||||
{statusInfo.text}
|
{t(statusInfo.text)}
|
||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -168,7 +172,8 @@ const Orders = () => {
|
|||||||
{"so'm"}
|
{"so'm"}
|
||||||
</p>
|
</p>
|
||||||
<p className="text-[10px] md:text-xs text-muted-foreground">
|
<p className="text-[10px] md:text-xs text-muted-foreground">
|
||||||
Yetkazish: {order.deliveryFee.toLocaleString()} {"so'm"}
|
{t('Yetkazish')}: {order.deliveryFee.toLocaleString()}{' '}
|
||||||
|
{"so'm"}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,264 +1,34 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { PartnershipForm } from '@/features/about/ui/AboutPage';
|
|
||||||
import Faq from '@/features/faq/ui/Faq';
|
|
||||||
import Favourite from '@/features/favourite/ui/Favourite';
|
|
||||||
import { useRouter } from '@/shared/config/i18n/navigation';
|
import { useRouter } from '@/shared/config/i18n/navigation';
|
||||||
|
import { getMe, removeToken } from '@/shared/lib/token';
|
||||||
import { Avatar, AvatarFallback, AvatarImage } from '@/shared/ui/avatar';
|
import { Avatar, AvatarFallback, AvatarImage } from '@/shared/ui/avatar';
|
||||||
import { Badge } from '@/shared/ui/badge';
|
|
||||||
import { Button } from '@/shared/ui/button';
|
import { Button } from '@/shared/ui/button';
|
||||||
import { Card, CardContent } from '@/shared/ui/card';
|
import { useQueryClient } from '@tanstack/react-query';
|
||||||
import {
|
import { Headset, Home, LogOut } from 'lucide-react';
|
||||||
CheckCircle,
|
import { useTranslations } from 'next-intl';
|
||||||
ChevronRight,
|
|
||||||
History,
|
|
||||||
Home,
|
|
||||||
LogOut,
|
|
||||||
MapPin,
|
|
||||||
Package,
|
|
||||||
RefreshCw,
|
|
||||||
ShoppingBag,
|
|
||||||
Truck,
|
|
||||||
} from 'lucide-react';
|
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { orders, user } from '../lib/data';
|
|
||||||
import HistoryTabs from './History';
|
import HistoryTabs from './History';
|
||||||
import Orders from './Orders';
|
|
||||||
import CustomerSupport from './Support';
|
|
||||||
|
|
||||||
const Profile = () => {
|
const Profile = () => {
|
||||||
const [activeSection, setActiveSection] = useState('overview');
|
const [activeSection, setActiveSection] = useState('overview');
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const t = useTranslations();
|
||||||
const getStatusInfo = (status: string) => {
|
const queryClient = useQueryClient();
|
||||||
const statusMap: Record<
|
const user = getMe();
|
||||||
string,
|
|
||||||
{ text: string; color: string; bgColor: string }
|
|
||||||
> = {
|
|
||||||
inTransit: {
|
|
||||||
text: "Yo'lda",
|
|
||||||
color: 'text-blue-600',
|
|
||||||
bgColor: 'bg-blue-100',
|
|
||||||
},
|
|
||||||
atPickup: {
|
|
||||||
text: 'Punktda',
|
|
||||||
color: 'text-amber-600',
|
|
||||||
bgColor: 'bg-amber-100',
|
|
||||||
},
|
|
||||||
delivered: {
|
|
||||||
text: 'Yetkazildi',
|
|
||||||
color: 'text-emerald-600',
|
|
||||||
bgColor: 'bg-emerald-100',
|
|
||||||
},
|
|
||||||
cancelled: {
|
|
||||||
text: 'Bekor qilindi',
|
|
||||||
color: 'text-red-600',
|
|
||||||
bgColor: 'bg-red-100',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
return (
|
|
||||||
statusMap[status] || {
|
|
||||||
text: status,
|
|
||||||
color: 'text-muted-foreground',
|
|
||||||
bgColor: 'bg-muted',
|
|
||||||
}
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const getStatusIcon = (status: string) => {
|
|
||||||
switch (status) {
|
|
||||||
case 'inTransit':
|
|
||||||
return <Truck className="w-4 h-4" />;
|
|
||||||
case 'atPickup':
|
|
||||||
return <MapPin className="w-4 h-4" />;
|
|
||||||
case 'delivered':
|
|
||||||
return <CheckCircle className="w-4 h-4" />;
|
|
||||||
default:
|
|
||||||
return <Package className="w-4 h-4" />;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const menuItems = [
|
const menuItems = [
|
||||||
{ id: 'overview', label: 'Umumiy', icon: Home },
|
{ id: 'overview', label: 'Umumiy', icon: Home },
|
||||||
{ id: 'orders', label: 'Buyurtmalar', icon: ShoppingBag },
|
{ id: 'support', label: "Qo'llab-quvatlash", icon: Headset },
|
||||||
{ id: 'history', label: 'Tarix', icon: History },
|
|
||||||
];
|
];
|
||||||
|
|
||||||
const renderContent = () => {
|
const renderContent = () => {
|
||||||
switch (activeSection) {
|
switch (activeSection) {
|
||||||
case 'orders':
|
|
||||||
return <Orders />;
|
|
||||||
|
|
||||||
case 'history':
|
|
||||||
return <HistoryTabs />;
|
|
||||||
|
|
||||||
case 'favorites':
|
|
||||||
return <Favourite />;
|
|
||||||
|
|
||||||
case 'agency':
|
|
||||||
return <PartnershipForm />;
|
|
||||||
|
|
||||||
case 'faq':
|
|
||||||
return <Faq />;
|
|
||||||
case 'support':
|
case 'support':
|
||||||
return <CustomerSupport />;
|
router.push('https://t.me/web_app_0515_bot');
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return (
|
return <HistoryTabs />;
|
||||||
<>
|
|
||||||
{/* Active Orders Section */}
|
|
||||||
<div className="mb-6 md:mb-8">
|
|
||||||
<div className="flex items-center justify-between mb-3 md:mb-4">
|
|
||||||
<h3 className="text-base md:text-lg font-semibold text-foreground">
|
|
||||||
Faol buyurtmalar
|
|
||||||
</h3>
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
className="text-emerald-600 hover:text-emerald-700 h-8 text-xs md:text-sm"
|
|
||||||
onClick={() => setActiveSection('orders')}
|
|
||||||
>
|
|
||||||
Barchasi
|
|
||||||
<ChevronRight className="w-3 h-3 md:w-4 md:h-4 ml-1" />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-3">
|
|
||||||
{orders
|
|
||||||
.filter((o) => o.status !== 'delivered')
|
|
||||||
.slice(0, 2)
|
|
||||||
.map((order) => {
|
|
||||||
const statusInfo = getStatusInfo(order.status);
|
|
||||||
return (
|
|
||||||
<Card
|
|
||||||
key={order.id}
|
|
||||||
className="border-0 shadow-sm hover:shadow-md transition-shadow"
|
|
||||||
>
|
|
||||||
<CardContent className="p-3 md:p-5">
|
|
||||||
<div className="flex items-start justify-between mb-3 md:mb-4">
|
|
||||||
<div className="flex items-center gap-2 md:gap-3">
|
|
||||||
<div
|
|
||||||
className={`w-10 h-10 md:w-12 md:h-12 rounded-xl ${statusInfo.bgColor} flex items-center justify-center shrink-0`}
|
|
||||||
>
|
|
||||||
<span className={statusInfo.color}>
|
|
||||||
{getStatusIcon(order.status)}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<p className="font-semibold text-sm md:text-base text-foreground">
|
|
||||||
{order.id}
|
|
||||||
</p>
|
|
||||||
<p className="text-xs md:text-sm text-muted-foreground">
|
|
||||||
{order.date} • {order.time}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<Badge
|
|
||||||
className={`${statusInfo.bgColor} ${statusInfo.color} border-0 text-xs`}
|
|
||||||
>
|
|
||||||
{statusInfo.text}
|
|
||||||
</Badge>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex flex-wrap gap-1.5 md:gap-2 mb-3 md:mb-4">
|
|
||||||
{order.items.map((item, idx) => (
|
|
||||||
<span
|
|
||||||
key={idx}
|
|
||||||
className="px-2 md:px-3 py-1 bg-slate-100 rounded-full text-xs text-muted-foreground"
|
|
||||||
>
|
|
||||||
{item.name} ×{item.quantity}
|
|
||||||
</span>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex flex-col md:flex-row md:items-center justify-between pt-3 md:pt-4 border-t border-slate-100 gap-2">
|
|
||||||
<div className="flex items-center gap-2 text-xs md:text-sm text-muted-foreground">
|
|
||||||
<MapPin className="w-3 h-3 md:w-4 md:h-4 shrink-0" />
|
|
||||||
<span className="truncate">{order.address}</span>
|
|
||||||
</div>
|
|
||||||
<p className="font-bold text-sm md:text-base text-foreground">
|
|
||||||
{(
|
|
||||||
order.total + order.deliveryFee
|
|
||||||
).toLocaleString()}{' '}
|
|
||||||
{"so'm"}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Order History */}
|
|
||||||
<div>
|
|
||||||
<div className="flex items-center justify-between mb-3 md:mb-4">
|
|
||||||
<h3 className="text-base md:text-lg font-semibold text-foreground">
|
|
||||||
Buyurtmalar tarixi
|
|
||||||
</h3>
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
className="text-emerald-600 hover:text-emerald-700 h-8 text-xs md:text-sm"
|
|
||||||
onClick={() => setActiveSection('history')}
|
|
||||||
>
|
|
||||||
Barchasi{' '}
|
|
||||||
<ChevronRight className="w-3 h-3 md:w-4 md:h-4 ml-1" />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-3">
|
|
||||||
{orders
|
|
||||||
.filter((o) => o.status === 'delivered')
|
|
||||||
.slice(0, 2)
|
|
||||||
.map((order) => (
|
|
||||||
<Card
|
|
||||||
key={order.id}
|
|
||||||
className="border-0 shadow-sm hover:shadow-md transition-shadow"
|
|
||||||
>
|
|
||||||
<CardContent className="p-3 md:p-4">
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<div className="flex items-center gap-3 md:gap-4">
|
|
||||||
<div className="w-10 h-10 md:w-12 md:h-12 rounded-xl bg-emerald-100 flex items-center justify-center shrink-0">
|
|
||||||
<CheckCircle className="w-4 h-4 md:w-5 md:h-5 text-emerald-600" />
|
|
||||||
</div>
|
|
||||||
<div className="min-w-0">
|
|
||||||
<p className="font-semibold text-sm md:text-base text-foreground">
|
|
||||||
{order.id}
|
|
||||||
</p>
|
|
||||||
<p className="text-xs md:text-sm text-muted-foreground truncate">
|
|
||||||
{order.items.map((i) => i.name).join(', ')}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="text-right shrink-0 ml-2">
|
|
||||||
<p className="font-bold text-sm md:text-base text-foreground">
|
|
||||||
{(
|
|
||||||
order.total + order.deliveryFee
|
|
||||||
).toLocaleString()}
|
|
||||||
</p>
|
|
||||||
<p className="text-xs text-muted-foreground">
|
|
||||||
{order.date}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex gap-2 mt-3 md:mt-4">
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
className="flex-1 gap-1 md:gap-2 bg-transparent h-8 md:h-9 text-xs md:text-sm"
|
|
||||||
>
|
|
||||||
<RefreshCw className="w-3 h-3 md:w-4 md:h-4" />
|
|
||||||
Qayta
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -279,7 +49,7 @@ const Profile = () => {
|
|||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<Icon className="w-4 h-4" />
|
<Icon className="w-4 h-4" />
|
||||||
{item.label}
|
{t(item.label)}
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
@@ -289,20 +59,18 @@ const Profile = () => {
|
|||||||
<div>
|
<div>
|
||||||
<div className="flex gap-4 md:gap-6">
|
<div className="flex gap-4 md:gap-6">
|
||||||
{/* Desktop Sidebar - hidden on mobile */}
|
{/* Desktop Sidebar - hidden on mobile */}
|
||||||
<div className="hidden md:block w-80 shrink-0">
|
<div className="hidden lg:block w-80 shrink-0">
|
||||||
<div className="flex items-center gap-4 mb-8">
|
<div className="flex items-center gap-4 mb-8">
|
||||||
<Avatar className="w-14 h-14 ring-2 ring-emerald-500 ring-offset-2 flex items-center justify-center">
|
<Avatar className="w-14 h-14 ring-2 ring-emerald-500 ring-offset-2 flex items-center justify-center">
|
||||||
<AvatarImage
|
<AvatarImage />
|
||||||
src={user.avatar || '/placeholder.svg'}
|
<AvatarFallback className="text-muted-foreground font-semibold">
|
||||||
alt={user.phone}
|
{user?.slice(0, 1).toUpperCase()}
|
||||||
className="h-12 w-12"
|
|
||||||
/>
|
|
||||||
<AvatarFallback className="bg-emerald-500 text-white font-semibold">
|
|
||||||
U
|
|
||||||
</AvatarFallback>
|
</AvatarFallback>
|
||||||
</Avatar>
|
</Avatar>
|
||||||
<div>
|
<div>
|
||||||
<p className="text-sm text-muted-foreground">{user.phone}</p>
|
<p className="text-lg text-muted-foreground font-medium">
|
||||||
|
{user && user.charAt(0).toUpperCase() + user.slice(1)}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -319,7 +87,7 @@ const Profile = () => {
|
|||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<item.icon className="w-5 h-5" />
|
<item.icon className="w-5 h-5" />
|
||||||
{item.label}
|
{t(item.label)}
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
</nav>
|
</nav>
|
||||||
@@ -328,13 +96,16 @@ const Profile = () => {
|
|||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
localStorage.removeItem('user');
|
queryClient.refetchQueries({ queryKey: ['product_list'] });
|
||||||
|
queryClient.refetchQueries({ queryKey: ['favourite_product'] });
|
||||||
|
queryClient.refetchQueries({ queryKey: ['search'] });
|
||||||
|
removeToken();
|
||||||
router.push('/');
|
router.push('/');
|
||||||
}}
|
}}
|
||||||
className="w-full justify-start gap-3 text-red-500 hover:text-red-600 hover:bg-red-50 mt-4"
|
className="w-full justify-start gap-3 text-red-500 hover:text-red-600 hover:bg-red-50 mt-4"
|
||||||
>
|
>
|
||||||
<LogOut className="w-5 h-5" />
|
<LogOut className="w-5 h-5" />
|
||||||
Chiqish
|
{t('Chiqish')}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -343,17 +114,14 @@ const Profile = () => {
|
|||||||
<div className="lg:hidden flex items-center justify-between mb-4 md:mb-6">
|
<div className="lg:hidden flex items-center justify-between mb-4 md:mb-6">
|
||||||
<div className="flex items-center gap-2 md:gap-3">
|
<div className="flex items-center gap-2 md:gap-3">
|
||||||
<Avatar className="w-10 h-10 md:w-12 md:h-12 ring-2 ring-emerald-500 ring-offset-2">
|
<Avatar className="w-10 h-10 md:w-12 md:h-12 ring-2 ring-emerald-500 ring-offset-2">
|
||||||
<AvatarImage
|
<AvatarImage />
|
||||||
src={user.avatar || '/placeholder.svg'}
|
<AvatarFallback className="text-muted-foreground font-semibold">
|
||||||
alt={user.phone}
|
{user?.slice(0, 1).toUpperCase()}
|
||||||
/>
|
|
||||||
<AvatarFallback className="bg-emerald-500 text-white text-sm md:text-base">
|
|
||||||
U
|
|
||||||
</AvatarFallback>
|
</AvatarFallback>
|
||||||
</Avatar>
|
</Avatar>
|
||||||
<div>
|
<div>
|
||||||
<p className="text-xs md:text-sm text-muted-foreground">
|
<p className="text-md md:text-xl text-muted-foreground">
|
||||||
{user.phone}
|
{user && user.charAt(0).toUpperCase() + user.slice(1)}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -361,7 +129,12 @@ const Profile = () => {
|
|||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
localStorage.removeItem('user');
|
queryClient.refetchQueries({ queryKey: ['product_list'] });
|
||||||
|
queryClient.refetchQueries({
|
||||||
|
queryKey: ['favourite_product'],
|
||||||
|
});
|
||||||
|
queryClient.refetchQueries({ queryKey: ['search'] });
|
||||||
|
removeToken();
|
||||||
router.push('/');
|
router.push('/');
|
||||||
}}
|
}}
|
||||||
className="w-9 h-9 md:w-10 md:h-10"
|
className="w-9 h-9 md:w-10 md:h-10"
|
||||||
|
|||||||
@@ -41,7 +41,6 @@ export default function CustomerSupport() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const category = categories.find((c) => c.id === selectedCategory);
|
const category = categories.find((c) => c.id === selectedCategory);
|
||||||
|
|
||||||
if (!category) {
|
if (!category) {
|
||||||
alert('Kategoriya topilmadi');
|
alert('Kategoriya topilmadi');
|
||||||
return;
|
return;
|
||||||
@@ -49,19 +48,27 @@ export default function CustomerSupport() {
|
|||||||
|
|
||||||
const telegramText = `🔔 Yangi murojaat\n\n📋 Kategoriya: ${category.title}\n\n💬 Xabar:\n${message}`;
|
const telegramText = `🔔 Yangi murojaat\n\n📋 Kategoriya: ${category.title}\n\n💬 Xabar:\n${message}`;
|
||||||
|
|
||||||
const telegramUsername = 'web_app_0515_bot';
|
// Foydalanuvchi ID sini oling (masalan, auth orqali)
|
||||||
const telegramUrl = `https://t.me/${telegramUsername}?text=${encodeURIComponent(
|
const userId = '6487794662'; // <-- bu yerda dinamik ID bo'lishi kerak
|
||||||
|
|
||||||
|
// Telegram link bot orqali yuborish
|
||||||
|
const botToken = 'web_app_0515_bot';
|
||||||
|
const telegramUrl = `https://api.telegram.org/bot${botToken}/sendMessage?chat_id=${userId}&text=${encodeURIComponent(
|
||||||
telegramText,
|
telegramText,
|
||||||
)}`;
|
)}`;
|
||||||
|
|
||||||
window.open(telegramUrl, '_blank');
|
fetch(telegramUrl)
|
||||||
|
.then(() => {
|
||||||
setShowSuccess(true);
|
setShowSuccess(true);
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
setShowSuccess(false);
|
setShowSuccess(false);
|
||||||
setSelectedCategory('');
|
setSelectedCategory('');
|
||||||
setMessage('');
|
setMessage('');
|
||||||
}, 3000);
|
}, 3000);
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
alert('Xabar yuborishda xatolik yuz berdi');
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,110 +1,85 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { Input } from '@/shared/ui/input';
|
import { product_api } from '@/shared/config/api/product/api';
|
||||||
import {
|
import {
|
||||||
categories,
|
ProductListResult,
|
||||||
Product,
|
SearchDataPro,
|
||||||
ProductDetail,
|
} from '@/shared/config/api/product/type';
|
||||||
} from '@/widgets/categories/lib/data';
|
import { useRouter } from '@/shared/config/i18n/navigation';
|
||||||
|
import { Input } from '@/shared/ui/input';
|
||||||
import { ProductCard } from '@/widgets/categories/ui/product-card';
|
import { ProductCard } from '@/widgets/categories/ui/product-card';
|
||||||
|
import { useQuery } from '@tanstack/react-query';
|
||||||
import { Search, X } from 'lucide-react';
|
import { Search, X } from 'lucide-react';
|
||||||
import { useRouter, useSearchParams } from 'next/navigation';
|
import { useTranslations } from 'next-intl';
|
||||||
import { useEffect, useMemo, useState } from 'react';
|
import { useSearchParams } from 'next/navigation';
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
const SearchResult: React.FC = () => {
|
const SearchResult = () => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const t = useTranslations();
|
||||||
const searchParams = useSearchParams();
|
const searchParams = useSearchParams();
|
||||||
|
const query = searchParams.get('q') || '';
|
||||||
|
const [searchRes, setSearchRes] = useState<
|
||||||
|
ProductListResult[] | SearchDataPro[] | []
|
||||||
|
>([]);
|
||||||
|
|
||||||
const queryFromUrl = searchParams.get('q') ?? '';
|
const { data: product } = useQuery({
|
||||||
|
queryKey: ['product_list'],
|
||||||
|
queryFn: () => product_api.list({ page: 1, page_size: 12 }),
|
||||||
|
select(data) {
|
||||||
|
return data.data.results;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const [query, setQuery] = useState(queryFromUrl);
|
const { data, isLoading } = useQuery({
|
||||||
const [loading, setLoading] = useState(false);
|
queryKey: ['search', query],
|
||||||
const [results, setResults] = useState<ProductDetail[]>([]);
|
queryFn: () => product_api.search({ search: query, page: 1, page_szie: 5 }),
|
||||||
|
select(data) {
|
||||||
const allProducts = useMemo<ProductDetail[]>(() => {
|
return data.data.products;
|
||||||
return categories.flatMap((category: Product) =>
|
},
|
||||||
category.products.map((product) => ({
|
enabled: !!query,
|
||||||
...product,
|
});
|
||||||
categoryName: category.name,
|
|
||||||
})),
|
|
||||||
);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const recommendedProducts = useMemo<ProductDetail[]>(() => {
|
|
||||||
return allProducts.filter((product) => product.rating >= 4.5).slice(0, 8);
|
|
||||||
}, [allProducts]);
|
|
||||||
|
|
||||||
const handleSearch = (searchQuery: string) => {
|
|
||||||
if (!searchQuery.trim()) return;
|
|
||||||
|
|
||||||
setLoading(true);
|
|
||||||
router.push(`/search?q=${encodeURIComponent(searchQuery)}`);
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
const filtered = allProducts.filter(
|
|
||||||
(product) =>
|
|
||||||
product.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
|
||||||
product.categoryName
|
|
||||||
?.toLowerCase()
|
|
||||||
.includes(searchQuery.toLowerCase()),
|
|
||||||
);
|
|
||||||
|
|
||||||
setResults(filtered);
|
|
||||||
setLoading(false);
|
|
||||||
}, 300);
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (queryFromUrl) {
|
if (data) {
|
||||||
handleSearch(queryFromUrl);
|
setSearchRes(data);
|
||||||
|
} else if (query.length === 0 && product && product.length > 0) {
|
||||||
|
setSearchRes(product);
|
||||||
|
} else {
|
||||||
|
setSearchRes([]);
|
||||||
}
|
}
|
||||||
}, [queryFromUrl]);
|
}, [product, data]);
|
||||||
|
|
||||||
|
const handleSearch = (value: string) => {
|
||||||
|
if (!value.trim()) {
|
||||||
|
router.push('/search');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
router.push(`/search?q=${encodeURIComponent(value)}`);
|
||||||
|
};
|
||||||
|
|
||||||
const clearSearch = () => {
|
const clearSearch = () => {
|
||||||
setQuery('');
|
|
||||||
setResults([]);
|
|
||||||
router.push('/search');
|
router.push('/search');
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleRemove = (id: number) => {
|
|
||||||
setResults((prev) =>
|
|
||||||
prev.map((product) =>
|
|
||||||
product.id === id ? { ...product, liked: false } : product,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleLiked = (id: number) => {
|
|
||||||
setResults((prev) =>
|
|
||||||
prev.map((product) =>
|
|
||||||
product.id === id ? { ...product, liked: true } : product,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="custom-container justify-center items-center h-screen">
|
<div className="custom-container min-h-screen">
|
||||||
<div className="lg:hidden">
|
{/* Search input (mobile) */}
|
||||||
|
<div className="lg:hidden mb-6">
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-5 h-5 text-slate-400" />
|
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-5 h-5 text-slate-400" />
|
||||||
|
|
||||||
<Input
|
<Input
|
||||||
value={query}
|
value={query}
|
||||||
placeholder="Mahsulot nomi"
|
placeholder={t('Mahsulot nomi')}
|
||||||
onChange={(e) => {
|
onChange={(e) => handleSearch(e.target.value)}
|
||||||
setQuery(e.target.value);
|
className="w-full pl-10 pr-10 h-12"
|
||||||
handleSearch(e.target.value);
|
|
||||||
}}
|
|
||||||
onKeyDown={(e) => {
|
|
||||||
if (e.key === 'Enter') handleSearch(query);
|
|
||||||
}}
|
|
||||||
className="w-full border rounded-lg pl-10 pr-10 h-12"
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{query && (
|
{query && (
|
||||||
<button
|
<button
|
||||||
onClick={clearSearch}
|
onClick={clearSearch}
|
||||||
className="absolute right-7 top-1/2 -translate-y-1/2"
|
className="absolute right-3 top-1/2 -translate-y-1/2"
|
||||||
>
|
>
|
||||||
<X />
|
<X />
|
||||||
</button>
|
</button>
|
||||||
@@ -112,43 +87,19 @@ const SearchResult: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="px-4 py-8">
|
{isLoading ? (
|
||||||
{loading ? (
|
<div className="text-center py-20">{t('Yuklanmoqda')}</div>
|
||||||
<div className="text-center py-20">Yuklanmoqda...</div>
|
) : searchRes && searchRes.length > 0 ? (
|
||||||
) : query ? (
|
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-6 gap-4">
|
||||||
results.length ? (
|
{searchRes
|
||||||
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-6 gap-4">
|
.filter((product) => product.is_active)
|
||||||
{results.map((product) => (
|
.map((product) => (
|
||||||
<ProductCard
|
<ProductCard key={product.id} product={product} />
|
||||||
key={product.id}
|
))}
|
||||||
product={product}
|
</div>
|
||||||
handleRemove={handleRemove}
|
) : (
|
||||||
handleLiked={handleLiked}
|
<div className="text-center py-20">{t('Natija topilmadi')}</div>
|
||||||
/>
|
)}
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="text-center py-20">Natija topilmadi</div>
|
|
||||||
)
|
|
||||||
) : (
|
|
||||||
<div className="lg:hidden">
|
|
||||||
<h2 className="text-lg font-semibold mb-4">
|
|
||||||
Tavsiya etilgan mahsulotlar
|
|
||||||
</h2>
|
|
||||||
|
|
||||||
<div className="grid grid-cols-2 gap-4">
|
|
||||||
{recommendedProducts.map((product) => (
|
|
||||||
<ProductCard
|
|
||||||
key={product.id}
|
|
||||||
product={product}
|
|
||||||
handleRemove={handleRemove}
|
|
||||||
handleLiked={handleLiked}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,6 +1,24 @@
|
|||||||
const BASE_URL =
|
export const BASE_URL =
|
||||||
process.env.NEXT_PUBLIC_API_URL || 'https://jsonplaceholder.typicode.com';
|
process.env.NEXT_PUBLIC_API_URL || 'https://api.gastro.felixits.uz';
|
||||||
|
|
||||||
const ENDP_POSTS = '/posts/';
|
export const API_V = '/api/v1/';
|
||||||
|
|
||||||
export { BASE_URL, ENDP_POSTS };
|
export const API_URLS = {
|
||||||
|
Banner: `${API_V}shared/banner/list/`,
|
||||||
|
Category: `${API_V}products/category/list/`,
|
||||||
|
Product: `${API_V}products/product/`,
|
||||||
|
Login: `${API_V}accounts/login/`,
|
||||||
|
Search_Product: `${API_V}products/search/`,
|
||||||
|
Favourite: (product_id: string) => `${API_V}accounts/${product_id}/like/`,
|
||||||
|
FavouriteProduct: `${API_V}accounts/liked_products/`,
|
||||||
|
Partners: `${API_V}accounts/questionnaire/send/`,
|
||||||
|
Faq: `${API_V}shared/faq/list/`,
|
||||||
|
CartCrate: `${API_V}orders/cart/create/`,
|
||||||
|
CartItem: `${API_V}orders/cart-item/create/`,
|
||||||
|
CartClear: (id: number | string) => `${API_V}orders/cart/${id}/clear/`,
|
||||||
|
CartItemList: (id: string) => `${API_V}orders/cart/${id}/`,
|
||||||
|
CartItemUpdate: (id: string) => `${API_V}orders/cart-item/${id}/update/`,
|
||||||
|
CartItemDelete: (id: string) => `${API_V}orders/cart-item/${id}/delete/`,
|
||||||
|
CreateOrder: `${API_V}orders/order/create/`,
|
||||||
|
OrderList: `${API_V}orders/order/list/`,
|
||||||
|
};
|
||||||
|
|||||||
14
src/shared/config/api/category/api.ts
Normal file
14
src/shared/config/api/category/api.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import { AxiosResponse } from 'axios';
|
||||||
|
import httpClient from '../httpClient';
|
||||||
|
import { API_URLS } from '../URLs';
|
||||||
|
import { Category } from './type';
|
||||||
|
|
||||||
|
export const category_api = {
|
||||||
|
async getCategory(params: {
|
||||||
|
page: number;
|
||||||
|
page_size: number;
|
||||||
|
}): Promise<AxiosResponse<Category>> {
|
||||||
|
const res = await httpClient.get(API_URLS.Category, { params });
|
||||||
|
return res;
|
||||||
|
},
|
||||||
|
};
|
||||||
15
src/shared/config/api/category/type.ts
Normal file
15
src/shared/config/api/category/type.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
export interface Category {
|
||||||
|
total: number;
|
||||||
|
page: number;
|
||||||
|
page_size: number;
|
||||||
|
total_pages: number;
|
||||||
|
has_next: boolean;
|
||||||
|
has_previous: boolean;
|
||||||
|
results: CategoryResult[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CategoryResult {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
image: string;
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import getLocaleCS from '@/shared/lib/getLocaleCS';
|
import getLocaleCS from '@/shared/lib/getLocaleCS';
|
||||||
import axios from 'axios';
|
import { getToken, removeToken } from '@/shared/lib/token';
|
||||||
|
import axios, { AxiosError } from 'axios';
|
||||||
import { getLocale } from 'next-intl/server';
|
import { getLocale } from 'next-intl/server';
|
||||||
import { LanguageRoutes } from '../i18n/types';
|
import { LanguageRoutes } from '../i18n/types';
|
||||||
import { BASE_URL } from './URLs';
|
import { BASE_URL } from './URLs';
|
||||||
@@ -20,10 +21,10 @@ httpClient.interceptors.request.use(
|
|||||||
}
|
}
|
||||||
|
|
||||||
config.headers['Accept-Language'] = language;
|
config.headers['Accept-Language'] = language;
|
||||||
// const accessToken = localStorage.getItem('accessToken');
|
const accessToken = getToken();
|
||||||
// if (accessToken) {
|
if (accessToken) {
|
||||||
// config.headers['Authorization'] = `Bearer ${accessToken}`;
|
config.headers['Authorization'] = `Bearer ${accessToken}`;
|
||||||
// }
|
}
|
||||||
|
|
||||||
return config;
|
return config;
|
||||||
},
|
},
|
||||||
@@ -34,6 +35,11 @@ httpClient.interceptors.response.use(
|
|||||||
(response) => response,
|
(response) => response,
|
||||||
(error) => {
|
(error) => {
|
||||||
console.error('API error:', error);
|
console.error('API error:', error);
|
||||||
|
if ((error as AxiosError)?.status === 403) {
|
||||||
|
removeToken();
|
||||||
|
} else if ((error as AxiosError)?.status === 401) {
|
||||||
|
removeToken();
|
||||||
|
}
|
||||||
return Promise.reject(error);
|
return Promise.reject(error);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|||||||
0
src/shared/config/api/order/api.ts
Normal file
0
src/shared/config/api/order/api.ts
Normal file
0
src/shared/config/api/order/type.ts
Normal file
0
src/shared/config/api/order/type.ts
Normal file
60
src/shared/config/api/product/api.ts
Normal file
60
src/shared/config/api/product/api.ts
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
import httpClient from '@/shared/config/api/httpClient';
|
||||||
|
import { API_URLS } from '@/shared/config/api/URLs';
|
||||||
|
import { AxiosResponse } from 'axios';
|
||||||
|
import {
|
||||||
|
FavouriteProduct,
|
||||||
|
ProductDetail,
|
||||||
|
ProductList,
|
||||||
|
SearchData,
|
||||||
|
} from './type';
|
||||||
|
|
||||||
|
export const product_api = {
|
||||||
|
async list(params: {
|
||||||
|
page: number;
|
||||||
|
page_size: number;
|
||||||
|
}): Promise<AxiosResponse<ProductList>> {
|
||||||
|
const res = await httpClient.get(`${API_URLS.Product}list/`, { params });
|
||||||
|
return res;
|
||||||
|
},
|
||||||
|
|
||||||
|
async listGetCategoryId({
|
||||||
|
params,
|
||||||
|
category_id,
|
||||||
|
}: {
|
||||||
|
params: {
|
||||||
|
page: number;
|
||||||
|
page_size: number;
|
||||||
|
};
|
||||||
|
category_id: string;
|
||||||
|
}): Promise<AxiosResponse<ProductList>> {
|
||||||
|
const res = await httpClient.get(
|
||||||
|
`${API_URLS.Product}${category_id}/list/`,
|
||||||
|
{ params },
|
||||||
|
);
|
||||||
|
return res;
|
||||||
|
},
|
||||||
|
|
||||||
|
async detail(id: string): Promise<AxiosResponse<ProductDetail>> {
|
||||||
|
const res = await httpClient.get(`${API_URLS.Product}${id}`);
|
||||||
|
return res;
|
||||||
|
},
|
||||||
|
|
||||||
|
async search(params: {
|
||||||
|
search?: string;
|
||||||
|
page?: number;
|
||||||
|
page_szie?: number;
|
||||||
|
}): Promise<AxiosResponse<SearchData>> {
|
||||||
|
const res = await httpClient.get(`${API_URLS.Search_Product}`, { params });
|
||||||
|
return res;
|
||||||
|
},
|
||||||
|
|
||||||
|
async favourite(product_id: string) {
|
||||||
|
const res = await httpClient.get(API_URLS.Favourite(product_id));
|
||||||
|
return res;
|
||||||
|
},
|
||||||
|
|
||||||
|
async favouuriteProduct(): Promise<AxiosResponse<FavouriteProduct>> {
|
||||||
|
const res = await httpClient.get(API_URLS.FavouriteProduct);
|
||||||
|
return res;
|
||||||
|
},
|
||||||
|
};
|
||||||
98
src/shared/config/api/product/type.ts
Normal file
98
src/shared/config/api/product/type.ts
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
export interface ProductList {
|
||||||
|
total: number;
|
||||||
|
page: number;
|
||||||
|
page_size: number;
|
||||||
|
total_pages: number;
|
||||||
|
has_next: boolean;
|
||||||
|
has_previous: boolean;
|
||||||
|
results: ProductListResult[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ProductListResult {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
image: string;
|
||||||
|
price: number;
|
||||||
|
description: string;
|
||||||
|
liked: boolean;
|
||||||
|
unity: {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
};
|
||||||
|
min_quantity: number;
|
||||||
|
is_active: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ProductDetail {
|
||||||
|
brand: string;
|
||||||
|
description: string;
|
||||||
|
expires_date: null | string;
|
||||||
|
id: string;
|
||||||
|
image: string;
|
||||||
|
images: {
|
||||||
|
id: string;
|
||||||
|
image: string;
|
||||||
|
}[];
|
||||||
|
is_active: boolean;
|
||||||
|
liked: boolean;
|
||||||
|
manufacturer: string;
|
||||||
|
min_quantity: number;
|
||||||
|
name: string;
|
||||||
|
price: number;
|
||||||
|
return_date: null | string;
|
||||||
|
volume: string;
|
||||||
|
unity: {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SearchData {
|
||||||
|
products: SearchDataPro[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SearchDataPro {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
image: string;
|
||||||
|
price: number;
|
||||||
|
description: string;
|
||||||
|
liked: boolean;
|
||||||
|
unity: {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
};
|
||||||
|
min_quantity: number;
|
||||||
|
is_active: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FavouriteProduct {
|
||||||
|
total: number;
|
||||||
|
page: number;
|
||||||
|
page_size: number;
|
||||||
|
total_pages: number;
|
||||||
|
has_next: boolean;
|
||||||
|
has_previous: boolean;
|
||||||
|
results: FavouriteProductRes[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FavouriteProductRes {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
image: string;
|
||||||
|
price: number;
|
||||||
|
description: string;
|
||||||
|
unity: {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
};
|
||||||
|
min_quantity: number;
|
||||||
|
is_active: boolean;
|
||||||
|
liked: boolean;
|
||||||
|
brand: null | string;
|
||||||
|
return_date: null | string;
|
||||||
|
expires_date: null | string;
|
||||||
|
manufacturer: null | string;
|
||||||
|
volume: null | string;
|
||||||
|
images: { id: string; image: string }[];
|
||||||
|
}
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
import { ENDP_POSTS } from '@/shared/config/api/URLs';
|
|
||||||
import { ReqWithPagination } from './types';
|
|
||||||
import { AxiosResponse } from 'axios';
|
|
||||||
import { TestApiType } from '@/shared/types/testApi';
|
|
||||||
import httpClient from './httpClient';
|
|
||||||
|
|
||||||
const getPosts = async (
|
|
||||||
pagination?: ReqWithPagination,
|
|
||||||
): Promise<AxiosResponse<TestApiType>> => {
|
|
||||||
const response = await httpClient.get(ENDP_POSTS, { params: pagination });
|
|
||||||
return response;
|
|
||||||
};
|
|
||||||
|
|
||||||
export { getPosts };
|
|
||||||
|
|||||||
@@ -1,6 +1,196 @@
|
|||||||
{
|
{
|
||||||
"HomePage": {
|
"Biz haqimizda": "О нас",
|
||||||
"title": "Hello world!",
|
"Maxfiylik siyosati": "Политика конфиденциальности",
|
||||||
"about": "Go to the about page"
|
"Savol-javob": "Вопросы и ответы",
|
||||||
}
|
"Sahifalar": "Страницы",
|
||||||
|
"Biz bilan bog'laning": "Свяжитесь с нами",
|
||||||
|
"Русский": "Русский",
|
||||||
|
"O'zbekcha": "Узбекский",
|
||||||
|
"Mahsulot nomi": "Название продукта",
|
||||||
|
"Tizimga kirilmagan": "Нет входа в систему",
|
||||||
|
"Mahsulotni yuklab bo‘lmadi": "Не удалось загрузить товар. Попробуйте ещё раз.",
|
||||||
|
"Savatga": "В корзину",
|
||||||
|
"Kategoriyalar": "Категории",
|
||||||
|
"Gastronomiya va kulinariya san'ati haqidagi yetakchi onlayn magazin": "Ведущий интернет-магазин по гастрономии и кулинарному искусству",
|
||||||
|
"Bizning maqsadimiz": "Наша цель",
|
||||||
|
"Sifatli Kontent": "Качественный контент",
|
||||||
|
"Jahon oshpazlik san'ati va zamonaviy gastronomiya tendentsiyalari haqida chuqur maqolalar va tahlillar": "Углубленные статьи и анализы о мировом кулинарном искусстве и современных гастрономических тенденциях.",
|
||||||
|
"Professional Jamoa": "Профессиональная команда",
|
||||||
|
"Tajribali kulinariya mutaxassislari va oshpazlar tomonidan tayyorlangan kontent": "Контент подготовлен опытными кулинарами и поварами.",
|
||||||
|
"Yangiliklar": "Новости",
|
||||||
|
"Gastronomiya sohasidagi so'nggi yangiliklar va eng yangi trendlar haqida xabarlar": "Новости о последних новостях и новейших тенденциях в сфере гастрономии",
|
||||||
|
"Innovatsiya, sifat va professionallik": "Инновации, качество и профессионализм",
|
||||||
|
"Gastro Market – bu gastronomiya dunyosidagi eng so'nggi yangiliklarni": "Gastro Market — онлайн-платформа, на которой представлены последние новости, рецепты и тенденции в мире гастрономии. Мы стремимся предоставить нашим читателям качественный и интересный контент.",
|
||||||
|
"Bizning jamoamiz tajribali kulinariya mutaxassislari": "Наша команда состоит из опытных кулинаров, поваров и экспертов в области гастрономии. В каждой статье мы уделяем особое внимание качеству и профессионализму.",
|
||||||
|
"Bizning dunyo": "Наш мир",
|
||||||
|
"Hamkor bo'ling": "Станьте партнером",
|
||||||
|
"Gastro Market bilan hamkorlik qilishni xohlaysizmi?": "Хотите сотрудничать с Gastro Market? Заполните форму ниже, и мы свяжемся с вами в ближайшее время.",
|
||||||
|
"Kompaniya nomi": "Название компании",
|
||||||
|
"Website": "Веб-сайт",
|
||||||
|
"Ism Familiya": "Имя и фамилия",
|
||||||
|
"Email": "Электронная почта",
|
||||||
|
"Telefon raqami": "Телефон",
|
||||||
|
"Kompaniya hujjati": "Документ компании",
|
||||||
|
"Faylni tanlang": "Выберите файл",
|
||||||
|
"Tanlangan fayl": "Выбранный файл",
|
||||||
|
"PDF yoki Word formatida (maksimal 5MB)": "В формате PDF или Word (макс. 5MB)",
|
||||||
|
"So'rov yuborish": "Отправить запрос",
|
||||||
|
"Kompaniya nomi kamida 2 ta belgidan iborat bo'lishi kerak": "Название компании должно содержать не менее 2 символов.",
|
||||||
|
"Ism kamida 2 ta belgidan iborat bo'lishi kerak": "Имя должно содержать не менее 2 символов",
|
||||||
|
"To'g'ri email manzilini kiriting": "Введите правильный адрес электронной почты",
|
||||||
|
"To'g'ri telefon raqamini kiriting": "Введите правильный номер телефона",
|
||||||
|
"File yuklash majburiy": "Загрузка файла обязательна",
|
||||||
|
|
||||||
|
"Maxfiylik Siyosati": "Политика конфиденциальности",
|
||||||
|
"Gastro Market sizning ma'lumotlaringiz xavfsizligini jiddiy qabul qiladi": "Gastro Market серьезно относится к безопасности ваших данных",
|
||||||
|
"Oxirgi yangilanish: 16 Dekabr 2025": "Последнее обновление: 16 декабря 2025",
|
||||||
|
"Ushbu Maxfiylik Siyosati Gastro Market online magazinida siz tomonidan taqdim etilgan shaxsiy ma'lumotlarni qanday to'playmiz": "Эта Политика конфиденциальности объясняет, как Gastro Market собирает, использует и защищает персональные данные, предоставленные вами в онлайн-магазине. Используя наши услуги, вы соглашаетесь с описанными практиками.",
|
||||||
|
|
||||||
|
"Biz To'playdigan Ma'lumotlar": "1. Сбор информации",
|
||||||
|
"Sizning tajribangizni yaxshilash uchun biz quyidagi ma'lumotlarni to'playmiz": "Имя, адрес электронной почты, номер телефона",
|
||||||
|
"Kompaniya nomi, website, hamkorlik so'rovlari": "Название компании, веб-сайт, запросы на сотрудничество",
|
||||||
|
"Hamkorlik uchun yuklangan hujjatlar": "Документы, загруженные для сотрудничества",
|
||||||
|
"IP manzil, brauzer turi, qurilma ma'lumotlari": "IP-адрес, тип браузера, данные устройства",
|
||||||
|
"Shaxsiy Ma'lumotlar": "Персональные данные",
|
||||||
|
"Kompaniya Ma'lumotlari": "Информация о компании",
|
||||||
|
"Fayllar:": "Файлы:",
|
||||||
|
"Texnik Ma'lumotlar": "Технические данные",
|
||||||
|
|
||||||
|
"Ma'lumotlardan Foydalanish": "2. Использование данных",
|
||||||
|
"To'plangan ma'lumotlaringizdan quyidagi maqsadlarda foydalanamiz:": "Мы будем использовать собранные вами данные для следующих целей:",
|
||||||
|
"Hamkorlik so'rovlarini qayta ishlash va javob berish": "Обработка и ответ на запросы на сотрудничество",
|
||||||
|
"Sizga xizmatlarimiz va yangiliklar haqida ma'lumot berish": "Предоставление информации о наших услугах и новостях",
|
||||||
|
"Sayt xavfsizligini ta'minlash va firibgarlikka qarshi kurashish": "Обеспечение безопасности сайта и предотвращение мошенничества",
|
||||||
|
"Foydalanuvchi tajribasini tahlil qilish va yaxshilash": "Анализ и улучшение пользовательского опыта",
|
||||||
|
"Qonuniy talablarni bajarish": "Соблюдение законных требований",
|
||||||
|
|
||||||
|
"Ma'lumotlar Xavfsizligi": "3. Безопасность данных",
|
||||||
|
"Biz sizning shaxsiy ma'lumotlaringizni himoya qilish uchun zamonaviy xavfsizlik choralarini qo'llaymiz:": "Мы применяем современные меры безопасности для защиты ваших персональных данных:",
|
||||||
|
"SSL/TLS shifrlash orqali ma'lumotlar uzatish": "Передача данных с шифрованием SSL/TLS",
|
||||||
|
"Xavfsiz serverlar va ma'lumotlar bazasida saqlash": "Хранение на безопасных серверах и в базе данных",
|
||||||
|
"Cheklangan kirish huquqlari va autentifikatsiya": "Ограниченный доступ и аутентификация",
|
||||||
|
"Doimiy xavfsizlik monitoringi va yangilanishlar": "Постоянный мониторинг безопасности и обновления",
|
||||||
|
|
||||||
|
"Ma'lumotlarni Ulashish": "4. Раскрытие данных",
|
||||||
|
"Biz sizning shaxsiy ma'lumotlaringizni uchinchi shaxslarga sotmaymiz": "Мы не будем продавать ваши персональные данные третьим лицам.",
|
||||||
|
"Sizning roziligingiz bilan": "С вашего согласия",
|
||||||
|
"Qonuniy talablar bo'yicha": "В соответствии с законом",
|
||||||
|
"Xizmat ko'rsatuvchi ishonchli hamkorlar bilan (maxfiylik shartnomalari ostida)": "С надежными партнерами (соглашения о конфиденциальности)",
|
||||||
|
"Kompaniya birlashuvi yoki sotilishi holatida": "В случае слияния или продажи компании",
|
||||||
|
|
||||||
|
"Biz Bilan Bog'lanish": "Связь с нами",
|
||||||
|
"Ushbu Maxfiylik Siyosati yoki shaxsiy ma'lumotlaringiz haqida savollaringiz bo'lsa, biz bilan bog'laning:": "Если у вас есть вопросы по данной Политике конфиденциальности или вашим данным, свяжитесь с нами:",
|
||||||
|
"Telefon": "Телефон",
|
||||||
|
"Toshkent, O'zbekiston": "Ташкент, Узбекистан",
|
||||||
|
"Manzil": "Адрес",
|
||||||
|
|
||||||
|
"Miqdor": "Количество",
|
||||||
|
"Jami": "Итого",
|
||||||
|
"Bepul yetkazib berish": "Бесплатная доставка",
|
||||||
|
"Kafolat": "Гарантия",
|
||||||
|
"Xususiyatlari": "Характеристики",
|
||||||
|
"Qadoq turi": "Тип упаковки",
|
||||||
|
"Brandi": "Бренд",
|
||||||
|
"Ishlab chiqaruvchi": "Производитель",
|
||||||
|
"Hajmi": "Объем",
|
||||||
|
"O'xshash mahsulotlar": "Похожие товары",
|
||||||
|
|
||||||
|
"Hech narsa topilmadi": "Не найдено",
|
||||||
|
"Qidiruv natijalari": "Результаты поиска",
|
||||||
|
"Tavsiya etiladi": "Рекомендуется",
|
||||||
|
"Yuklanmoqda": "Загрузка...",
|
||||||
|
"Natija topilmadi": "Нет результатов",
|
||||||
|
|
||||||
|
"Asosiy": "Основная",
|
||||||
|
"Katalog": "Каталог",
|
||||||
|
"Sevimli": "Избранное",
|
||||||
|
"Savatda": "В корзине",
|
||||||
|
"Profil": "Профиль",
|
||||||
|
|
||||||
|
"Username yoki parol xato kiritildi": "Неверное имя пользователя или пароль",
|
||||||
|
"Tizimga kirish": "Войти в систему",
|
||||||
|
"Username": "Имя пользователя",
|
||||||
|
"Parol": "Пароль",
|
||||||
|
"Kirish": "Вход",
|
||||||
|
|
||||||
|
"Savatingiz bo'sh": "Ваша корзина пуста",
|
||||||
|
"Mahsulotlar qo'shish uchun katalogga o'ting": "Перейти в каталог, чтобы добавить товары",
|
||||||
|
"Xarid qilishni boshlash": "Начать покупку",
|
||||||
|
"Savat": "Корзина",
|
||||||
|
"ta mahsulot": "товар(ов)",
|
||||||
|
"Buyurtma haqida": "О заказе",
|
||||||
|
"Mahsulotlar narxi": "Цена товаров",
|
||||||
|
"Chegirma": "Скидка",
|
||||||
|
"Yetkazib berish": "Доставка",
|
||||||
|
"Bepul": "Бесплатно",
|
||||||
|
"Buyurtmani rasmiylashtirish": "Оформить заказ",
|
||||||
|
"Xaridni davom ettirish": "Продолжить покупки",
|
||||||
|
"Tez yetkazib berish 1-2 kun ichida": "Быстрая доставка в течение 1-2 дней",
|
||||||
|
"Xavfsiz to'lov usullari": "Безопасные способы оплаты",
|
||||||
|
"Buyurtma qabul qilindi!": "Заказ принят!",
|
||||||
|
"Buyurtma raqami": "Номер заказа",
|
||||||
|
"Buyurtmangiz muvaffaqiyatli qabul qilindi": "Ваш заказ успешно принят.",
|
||||||
|
"Bosh sahifaga qaytish": "Вернуться на главную",
|
||||||
|
"Ma'lumotlaringizni to'ldiring": "Заполните ваши данные",
|
||||||
|
"Shaxsiy ma'lumotlar": "Личные данные",
|
||||||
|
"Ism": "Имя",
|
||||||
|
"Ismingiz": "Ваше имя",
|
||||||
|
"Familiya": "Фамилия",
|
||||||
|
"Familiyangiz": "Ваша фамилия",
|
||||||
|
"Telefon raqam": "Номер телефона",
|
||||||
|
"Izoh": "Комментарий",
|
||||||
|
"Yetkazib berish manzili": "Адрес доставки",
|
||||||
|
"Manzilni qidirish": "Поиск адреса",
|
||||||
|
"Toshkent": "Ташкент",
|
||||||
|
"Mening joylashuvim": "Моё местоположение",
|
||||||
|
|
||||||
|
"Yetkazib berish usuli": "Способ доставки",
|
||||||
|
"Standart yetkazib berish": "Стандартная доставка",
|
||||||
|
"2-3 kun ichida": "В течение 2–3 дней",
|
||||||
|
"Tez yetkazib berish": "Экспресс-доставка",
|
||||||
|
"1 kun ichida": "В течение 1 дня",
|
||||||
|
|
||||||
|
"To'lov usuli": "Способ оплаты",
|
||||||
|
"Naqd pul": "Наличные",
|
||||||
|
"Yetkazib berishda to'lash": "Оплата при доставке",
|
||||||
|
"Plastik karta": "Банковская карта",
|
||||||
|
"Online to'lov": "Онлайн-оплата",
|
||||||
|
|
||||||
|
"Mahsulotlar": "Товары",
|
||||||
|
"Buyurtmani tasdiqlash": "Подтвердить заказ",
|
||||||
|
|
||||||
|
"Majburiy maydon": "Обязательное поле",
|
||||||
|
"Xato raqam kiritildi": "Введен неверный номер",
|
||||||
|
"Orqaga": "Назад",
|
||||||
|
|
||||||
|
"Sevimlilar bo'sh": "Избранное пусто",
|
||||||
|
"Hali hech qanday mahsulotni sevimlilarga qo'shmadingiz": "Вы пока не добавили ни одного товара в избранное. Перейдите в каталог и сохраните понравившиеся товары.",
|
||||||
|
"Sevimli mahsulotlar": "Избранные товары",
|
||||||
|
"Ism email manzil telefon raqami": "Имя, адрес электронной почты, номер телефона",
|
||||||
|
|
||||||
|
"Faol buyurtmalar": "Активные заказы",
|
||||||
|
"Barchasi": "Все",
|
||||||
|
"Buyurtmalar tarixi": "История заказов",
|
||||||
|
"Qayta": "Повторить",
|
||||||
|
"Chiqish": "Выйти",
|
||||||
|
"Umumiy": "Общее",
|
||||||
|
"Buyurtmalar": "Заказы",
|
||||||
|
"Tarix": "История",
|
||||||
|
|
||||||
|
"Faol": "Активно",
|
||||||
|
"Tugadi": "Заканчивается",
|
||||||
|
"Yetkazish": "Доставка",
|
||||||
|
"Yo'lda": "В пути",
|
||||||
|
"Punktda": "В пункте",
|
||||||
|
"Yetkazildi": "Доставлено",
|
||||||
|
"Bekor qilindi": "Отменено",
|
||||||
|
"Bu hafta": "На этой неделе",
|
||||||
|
"Bu oy": "В этом месяце",
|
||||||
|
"Qo'llab-quvatlash": "Поддержка",
|
||||||
|
"Agarda sizda kirish uchun login va parol yo'q bolsa iltimos bizga murojat qiling": "Если у вас нет логина и пароля для входа, пожалуйста, свяжитесь с нами",
|
||||||
|
"Murojat qilish": "Заявление",
|
||||||
|
"So'rov yuborildi!": "Запрос успешно отправлен!",
|
||||||
|
|
||||||
|
"Tez-tez So'raladigan Savollar": "Часто задаваемые вопросы",
|
||||||
|
"Gastro Market haqida eng ko'p so'raladigan savollarga javoblar":"Ответы на самые часто задаваемые вопросы о Gastro Market"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,9 +2,196 @@
|
|||||||
// See: https://next-intl.dev/docs/workflows/typescript#messages-arguments
|
// See: https://next-intl.dev/docs/workflows/typescript#messages-arguments
|
||||||
|
|
||||||
declare const messages: {
|
declare const messages: {
|
||||||
HomePage: {
|
'Biz haqimizda': 'Biz haqimizda';
|
||||||
title: 'Salom dunyo!';
|
'Maxfiylik siyosati': 'Maxfiylik siyosati';
|
||||||
about: 'Go to the about page';
|
'Savol-javob': 'Savol-javob';
|
||||||
};
|
Sahifalar: 'Sahifalar';
|
||||||
|
"Biz bilan bog'laning": "Biz bilan bog'laning";
|
||||||
|
Русский: 'Русский';
|
||||||
|
"O'zbekcha": "O'zbekcha";
|
||||||
|
'Mahsulot nomi': 'Mahsulot nomi';
|
||||||
|
'Tizimga kirilmagan': 'Tizimga kirilmagan';
|
||||||
|
'Mahsulotni yuklab bo‘lmadi': 'Mahsulotni yuklab bo‘lmadi. Qayta urinib ko‘ring.';
|
||||||
|
Savatga: 'Savatga';
|
||||||
|
Kategoriyalar: 'Kategoriyalar';
|
||||||
|
"Gastronomiya va kulinariya san'ati haqidagi yetakchi onlayn magazin": "Gastronomiya va kulinariya san'ati haqidagi yetakchi onlayn magazin";
|
||||||
|
'Bizning maqsadimiz': 'Bizning maqsadimiz';
|
||||||
|
'Sifatli Kontent': 'Sifatli Kontent';
|
||||||
|
"Jahon oshpazlik san'ati va zamonaviy gastronomiya tendentsiyalari haqida chuqur maqolalar va tahlillar": "Jahon oshpazlik san'ati va zamonaviy gastronomiya tendentsiyalari haqida chuqur maqolalar va tahlillar";
|
||||||
|
'Professional Jamoa': 'Professional Jamoa';
|
||||||
|
'Tajribali kulinariya mutaxassislari va oshpazlar tomonidan tayyorlangan kontent': 'Tajribali kulinariya mutaxassislari va oshpazlar tomonidan tayyorlangan kontent';
|
||||||
|
Yangiliklar: 'Yangiliklar';
|
||||||
|
"Gastronomiya sohasidagi so'nggi yangiliklar va eng yangi trendlar haqida xabarlar": "Gastronomiya sohasidagi so'nggi yangiliklar va eng yangi trendlar haqida xabarlar";
|
||||||
|
'Innovatsiya, sifat va professionallik': 'Innovatsiya, sifat va professionallik';
|
||||||
|
"Gastro Market – bu gastronomiya dunyosidagi eng so'nggi yangiliklarni": "Gastro Market – bu gastronomiya dunyosidagi eng so'nggi yangiliklarni, retseptlarni va tendentsiyalarni taqdim etuvchi onlayn platforma. Biz o'quvchilarimizga sifatli va qiziqarli kontent taqdim etishga intilamiz.";
|
||||||
|
'Bizning jamoamiz tajribali kulinariya mutaxassislari': "Bizning jamoamiz tajribali kulinariya mutaxassislari, oshpazlar va gastronomiya sohasidagi ekspertlardan iborat. Biz har bir maqolada sifat va professionallikka e'tibor qaratamiz.";
|
||||||
|
'Bizning dunyo': 'Bizning dunyo';
|
||||||
|
"Hamkor bo'ling": "Hamkor bo'ling";
|
||||||
|
'Gastro Market bilan hamkorlik qilishni xohlaysizmi?': "Gastro Market bilan hamkorlik qilishni xohlaysizmi? Quyidagi formani to'ldiring va biz siz bilan tez orada bog'lanamiz.";
|
||||||
|
'Kompaniya nomi': 'Kompaniya nomi';
|
||||||
|
Website: 'Website';
|
||||||
|
'Ism Familiya': 'Ism Familiya';
|
||||||
|
Email: 'Email';
|
||||||
|
'Telefon raqami': 'Telefon raqami';
|
||||||
|
'Kompaniya hujjati': 'Kompaniya hujjati';
|
||||||
|
'Faylni tanlang': 'Faylni tanlang';
|
||||||
|
'Tanlangan fayl': 'Tanlangan fayl';
|
||||||
|
'PDF yoki Word formatida (maksimal 5MB)': 'PDF yoki Word formatida (maksimal 5MB)';
|
||||||
|
"So'rov yuborish": "So'rov yuborish";
|
||||||
|
"Kompaniya nomi kamida 2 ta belgidan iborat bo'lishi kerak": "Kompaniya nomi kamida 2 ta belgidan iborat bo'lishi kerak";
|
||||||
|
"Ism kamida 2 ta belgidan iborat bo'lishi kerak": "Ism kamida 2 ta belgidan iborat bo'lishi kerak";
|
||||||
|
"To'g'ri email manzilini kiriting": "To'g'ri email manzilini kiriting";
|
||||||
|
"To'g'ri telefon raqamini kiriting": "To'g'ri telefon raqamini kiriting";
|
||||||
|
'File yuklash majburiy': 'File yuklash majburiy';
|
||||||
|
|
||||||
|
'Maxfiylik Siyosati': 'Maxfiylik Siyosati';
|
||||||
|
"Gastro Market sizning ma'lumotlaringiz xavfsizligini jiddiy qabul qiladi": "Gastro Market sizning ma'lumotlaringiz xavfsizligini jiddiy qabul qiladi";
|
||||||
|
'Oxirgi yangilanish: 16 Dekabr 2025': 'Oxirgi yangilanish: 16 Dekabr 2025';
|
||||||
|
"Ushbu Maxfiylik Siyosati Gastro Market online magazinida siz tomonidan taqdim etilgan shaxsiy ma'lumotlarni qanday to'playmiz": "Ushbu Maxfiylik Siyosati Gastro Market online magazinida siz tomonidan taqdim etilgan shaxsiy ma'lumotlarni qanday to'playmiz, ishlatamiz va himoya qilishimizni tushuntiradi. Xizmatlarimizdan foydalanish orqali siz ushbu siyosatda tasvirlangan amaliyotlarga rozilik bildirasiz.";
|
||||||
|
|
||||||
|
"Biz To'playdigan Ma'lumotlar": "1. Biz To'playdigan Ma'lumotlar";
|
||||||
|
"Sizning tajribangizni yaxshilash uchun biz quyidagi ma'lumotlarni to'playmiz": "Sizning tajribangizni yaxshilash uchun biz quyidagi ma'lumotlarni to'playmiz:";
|
||||||
|
'Ism email manzil telefon raqami': 'Ism, email manzil, telefon raqami';
|
||||||
|
"Kompaniya nomi, website, hamkorlik so'rovlari": "Kompaniya nomi, website, hamkorlik so'rovlari";
|
||||||
|
'Hamkorlik uchun yuklangan hujjatlar': 'Hamkorlik uchun yuklangan hujjatlar';
|
||||||
|
"IP manzil, brauzer turi, qurilma ma'lumotlari": "IP manzil, brauzer turi, qurilma ma'lumotlari";
|
||||||
|
"Shaxsiy Ma'lumotlar": "Shaxsiy Ma'lumotlar";
|
||||||
|
"Kompaniya Ma'lumotlari": "Kompaniya Ma'lumotlari";
|
||||||
|
'Fayllar:': 'Fayllar:';
|
||||||
|
"Texnik Ma'lumotlar": "Texnik Ma'lumotlar";
|
||||||
|
|
||||||
|
"Ma'lumotlardan Foydalanish": "2. Ma'lumotlardan Foydalanish";
|
||||||
|
"To'plangan ma'lumotlaringizdan quyidagi maqsadlarda foydalanamiz:": "To'plangan ma'lumotlaringizdan quyidagi maqsadlarda foydalanamiz:";
|
||||||
|
"Hamkorlik so'rovlarini qayta ishlash va javob berish": "Hamkorlik so'rovlarini qayta ishlash va javob berish";
|
||||||
|
"Sizga xizmatlarimiz va yangiliklar haqida ma'lumot berish": "Sizga xizmatlarimiz va yangiliklar haqida ma'lumot berish";
|
||||||
|
"Sayt xavfsizligini ta'minlash va firibgarlikka qarshi kurashish": "Sayt xavfsizligini ta'minlash va firibgarlikka qarshi kurashish";
|
||||||
|
'Foydalanuvchi tajribasini tahlil qilish va yaxshilash': 'Foydalanuvchi tajribasini tahlil qilish va yaxshilash';
|
||||||
|
'Qonuniy talablarni bajarish': 'Qonuniy talablarni bajarish';
|
||||||
|
|
||||||
|
"Ma'lumotlar Xavfsizligi": "3. Ma'lumotlar Xavfsizligi";
|
||||||
|
"Biz sizning shaxsiy ma'lumotlaringizni himoya qilish uchun zamonaviy xavfsizlik choralarini qo'llaymiz:": "Biz sizning shaxsiy ma'lumotlaringizni himoya qilish uchun zamonaviy xavfsizlik choralarini qo'llaymiz:";
|
||||||
|
"SSL/TLS shifrlash orqali ma'lumotlar uzatish": "SSL/TLS shifrlash orqali ma'lumotlar uzatish";
|
||||||
|
"Xavfsiz serverlar va ma'lumotlar bazasida saqlash": "Xavfsiz serverlar va ma'lumotlar bazasida saqlash";
|
||||||
|
'Cheklangan kirish huquqlari va autentifikatsiya': 'Cheklangan kirish huquqlari va autentifikatsiya';
|
||||||
|
'Doimiy xavfsizlik monitoringi va yangilanishlar': 'Doimiy xavfsizlik monitoringi va yangilanishlar';
|
||||||
|
|
||||||
|
"Ma'lumotlarni Ulashish": "4. Ma'lumotlarni Ulashish";
|
||||||
|
"Biz sizning shaxsiy ma'lumotlaringizni uchinchi shaxslarga sotmaymiz": "Biz sizning shaxsiy ma'lumotlaringizni uchinchi shaxslarga sotmaymiz";
|
||||||
|
'Sizning roziligingiz bilan': 'Sizning roziligingiz bilan';
|
||||||
|
"Qonuniy talablar bo'yicha": "Qonuniy talablar bo'yicha";
|
||||||
|
"Xizmat ko'rsatuvchi ishonchli hamkorlar bilan (maxfiylik shartnomalari ostida)": "Xizmat ko'rsatuvchi ishonchli hamkorlar bilan (maxfiylik shartnomalari ostida)";
|
||||||
|
'Kompaniya birlashuvi yoki sotilishi holatida': 'Kompaniya birlashuvi yoki sotilishi holatida';
|
||||||
|
|
||||||
|
"Biz Bilan Bog'lanish": "Biz Bilan Bog'lanish";
|
||||||
|
"Ushbu Maxfiylik Siyosati yoki shaxsiy ma'lumotlaringiz haqida savollaringiz bo'lsa, biz bilan bog'laning:": "Ushbu Maxfiylik Siyosati yoki shaxsiy ma'lumotlaringiz haqida savollaringiz bo'lsa, biz bilan bog'laning:";
|
||||||
|
Telefon: 'Telefon';
|
||||||
|
"Toshkent, O'zbekiston": "Toshkent, O'zbekiston";
|
||||||
|
Manzil: 'Manzil';
|
||||||
|
|
||||||
|
Miqdor: 'Miqdor';
|
||||||
|
Jami: 'Jami';
|
||||||
|
'Bepul yetkazib berish': 'Bepul yetkazib berish';
|
||||||
|
Kafolat: 'Kafolat';
|
||||||
|
Xususiyatlari: 'Xususiyatlari';
|
||||||
|
'Qadoq turi': 'Qadoq turi';
|
||||||
|
Brandi: 'Brend';
|
||||||
|
'Ishlab chiqaruvchi': 'Ishlab chiqaruvchi';
|
||||||
|
Hajmi: 'Hajmi';
|
||||||
|
"O'xshash mahsulotlar": "O'xshash mahsulotlar";
|
||||||
|
|
||||||
|
'Hech narsa topilmadi': 'Hech narsa topilmadi';
|
||||||
|
'Qidiruv natijalari': 'Qidiruv natijalari';
|
||||||
|
'Tavsiya etiladi': 'Tavsiya etiladi';
|
||||||
|
Yuklanmoqda: 'Yuklanmoqda....';
|
||||||
|
'Natija topilmadi': 'Natija topilmadi';
|
||||||
|
|
||||||
|
Asosiy: 'Asosiy';
|
||||||
|
Katalog: 'Katalog';
|
||||||
|
Sevimli: 'Sevimli';
|
||||||
|
Savatda: 'Savatda';
|
||||||
|
Profil: 'Profil';
|
||||||
|
|
||||||
|
'Username yoki parol xato kiritildi': 'Username yoki parol xato kiritildi';
|
||||||
|
'Tizimga kirish': 'Tizimga kirish';
|
||||||
|
Username: 'Username';
|
||||||
|
Parol: 'Parol';
|
||||||
|
Kirish: 'Kirish';
|
||||||
|
|
||||||
|
"Savatingiz bo'sh": "Savatingiz bo'sh";
|
||||||
|
"Mahsulotlar qo'shish uchun katalogga o'ting": "Mahsulotlar qo'shish uchun katalogga o'ting";
|
||||||
|
'Xarid qilishni boshlash': 'Xarid qilishni boshlash';
|
||||||
|
Savat: 'Savat';
|
||||||
|
'ta mahsulot': 'ta mahsulot';
|
||||||
|
'Buyurtma haqida': 'Buyurtma haqida';
|
||||||
|
'Mahsulotlar narxi': 'Mahsulotlar narxi';
|
||||||
|
Chegirma: 'Chegirma';
|
||||||
|
'Yetkazib berish': 'Yetkazib berish';
|
||||||
|
Bepul: 'Bepul';
|
||||||
|
'Buyurtmani rasmiylashtirish': 'Buyurtmani rasmiylashtirish';
|
||||||
|
'Xaridni davom ettirish': 'Xaridni davom ettirish';
|
||||||
|
'Tez yetkazib berish 1-2 kun ichida': 'Tez yetkazib berish 1-2 kun ichida';
|
||||||
|
"Xavfsiz to'lov usullari": "Xavfsiz to'lov usullari";
|
||||||
|
|
||||||
|
'Buyurtma qabul qilindi!': 'Buyurtma qabul qilindi!';
|
||||||
|
'Buyurtma raqami': 'Buyurtma raqami';
|
||||||
|
'Buyurtmangiz muvaffaqiyatli qabul qilindi': 'Buyurtmangiz muvaffaqiyatli qabul qilindi.';
|
||||||
|
'Bosh sahifaga qaytish': 'Bosh sahifaga qaytish';
|
||||||
|
"Ma'lumotlaringizni to'ldiring": "Ma'lumotlaringizni to'ldiring";
|
||||||
|
"Shaxsiy ma'lumotlar": "Shaxsiy ma'lumotlar";
|
||||||
|
Ism: 'Ism';
|
||||||
|
Ismingiz: 'Ismingiz';
|
||||||
|
Familiya: 'Familiya';
|
||||||
|
Familiyangiz: 'Familiyangiz';
|
||||||
|
'Telefon raqam': 'Telefon raqam';
|
||||||
|
Izoh: 'Izoh';
|
||||||
|
'Yetkazib berish manzili': 'Yetkazib berish manzili';
|
||||||
|
'Manzilni qidirish': 'Manzilni qidirish';
|
||||||
|
Toshkent: 'Toshkent';
|
||||||
|
'Mening joylashuvim': 'Mening joylashuvim';
|
||||||
|
'Yetkazib berish usuli': 'Yetkazib berish usuli';
|
||||||
|
'Standart yetkazib berish': 'Standart yetkazib berish';
|
||||||
|
'2-3 kun ichida': '2-3 kun ichida';
|
||||||
|
'Tez yetkazib berish': 'Tez yetkazib berish';
|
||||||
|
'1 kun ichida': '1 kun ichida';
|
||||||
|
"To'lov usuli": "To'lov usuli";
|
||||||
|
'Naqd pul': 'Naqd pul';
|
||||||
|
"Yetkazib berishda to'lash": "Yetkazib berishda to'lash";
|
||||||
|
'Plastik karta': 'Plastik karta';
|
||||||
|
"Online to'lov": "Online to'lov";
|
||||||
|
Mahsulotlar: 'Mahsulotlar';
|
||||||
|
'Buyurtmani tasdiqlash': 'Buyurtmani tasdiqlash';
|
||||||
|
'Majburiy maydon': 'Majburiy maydon';
|
||||||
|
'Xato raqam kiritildi': 'Xato raqam kiritildi';
|
||||||
|
Orqaga: 'Orqaga';
|
||||||
|
|
||||||
|
"Sevimlilar bo'sh": "Sevimlilar bo'sh";
|
||||||
|
"Hali hech qanday mahsulotni sevimlilarga qo'shmadingiz": "Hali hech qanday mahsulotni sevimlilarga qo'shmadingiz. Mahsulotlar ro'yxatiga o'ting va yoqqan mahsulotlaringizni saqlang.";
|
||||||
|
'Sevimli mahsulotlar': 'Sevimli mahsulotlar';
|
||||||
|
|
||||||
|
'Faol buyurtmalar': 'Faol buyurtmalar';
|
||||||
|
Barchasi: 'Barchasi';
|
||||||
|
'Buyurtmalar tarixi': 'Buyurtmalar tarixi';
|
||||||
|
Qayta: 'Qayta';
|
||||||
|
Chiqish: 'Chiqish';
|
||||||
|
Umumiy: 'Umumiy';
|
||||||
|
Buyurtmalar: 'Buyurtmalar';
|
||||||
|
Tarix: 'Tarix';
|
||||||
|
|
||||||
|
Faol: 'Faol';
|
||||||
|
Tugadi: 'Tugadi';
|
||||||
|
Yetkazish: 'Yetkazish';
|
||||||
|
"Yo'lda": "Yo'lda";
|
||||||
|
Punktda: 'Punktda';
|
||||||
|
Yetkazildi: 'Yetkazildi';
|
||||||
|
'Bekor qilindi': 'Bekor qilindi';
|
||||||
|
'Bu hafta': 'Bu hafta';
|
||||||
|
'Bu oy': 'Bu oy';
|
||||||
|
"Qo'llab-quvatlash": "Qo'llab-quvatlash";
|
||||||
|
"Agarda sizda kirish uchun login va parol yo'q bolsa iltimos bizga murojat qiling": "Agarda sizda kirish uchun login va parol yo'q bolsa iltimos bizga murojat qiling";
|
||||||
|
'Murojat qilish': 'Murojat qilish';
|
||||||
|
"So'rov yuborildi!": "So'rov yuborildi!";
|
||||||
|
|
||||||
|
"Tez-tez So'raladigan Savollar": "Tez-tez So'raladigan Savollar";
|
||||||
|
"Gastro Market haqida eng ko'p so'raladigan savollarga javoblar": "Gastro Market haqida eng ko'p so'raladigan savollarga javoblar";
|
||||||
};
|
};
|
||||||
export default messages;
|
export default messages;
|
||||||
|
|||||||
@@ -1,6 +1,193 @@
|
|||||||
{
|
{
|
||||||
"HomePage": {
|
"Biz haqimizda": "Biz haqimizda",
|
||||||
"title": "Salom dunyo!",
|
"Maxfiylik siyosati": "Maxfiylik siyosati",
|
||||||
"about": "Go to the about page"
|
"Savol-javob": "Savol-javob",
|
||||||
}
|
"Sahifalar": "Sahifalar",
|
||||||
|
"Biz bilan bog'laning": "Biz bilan bog'laning",
|
||||||
|
"Русский": "Русский",
|
||||||
|
"O'zbekcha": "O'zbekcha",
|
||||||
|
"Mahsulot nomi": "Mahsulot nomi",
|
||||||
|
"Tizimga kirilmagan": "Tizimga kirilmagan",
|
||||||
|
"Mahsulotni yuklab bo‘lmadi": "Mahsulotni yuklab bo‘lmadi. Qayta urinib ko‘ring.",
|
||||||
|
"Savatga": "Savatga",
|
||||||
|
"Kategoriyalar": "Kategoriyalar",
|
||||||
|
"Gastronomiya va kulinariya san'ati haqidagi yetakchi onlayn magazin": "Gastronomiya va kulinariya san'ati haqidagi yetakchi onlayn magazin",
|
||||||
|
"Bizning maqsadimiz": "Bizning maqsadimiz",
|
||||||
|
"Sifatli Kontent": "Sifatli Kontent",
|
||||||
|
"Jahon oshpazlik san'ati va zamonaviy gastronomiya tendentsiyalari haqida chuqur maqolalar va tahlillar": "Jahon oshpazlik san'ati va zamonaviy gastronomiya tendentsiyalari haqida chuqur maqolalar va tahlillar",
|
||||||
|
"Professional Jamoa": "Professional Jamoa",
|
||||||
|
"Tajribali kulinariya mutaxassislari va oshpazlar tomonidan tayyorlangan kontent": "Tajribali kulinariya mutaxassislari va oshpazlar tomonidan tayyorlangan kontent",
|
||||||
|
"Yangiliklar": "Yangiliklar",
|
||||||
|
"Gastronomiya sohasidagi so'nggi yangiliklar va eng yangi trendlar haqida xabarlar": "Gastronomiya sohasidagi so'nggi yangiliklar va eng yangi trendlar haqida xabarlar",
|
||||||
|
"Innovatsiya, sifat va professionallik": "Innovatsiya, sifat va professionallik",
|
||||||
|
"Gastro Market – bu gastronomiya dunyosidagi eng so'nggi yangiliklarni": "Gastro Market – bu gastronomiya dunyosidagi eng so'nggi yangiliklarni, retseptlarni va tendentsiyalarni taqdim etuvchi onlayn platforma. Biz o'quvchilarimizga sifatli va qiziqarli kontent taqdim etishga intilamiz.",
|
||||||
|
"Bizning jamoamiz tajribali kulinariya mutaxassislari": "Bizning jamoamiz tajribali kulinariya mutaxassislari, oshpazlar va gastronomiya sohasidagi ekspertlardan iborat. Biz har bir maqolada sifat va professionallikka e'tibor qaratamiz.",
|
||||||
|
"Bizning dunyo": "Bizning dunyo",
|
||||||
|
"Hamkor bo'ling": "Hamkor bo'ling",
|
||||||
|
"Gastro Market bilan hamkorlik qilishni xohlaysizmi?": "Gastro Market bilan hamkorlik qilishni xohlaysizmi? Quyidagi formani to'ldiring va biz siz bilan tez orada bog'lanamiz.",
|
||||||
|
"Kompaniya nomi": "Kompaniya nomi",
|
||||||
|
"Website": "Website",
|
||||||
|
"Ism Familiya": "Ism Familiya",
|
||||||
|
"Email": "Email",
|
||||||
|
"Telefon raqami": "Telefon raqami",
|
||||||
|
"Kompaniya hujjati": "Kompaniya hujjati",
|
||||||
|
"Faylni tanlang": "Faylni tanlang",
|
||||||
|
"Tanlangan fayl": "Tanlangan fayl",
|
||||||
|
"PDF yoki Word formatida (maksimal 5MB)": "PDF yoki Word formatida (maksimal 5MB)",
|
||||||
|
"So'rov yuborish": "So'rov yuborish",
|
||||||
|
"Kompaniya nomi kamida 2 ta belgidan iborat bo'lishi kerak": "Kompaniya nomi kamida 2 ta belgidan iborat bo'lishi kerak",
|
||||||
|
"Ism kamida 2 ta belgidan iborat bo'lishi kerak": "Ism kamida 2 ta belgidan iborat bo'lishi kerak",
|
||||||
|
"To'g'ri email manzilini kiriting": "To'g'ri email manzilini kiriting",
|
||||||
|
"To'g'ri telefon raqamini kiriting": "To'g'ri telefon raqamini kiriting",
|
||||||
|
"File yuklash majburiy": "File yuklash majburiy",
|
||||||
|
|
||||||
|
"Maxfiylik Siyosati": "Maxfiylik Siyosati",
|
||||||
|
"Gastro Market sizning ma'lumotlaringiz xavfsizligini jiddiy qabul qiladi": "Gastro Market sizning ma'lumotlaringiz xavfsizligini jiddiy qabul qiladi",
|
||||||
|
"Oxirgi yangilanish: 16 Dekabr 2025": "Oxirgi yangilanish: 16 Dekabr 2025",
|
||||||
|
"Ushbu Maxfiylik Siyosati Gastro Market online magazinida siz tomonidan taqdim etilgan shaxsiy ma'lumotlarni qanday to'playmiz": "Ushbu Maxfiylik Siyosati Gastro Market online magazinida siz tomonidan taqdim etilgan shaxsiy ma'lumotlarni qanday to'playmiz, ishlatamiz va himoya qilishimizni tushuntiradi. Xizmatlarimizdan foydalanish orqali siz ushbu siyosatda tasvirlangan amaliyotlarga rozilik bildirasiz.",
|
||||||
|
|
||||||
|
"Biz To'playdigan Ma'lumotlar": "1. Biz To'playdigan Ma'lumotlar",
|
||||||
|
"Sizning tajribangizni yaxshilash uchun biz quyidagi ma'lumotlarni to'playmiz": "Sizning tajribangizni yaxshilash uchun biz quyidagi ma'lumotlarni to'playmiz:",
|
||||||
|
"Ism email manzil telefon raqami": "Ism, email manzil, telefon raqami",
|
||||||
|
"Kompaniya nomi, website, hamkorlik so'rovlari": "Kompaniya nomi, website, hamkorlik so'rovlari",
|
||||||
|
"Hamkorlik uchun yuklangan hujjatlar": "Hamkorlik uchun yuklangan hujjatlar",
|
||||||
|
"IP manzil, brauzer turi, qurilma ma'lumotlari": "IP manzil, brauzer turi, qurilma ma'lumotlari",
|
||||||
|
"Shaxsiy Ma'lumotlar": "Shaxsiy Ma'lumotlar",
|
||||||
|
"Kompaniya Ma'lumotlari": "Kompaniya Ma'lumotlari",
|
||||||
|
"Fayllar:": "Fayllar:",
|
||||||
|
"Texnik Ma'lumotlar": "Texnik Ma'lumotlar",
|
||||||
|
|
||||||
|
"Ma'lumotlardan Foydalanish": "2. Ma'lumotlardan Foydalanish",
|
||||||
|
"To'plangan ma'lumotlaringizdan quyidagi maqsadlarda foydalanamiz:": "To'plangan ma'lumotlaringizdan quyidagi maqsadlarda foydalanamiz:",
|
||||||
|
"Hamkorlik so'rovlarini qayta ishlash va javob berish": "Hamkorlik so'rovlarini qayta ishlash va javob berish",
|
||||||
|
"Sizga xizmatlarimiz va yangiliklar haqida ma'lumot berish": "Sizga xizmatlarimiz va yangiliklar haqida ma'lumot berish",
|
||||||
|
"Sayt xavfsizligini ta'minlash va firibgarlikka qarshi kurashish": "Sayt xavfsizligini ta'minlash va firibgarlikka qarshi kurashish",
|
||||||
|
"Foydalanuvchi tajribasini tahlil qilish va yaxshilash": "Foydalanuvchi tajribasini tahlil qilish va yaxshilash",
|
||||||
|
"Qonuniy talablarni bajarish": "Qonuniy talablarni bajarish",
|
||||||
|
|
||||||
|
"Ma'lumotlar Xavfsizligi": "3. Ma'lumotlar Xavfsizligi",
|
||||||
|
"Biz sizning shaxsiy ma'lumotlaringizni himoya qilish uchun zamonaviy xavfsizlik choralarini qo'llaymiz:": "Biz sizning shaxsiy ma'lumotlaringizni himoya qilish uchun zamonaviy xavfsizlik choralarini qo'llaymiz:",
|
||||||
|
"SSL/TLS shifrlash orqali ma'lumotlar uzatish": "SSL/TLS shifrlash orqali ma'lumotlar uzatish",
|
||||||
|
"Xavfsiz serverlar va ma'lumotlar bazasida saqlash": "Xavfsiz serverlar va ma'lumotlar bazasida saqlash",
|
||||||
|
"Cheklangan kirish huquqlari va autentifikatsiya": "Cheklangan kirish huquqlari va autentifikatsiya",
|
||||||
|
"Doimiy xavfsizlik monitoringi va yangilanishlar": "Doimiy xavfsizlik monitoringi va yangilanishlar",
|
||||||
|
|
||||||
|
"Ma'lumotlarni Ulashish": "4. Ma'lumotlarni Ulashish",
|
||||||
|
"Biz sizning shaxsiy ma'lumotlaringizni uchinchi shaxslarga sotmaymiz": "Biz sizning shaxsiy ma'lumotlaringizni uchinchi shaxslarga sotmaymiz",
|
||||||
|
"Sizning roziligingiz bilan": "Sizning roziligingiz bilan",
|
||||||
|
"Qonuniy talablar bo'yicha": "Qonuniy talablar bo'yicha",
|
||||||
|
"Xizmat ko'rsatuvchi ishonchli hamkorlar bilan (maxfiylik shartnomalari ostida)": "Xizmat ko'rsatuvchi ishonchli hamkorlar bilan (maxfiylik shartnomalari ostida)",
|
||||||
|
"Kompaniya birlashuvi yoki sotilishi holatida": "Kompaniya birlashuvi yoki sotilishi holatida",
|
||||||
|
|
||||||
|
"Biz Bilan Bog'lanish": "Biz Bilan Bog'lanish",
|
||||||
|
"Ushbu Maxfiylik Siyosati yoki shaxsiy ma'lumotlaringiz haqida savollaringiz bo'lsa, biz bilan bog'laning:": "Ushbu Maxfiylik Siyosati yoki shaxsiy ma'lumotlaringiz haqida savollaringiz bo'lsa, biz bilan bog'laning:",
|
||||||
|
"Telefon": "Telefon",
|
||||||
|
"Toshkent, O'zbekiston": "Toshkent, O'zbekiston",
|
||||||
|
"Manzil": "Manzil",
|
||||||
|
|
||||||
|
"Miqdor": "Miqdor",
|
||||||
|
"Jami": "Jami",
|
||||||
|
"Bepul yetkazib berish": "Bepul yetkazib berish",
|
||||||
|
"Kafolat": "Kafolat",
|
||||||
|
"Xususiyatlari": "Xususiyatlari",
|
||||||
|
"Qadoq turi": "Qadoq turi",
|
||||||
|
"Brandi": "Brend",
|
||||||
|
"Ishlab chiqaruvchi": "Ishlab chiqaruvchi",
|
||||||
|
"Hajmi": "Hajmi",
|
||||||
|
"O'xshash mahsulotlar": "O'xshash mahsulotlar",
|
||||||
|
|
||||||
|
"Hech narsa topilmadi": "Hech narsa topilmadi",
|
||||||
|
"Qidiruv natijalari": "Qidiruv natijalari",
|
||||||
|
"Tavsiya etiladi": "Tavsiya etiladi",
|
||||||
|
"Yuklanmoqda": "Yuklanmoqda....",
|
||||||
|
"Natija topilmadi": "Natija topilmadi",
|
||||||
|
|
||||||
|
"Asosiy": "Asosiy",
|
||||||
|
"Katalog": "Katalog",
|
||||||
|
"Sevimli": "Sevimli",
|
||||||
|
"Savatda": "Savatda",
|
||||||
|
"Profil": "Profil",
|
||||||
|
|
||||||
|
"Username yoki parol xato kiritildi": "Username yoki parol xato kiritildi",
|
||||||
|
"Tizimga kirish": "Tizimga kirish",
|
||||||
|
"Username": "Username",
|
||||||
|
"Parol": "Parol",
|
||||||
|
"Kirish": "Kirish",
|
||||||
|
|
||||||
|
"Savatingiz bo'sh": "Savatingiz bo'sh",
|
||||||
|
"Mahsulotlar qo'shish uchun katalogga o'ting": "Mahsulotlar qo'shish uchun katalogga o'ting",
|
||||||
|
"Xarid qilishni boshlash": "Xarid qilishni boshlash",
|
||||||
|
"Savat": "Savat",
|
||||||
|
"ta mahsulot": "ta mahsulot",
|
||||||
|
"Buyurtma haqida": "Buyurtma haqida",
|
||||||
|
"Mahsulotlar narxi": "Mahsulotlar narxi",
|
||||||
|
"Chegirma": "Chegirma",
|
||||||
|
"Yetkazib berish": "Yetkazib berish",
|
||||||
|
"Bepul": "Bepul",
|
||||||
|
"Buyurtmani rasmiylashtirish": "Buyurtmani rasmiylashtirish",
|
||||||
|
"Xaridni davom ettirish": "Xaridni davom ettirish",
|
||||||
|
"Tez yetkazib berish 1-2 kun ichida": "Tez yetkazib berish 1-2 kun ichida",
|
||||||
|
"Xavfsiz to'lov usullari": "Xavfsiz to'lov usullari",
|
||||||
|
|
||||||
|
"Buyurtma qabul qilindi!": "Buyurtma qabul qilindi!",
|
||||||
|
"Buyurtma raqami": "Buyurtma raqami",
|
||||||
|
"Buyurtmangiz muvaffaqiyatli qabul qilindi": "Buyurtmangiz muvaffaqiyatli qabul qilindi.",
|
||||||
|
"Bosh sahifaga qaytish": "Bosh sahifaga qaytish",
|
||||||
|
"Ma'lumotlaringizni to'ldiring": "Ma'lumotlaringizni to'ldiring",
|
||||||
|
"Shaxsiy ma'lumotlar": "Shaxsiy ma'lumotlar",
|
||||||
|
"Ism": "Ism",
|
||||||
|
"Ismingiz": "Ismingiz",
|
||||||
|
"Familiya": "Familiya",
|
||||||
|
"Familiyangiz": "Familiyangiz",
|
||||||
|
"Telefon raqam": "Telefon raqam",
|
||||||
|
"Izoh": "Izoh",
|
||||||
|
"Yetkazib berish manzili": "Yetkazib berish manzili",
|
||||||
|
"Manzilni qidirish": "Manzilni qidirish",
|
||||||
|
"Toshkent": "Toshkent",
|
||||||
|
"Mening joylashuvim": "Mening joylashuvim",
|
||||||
|
"Yetkazib berish usuli": "Yetkazib berish usuli",
|
||||||
|
"Standart yetkazib berish": "Standart yetkazib berish",
|
||||||
|
"2-3 kun ichida": "2-3 kun ichida",
|
||||||
|
"Tez yetkazib berish": "Tez yetkazib berish",
|
||||||
|
"1 kun ichida": "1 kun ichida",
|
||||||
|
"To'lov usuli": "To'lov usuli",
|
||||||
|
"Naqd pul": "Naqd pul",
|
||||||
|
"Yetkazib berishda to'lash": "Yetkazib berishda to'lash",
|
||||||
|
"Plastik karta": "Plastik karta",
|
||||||
|
"Online to'lov": "Online to'lov",
|
||||||
|
"Mahsulotlar": "Mahsulotlar",
|
||||||
|
"Buyurtmani tasdiqlash": "Buyurtmani tasdiqlash",
|
||||||
|
"Majburiy maydon": "Majburiy maydon",
|
||||||
|
"Xato raqam kiritildi": "Xato raqam kiritildi",
|
||||||
|
"Orqaga": "Orqaga",
|
||||||
|
|
||||||
|
"Sevimlilar bo'sh": "Sevimlilar bo'sh",
|
||||||
|
"Hali hech qanday mahsulotni sevimlilarga qo'shmadingiz": "Hali hech qanday mahsulotni sevimlilarga qo'shmadingiz. Mahsulotlar ro'yxatiga o'ting va yoqqan mahsulotlaringizni saqlang.",
|
||||||
|
"Sevimli mahsulotlar": "Sevimli mahsulotlar",
|
||||||
|
|
||||||
|
"Faol buyurtmalar": "Faol buyurtmalar",
|
||||||
|
"Barchasi": "Barchasi",
|
||||||
|
"Buyurtmalar tarixi": "Buyurtmalar tarixi",
|
||||||
|
"Qayta": "Qayta",
|
||||||
|
"Chiqish": "Chiqish",
|
||||||
|
"Umumiy": "Umumiy",
|
||||||
|
"Buyurtmalar": "Buyurtmalar",
|
||||||
|
"Tarix": "Tarix",
|
||||||
|
|
||||||
|
"Faol": "Faol",
|
||||||
|
"Tugadi": "Tugadi",
|
||||||
|
"Yetkazish": "Yetkazish",
|
||||||
|
"Yo'lda": "Yo'lda",
|
||||||
|
"Punktda": "Punktda",
|
||||||
|
"Yetkazildi": "Yetkazildi",
|
||||||
|
"Bekor qilindi": "Bekor qilindi",
|
||||||
|
"Bu hafta": "Bu hafta",
|
||||||
|
"Bu oy": "Bu oy",
|
||||||
|
"Qo'llab-quvatlash": "Qo'llab-quvatlash",
|
||||||
|
"Agarda sizda kirish uchun login va parol yo'q bolsa iltimos bizga murojat qiling": "Agarda sizda kirish uchun login va parol yo'q bolsa iltimos bizga murojat qiling",
|
||||||
|
"Murojat qilish": "Murojat qilish",
|
||||||
|
"So'rov yuborildi!": "So'rov yuborildi!",
|
||||||
|
|
||||||
|
"Tez-tez So'raladigan Savollar": "Tez-tez So'raladigan Savollar",
|
||||||
|
"Gastro Market haqida eng ko'p so'raladigan savollarga javoblar": "Gastro Market haqida eng ko'p so'raladigan savollarga javoblar"
|
||||||
}
|
}
|
||||||
|
|||||||
14
src/shared/hooks/cartId.ts
Normal file
14
src/shared/hooks/cartId.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import { create } from 'zustand';
|
||||||
|
|
||||||
|
type State = {
|
||||||
|
cart_id: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
type Actions = {
|
||||||
|
setCartId: (cartId: string | null) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useCartId = create<State & Actions>((set) => ({
|
||||||
|
cart_id: null,
|
||||||
|
setCartId: (cartId: string | null) => set(() => ({ cart_id: cartId })),
|
||||||
|
}));
|
||||||
6
src/shared/lib/onlyNumber.ts
Normal file
6
src/shared/lib/onlyNumber.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
const onlyNumber = (digits: string | number) => {
|
||||||
|
const phone = digits.toString();
|
||||||
|
return phone.replace(/\D/g, '');
|
||||||
|
};
|
||||||
|
|
||||||
|
export default onlyNumber;
|
||||||
28
src/shared/lib/token.ts
Normal file
28
src/shared/lib/token.ts
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import cookie from 'js-cookie';
|
||||||
|
|
||||||
|
const TOKEN = 'gastro-token';
|
||||||
|
const USER = 'gastro-user';
|
||||||
|
|
||||||
|
export const getToken = () => {
|
||||||
|
return cookie.get(TOKEN);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getMe = () => {
|
||||||
|
return cookie.get(USER);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const removeToken = () => {
|
||||||
|
cookie.remove(TOKEN);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const removeUser = () => {
|
||||||
|
cookie.remove(USER);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const setToken = (value: string) => {
|
||||||
|
cookie.set(TOKEN, value);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const setUser = (value: string) => {
|
||||||
|
cookie.set(USER, value);
|
||||||
|
};
|
||||||
66
src/shared/ui/alert.tsx
Normal file
66
src/shared/ui/alert.tsx
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import { cva, type VariantProps } from 'class-variance-authority';
|
||||||
|
|
||||||
|
import { cn } from '@/shared/lib/utils';
|
||||||
|
|
||||||
|
const alertVariants = cva(
|
||||||
|
'relative w-full rounded-lg border px-4 py-3 text-sm grid has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] grid-cols-[0_1fr] has-[>svg]:gap-x-3 gap-y-0.5 items-start [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current',
|
||||||
|
{
|
||||||
|
variants: {
|
||||||
|
variant: {
|
||||||
|
default: 'bg-card text-card-foreground',
|
||||||
|
destructive:
|
||||||
|
'text-destructive bg-card [&>svg]:text-current *:data-[slot=alert-description]:text-destructive/90',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
variant: 'default',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
function Alert({
|
||||||
|
className,
|
||||||
|
variant,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<'div'> & VariantProps<typeof alertVariants>) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="alert"
|
||||||
|
role="alert"
|
||||||
|
className={cn(alertVariants({ variant }), className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function AlertTitle({ className, ...props }: React.ComponentProps<'div'>) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="alert-title"
|
||||||
|
className={cn(
|
||||||
|
'col-start-2 line-clamp-1 min-h-4 font-medium tracking-tight',
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function AlertDescription({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<'div'>) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="alert-description"
|
||||||
|
className={cn(
|
||||||
|
'text-muted-foreground col-start-2 grid justify-items-start gap-1 text-sm [&_p]:leading-relaxed',
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Alert, AlertTitle, AlertDescription };
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import * as React from 'react';
|
|
||||||
import type * as LabelPrimitive from '@radix-ui/react-label';
|
import type * as LabelPrimitive from '@radix-ui/react-label';
|
||||||
import { Slot } from '@radix-ui/react-slot';
|
import { Slot } from '@radix-ui/react-slot';
|
||||||
|
import * as React from 'react';
|
||||||
import {
|
import {
|
||||||
Controller,
|
Controller,
|
||||||
FormProvider,
|
FormProvider,
|
||||||
@@ -15,6 +15,7 @@ import {
|
|||||||
|
|
||||||
import { cn } from '@/shared/lib/utils';
|
import { cn } from '@/shared/lib/utils';
|
||||||
import { Label } from '@/shared/ui/label';
|
import { Label } from '@/shared/ui/label';
|
||||||
|
import { useTranslations } from 'next-intl';
|
||||||
|
|
||||||
const Form = FormProvider;
|
const Form = FormProvider;
|
||||||
|
|
||||||
@@ -137,6 +138,7 @@ function FormDescription({ className, ...props }: React.ComponentProps<'p'>) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function FormMessage({ className, ...props }: React.ComponentProps<'p'>) {
|
function FormMessage({ className, ...props }: React.ComponentProps<'p'>) {
|
||||||
|
const t = useTranslations();
|
||||||
const { error, formMessageId } = useFormField();
|
const { error, formMessageId } = useFormField();
|
||||||
const body = error ? String(error?.message ?? '') : props.children;
|
const body = error ? String(error?.message ?? '') : props.children;
|
||||||
|
|
||||||
@@ -151,18 +153,18 @@ function FormMessage({ className, ...props }: React.ComponentProps<'p'>) {
|
|||||||
className={cn('text-destructive text-sm', className)}
|
className={cn('text-destructive text-sm', className)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{body}
|
{error && error.message ? t(error.message) : body}
|
||||||
</p>
|
</p>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export {
|
export {
|
||||||
useFormField,
|
|
||||||
Form,
|
Form,
|
||||||
FormItem,
|
|
||||||
FormLabel,
|
|
||||||
FormControl,
|
FormControl,
|
||||||
FormDescription,
|
FormDescription,
|
||||||
FormMessage,
|
|
||||||
FormField,
|
FormField,
|
||||||
|
FormItem,
|
||||||
|
FormLabel,
|
||||||
|
FormMessage,
|
||||||
|
useFormField,
|
||||||
};
|
};
|
||||||
|
|||||||
102
src/shared/ui/global-pagination.tsx
Normal file
102
src/shared/ui/global-pagination.tsx
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { ChevronLeft, ChevronRight } from 'lucide-react';
|
||||||
|
import { Button } from './button';
|
||||||
|
|
||||||
|
type GlobalPaginationProps = {
|
||||||
|
page: number;
|
||||||
|
total: number;
|
||||||
|
pageSize?: number;
|
||||||
|
onChange: (page: number) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getPages = (current: number, total: number) => {
|
||||||
|
const pages: (number | 'dots')[] = [];
|
||||||
|
|
||||||
|
if (total <= 7) {
|
||||||
|
return Array.from({ length: total }, (_, i) => i + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
pages.push(1);
|
||||||
|
|
||||||
|
if (current > 4) {
|
||||||
|
pages.push('dots');
|
||||||
|
}
|
||||||
|
|
||||||
|
const start = Math.max(2, current - 1);
|
||||||
|
const end = Math.min(total - 1, current + 1);
|
||||||
|
|
||||||
|
for (let i = start; i <= end; i++) {
|
||||||
|
pages.push(i);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (current < total - 3) {
|
||||||
|
pages.push('dots');
|
||||||
|
}
|
||||||
|
|
||||||
|
pages.push(total);
|
||||||
|
|
||||||
|
return pages;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const GlobalPagination = ({
|
||||||
|
page,
|
||||||
|
total,
|
||||||
|
pageSize = 36,
|
||||||
|
onChange,
|
||||||
|
}: GlobalPaginationProps) => {
|
||||||
|
const totalPages = Math.ceil(total / pageSize);
|
||||||
|
if (totalPages <= 1) return null;
|
||||||
|
|
||||||
|
const pages = getPages(page, totalPages);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex items-center justify-center gap-2">
|
||||||
|
<Button
|
||||||
|
variant={'outline'}
|
||||||
|
onClick={() => page > 1 && onChange(page - 1)}
|
||||||
|
disabled={page === 1}
|
||||||
|
className="flex items-center justify-center w-10 cursor-pointer h-10 rounded-lg transition-all duration-200 hover:bg-gray-100 disabled:opacity-40 disabled:cursor-not-allowed disabled:hover:bg-transparent"
|
||||||
|
>
|
||||||
|
<ChevronLeft className="!w-6 !h-6 text-gray-600" />
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
{/* Page Numbers */}
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
{pages.map((p, i) => (
|
||||||
|
<div key={i}>
|
||||||
|
{p === 'dots' ? (
|
||||||
|
<span className="flex items-center justify-center w-9 h-9 text-gray-400 font-medium">
|
||||||
|
···
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<Button
|
||||||
|
variant={'outline'}
|
||||||
|
onClick={() => onChange(p)}
|
||||||
|
className={`
|
||||||
|
flex items-center justify-center w-10 h-10 px-3 rounded-lg font-medium transition-all duration-200
|
||||||
|
${
|
||||||
|
p === page
|
||||||
|
? 'bg-blue-600 text-white shadow-md shadow-blue-200 scale-105'
|
||||||
|
: 'text-gray-700 hover:bg-gray-100 hover:scale-105'
|
||||||
|
}
|
||||||
|
`}
|
||||||
|
>
|
||||||
|
{p}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
onClick={() => page < totalPages && onChange(page + 1)}
|
||||||
|
disabled={page === totalPages}
|
||||||
|
variant={'outline'}
|
||||||
|
className="flex items-center justify-center w-10 cursor-pointer h-10 rounded-lg transition-all duration-200 hover:bg-gray-100 disabled:opacity-40 disabled:cursor-not-allowed disabled:hover:bg-transparent"
|
||||||
|
>
|
||||||
|
<ChevronRight className="!w-6 !h-6 text-gray-600" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
126
src/shared/ui/pagination.tsx
Normal file
126
src/shared/ui/pagination.tsx
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
import {
|
||||||
|
ChevronLeftIcon,
|
||||||
|
ChevronRightIcon,
|
||||||
|
MoreHorizontalIcon,
|
||||||
|
} from 'lucide-react';
|
||||||
|
import * as React from 'react';
|
||||||
|
|
||||||
|
import { cn } from '@/shared/lib/utils';
|
||||||
|
import { buttonVariants, type Button } from '@/shared/ui/button';
|
||||||
|
|
||||||
|
function Pagination({ className, ...props }: React.ComponentProps<'nav'>) {
|
||||||
|
return (
|
||||||
|
<nav
|
||||||
|
role="navigation"
|
||||||
|
aria-label="pagination"
|
||||||
|
data-slot="pagination"
|
||||||
|
className={cn('mx-auto flex w-full justify-center', className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function PaginationContent({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<'ul'>) {
|
||||||
|
return (
|
||||||
|
<ul
|
||||||
|
data-slot="pagination-content"
|
||||||
|
className={cn('flex flex-row items-center gap-1', className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function PaginationItem({ ...props }: React.ComponentProps<'li'>) {
|
||||||
|
return <li data-slot="pagination-item" {...props} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
type PaginationLinkProps = {
|
||||||
|
isActive?: boolean;
|
||||||
|
} & Pick<React.ComponentProps<typeof Button>, 'size'> &
|
||||||
|
React.ComponentProps<'a'>;
|
||||||
|
|
||||||
|
function PaginationLink({
|
||||||
|
className,
|
||||||
|
isActive,
|
||||||
|
size = 'icon',
|
||||||
|
...props
|
||||||
|
}: PaginationLinkProps) {
|
||||||
|
return (
|
||||||
|
<a
|
||||||
|
aria-current={isActive ? 'page' : undefined}
|
||||||
|
data-slot="pagination-link"
|
||||||
|
data-active={isActive}
|
||||||
|
className={cn(
|
||||||
|
buttonVariants({
|
||||||
|
variant: isActive ? 'outline' : 'ghost',
|
||||||
|
size,
|
||||||
|
}),
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function PaginationPrevious({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof PaginationLink>) {
|
||||||
|
return (
|
||||||
|
<PaginationLink
|
||||||
|
aria-label="Go to previous page"
|
||||||
|
size="default"
|
||||||
|
className={cn('gap-1 px-2.5 sm:pl-2.5', className)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<ChevronLeftIcon />
|
||||||
|
<span className="hidden sm:block">Previous</span>
|
||||||
|
</PaginationLink>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function PaginationNext({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof PaginationLink>) {
|
||||||
|
return (
|
||||||
|
<PaginationLink
|
||||||
|
aria-label="Go to next page"
|
||||||
|
size="default"
|
||||||
|
className={cn('gap-1 px-2.5 sm:pr-2.5', className)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<ChevronRightIcon />
|
||||||
|
</PaginationLink>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function PaginationEllipsis({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<'span'>) {
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
aria-hidden
|
||||||
|
data-slot="pagination-ellipsis"
|
||||||
|
className={cn('flex size-9 items-center justify-center', className)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<MoreHorizontalIcon className="size-4" />
|
||||||
|
<span className="sr-only">More pages</span>
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
Pagination,
|
||||||
|
PaginationContent,
|
||||||
|
PaginationEllipsis,
|
||||||
|
PaginationItem,
|
||||||
|
PaginationLink,
|
||||||
|
PaginationNext,
|
||||||
|
PaginationPrevious,
|
||||||
|
};
|
||||||
13
src/shared/ui/skeleton.tsx
Normal file
13
src/shared/ui/skeleton.tsx
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import { cn } from '@/shared/lib/utils';
|
||||||
|
|
||||||
|
function Skeleton({ className, ...props }: React.ComponentProps<'div'>) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="skeleton"
|
||||||
|
className={cn('bg-accent animate-pulse rounded-md', className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Skeleton };
|
||||||
42
src/widgets/animation/FlyingAnimationPortal.tsx
Normal file
42
src/widgets/animation/FlyingAnimationPortal.tsx
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { ProductListResult } from '@/shared/config/api/product/type';
|
||||||
|
import {
|
||||||
|
Dispatch,
|
||||||
|
RefObject,
|
||||||
|
SetStateAction,
|
||||||
|
useEffect,
|
||||||
|
useState,
|
||||||
|
} from 'react';
|
||||||
|
import { createPortal } from 'react-dom';
|
||||||
|
import Animation from '../categories/ui/animation';
|
||||||
|
|
||||||
|
export function FlyingAnimationPortal({
|
||||||
|
product,
|
||||||
|
animated,
|
||||||
|
imageRef,
|
||||||
|
setAnimated,
|
||||||
|
}: {
|
||||||
|
product: ProductListResult;
|
||||||
|
animated: boolean;
|
||||||
|
imageRef: RefObject<HTMLDivElement | null>;
|
||||||
|
setAnimated: Dispatch<SetStateAction<boolean>>;
|
||||||
|
}) {
|
||||||
|
const [mounted, setMounted] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setMounted(true);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
if (!mounted) return null;
|
||||||
|
|
||||||
|
return createPortal(
|
||||||
|
<Animation
|
||||||
|
product={product}
|
||||||
|
animated={animated}
|
||||||
|
imageRef={imageRef}
|
||||||
|
setAnimated={setAnimated}
|
||||||
|
/>,
|
||||||
|
document.body,
|
||||||
|
);
|
||||||
|
}
|
||||||
0
src/widgets/categories/lib/api.ts
Normal file
0
src/widgets/categories/lib/api.ts
Normal file
0
src/widgets/categories/lib/type.ts
Normal file
0
src/widgets/categories/lib/type.ts
Normal file
204
src/widgets/categories/ui/animation.tsx
Normal file
204
src/widgets/categories/ui/animation.tsx
Normal file
@@ -0,0 +1,204 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { ProductListResult } from '@/shared/config/api/product/type';
|
||||||
|
import { BASE_URL } from '@/shared/config/api/URLs';
|
||||||
|
import React, { useEffect, useRef } from 'react';
|
||||||
|
|
||||||
|
interface AnimationProps {
|
||||||
|
product: ProductListResult;
|
||||||
|
animated: boolean;
|
||||||
|
imageRef: React.RefObject<HTMLDivElement | null>;
|
||||||
|
setAnimated: (value: boolean) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Animation: React.FC<AnimationProps> = ({
|
||||||
|
product,
|
||||||
|
animated,
|
||||||
|
imageRef,
|
||||||
|
setAnimated,
|
||||||
|
}) => {
|
||||||
|
const flyingImageRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (
|
||||||
|
!animated ||
|
||||||
|
!imageRef.current ||
|
||||||
|
!flyingImageRef.current ||
|
||||||
|
window.innerWidth <= 1024
|
||||||
|
)
|
||||||
|
return;
|
||||||
|
|
||||||
|
const cartIcon = document.getElementById('cart-icon');
|
||||||
|
if (!cartIcon) {
|
||||||
|
setAnimated(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const flyingImg = flyingImageRef.current;
|
||||||
|
const startRect = imageRef.current.getBoundingClientRect();
|
||||||
|
const endRect = cartIcon.getBoundingClientRect();
|
||||||
|
|
||||||
|
// Markaz nuqtalar
|
||||||
|
const startCenterX = startRect.left + startRect.width / 2;
|
||||||
|
const startCenterY = startRect.top + startRect.height / 2;
|
||||||
|
const endCenterX = endRect.left + endRect.width / 2;
|
||||||
|
const endCenterY = endRect.top + endRect.height / 2;
|
||||||
|
|
||||||
|
// Boshlang'ich holat — aniq markazda, fixed
|
||||||
|
Object.assign(flyingImg.style, {
|
||||||
|
position: 'fixed',
|
||||||
|
left: `${startCenterX}px`,
|
||||||
|
top: `${startCenterY}px`,
|
||||||
|
width: `${startRect.width}px`,
|
||||||
|
height: `${startRect.height}px`,
|
||||||
|
opacity: '1',
|
||||||
|
transform: 'translate(-50%, -50%) scale(1)',
|
||||||
|
transition: 'none',
|
||||||
|
pointerEvents: 'none',
|
||||||
|
zIndex: '9999',
|
||||||
|
});
|
||||||
|
|
||||||
|
// Keyingi frame'da animatsiya boshlanadi
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
Object.assign(flyingImg.style, {
|
||||||
|
transition: 'all 0.9s cubic-bezier(0.2, 0.8, 0.4, 1)',
|
||||||
|
left: `${endCenterX}px`,
|
||||||
|
top: `${endCenterY}px`,
|
||||||
|
width: '60px',
|
||||||
|
height: '60px',
|
||||||
|
transform: 'translate(-50%, -50%) scale(0.3)',
|
||||||
|
opacity: '0',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Cart bounce — class emas, inline style bilan (ishonchliroq)
|
||||||
|
cartIcon.style.transform = 'scale(1.3)';
|
||||||
|
cartIcon.style.transition = 'transform 0.8s cubic-bezier(0.2, 0.8, 0.4, 1)';
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
cartIcon.style.transform = 'scale(1)';
|
||||||
|
}, 800);
|
||||||
|
|
||||||
|
// Tozalash
|
||||||
|
const timer = setTimeout(() => {
|
||||||
|
setAnimated(false);
|
||||||
|
}, 1000);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
clearTimeout(timer);
|
||||||
|
cartIcon.style.transform = '';
|
||||||
|
cartIcon.style.transition = '';
|
||||||
|
};
|
||||||
|
}, [animated, imageRef, setAnimated]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (
|
||||||
|
!animated ||
|
||||||
|
!imageRef.current ||
|
||||||
|
!flyingImageRef.current ||
|
||||||
|
window.innerWidth >= 1024
|
||||||
|
)
|
||||||
|
return;
|
||||||
|
|
||||||
|
// Mobil va desktopdagi cart iconlarni izlash
|
||||||
|
const cartIconMobile = document.getElementById('cart-icon-mobile');
|
||||||
|
|
||||||
|
// Avval mobilni tekshir, chunki lg:hidden bo'lsa ham DOMda bo'lishi mumkin
|
||||||
|
const cartIcon = cartIconMobile;
|
||||||
|
|
||||||
|
if (!cartIcon) {
|
||||||
|
console.warn('Cart icon topilmadi (mobil yoki desktop)');
|
||||||
|
setAnimated(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const flyingImg = flyingImageRef.current;
|
||||||
|
const startRect = imageRef.current.getBoundingClientRect();
|
||||||
|
const endRect = cartIcon.getBoundingClientRect();
|
||||||
|
|
||||||
|
// Markaz nuqtalar
|
||||||
|
const startCenterX = startRect.left + startRect.width / 2;
|
||||||
|
const startCenterY = startRect.top + startRect.height / 2;
|
||||||
|
const endCenterX = endRect.left + endRect.width / 2;
|
||||||
|
const endCenterY = endRect.top + endRect.height / 2;
|
||||||
|
|
||||||
|
// Boshlang'ich holat
|
||||||
|
Object.assign(flyingImg.style, {
|
||||||
|
position: 'fixed',
|
||||||
|
left: `${startCenterX}px`,
|
||||||
|
top: `${startCenterY}px`,
|
||||||
|
width: `${startRect.width}px`,
|
||||||
|
height: `${startRect.height}px`,
|
||||||
|
opacity: '1',
|
||||||
|
transform: 'translate(-50%, -50%) scale(1)',
|
||||||
|
transition: 'none',
|
||||||
|
pointerEvents: 'none',
|
||||||
|
zIndex: '9999',
|
||||||
|
});
|
||||||
|
|
||||||
|
// Animatsiya boshlanishi
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
Object.assign(flyingImg.style, {
|
||||||
|
transition: 'all 0.9s cubic-bezier(0.2, 0.8, 0.4, 1)',
|
||||||
|
left: `${endCenterX}px`,
|
||||||
|
top: `${endCenterY}px`,
|
||||||
|
width: '60px',
|
||||||
|
height: '60px',
|
||||||
|
transform: 'translate(-50%, -50%) scale(0.3)',
|
||||||
|
opacity: '0',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Cart bounce effekti
|
||||||
|
cartIcon.style.transform = 'scale(1.4)';
|
||||||
|
cartIcon.style.transition = 'transform 0.6s cubic-bezier(0.2, 0.8, 0.4, 1)';
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
cartIcon.style.transform = 'scale(1)';
|
||||||
|
}, 600);
|
||||||
|
|
||||||
|
// Tozalash
|
||||||
|
const timer = setTimeout(() => {
|
||||||
|
setAnimated(false);
|
||||||
|
}, 1000);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
clearTimeout(timer);
|
||||||
|
if (cartIcon) {
|
||||||
|
cartIcon.style.transform = '';
|
||||||
|
cartIcon.style.transition = '';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [animated, imageRef, setAnimated]);
|
||||||
|
|
||||||
|
if (!animated) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div
|
||||||
|
ref={flyingImageRef}
|
||||||
|
className="rounded-xl overflow-hidden shadow-2xl border-4 border-green-500 pointer-events-none"
|
||||||
|
style={{
|
||||||
|
opacity: 0,
|
||||||
|
transform: 'translate(-50%, -50%)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="relative w-full h-full bg-white">
|
||||||
|
<img
|
||||||
|
src={
|
||||||
|
product?.image?.includes(BASE_URL)
|
||||||
|
? product.image
|
||||||
|
: BASE_URL + product.image
|
||||||
|
}
|
||||||
|
alt={product.name}
|
||||||
|
className="w-full h-full object-contain p-3"
|
||||||
|
/>
|
||||||
|
<div className="absolute inset-0 bg-gradient-to-br from-green-400/40 to-transparent" />
|
||||||
|
<div className="absolute inset-0 ring-8 ring-green-400/40 rounded-xl animate-ping" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Animation;
|
||||||
@@ -1,46 +1,81 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { SubCategory } from '@/features/category/lib/data';
|
import { CategoryResult } from '@/shared/config/api/category/type';
|
||||||
|
import { product_api } from '@/shared/config/api/product/api';
|
||||||
import { useRouter } from '@/shared/config/i18n/navigation';
|
import { useRouter } from '@/shared/config/i18n/navigation';
|
||||||
|
import { cn } from '@/shared/lib/utils';
|
||||||
|
import { Button } from '@/shared/ui/button';
|
||||||
|
import { Card } from '@/shared/ui/card';
|
||||||
import {
|
import {
|
||||||
Carousel,
|
Carousel,
|
||||||
|
CarouselApi,
|
||||||
CarouselContent,
|
CarouselContent,
|
||||||
CarouselItem,
|
CarouselItem,
|
||||||
CarouselNext,
|
|
||||||
CarouselPrevious,
|
|
||||||
} from '@/shared/ui/carousel';
|
} from '@/shared/ui/carousel';
|
||||||
import { ChevronRight } from 'lucide-react';
|
import { Skeleton } from '@/shared/ui/skeleton';
|
||||||
import { useState } from 'react';
|
import { useQuery } from '@tanstack/react-query';
|
||||||
|
import { ChevronLeft, ChevronRight } from 'lucide-react';
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
import { ProductCard } from './product-card';
|
import { ProductCard } from './product-card';
|
||||||
|
|
||||||
export function CategoryCarousel({ category }: { category: SubCategory }) {
|
export function CategoryCarousel({ category }: { category: CategoryResult }) {
|
||||||
const [products, setProducts] = useState(category.products);
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const [api, setApi] = useState<CarouselApi>();
|
||||||
|
const [canScrollPrev, setCanScrollPrev] = useState(false);
|
||||||
|
const [canScrollNext, setCanScrollNext] = useState(false);
|
||||||
|
|
||||||
const handleRemove = (id: number) => {
|
useEffect(() => {
|
||||||
setProducts((prev) =>
|
if (!api) return;
|
||||||
prev.map((product) =>
|
|
||||||
product.id === id ? { ...product, liked: false } : product,
|
const updateButtons = () => {
|
||||||
),
|
setCanScrollPrev(api.canScrollPrev());
|
||||||
);
|
setCanScrollNext(api.canScrollNext());
|
||||||
|
};
|
||||||
|
|
||||||
|
updateButtons();
|
||||||
|
api.on('select', updateButtons);
|
||||||
|
api.on('reInit', updateButtons);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
api.off('select', updateButtons);
|
||||||
|
api.off('reInit', updateButtons);
|
||||||
|
};
|
||||||
|
}, [api]);
|
||||||
|
|
||||||
|
const scrollPrev = () => {
|
||||||
|
if (api) {
|
||||||
|
api?.scrollPrev();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleLiked = (id: number) => {
|
const scrollNext = () => {
|
||||||
setProducts((prev) =>
|
if (api) {
|
||||||
prev.map((product) =>
|
api?.scrollNext();
|
||||||
product.id === id ? { ...product, liked: true } : product,
|
}
|
||||||
),
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const { data: product, isLoading } = useQuery({
|
||||||
|
queryKey: ['product_list', category],
|
||||||
|
queryFn: () =>
|
||||||
|
product_api.listGetCategoryId({
|
||||||
|
category_id: category.id,
|
||||||
|
params: { page: 1, page_size: 16 },
|
||||||
|
}),
|
||||||
|
select(data) {
|
||||||
|
return data.data;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (product?.results.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="relative custom-container mt-8 justify-center items-center">
|
<section className="relative custom-container mt-5 justify-center items-center border-b border-slate-200">
|
||||||
<div className="flex items-center justify-between mb-6 pb-3 border-b border-slate-200">
|
<div className="flex items-center justify-between pb-3">
|
||||||
<div
|
<div
|
||||||
className="flex items-center gap-2 group cursor-pointer"
|
className="flex items-center gap-2 group cursor-pointer"
|
||||||
onClick={() =>
|
onClick={() => router.push(`/category/${category.id}/`)}
|
||||||
router.push(`/category/${category.category}/${category.name}`)
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
<h2 className="text-2xl font-bold text-slate-800 group-hover:text-blue-600 transition-colors">
|
<h2 className="text-2xl font-bold text-slate-800 group-hover:text-blue-600 transition-colors">
|
||||||
{category.name}
|
{category.name}
|
||||||
@@ -51,25 +86,62 @@ export function CategoryCarousel({ category }: { category: SubCategory }) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Carousel className="w-full">
|
<Carousel className="w-full mt-2" setApi={setApi}>
|
||||||
<CarouselContent className="pr-[12%] sm:pr-0">
|
<CarouselContent className="pr-[12%] sm:pr-0">
|
||||||
{products.slice(0, 12).map((product) => (
|
{isLoading &&
|
||||||
<CarouselItem
|
Array.from({ length: 6 }).map((__, index) => (
|
||||||
key={product.id}
|
<CarouselItem
|
||||||
className="basis-1/2 sm:basis-1/3 md:basis-1/4 lg:basis-1/6 pb-2"
|
key={index}
|
||||||
>
|
className="basis-1/2 sm:basis-1/3 md:basis-1/4 lg:basis-1/5 xl:basis-1/6 pb-2"
|
||||||
<ProductCard
|
>
|
||||||
product={product}
|
<Card className="p-3 space-y-3 rounded-xl">
|
||||||
handleRemove={handleRemove}
|
<Skeleton className="h-40 sm:h-48 md:h-56 w-full rounded-lg" />
|
||||||
handleLiked={handleLiked}
|
<Skeleton className="h-4 w-3/4" />
|
||||||
/>
|
<Skeleton className="h-4 w-1/2" />
|
||||||
</CarouselItem>
|
<Skeleton className="h-10 w-full rounded-lg" />
|
||||||
))}
|
</Card>
|
||||||
|
</CarouselItem>
|
||||||
|
))}
|
||||||
|
{product &&
|
||||||
|
!isLoading &&
|
||||||
|
product.results
|
||||||
|
.filter((product) => product.is_active)
|
||||||
|
.map((product) => (
|
||||||
|
<CarouselItem
|
||||||
|
key={product.id}
|
||||||
|
className="basis-1/2 sm:basis-1/3 md:basis-1/4 lg:basis-1/5 xl:basis-1/6 pb-2"
|
||||||
|
>
|
||||||
|
<ProductCard product={product} />
|
||||||
|
</CarouselItem>
|
||||||
|
))}
|
||||||
</CarouselContent>
|
</CarouselContent>
|
||||||
|
|
||||||
<CarouselPrevious className="hidden lg:flex -top-12 right-12 w-9 h-9 bg-green-600 hover:bg-green-600 text-white border-0 cursor-pointer" />
|
|
||||||
<CarouselNext className="hidden lg:flex -top-12 right-0 w-9 h-9 bg-green-600 text-white border-0 hover:bg-green-600 cursor-pointer" />
|
|
||||||
</Carousel>
|
</Carousel>
|
||||||
|
<Button
|
||||||
|
onClick={scrollNext}
|
||||||
|
className={cn(
|
||||||
|
'absolute top-0 right-4 max-lg:hidden text-white cursor-pointer',
|
||||||
|
canScrollNext
|
||||||
|
? 'bg-green-600 hover:bg-green-600/70'
|
||||||
|
: 'bg-green-600/50 cursor-not-allowed',
|
||||||
|
)}
|
||||||
|
disabled={!canScrollNext}
|
||||||
|
size="icon"
|
||||||
|
>
|
||||||
|
<ChevronRight className="size-6" />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={scrollPrev}
|
||||||
|
className={cn(
|
||||||
|
'absolute top-0 right-16 max-lg:hidden text-white cursor-pointer',
|
||||||
|
canScrollPrev
|
||||||
|
? 'bg-green-600 hover:bg-green-600/70'
|
||||||
|
: 'bg-green-600/50 cursor-not-allowed',
|
||||||
|
)}
|
||||||
|
disabled={!canScrollPrev}
|
||||||
|
size="icon"
|
||||||
|
>
|
||||||
|
<ChevronLeft className="size-6" />
|
||||||
|
</Button>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,170 +1,331 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
|
import { cart_api } from '@/features/cart/lib/api';
|
||||||
|
import { product_api } from '@/shared/config/api/product/api';
|
||||||
|
import { ProductListResult } from '@/shared/config/api/product/type';
|
||||||
|
import { BASE_URL } from '@/shared/config/api/URLs';
|
||||||
import { useRouter } from '@/shared/config/i18n/navigation';
|
import { useRouter } from '@/shared/config/i18n/navigation';
|
||||||
|
import { useCartId } from '@/shared/hooks/cartId';
|
||||||
import formatPrice from '@/shared/lib/formatPrice';
|
import formatPrice from '@/shared/lib/formatPrice';
|
||||||
|
import { Alert, AlertDescription, AlertTitle } from '@/shared/ui/alert';
|
||||||
import { Button } from '@/shared/ui/button';
|
import { Button } from '@/shared/ui/button';
|
||||||
import { Card, CardContent } from '@/shared/ui/card';
|
import { Card, CardContent } from '@/shared/ui/card';
|
||||||
import { Input } from '@/shared/ui/input';
|
import { Input } from '@/shared/ui/input';
|
||||||
import { Heart, Minus, Plus, ShoppingCart, Star } from 'lucide-react';
|
import { FlyingAnimationPortal } from '@/widgets/animation/FlyingAnimationPortal';
|
||||||
|
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||||
|
import { AxiosError } from 'axios';
|
||||||
|
import { Heart, Minus, Plus, ShoppingCart } from 'lucide-react';
|
||||||
|
import { useTranslations } from 'next-intl';
|
||||||
import Image from 'next/image';
|
import Image from 'next/image';
|
||||||
import { MouseEvent, useState } from 'react';
|
import { MouseEvent, useEffect, useRef, useState } from 'react';
|
||||||
|
import { toast } from 'sonner';
|
||||||
interface Product {
|
|
||||||
id: number;
|
|
||||||
name: string;
|
|
||||||
price: number;
|
|
||||||
oldPrice: number;
|
|
||||||
image: string;
|
|
||||||
rating: number;
|
|
||||||
reviews: number;
|
|
||||||
discount: number;
|
|
||||||
inStock: boolean;
|
|
||||||
liked: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function ProductCard({
|
export function ProductCard({
|
||||||
product,
|
product,
|
||||||
handleRemove,
|
error,
|
||||||
handleLiked,
|
|
||||||
}: {
|
}: {
|
||||||
product: Product;
|
product: ProductListResult;
|
||||||
handleRemove: (id: number) => void;
|
error?: boolean;
|
||||||
handleLiked?: (id: number) => void;
|
|
||||||
}) {
|
}) {
|
||||||
const [quantity, setQuantity] = useState<number | ''>(0);
|
const [quantity, setQuantity] = useState<number | ''>(0);
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
const t = useTranslations();
|
||||||
|
const { cart_id } = useCartId();
|
||||||
|
const [animated, setAnimated] = useState<boolean>(false);
|
||||||
|
const debounceRef = useRef<NodeJS.Timeout | null>(null);
|
||||||
|
const imageRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
const { mutate } = useMutation({
|
||||||
|
mutationFn: (body: { product: string; quantity: number; cart: string }) =>
|
||||||
|
cart_api.cart_item(body),
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.refetchQueries({ queryKey: ['cart_items'] });
|
||||||
|
setAnimated(true);
|
||||||
|
},
|
||||||
|
onError: (err: AxiosError) => {
|
||||||
|
const detail = (err.response?.data as { detail: string }).detail;
|
||||||
|
toast.error(detail || err.message, {
|
||||||
|
richColors: true,
|
||||||
|
position: 'top-center',
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const { mutate: updateCartItem } = useMutation({
|
||||||
|
mutationFn: ({
|
||||||
|
body,
|
||||||
|
cart_item_id,
|
||||||
|
}: {
|
||||||
|
body: { quantity: number };
|
||||||
|
cart_item_id: string;
|
||||||
|
}) => cart_api.update_cart_item({ body, cart_item_id }),
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.refetchQueries({ queryKey: ['cart_items'] });
|
||||||
|
setAnimated(true);
|
||||||
|
},
|
||||||
|
onError: (err: AxiosError) => {
|
||||||
|
toast.error(err.message, { richColors: true, position: 'top-center' });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const { mutate: deleteCartItem } = useMutation({
|
||||||
|
mutationFn: ({ cart_item_id }: { cart_item_id: string }) =>
|
||||||
|
cart_api.delete_cart_item(cart_item_id),
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.refetchQueries({ queryKey: ['cart_items'] });
|
||||||
|
setAnimated(true);
|
||||||
|
},
|
||||||
|
onError: (err: AxiosError) => {
|
||||||
|
toast.error(err.message, { richColors: true, position: 'top-center' });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const { data: cartItems } = useQuery({
|
||||||
|
queryKey: ['cart_items', cart_id],
|
||||||
|
queryFn: () => cart_api.get_cart_items(cart_id!),
|
||||||
|
enabled: !!cart_id,
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const item = cartItems?.data?.cart_item?.find(
|
||||||
|
(item) => item.product_id === product.id,
|
||||||
|
);
|
||||||
|
|
||||||
|
setQuantity(item ? item.quantity : 0);
|
||||||
|
}, [cartItems, product.id]);
|
||||||
|
|
||||||
|
const favouriteMutation = useMutation({
|
||||||
|
mutationFn: (productId: string) => product_api.favourite(productId),
|
||||||
|
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.refetchQueries({ queryKey: ['product_list'] });
|
||||||
|
queryClient.refetchQueries({ queryKey: ['favourite_product'] });
|
||||||
|
queryClient.refetchQueries({ queryKey: ['search'] });
|
||||||
|
queryClient.refetchQueries({ queryKey: ['product_detail', product] });
|
||||||
|
},
|
||||||
|
|
||||||
|
onError: () => {
|
||||||
|
toast.error(t('Tizimga kirilmagan'), {
|
||||||
|
richColors: true,
|
||||||
|
position: 'top-center',
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const increase = (e: MouseEvent<HTMLButtonElement>) => {
|
const increase = (e: MouseEvent<HTMLButtonElement>) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
setQuantity((q) => {
|
const newQty = (quantity === '' ? 0 : quantity) + 1;
|
||||||
if (q === '' || q < 1) return 1;
|
setQuantity(newQty);
|
||||||
return q >= 999 ? 999 : q + 1;
|
|
||||||
});
|
if (newQty > 1) {
|
||||||
|
const cartItemId = cartItems?.data?.cart_item.find(
|
||||||
|
(item) => item.product_id === product.id,
|
||||||
|
)?.id;
|
||||||
|
if (cartItemId) {
|
||||||
|
updateCartItem({
|
||||||
|
body: { quantity: newQty },
|
||||||
|
cart_item_id: cartItemId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const decrease = (e: MouseEvent<HTMLButtonElement>) => {
|
const decrease = (e: MouseEvent<HTMLButtonElement>) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
setQuantity((q) => {
|
|
||||||
if (q === '' || q <= 1) return 0;
|
if (!cartItems) return;
|
||||||
return q - 1;
|
|
||||||
|
const currentQty = quantity === '' ? 0 : quantity;
|
||||||
|
const newQty = currentQty - 1;
|
||||||
|
|
||||||
|
const cartItemId = cartItems.data.cart_item.find(
|
||||||
|
(item) => item.product_id === product.id,
|
||||||
|
)?.id;
|
||||||
|
|
||||||
|
if (!cartItemId) return;
|
||||||
|
|
||||||
|
if (newQty <= 0) {
|
||||||
|
setQuantity(0);
|
||||||
|
deleteCartItem({ cart_item_id: cartItemId });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setQuantity(newQty);
|
||||||
|
updateCartItem({
|
||||||
|
body: { quantity: newQty },
|
||||||
|
cart_item_id: cartItemId,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return (
|
||||||
|
<Card className="p-4 rounded-xl">
|
||||||
|
<Alert variant="destructive">
|
||||||
|
<AlertTitle>Xatolik</AlertTitle>
|
||||||
|
<AlertDescription>{t('Mahsulotni yuklab bo‘lmadi')}</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card
|
<>
|
||||||
onClick={() => router.push(`/product/${product.id}`)}
|
<Card
|
||||||
className="group relative p-0 overflow-hidden border border-slate-200 bg-white shadow-sm hover:shadow-lg transition-all rounded-xl sm:rounded-2xl hover:border-green-400"
|
onClick={(e) => {
|
||||||
>
|
e.stopPropagation();
|
||||||
<CardContent className="p-0">
|
router.push(`/product/${product.id}`);
|
||||||
<div className="relative overflow-hidden">
|
}}
|
||||||
{product.discount > 0 && (
|
className="group relative p-0 h-full flex flex-col overflow-hidden border border-slate-200 bg-white shadow-sm hover:shadow-lg transition-all rounded-xl sm:rounded-2xl hover:border-green-400"
|
||||||
|
>
|
||||||
|
<CardContent className="p-0 flex flex-col h-full">
|
||||||
|
<div className="relative overflow-hidden">
|
||||||
|
{/* {product. > 0 && (
|
||||||
<div className="absolute top-2 left-2 z-10 bg-orange-500 text-white px-2 py-0.5 rounded-full text-xs sm:text-sm font-bold">
|
<div className="absolute top-2 left-2 z-10 bg-orange-500 text-white px-2 py-0.5 rounded-full text-xs sm:text-sm font-bold">
|
||||||
-{product.discount}%
|
-{product.discount}%
|
||||||
</div>
|
</div>
|
||||||
)}
|
)} */}
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
|
favouriteMutation.mutate(product.id);
|
||||||
|
}}
|
||||||
|
className="absolute top-2 right-2 z-10 bg-white hover:bg-gray-200 cursor-pointer rounded-full p-1.5 sm:p-2 shadow hover:scale-110"
|
||||||
|
>
|
||||||
|
<Heart
|
||||||
|
className={`w-4 h-4 sm:w-5 sm:h-5 ${
|
||||||
|
product.liked
|
||||||
|
? 'fill-red-500 text-red-500'
|
||||||
|
: 'text-slate-400 hover:text-red-400'
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
</Button>
|
||||||
|
|
||||||
if (product.liked) {
|
<div ref={imageRef} className="relative h-40 sm:h-48 md:h-56">
|
||||||
handleRemove(product.id);
|
<Image
|
||||||
} else if (handleLiked) {
|
fill
|
||||||
handleLiked(product.id);
|
src={
|
||||||
}
|
product?.image?.includes(BASE_URL)
|
||||||
}}
|
? product.image
|
||||||
className="absolute top-2 right-2 z-10 bg-white hover:bg-gray-200 cursor-pointer rounded-full p-1.5 sm:p-2 shadow hover:scale-110"
|
: BASE_URL + product.image
|
||||||
>
|
}
|
||||||
<Heart
|
alt={product.name}
|
||||||
className={`w-4 h-4 sm:w-5 sm:h-5 ${
|
className="object-contain"
|
||||||
product.liked
|
/>
|
||||||
? 'fill-red-500 text-red-500'
|
</div>
|
||||||
: 'text-slate-400 hover:text-red-400'
|
|
||||||
}`}
|
|
||||||
/>
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<div className="relative h-40 sm:h-48 md:h-56 bg-slate-50">
|
|
||||||
<Image
|
|
||||||
fill
|
|
||||||
src={product.image || '/placeholder.svg'}
|
|
||||||
alt={product.name}
|
|
||||||
className="object-cover group-hover:scale-105 transition-transform"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="p-3 sm:p-4 space-y-1 sm:space-y-1">
|
<div className="p-3 sm:p-4 space-y-1 flex-1">
|
||||||
<div className="flex items-center gap-2">
|
{/* <div className="flex items-center gap-2">
|
||||||
<Star className="w-3.5 h-3.5 sm:w-4 sm:h-4 fill-orange-400 text-orange-400" />
|
<Star className="w-3.5 h-3.5 sm:w-4 sm:h-4 fill-orange-400 text-orange-400" />
|
||||||
<span className="text-xs sm:text-sm font-semibold text-orange-600">
|
<span className="text-xs sm:text-sm font-semibold text-orange-600">
|
||||||
{product.rating}
|
{product.rating}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div> */}
|
||||||
|
|
||||||
<h3 className="text-sm sm:text-base font-semibold text-slate-800 line-clamp-1">
|
<h3 className="text-sm sm:text-base font-semibold text-slate-800 line-clamp-1">
|
||||||
{product.name}
|
{product.name}
|
||||||
</h3>
|
</h3>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<span className="text-lg sm:text-xl font-bold text-green-600">
|
<span className="text-lg sm:text-xl font-bold text-green-600">
|
||||||
{formatPrice(product.price, true)}
|
{formatPrice(product.price, true)}
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
{product.oldPrice && (
|
{/* {product. && (
|
||||||
<div className="text-xs sm:text-sm text-slate-400 line-through">
|
<div className="text-xs sm:text-sm text-slate-400 line-through">
|
||||||
{formatPrice(product.oldPrice, true)}
|
{formatPrice(product.oldPrice, true)}
|
||||||
</div>
|
</div>
|
||||||
|
)} */}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="p-3 sm:p-4 pt-0">
|
||||||
|
{quantity === 0 ? (
|
||||||
|
<Button
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
mutate({
|
||||||
|
product: product.id,
|
||||||
|
quantity: 1,
|
||||||
|
cart: cart_id!,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
className="w-full h-9 sm:h-11 text-sm bg-green-600 hover:bg-green-600/80"
|
||||||
|
>
|
||||||
|
<ShoppingCart className="w-4 h-4 mr-1" />
|
||||||
|
{t('Savatga')}
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
|
<div
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
className="flex items-center justify-between border border-green-500 rounded-lg h-9 sm:h-11"
|
||||||
|
>
|
||||||
|
<Button size="icon" variant="ghost" onClick={decrease}>
|
||||||
|
<Minus className="w-4 h-4" />
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Input
|
||||||
|
value={quantity}
|
||||||
|
className="border-none text-center focus-visible:border-none focus-visible:ring-0"
|
||||||
|
onChange={(e) => {
|
||||||
|
const v = e.target.value;
|
||||||
|
|
||||||
|
// ❌ faqat raqam
|
||||||
|
if (!/^\d*$/.test(v)) return;
|
||||||
|
|
||||||
|
// ⛔ oldingi debounce'ni tozalaymiz
|
||||||
|
if (debounceRef.current) {
|
||||||
|
clearTimeout(debounceRef.current);
|
||||||
|
}
|
||||||
|
|
||||||
|
// bo‘sh input — faqat UI
|
||||||
|
if (v === '') {
|
||||||
|
setQuantity('');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const num = Number(v);
|
||||||
|
setQuantity(num);
|
||||||
|
|
||||||
|
if (!cartItems) return;
|
||||||
|
|
||||||
|
const cartItemId = cartItems.data.cart_item.find(
|
||||||
|
(item) => item.product_id === product.id,
|
||||||
|
)?.id;
|
||||||
|
|
||||||
|
if (!cartItemId) return;
|
||||||
|
|
||||||
|
// ❗ 0 bo‘lsa — DELETE (darhol)
|
||||||
|
if (num === 0) {
|
||||||
|
deleteCartItem({ cart_item_id: cartItemId });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 🕒 debounce bilan UPDATE
|
||||||
|
debounceRef.current = setTimeout(() => {
|
||||||
|
updateCartItem({
|
||||||
|
body: { quantity: num },
|
||||||
|
cart_item_id: cartItemId,
|
||||||
|
});
|
||||||
|
}, 500);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Button size="icon" variant="ghost" onClick={increase}>
|
||||||
|
<Plus className="w-4 h-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
</CardContent>
|
||||||
{quantity === 0 ? (
|
</Card>
|
||||||
<Button
|
<FlyingAnimationPortal
|
||||||
disabled={!product.inStock}
|
product={product}
|
||||||
onClick={(e) => {
|
animated={animated}
|
||||||
e.stopPropagation();
|
imageRef={imageRef}
|
||||||
setQuantity(1);
|
setAnimated={setAnimated}
|
||||||
}}
|
/>
|
||||||
className="w-full h-9 sm:h-11 text-sm bg-green-600 hover:bg-green-600/80 cursor-pointer"
|
</>
|
||||||
>
|
|
||||||
<ShoppingCart className="w-4 h-4 mr-1" />
|
|
||||||
Savatga
|
|
||||||
</Button>
|
|
||||||
) : (
|
|
||||||
<div
|
|
||||||
onClick={(e) => e.stopPropagation()}
|
|
||||||
className="flex items-center justify-between border border-green-500 rounded-lg h-9 sm:h-11"
|
|
||||||
>
|
|
||||||
<Button size="icon" variant="ghost" onClick={decrease}>
|
|
||||||
<Minus className="w-4 h-4" />
|
|
||||||
</Button>
|
|
||||||
<Input
|
|
||||||
value={quantity}
|
|
||||||
onChange={(e) => {
|
|
||||||
const v = e.target.value;
|
|
||||||
|
|
||||||
if (!/^\d*$/.test(v)) return;
|
|
||||||
|
|
||||||
if (v === '') {
|
|
||||||
setQuantity('');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const num = Number(v);
|
|
||||||
|
|
||||||
setQuantity(num > 999 ? 999 : num);
|
|
||||||
}}
|
|
||||||
inputMode="numeric"
|
|
||||||
className="w-full border-none text-center text-sm !p-0 focus-visible:ring-0"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Button size="icon" variant="ghost" onClick={increase}>
|
|
||||||
<Plus className="w-4 h-4" />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,25 @@
|
|||||||
|
import { category_api } from '@/shared/config/api/category/api';
|
||||||
import { Link } from '@/shared/config/i18n/navigation';
|
import { Link } from '@/shared/config/i18n/navigation';
|
||||||
import { PRODUCT_INFO } from '@/shared/constants/data';
|
import { PRODUCT_INFO } from '@/shared/constants/data';
|
||||||
import formatPhone from '@/shared/lib/formatPhone';
|
import formatPhone from '@/shared/lib/formatPhone';
|
||||||
import { categoryList } from '@/widgets/welcome/lib/data';
|
import { useQuery } from '@tanstack/react-query';
|
||||||
import { Facebook, Instagram, Mail, Phone, Send, Twitter } from 'lucide-react';
|
import { Facebook, Instagram, Mail, Phone, Send, Twitter } from 'lucide-react';
|
||||||
|
import { useTranslations } from 'next-intl';
|
||||||
import Image from 'next/image';
|
import Image from 'next/image';
|
||||||
import { Fragment } from 'react';
|
import { Fragment } from 'react';
|
||||||
|
|
||||||
const Footer = () => {
|
const Footer = () => {
|
||||||
|
const t = useTranslations();
|
||||||
|
const { data: category } = useQuery({
|
||||||
|
queryKey: ['category_list'],
|
||||||
|
queryFn: () => category_api.getCategory({ page: 1, page_size: 12 }),
|
||||||
|
select(data) {
|
||||||
|
return data.data.results;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="max-lg:py-9 max-lg:bg-white max-lg:border-none py-12 z-50 w-full bg-[#57A595] border-t border-slate-200">
|
<section className="max-lg:py-9 max-lg:bg-white max-lg:border-none max-lg:hidden py-12 z-50 w-full bg-[#57A595] border-t border-slate-200">
|
||||||
<div className="custom-container max-lg:hidden">
|
<div className="custom-container max-lg:hidden">
|
||||||
<div className="flex w-full gap-10 flex-col items-center justify-between text-center lg:flex-row lg:items-start lg:text-left">
|
<div className="flex w-full gap-10 flex-col items-center justify-between text-center lg:flex-row lg:items-start lg:text-left">
|
||||||
<div className="flex w-fit flex-col items-center justify-between gap-4 lg:items-start mb-8 lg:mb-0">
|
<div className="flex w-fit flex-col items-center justify-between gap-4 lg:items-start mb-8 lg:mb-0">
|
||||||
@@ -27,49 +38,52 @@ const Footer = () => {
|
|||||||
{PRODUCT_INFO.name}
|
{PRODUCT_INFO.name}
|
||||||
</h2>
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-white max-w-xs leading-relaxed text-sm">
|
<p className="text-white font-semibold text-md">
|
||||||
Lorem ipsum dolor sit amet consectetur, adipisicing elit. Est,
|
{t(
|
||||||
totam?
|
"Gastronomiya va kulinariya san'ati haqidagi yetakchi onlayn magazin",
|
||||||
|
)}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="grid w-full grid-cols-1 md:grid-cols-3 gap-8 lg:gap-16">
|
<div className="grid w-full grid-cols-1 md:grid-cols-3 gap-8 lg:gap-16">
|
||||||
<div>
|
<div>
|
||||||
<h3 className="mb-2 font-bold text-lg text-muted">
|
<h3 className="mb-2 font-bold text-lg text-muted">
|
||||||
Kategoriyalar
|
{t('Kategoriyalar')}
|
||||||
</h3>
|
</h3>
|
||||||
<ul className="space-y-2 text-sm">
|
<ul className="space-y-2 text-sm">
|
||||||
{categoryList.slice(0, 3).map((link) => (
|
{category?.slice(0, 6)?.map((link) => (
|
||||||
<Fragment key={link.name}>
|
<Fragment key={link.name}>
|
||||||
{link.subCategories.slice(0, 2).map((e, linkIdx) => (
|
<li
|
||||||
<li
|
key={link.id}
|
||||||
key={linkIdx}
|
className="text-white hover:text-gray-300 transition-colors cursor-pointer"
|
||||||
className="text-white hover:text-gray-300 transition-colors cursor-pointer"
|
>
|
||||||
>
|
<Link href={`/category/${link.id}/`}>{link.name}</Link>
|
||||||
<Link href={`/category/${link.name}/${e.name}`}>
|
</li>
|
||||||
{e.name}
|
|
||||||
</Link>
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</Fragment>
|
</Fragment>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h3 className="mb-2 font-bold text-base text-muted">Sahifalar</h3>
|
<h3 className="mb-2 font-bold text-base text-muted">
|
||||||
|
{t('Sahifalar')}
|
||||||
|
</h3>
|
||||||
<ul className="space-y-2 text-sm">
|
<ul className="space-y-2 text-sm">
|
||||||
<li className="text-muted hover:text-gray-300 transition-colors cursor-pointer">
|
<li className="text-muted hover:text-gray-300 transition-colors cursor-pointer">
|
||||||
<Link href={'/about'}>Biz haqimizda</Link>
|
<Link href={'/about'}>{t('Biz haqimizda')}</Link>
|
||||||
</li>
|
</li>
|
||||||
<li className="text-muted hover:text-gray-300 transition-colors cursor-pointer">
|
<li className="text-muted hover:text-gray-300 transition-colors cursor-pointer">
|
||||||
<Link href={'/privacy-policy'}>Maxfiylik siyosati</Link>
|
<Link href={'/privacy-policy'}>
|
||||||
|
{t('Maxfiylik siyosati')}
|
||||||
|
</Link>
|
||||||
</li>
|
</li>
|
||||||
<li className="text-muted hover:text-gray-300 transition-colors cursor-pointer">
|
<li className="text-muted hover:text-gray-300 transition-colors cursor-pointer">
|
||||||
<Link href={'/faq'}>Savol va javoblar</Link>
|
<Link href={'/faq'}>{t('Savol-javob')}</Link>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h3 className="mb-2 font-bold text-base text-muted">Aloqa</h3>
|
<h3 className="mb-2 font-bold text-base text-muted">
|
||||||
|
{t("Biz bilan bog'laning")}
|
||||||
|
</h3>
|
||||||
<ul className="space-y-2 text-sm">
|
<ul className="space-y-2 text-sm">
|
||||||
<li className="text-muted hover:text-gray-300 transition-colors cursor-pointer">
|
<li className="text-muted hover:text-gray-300 transition-colors cursor-pointer">
|
||||||
<a href={'#'} className="flex items-center gap-2">
|
<a href={'#'} className="flex items-center gap-2">
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
DropdownMenuItem,
|
DropdownMenuItem,
|
||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
} from '@/shared/ui/dropdown-menu';
|
} from '@/shared/ui/dropdown-menu';
|
||||||
|
import { useTranslations } from 'next-intl';
|
||||||
import Image from 'next/image';
|
import Image from 'next/image';
|
||||||
import { useParams, usePathname, useRouter } from 'next/navigation';
|
import { useParams, usePathname, useRouter } from 'next/navigation';
|
||||||
import { languages } from '../lib/data';
|
import { languages } from '../lib/data';
|
||||||
@@ -16,6 +17,7 @@ export function ChangeLang() {
|
|||||||
const { locale } = useParams();
|
const { locale } = useParams();
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const t = useTranslations();
|
||||||
|
|
||||||
const changeLocale = (locale: LanguageRoutes) => {
|
const changeLocale = (locale: LanguageRoutes) => {
|
||||||
const segments = pathname.split('/');
|
const segments = pathname.split('/');
|
||||||
@@ -51,7 +53,7 @@ export function ChangeLang() {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<span className="text-white font-medium text-sm">
|
<span className="text-white font-medium text-sm">
|
||||||
{languages.find((e) => e.key === locale)?.name}
|
{t(languages.find((e) => e.key === locale)?.name ?? "O'zbekcha")}
|
||||||
</span>
|
</span>
|
||||||
</Button>
|
</Button>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
@@ -65,7 +67,7 @@ export function ChangeLang() {
|
|||||||
onClick={() => changeLocale(e.key)}
|
onClick={() => changeLocale(e.key)}
|
||||||
className="hover:bg-blue-50 cursor-pointer text-slate-700 hover:text-blue-700 px-3 py-2"
|
className="hover:bg-blue-50 cursor-pointer text-slate-700 hover:text-blue-700 px-3 py-2"
|
||||||
>
|
>
|
||||||
{e.name}
|
{t(e.name)}
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
))}
|
))}
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
|
|||||||
@@ -1,24 +1,27 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
|
import { cart_api } from '@/features/cart/lib/api';
|
||||||
|
import { Link } from '@/shared/config/i18n/navigation';
|
||||||
|
import { useCartId } from '@/shared/hooks/cartId';
|
||||||
|
import { getToken } from '@/shared/lib/token';
|
||||||
import { cn } from '@/shared/lib/utils';
|
import { cn } from '@/shared/lib/utils';
|
||||||
|
import { Badge } from '@/shared/ui/badge';
|
||||||
import { Button } from '@/shared/ui/button';
|
import { Button } from '@/shared/ui/button';
|
||||||
|
import { useQuery } from '@tanstack/react-query';
|
||||||
import { Heart, Home, LayoutGrid, ShoppingCart, User } from 'lucide-react';
|
import { Heart, Home, LayoutGrid, ShoppingCart, User } from 'lucide-react';
|
||||||
import Link from 'next/link';
|
import { useTranslations } from 'next-intl';
|
||||||
import { usePathname } from 'next/navigation';
|
import { usePathname } from 'next/navigation';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
const NavbarMobile = () => {
|
const NavbarMobile = () => {
|
||||||
const pathname = usePathname();
|
const [mounted, setMounted] = useState(false);
|
||||||
const [profile, setProfile] = useState<boolean>(false);
|
const t = useTranslations();
|
||||||
const user = localStorage.getItem('user');
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (user && user === 'true') {
|
setMounted(true);
|
||||||
setProfile(true);
|
}, []);
|
||||||
} else {
|
|
||||||
setProfile(false);
|
const pathname = usePathname();
|
||||||
}
|
const token = getToken();
|
||||||
}, [user]);
|
|
||||||
|
|
||||||
const navItems = [
|
const navItems = [
|
||||||
{ label: 'Asosiy', icon: Home, href: '/' },
|
{ label: 'Asosiy', icon: Home, href: '/' },
|
||||||
@@ -28,9 +31,29 @@ const NavbarMobile = () => {
|
|||||||
{
|
{
|
||||||
label: 'Profil',
|
label: 'Profil',
|
||||||
icon: User,
|
icon: User,
|
||||||
href: profile ? '/profile' : '/auth',
|
href: token ? '/profile' : '/auth',
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
const [cartQuenty, setCartQuenty] = useState<number>(0);
|
||||||
|
const { cart_id } = useCartId();
|
||||||
|
|
||||||
|
const { data: cartItems } = useQuery({
|
||||||
|
queryKey: ['cart_items', cart_id],
|
||||||
|
queryFn: () => cart_api.get_cart_items(cart_id!),
|
||||||
|
enabled: !!cart_id,
|
||||||
|
select(data) {
|
||||||
|
return data.data.cart_item;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (cartItems) {
|
||||||
|
const total = cartItems.reduce((sum, item) => sum + item.quantity, 0);
|
||||||
|
setCartQuenty(total > 9 ? 9 : total);
|
||||||
|
}
|
||||||
|
}, [cartItems]);
|
||||||
|
|
||||||
|
if (!mounted) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<nav className="fixed bottom-0 left-0 right-0 z-50 lg:hidden bg-white border-t shadow-xl rounded-t-3xl">
|
<nav className="fixed bottom-0 left-0 right-0 z-50 lg:hidden bg-white border-t shadow-xl rounded-t-3xl">
|
||||||
@@ -44,25 +67,33 @@ const NavbarMobile = () => {
|
|||||||
return (
|
return (
|
||||||
<Link key={item.href} href={item.href}>
|
<Link key={item.href} href={item.href}>
|
||||||
<Button
|
<Button
|
||||||
|
id={item.href === '/cart' ? 'cart-icon-mobile' : undefined}
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
className={cn(
|
className={cn(
|
||||||
'h-full w-full flex flex-col items-center justify-center gap-1 rounded-xl',
|
'h-full w-full flex flex-col items-center justify-center gap-1 rounded-xl',
|
||||||
isActive && 'text-green-500',
|
isActive && 'text-green-500',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<item.icon
|
<div className="relative w-full flex justify-center items-center">
|
||||||
className={cn(
|
<item.icon
|
||||||
'size-6 transition-colors',
|
className={cn(
|
||||||
isActive ? 'text-green-500' : 'text-gray-500',
|
'size-6 transition-colors',
|
||||||
|
isActive ? 'text-green-500' : 'text-gray-500',
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
{item.href === '/cart' && (
|
||||||
|
<Badge className="absolute -top-2 right-2 line-clamp-1 w-5 h-5 flex justify-center items-center">
|
||||||
|
{cartQuenty === 9 ? cartQuenty + '+' : cartQuenty}
|
||||||
|
</Badge>
|
||||||
)}
|
)}
|
||||||
/>
|
</div>
|
||||||
<span
|
<span
|
||||||
className={cn(
|
className={cn(
|
||||||
'text-[10px] font-medium',
|
'text-[10px] font-medium',
|
||||||
isActive ? 'text-green-500' : 'text-gray-500',
|
isActive ? 'text-green-500' : 'text-gray-500',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{item.label}
|
{t(item.label)}
|
||||||
</span>
|
</span>
|
||||||
</Button>
|
</Button>
|
||||||
</Link>
|
</Link>
|
||||||
|
|||||||
@@ -1,7 +1,15 @@
|
|||||||
|
import { product_api } from '@/shared/config/api/product/api';
|
||||||
|
import {
|
||||||
|
ProductListResult,
|
||||||
|
SearchDataPro,
|
||||||
|
} from '@/shared/config/api/product/type';
|
||||||
|
import { BASE_URL } from '@/shared/config/api/URLs';
|
||||||
import { useRouter } from '@/shared/config/i18n/navigation';
|
import { useRouter } from '@/shared/config/i18n/navigation';
|
||||||
import formatPrice from '@/shared/lib/formatPrice';
|
import formatPrice from '@/shared/lib/formatPrice';
|
||||||
import { categories, ProductDetail } from '@/widgets/categories/lib/data';
|
import { Skeleton } from '@/shared/ui/skeleton';
|
||||||
|
import { useQuery } from '@tanstack/react-query';
|
||||||
import { PackageOpen } from 'lucide-react';
|
import { PackageOpen } from 'lucide-react';
|
||||||
|
import { useTranslations } from 'next-intl';
|
||||||
import Image from 'next/image';
|
import Image from 'next/image';
|
||||||
import { Fragment, useEffect, useState } from 'react';
|
import { Fragment, useEffect, useState } from 'react';
|
||||||
|
|
||||||
@@ -10,62 +18,106 @@ type SearchResultProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const SearchResult = ({ query }: SearchResultProps) => {
|
export const SearchResult = ({ query }: SearchResultProps) => {
|
||||||
const [searchProduct, setSearchProduct] = useState<ProductDetail[]>([]);
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const t = useTranslations();
|
||||||
|
const [searchRes, setSearchRes] = useState<
|
||||||
|
ProductListResult[] | SearchDataPro[] | []
|
||||||
|
>([]);
|
||||||
|
|
||||||
|
const { data: product } = useQuery({
|
||||||
|
queryKey: ['product_list'],
|
||||||
|
queryFn: () => product_api.list({ page: 1, page_size: 99 }),
|
||||||
|
select(data) {
|
||||||
|
return data.data.results;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const { data, isLoading } = useQuery({
|
||||||
|
queryKey: ['search', query],
|
||||||
|
queryFn: () => product_api.search({ search: query, page: 1, page_szie: 5 }),
|
||||||
|
select(data) {
|
||||||
|
return data.data.products;
|
||||||
|
},
|
||||||
|
enabled: !!query,
|
||||||
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setSearchProduct(
|
if (data) {
|
||||||
categories.flatMap((cat) =>
|
setSearchRes(data);
|
||||||
cat.products.filter((pro) =>
|
} else if (product && product.length > 0) {
|
||||||
pro.name.toLowerCase().includes(query.toLowerCase()),
|
setSearchRes(product);
|
||||||
),
|
} else {
|
||||||
),
|
setSearchRes([]);
|
||||||
);
|
}
|
||||||
}, [query]);
|
}, [product, data]);
|
||||||
|
|
||||||
if (searchProduct.length === 0) {
|
if (searchRes && searchRes.length === 0) {
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col justify-center items-center min-h-[300px] max-h-[600px] gap-2">
|
<div className="flex flex-col justify-center items-center min-h-[300px] max-h-[600px] gap-2">
|
||||||
<PackageOpen className="size-22 text-muted-foreground" />
|
<PackageOpen className="size-22 text-muted-foreground" />
|
||||||
<p className="text-lg text-muted-foreground text-center">
|
<p className="text-lg text-muted-foreground text-center">
|
||||||
Hech narsa topilmadi
|
{t('Hech narsa topilmadi')}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return (
|
||||||
|
<div className="space-y-3">
|
||||||
|
{Array.from({ length: 5 }).map((_, i) => (
|
||||||
|
<div key={i} className="flex items-center gap-3 p-2 rounded-lg">
|
||||||
|
<Skeleton className="w-16 h-16 rounded-md" />
|
||||||
|
|
||||||
|
<div className="flex-1 space-y-2">
|
||||||
|
<Skeleton className="h-4 w-[70%]" />
|
||||||
|
<Skeleton className="h-3 w-[40%]" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<p className="text-sm font-semibold text-foreground">
|
<p className="text-sm font-semibold text-foreground">
|
||||||
{query.length > 0 ? 'Qidiruv natijalari' : 'Tavsiya etiladi'}
|
{query.length > 0 ? t('Qidiruv natijalari') : t('Tavsiya etiladi')}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
{searchProduct.slice(0, 5).map((product, index) => (
|
{searchRes &&
|
||||||
<Fragment key={index}>
|
searchRes
|
||||||
<div
|
.filter((product) => product.is_active)
|
||||||
className="flex items-center gap-3 p-2 rounded-lg hover:bg-slate-100 cursor-pointer transition"
|
.slice(0, 5)
|
||||||
onClick={() => router.push(`/product/${product.id}`)}
|
.map((product, index) => (
|
||||||
>
|
<Fragment key={index}>
|
||||||
<Image
|
<div
|
||||||
width={500}
|
className="flex items-center gap-3 p-2 rounded-lg hover:bg-slate-100 cursor-pointer transition"
|
||||||
height={500}
|
onClick={() => router.push(`/product/${product.id}`)}
|
||||||
src={product.image}
|
>
|
||||||
alt={product.name}
|
<Image
|
||||||
className="w-10 h-10 rounded-md object-cover"
|
width={500}
|
||||||
/>
|
height={500}
|
||||||
|
src={
|
||||||
|
product.image.includes(BASE_URL)
|
||||||
|
? product.image
|
||||||
|
: BASE_URL + product.image
|
||||||
|
}
|
||||||
|
alt={product.name}
|
||||||
|
className="w-16 h-16 rounded-md object-contain"
|
||||||
|
/>
|
||||||
|
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<p className="text-sm font-medium text-slate-800">
|
<p className="text-sm font-medium text-slate-800">
|
||||||
{product.name}
|
{product.name}
|
||||||
</p>
|
</p>
|
||||||
<p className="text-xs text-slate-500">{product.rating}</p>
|
<p className="text-xs text-slate-600">
|
||||||
<p className="text-xs text-slate-600">
|
{formatPrice(product.price)}
|
||||||
{formatPrice(product.price)}
|
</p>
|
||||||
</p>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</Fragment>
|
||||||
</Fragment>
|
))}
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,7 +1,11 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
|
import { cart_api } from '@/features/cart/lib/api';
|
||||||
import { Link, useRouter } from '@/shared/config/i18n/navigation';
|
import { Link, useRouter } from '@/shared/config/i18n/navigation';
|
||||||
|
import { useCartId } from '@/shared/hooks/cartId';
|
||||||
import formatPhone from '@/shared/lib/formatPhone';
|
import formatPhone from '@/shared/lib/formatPhone';
|
||||||
|
import { getToken } from '@/shared/lib/token';
|
||||||
|
import { Badge } from '@/shared/ui/badge';
|
||||||
import { Button } from '@/shared/ui/button';
|
import { Button } from '@/shared/ui/button';
|
||||||
import { Input } from '@/shared/ui/input';
|
import { Input } from '@/shared/ui/input';
|
||||||
import { Popover, PopoverContent } from '@/shared/ui/popover';
|
import { Popover, PopoverContent } from '@/shared/ui/popover';
|
||||||
@@ -15,12 +19,12 @@ import {
|
|||||||
} from '@/shared/ui/sheet';
|
} from '@/shared/ui/sheet';
|
||||||
import { categoryList } from '@/widgets/welcome/lib/data';
|
import { categoryList } from '@/widgets/welcome/lib/data';
|
||||||
import { PopoverTrigger } from '@radix-ui/react-popover';
|
import { PopoverTrigger } from '@radix-ui/react-popover';
|
||||||
|
import { useMutation, useQuery } from '@tanstack/react-query';
|
||||||
import {
|
import {
|
||||||
ChevronRight,
|
ChevronRight,
|
||||||
Facebook,
|
Facebook,
|
||||||
Heart,
|
Heart,
|
||||||
Instagram,
|
Instagram,
|
||||||
LayoutGrid,
|
|
||||||
Mail,
|
Mail,
|
||||||
MenuIcon,
|
MenuIcon,
|
||||||
Phone,
|
Phone,
|
||||||
@@ -29,8 +33,8 @@ import {
|
|||||||
ShoppingCart,
|
ShoppingCart,
|
||||||
Twitter,
|
Twitter,
|
||||||
User,
|
User,
|
||||||
XIcon,
|
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
|
import { useTranslations } from 'next-intl';
|
||||||
import Image from 'next/image';
|
import Image from 'next/image';
|
||||||
import { useSearchParams } from 'next/navigation';
|
import { useSearchParams } from 'next/navigation';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
@@ -44,19 +48,43 @@ const Navbar = () => {
|
|||||||
const [isSticky, setIsSticky] = useState(false);
|
const [isSticky, setIsSticky] = useState(false);
|
||||||
const [query, setQuery] = useState('');
|
const [query, setQuery] = useState('');
|
||||||
const searchParams = useSearchParams();
|
const searchParams = useSearchParams();
|
||||||
const [user, setUser] = useState<boolean>(false);
|
const token = getToken();
|
||||||
const users = localStorage.getItem('user');
|
const t = useTranslations();
|
||||||
|
const { cart_id } = useCartId();
|
||||||
|
const [cartQuenty, setCartQuenty] = useState<number>(0);
|
||||||
|
const { setCartId } = useCartId();
|
||||||
|
|
||||||
|
const { mutate: cart } = useMutation({
|
||||||
|
mutationFn: () => cart_api.create(),
|
||||||
|
onSuccess: (data) => {
|
||||||
|
setCartId(data.data.cart_id);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (users && users === 'true') {
|
if (token) {
|
||||||
setUser(true);
|
cart();
|
||||||
} else {
|
|
||||||
setUser(false);
|
|
||||||
}
|
}
|
||||||
}, [users]);
|
}, [token]);
|
||||||
|
|
||||||
const queryFromUrl = searchParams.get('q') ?? '';
|
const queryFromUrl = searchParams.get('q') ?? '';
|
||||||
|
|
||||||
|
const { data: cartItems } = useQuery({
|
||||||
|
queryKey: ['cart_items', cart_id],
|
||||||
|
queryFn: () => cart_api.get_cart_items(cart_id!),
|
||||||
|
enabled: !!cart_id,
|
||||||
|
select(data) {
|
||||||
|
return data.data.cart_item;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (cartItems) {
|
||||||
|
const total = cartItems.reduce((sum, item) => sum + item.quantity, 0);
|
||||||
|
setCartQuenty(total > 9 ? 9 : total);
|
||||||
|
}
|
||||||
|
}, [cartItems]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setQuery(queryFromUrl);
|
setQuery(queryFromUrl);
|
||||||
}, [queryFromUrl]);
|
}, [queryFromUrl]);
|
||||||
@@ -89,7 +117,7 @@ const Navbar = () => {
|
|||||||
href="/about"
|
href="/about"
|
||||||
className="flex items-center gap-1.5 font-medium"
|
className="flex items-center gap-1.5 font-medium"
|
||||||
>
|
>
|
||||||
Biz haqimizda
|
{t('Biz haqimizda')}
|
||||||
</Link>
|
</Link>
|
||||||
</li>
|
</li>
|
||||||
<li className="text-white hover:text-white/80 transition-colors cursor-pointer">
|
<li className="text-white hover:text-white/80 transition-colors cursor-pointer">
|
||||||
@@ -97,7 +125,7 @@ const Navbar = () => {
|
|||||||
href="/privacy-policy"
|
href="/privacy-policy"
|
||||||
className="flex items-center gap-1.5 font-medium"
|
className="flex items-center gap-1.5 font-medium"
|
||||||
>
|
>
|
||||||
Maxfiylik siyosati
|
{t('Maxfiylik siyosati')}
|
||||||
</Link>
|
</Link>
|
||||||
</li>
|
</li>
|
||||||
<li className="text-white hover:text-white/80 transition-colors cursor-pointer">
|
<li className="text-white hover:text-white/80 transition-colors cursor-pointer">
|
||||||
@@ -105,7 +133,7 @@ const Navbar = () => {
|
|||||||
href="/faq"
|
href="/faq"
|
||||||
className="flex items-center gap-1.5 font-medium"
|
className="flex items-center gap-1.5 font-medium"
|
||||||
>
|
>
|
||||||
Savol-javob
|
{t('Savol-javob')}
|
||||||
</Link>
|
</Link>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
@@ -205,7 +233,7 @@ const Navbar = () => {
|
|||||||
{/* Asosiy Sahifalar */}
|
{/* Asosiy Sahifalar */}
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-xs font-semibold text-slate-500 uppercase tracking-wider mb-3 px-2">
|
<h3 className="text-xs font-semibold text-slate-500 uppercase tracking-wider mb-3 px-2">
|
||||||
Sahifalar
|
{t('Sahifalar')}
|
||||||
</h3>
|
</h3>
|
||||||
<nav className="space-y-1">
|
<nav className="space-y-1">
|
||||||
<SheetClose asChild>
|
<SheetClose asChild>
|
||||||
@@ -215,7 +243,7 @@ const Navbar = () => {
|
|||||||
>
|
>
|
||||||
<div className="w-1 h-1 rounded-full bg-slate-400 group-hover:bg-[#57A595]" />
|
<div className="w-1 h-1 rounded-full bg-slate-400 group-hover:bg-[#57A595]" />
|
||||||
<span className="text-sm font-medium">
|
<span className="text-sm font-medium">
|
||||||
Biz haqimizda
|
{t('Biz haqimizda')}
|
||||||
</span>
|
</span>
|
||||||
<ChevronRight className="size-4 ml-auto opacity-0 group-hover:opacity-100 transition-opacity" />
|
<ChevronRight className="size-4 ml-auto opacity-0 group-hover:opacity-100 transition-opacity" />
|
||||||
</Link>
|
</Link>
|
||||||
@@ -227,7 +255,7 @@ const Navbar = () => {
|
|||||||
>
|
>
|
||||||
<div className="w-1 h-1 rounded-full bg-slate-400 group-hover:bg-[#57A595]" />
|
<div className="w-1 h-1 rounded-full bg-slate-400 group-hover:bg-[#57A595]" />
|
||||||
<span className="text-sm font-medium">
|
<span className="text-sm font-medium">
|
||||||
Maxfiylik siyosati
|
{t('Maxfiylik siyosati')}
|
||||||
</span>
|
</span>
|
||||||
<ChevronRight className="size-4 ml-auto opacity-0 group-hover:opacity-100 transition-opacity" />
|
<ChevronRight className="size-4 ml-auto opacity-0 group-hover:opacity-100 transition-opacity" />
|
||||||
</Link>
|
</Link>
|
||||||
@@ -239,7 +267,7 @@ const Navbar = () => {
|
|||||||
>
|
>
|
||||||
<div className="w-1 h-1 rounded-full bg-slate-400 group-hover:bg-[#57A595]" />
|
<div className="w-1 h-1 rounded-full bg-slate-400 group-hover:bg-[#57A595]" />
|
||||||
<span className="text-sm font-medium">
|
<span className="text-sm font-medium">
|
||||||
Savol va javoblar
|
{t('Savol-javob')}
|
||||||
</span>
|
</span>
|
||||||
<ChevronRight className="size-4 ml-auto opacity-0 group-hover:opacity-100 transition-opacity" />
|
<ChevronRight className="size-4 ml-auto opacity-0 group-hover:opacity-100 transition-opacity" />
|
||||||
</Link>
|
</Link>
|
||||||
@@ -253,7 +281,7 @@ const Navbar = () => {
|
|||||||
{/* Aloqa */}
|
{/* Aloqa */}
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-xs font-semibold text-slate-500 uppercase tracking-wider mb-3 px-2">
|
<h3 className="text-xs font-semibold text-slate-500 uppercase tracking-wider mb-3 px-2">
|
||||||
{"Biz bilan bog'laning"}
|
{t("Biz bilan bog'laning")}
|
||||||
</h3>
|
</h3>
|
||||||
<nav className="space-y-1">
|
<nav className="space-y-1">
|
||||||
<a
|
<a
|
||||||
@@ -322,7 +350,7 @@ const Navbar = () => {
|
|||||||
</Sheet>
|
</Sheet>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1 flex gap-3">
|
<div className="flex-1 flex gap-3">
|
||||||
<Button
|
{/* <Button
|
||||||
variant={'outline'}
|
variant={'outline'}
|
||||||
className="h-10 max-lg:hidden cursor-pointer"
|
className="h-10 max-lg:hidden cursor-pointer"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@@ -339,11 +367,11 @@ const Navbar = () => {
|
|||||||
<LayoutGrid className="size-4 text-foreground" />
|
<LayoutGrid className="size-4 text-foreground" />
|
||||||
)}
|
)}
|
||||||
<p className="text-foreground">Kataloglar</p>
|
<p className="text-foreground">Kataloglar</p>
|
||||||
</Button>
|
</Button> */}
|
||||||
|
|
||||||
<div className="relative w-full max-lg:hidden">
|
<div className="relative w-full max-lg:hidden">
|
||||||
<Input
|
<Input
|
||||||
placeholder="Mahsulot nomi"
|
placeholder={t('Mahsulot nomi')}
|
||||||
value={query}
|
value={query}
|
||||||
onFocus={() => setSearchOpen(true)}
|
onFocus={() => setSearchOpen(true)}
|
||||||
onBlur={() => setTimeout(() => setSearchOpen(false), 200)}
|
onBlur={() => setTimeout(() => setSearchOpen(false), 200)}
|
||||||
@@ -379,15 +407,19 @@ const Navbar = () => {
|
|||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant={'ghost'}
|
variant={'ghost'}
|
||||||
|
id="cart-icon"
|
||||||
onClick={() => router.push('/cart')}
|
onClick={() => router.push('/cart')}
|
||||||
className="h-10 max-lg:hidden cursor-pointer border border-slate-200"
|
className="h-10 relative max-lg:hidden cursor-pointer border border-slate-200"
|
||||||
>
|
>
|
||||||
<ShoppingCart className="size-4 text-foreground" />
|
<ShoppingCart className="size-4 text-foreground" />
|
||||||
|
<Badge className="absolute -top-2 -right-2 line-clamp-1 w-6 flex justify-center items-center">
|
||||||
|
{cartQuenty === 9 ? cartQuenty + '+' : cartQuenty}
|
||||||
|
</Badge>
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant={'ghost'}
|
variant={'ghost'}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (user) {
|
if (token) {
|
||||||
router.push('/profile');
|
router.push('/profile');
|
||||||
} else {
|
} else {
|
||||||
router.push('/auth');
|
router.push('/auth');
|
||||||
|
|||||||
11
src/widgets/welcome/lib/api.ts
Normal file
11
src/widgets/welcome/lib/api.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import httpClient from '@/shared/config/api/httpClient';
|
||||||
|
import { API_URLS } from '@/shared/config/api/URLs';
|
||||||
|
import { AxiosResponse } from 'axios';
|
||||||
|
import { BannerRes } from './type';
|
||||||
|
|
||||||
|
export const banner_api = {
|
||||||
|
async getBanner(): Promise<AxiosResponse<BannerRes[]>> {
|
||||||
|
const res = await httpClient.get(API_URLS.Banner);
|
||||||
|
return res;
|
||||||
|
},
|
||||||
|
};
|
||||||
4
src/widgets/welcome/lib/type.ts
Normal file
4
src/widgets/welcome/lib/type.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
export interface BannerRes {
|
||||||
|
id: string;
|
||||||
|
banner: string;
|
||||||
|
}
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import Banner from '@/assets/gemma-c-stpjHJGqZyw-unsplash.jpg';
|
import { category_api } from '@/shared/config/api/category/api';
|
||||||
import Banner_Two from '@/assets/photo-1506617420156-8e4536971650.jpg';
|
import { BASE_URL } from '@/shared/config/api/URLs';
|
||||||
import Banner_Three from '@/assets/pngtree-supermarket-aisle-with-empty-shopping-cart-at-grocery-store-retail-business-image_15646095.jpg';
|
import { Link } from '@/shared/config/i18n/navigation';
|
||||||
import { AspectRatio } from '@/shared/ui/aspect-ratio';
|
import { AspectRatio } from '@/shared/ui/aspect-ratio';
|
||||||
import { Button } from '@/shared/ui/button';
|
import { Button } from '@/shared/ui/button';
|
||||||
import {
|
import {
|
||||||
@@ -11,19 +11,34 @@ import {
|
|||||||
CarouselItem,
|
CarouselItem,
|
||||||
type CarouselApi,
|
type CarouselApi,
|
||||||
} from '@/shared/ui/carousel';
|
} from '@/shared/ui/carousel';
|
||||||
import useCategoryActive from '@/widgets/navbar/lib/openCategory';
|
import { Skeleton } from '@/shared/ui/skeleton';
|
||||||
import { ChevronLeft, ChevronRight } from 'lucide-react';
|
import { CategoryCarousel } from '@/widgets/categories/ui/category-carousel';
|
||||||
|
import { useQuery } from '@tanstack/react-query';
|
||||||
|
import { AlertCircle, ChevronLeft, ChevronRight } from 'lucide-react';
|
||||||
import Image from 'next/image';
|
import Image from 'next/image';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import 'swiper/css';
|
import 'swiper/css';
|
||||||
import { Swiper, SwiperSlide } from 'swiper/react';
|
import { banner_api } from '../lib/api';
|
||||||
import { categoryList } from '../lib/data';
|
|
||||||
|
|
||||||
const banner = [Banner, Banner_Two, Banner_Three];
|
|
||||||
|
|
||||||
const Welcome = () => {
|
const Welcome = () => {
|
||||||
const { setActive, setOpenToolbar } = useCategoryActive();
|
|
||||||
const [api, setApi] = useState<CarouselApi>();
|
const [api, setApi] = useState<CarouselApi>();
|
||||||
|
const [apiCat, setApiCat] = useState<CarouselApi>();
|
||||||
|
|
||||||
|
const { data, isLoading, isError } = useQuery({
|
||||||
|
queryKey: ['banner_list'],
|
||||||
|
queryFn: () => banner_api.getBanner(),
|
||||||
|
select(data) {
|
||||||
|
return data.data;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const { data: category } = useQuery({
|
||||||
|
queryKey: ['category_list'],
|
||||||
|
queryFn: () => category_api.getCategory({ page: 1, page_size: 99 }),
|
||||||
|
select(data) {
|
||||||
|
return data.data.results;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const scrollPrev = () => {
|
const scrollPrev = () => {
|
||||||
if (api?.canScrollPrev()) {
|
if (api?.canScrollPrev()) {
|
||||||
@@ -33,6 +48,10 @@ const Welcome = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const scrollPrevCar = () => {
|
||||||
|
apiCat?.scrollPrev();
|
||||||
|
};
|
||||||
|
|
||||||
const scrollNext = () => {
|
const scrollNext = () => {
|
||||||
if (api?.canScrollNext()) {
|
if (api?.canScrollNext()) {
|
||||||
api?.scrollNext();
|
api?.scrollNext();
|
||||||
@@ -41,78 +60,110 @@ const Welcome = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
const scrollNextCat = () => {
|
||||||
<div className="custom-container">
|
apiCat?.scrollNext();
|
||||||
<Carousel className="w-full" setApi={setApi}>
|
};
|
||||||
<CarouselContent className="!pr-[15%] lg:!pr-[8%] sm:pr-0">
|
|
||||||
{banner.map((e, index) => (
|
|
||||||
<CarouselItem key={index} className="relative">
|
|
||||||
<AspectRatio ratio={16 / 8}>
|
|
||||||
<div className="relative w-full h-full">
|
|
||||||
<Image
|
|
||||||
src={e || '/placeholder.svg'}
|
|
||||||
alt="Banner"
|
|
||||||
fill
|
|
||||||
className="rounded-2xl object-cover shadow-lg border border-slate-200"
|
|
||||||
priority
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</AspectRatio>
|
|
||||||
<Button
|
|
||||||
onClick={scrollNext}
|
|
||||||
className="absolute bottom-5 right-5 max-lg:hidden cursor-pointer"
|
|
||||||
variant={'secondary'}
|
|
||||||
size={'icon'}
|
|
||||||
>
|
|
||||||
<ChevronRight className="size-6" />
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
onClick={scrollPrev}
|
|
||||||
className="absolute bottom-5 right-16 cursor-pointer max-lg:hidden"
|
|
||||||
variant={'secondary'}
|
|
||||||
size={'icon'}
|
|
||||||
>
|
|
||||||
<ChevronLeft className="size-6" />
|
|
||||||
</Button>
|
|
||||||
</CarouselItem>
|
|
||||||
))}
|
|
||||||
</CarouselContent>
|
|
||||||
</Carousel>
|
|
||||||
|
|
||||||
{/* Category Slider */}
|
return (
|
||||||
<div className="mx-auto mt-5 max-lg:hidden">
|
<>
|
||||||
<Swiper
|
<div className="custom-container">
|
||||||
spaceBetween={4}
|
<Carousel className="w-full" setApi={setApi}>
|
||||||
slidesPerView={4}
|
<CarouselContent>
|
||||||
breakpoints={{
|
{isLoading && (
|
||||||
320: { slidesPerView: 1 },
|
<CarouselItem className="relative">
|
||||||
640: { slidesPerView: 2 },
|
<Skeleton className="w-full h-full" />
|
||||||
1024: { slidesPerView: 4 },
|
</CarouselItem>
|
||||||
}}
|
)}
|
||||||
>
|
{isError && (
|
||||||
{categoryList.map((item, index) => (
|
<CarouselItem className="relative gap-2 bg-gray-300/20 rounded-xl flex flex-col justify-center items-center">
|
||||||
<SwiperSlide key={index} className="py-3 px-1">
|
<AlertCircle className="size-10 text-red-500" />
|
||||||
<div
|
<p className="text-red-500">Banner yuklanmadi. Xatolik yuz</p>
|
||||||
className="flex gap-1 items-center justify-center bg-gray-100/60 p-3 rounded-lg shadow-sm cursor-pointer space-x-3"
|
</CarouselItem>
|
||||||
onClick={() => {
|
)}
|
||||||
setOpenToolbar(true);
|
{data &&
|
||||||
setActive(item);
|
data.map((banner, index) => (
|
||||||
}}
|
<CarouselItem key={index} className="relative">
|
||||||
>
|
<AspectRatio
|
||||||
<Image
|
ratio={16 / 7}
|
||||||
src={item.image}
|
className="relative overflow-hidden rounded-2xl"
|
||||||
alt={item.name}
|
>
|
||||||
className="w-7 h-7 object-contain"
|
<Image
|
||||||
/>
|
src={BASE_URL + banner.banner || '/placeholder.svg'}
|
||||||
<p className="text-sm font-bold truncate line-clamp-2 leading-tight text-slate-700">
|
alt={banner.id}
|
||||||
{item.name}
|
fill
|
||||||
</p>
|
className="object-cover"
|
||||||
</div>
|
priority={index === 0}
|
||||||
</SwiperSlide>
|
/>
|
||||||
))}
|
</AspectRatio>
|
||||||
</Swiper>
|
<Button
|
||||||
|
onClick={scrollNext}
|
||||||
|
className="absolute max-lg:w-6 max-lg:h-6 top-1/2 -translate-y-1/2 right-2 cursor-pointer"
|
||||||
|
variant={'secondary'}
|
||||||
|
size={'icon'}
|
||||||
|
>
|
||||||
|
<ChevronRight className="size-6 max-lg:size-5" />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={scrollPrev}
|
||||||
|
className="absolute max-lg:w-6 max-lg:h-6 top-1/2 -translate-y-1/2 left-5 cursor-pointer"
|
||||||
|
variant={'secondary'}
|
||||||
|
size={'icon'}
|
||||||
|
>
|
||||||
|
<ChevronLeft className="size-6 max-lg:size-5" />
|
||||||
|
</Button>
|
||||||
|
</CarouselItem>
|
||||||
|
))}
|
||||||
|
</CarouselContent>
|
||||||
|
</Carousel>
|
||||||
|
|
||||||
|
<Carousel className="w-full mt-5" setApi={setApiCat}>
|
||||||
|
<CarouselContent className="py-2 px-1 pr-[12%]">
|
||||||
|
{category &&
|
||||||
|
category.map((banner, index) => (
|
||||||
|
<CarouselItem
|
||||||
|
key={index}
|
||||||
|
className="basis-1/5 max-lg:basis-1/3 max-md:basis-1/2 max-xs:basis-1/1"
|
||||||
|
>
|
||||||
|
<Link href={`/category/${banner.id}`}>
|
||||||
|
<div className="flex flex-col gap-1 items-center justify-start bg-white p-3 rounded-lg shadow-md cursor-pointer space-x-3">
|
||||||
|
<Image
|
||||||
|
src={BASE_URL + banner.image}
|
||||||
|
alt={banner.name}
|
||||||
|
width={500}
|
||||||
|
height={500}
|
||||||
|
className="w-full h-16 object-contain"
|
||||||
|
/>
|
||||||
|
<p className="text-sm font-bold line-clamp-1 leading-tight text-slate-700">
|
||||||
|
{banner.name}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
</CarouselItem>
|
||||||
|
))}
|
||||||
|
</CarouselContent>
|
||||||
|
<Button
|
||||||
|
onClick={scrollNextCat}
|
||||||
|
className="absolute max-lg:w-8 max-lg:h-8 top-1/2 -translate-y-1/2 -right-2 cursor-pointer"
|
||||||
|
variant={'secondary'}
|
||||||
|
size={'icon'}
|
||||||
|
>
|
||||||
|
<ChevronRight className="size-6 max-lg:size-6" />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={scrollPrevCar}
|
||||||
|
className="absolute max-lg:w-8 max-lg:h-8 top-1/2 -translate-y-1/2 -left-2 cursor-pointer"
|
||||||
|
variant={'secondary'}
|
||||||
|
size={'icon'}
|
||||||
|
>
|
||||||
|
<ChevronLeft className="size-6 max-lg:size-6" />
|
||||||
|
</Button>
|
||||||
|
</Carousel>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
{category &&
|
||||||
|
category
|
||||||
|
.slice(0, 6)
|
||||||
|
.map((e) => <CategoryCarousel category={e} key={e.id} />)}
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user