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,
|
||||
// },
|
||||
images: {
|
||||
remotePatterns: [{ protocol: 'http', hostname: '**' }],
|
||||
remotePatterns: [
|
||||
{ protocol: 'http', hostname: '**' },
|
||||
{ protocol: 'https', hostname: '**' },
|
||||
],
|
||||
},
|
||||
};
|
||||
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",
|
||||
"prepare": "husky"
|
||||
},
|
||||
"packageManager": "pnpm@9.0.0",
|
||||
"dependencies": {
|
||||
"@dnd-kit/core": "^6.3.1",
|
||||
"@dnd-kit/modifiers": "^9.0.0",
|
||||
@@ -43,6 +44,8 @@
|
||||
"clsx": "^2.1.1",
|
||||
"dayjs": "^1.11.13",
|
||||
"embla-carousel-react": "^8.6.0",
|
||||
"framer-motion": "^12.23.26",
|
||||
"js-cookie": "^3.0.5",
|
||||
"lucide-react": "^0.503.0",
|
||||
"next": "^16.0.10",
|
||||
"next-intl": "^4.3.9",
|
||||
@@ -61,6 +64,7 @@
|
||||
"devDependencies": {
|
||||
"@eslint/eslintrc": "^3",
|
||||
"@tailwindcss/postcss": "^4",
|
||||
"@types/js-cookie": "^3.0.6",
|
||||
"@types/node": "^20",
|
||||
"@types/react": "^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';
|
||||
|
||||
const page = () => {
|
||||
return (
|
||||
<Suspense>
|
||||
<SubCategory />
|
||||
<Product />
|
||||
</Suspense>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -20,9 +20,11 @@ export default function LayoutShell({
|
||||
|
||||
return (
|
||||
<Suspense>
|
||||
<div className="flex min-h-screen flex-col">
|
||||
<Navbar />
|
||||
{children}
|
||||
<main className="flex-1 max-lg:mb-20">{children}</main>
|
||||
{!hideFooter && <Footer />}
|
||||
</div>
|
||||
</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';
|
||||
|
||||
export default async function Home() {
|
||||
return (
|
||||
<div>
|
||||
<Welcome />
|
||||
{subCategoriesData.slice(0, 6).map((e) => (
|
||||
<CategoryCarousel category={e} key={e.id} />
|
||||
))}
|
||||
</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 { useTranslations } from 'next-intl';
|
||||
import Image from 'next/image';
|
||||
|
||||
export function AboutContent() {
|
||||
const t = useTranslations();
|
||||
const features = [
|
||||
{
|
||||
number: '1',
|
||||
@@ -44,7 +46,7 @@ export function AboutContent() {
|
||||
{/* Mission Section */}
|
||||
<div className="mb-20">
|
||||
<h2 className="text-4xl md:text-5xl font-bold text-center mb-16 text-balance">
|
||||
Bizning maqsadimiz
|
||||
{t('Bizning maqsadimiz')}
|
||||
</h2>
|
||||
<div className="grid md:grid-cols-3 gap-8">
|
||||
{features.map((feature) => (
|
||||
@@ -55,9 +57,11 @@ export function AboutContent() {
|
||||
<div className="text-6xl font-bold text-primary mb-4">
|
||||
{feature.number}
|
||||
</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">
|
||||
{feature.description}
|
||||
{t(feature.description)}
|
||||
</p>
|
||||
</Card>
|
||||
))}
|
||||
@@ -67,25 +71,22 @@ export function AboutContent() {
|
||||
{/* About Text */}
|
||||
<div className="mb-20 max-w-4xl mx-auto text-center">
|
||||
<h2 className="text-3xl md:text-4xl font-bold mb-8 text-balance">
|
||||
Innovatsiya, sifat va professionallik
|
||||
{t('Innovatsiya, sifat va professionallik')}
|
||||
</h2>
|
||||
<p className="text-lg text-muted-foreground leading-relaxed mb-6">
|
||||
{`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.`}
|
||||
{t(
|
||||
`Gastro Market – bu gastronomiya dunyosidagi eng so'nggi yangiliklarni`,
|
||||
)}
|
||||
</p>
|
||||
<p className="text-lg text-muted-foreground leading-relaxed">
|
||||
{`Bizning jamoamiz tajribali kulinariya mutaxassislari, oshpazlar va
|
||||
gastronomiya sohasidagi ekspertlardan iborat. Biz har bir maqolada
|
||||
sifat va professionallikka e'tibor qaratamiz.`}
|
||||
{t(`Bizning jamoamiz tajribali kulinariya mutaxassislari`)}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Image Gallery */}
|
||||
<div className="mb-20">
|
||||
<h3 className="text-3xl font-bold text-center mb-12 text-balance">
|
||||
Bizning dunyo
|
||||
{t('Bizning dunyo')}
|
||||
</h3>
|
||||
<div className="grid md:grid-cols-3 gap-6">
|
||||
{images.map((image, idx) => (
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { useTranslations } from 'next-intl';
|
||||
import Image from 'next/image';
|
||||
|
||||
export function AboutHero() {
|
||||
const t = useTranslations();
|
||||
return (
|
||||
<section className="relative h-[60vh] min-h-[500px] flex items-center justify-center overflow-hidden">
|
||||
<div className="absolute inset-0 z-0">
|
||||
@@ -17,9 +19,9 @@ export function AboutHero() {
|
||||
Gastro Market
|
||||
</h1>
|
||||
<p className="text-xl md:text-2xl text-white/90 font-light leading-relaxed text-balance">
|
||||
{
|
||||
"Gastronomiya va kulinariya san'ati haqidagi yetakchi onlayn magazin"
|
||||
}
|
||||
{t(
|
||||
"Gastronomiya va kulinariya san'ati haqidagi yetakchi onlayn magazin",
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import formatPhone from '@/shared/lib/formatPhone';
|
||||
import onlyNumber from '@/shared/lib/onlyNumber';
|
||||
import { Button } from '@/shared/ui/button';
|
||||
import { Card } from '@/shared/ui/card';
|
||||
import {
|
||||
@@ -15,11 +16,13 @@ import {
|
||||
import { Input } from '@/shared/ui/input';
|
||||
import { Label } from '@/shared/ui/label';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { Upload } from 'lucide-react';
|
||||
import { useState } from 'react';
|
||||
import { useMutation } from '@tanstack/react-query';
|
||||
import { Loader2, Upload } from 'lucide-react';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { toast } from 'sonner';
|
||||
import * as z from 'zod';
|
||||
import { partner_api } from '../lib/api';
|
||||
|
||||
const MAX_FILE_SIZE = 5 * 1024 * 1024; // 5MB
|
||||
const ACCEPTED_FILE_TYPES = [
|
||||
@@ -32,18 +35,18 @@ const partnershipFormSchema = z.object({
|
||||
companyName: z.string().min(2, {
|
||||
message: "Kompaniya nomi kamida 2 ta belgidan iborat bo'lishi kerak",
|
||||
}),
|
||||
website: z
|
||||
.string()
|
||||
.url({ message: "To'g'ri website manzilini kiriting" })
|
||||
.optional()
|
||||
.or(z.literal('')),
|
||||
// website: z
|
||||
// .string()
|
||||
// .url({ message: "To'g'ri website manzilini kiriting" })
|
||||
// .optional()
|
||||
// .or(z.literal('')),
|
||||
contactPerson: z
|
||||
.string()
|
||||
.min(2, { message: "Ism kamida 2 ta belgidan iborat bo'lishi kerak" }),
|
||||
email: z
|
||||
.string()
|
||||
.email({ message: "To'g'ri email manzilini kiriting" })
|
||||
.optional(),
|
||||
// email: z
|
||||
// .string()
|
||||
// .email({ message: "To'g'ri email manzilini kiriting" })
|
||||
// .optional(),
|
||||
phone: z
|
||||
.string()
|
||||
.min(9, { message: "To'g'ri telefon raqamini kiriting" })
|
||||
@@ -66,53 +69,56 @@ const partnershipFormSchema = z.object({
|
||||
type PartnershipFormValues = z.infer<typeof partnershipFormSchema>;
|
||||
|
||||
export function PartnershipForm() {
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const t = useTranslations();
|
||||
|
||||
const form = useForm<PartnershipFormValues>({
|
||||
resolver: zodResolver(partnershipFormSchema),
|
||||
defaultValues: {
|
||||
companyName: '',
|
||||
website: '',
|
||||
contactPerson: '',
|
||||
email: '',
|
||||
phone: '+998',
|
||||
},
|
||||
});
|
||||
|
||||
async function onSubmit(data: PartnershipFormValues) {
|
||||
console.log(data);
|
||||
|
||||
setIsSubmitting(true);
|
||||
|
||||
try {
|
||||
await new Promise((resolve) => setTimeout(resolve, 2000));
|
||||
|
||||
toast.success("So'rov yuborildi!", {
|
||||
const { mutate, isPending } = useMutation({
|
||||
mutationFn: (body: FormData) => partner_api.send(body),
|
||||
onSuccess: () => {
|
||||
toast.success(t("So'rov yuborildi!"), {
|
||||
richColors: true,
|
||||
position: 'top-center',
|
||||
});
|
||||
|
||||
form.reset();
|
||||
} catch {
|
||||
},
|
||||
onError: () => {
|
||||
toast.error('Xatolik yuz berdi', {
|
||||
richColors: true,
|
||||
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 (
|
||||
<section className="px-4 mb-5">
|
||||
<section className="px-4 mb-5" id="contact">
|
||||
<div className="max-w-3xl mx-auto">
|
||||
<div className="text-center mb-5">
|
||||
<h2 className="text-2xl md:text-5xl font-bold mb-2 text-balance">
|
||||
{`Hamkor bo'ling`}
|
||||
{t(`Hamkor bo'ling`)}
|
||||
</h2>
|
||||
<p className="text-md text-muted-foreground leading-relaxed">
|
||||
{`Gastro Market bilan hamkorlik qilishni xohlaysizmi? Quyidagi formani
|
||||
to'ldiring va biz siz bilan tez orada bog'lanamiz.`}
|
||||
{t(`Gastro Market bilan hamkorlik qilishni xohlaysizmi?`)}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -124,7 +130,7 @@ export function PartnershipForm() {
|
||||
name="companyName"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<Label>Kompaniya nomi</Label>
|
||||
<Label>{t('Kompaniya nomi')}</Label>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder="Kompaniyangiz nomini kiriting"
|
||||
@@ -137,12 +143,12 @@ export function PartnershipForm() {
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
{/* <FormField
|
||||
control={form.control}
|
||||
name="website"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<Label>Website</Label>
|
||||
<Label>{t('Website')}</Label>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder="https://example.com"
|
||||
@@ -154,14 +160,14 @@ export function PartnershipForm() {
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
/> */}
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="contactPerson"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<Label>Ism Familiya</Label>
|
||||
<Label>{t('Ism Familiya')}</Label>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder="Ism va familiya"
|
||||
@@ -174,13 +180,13 @@ export function PartnershipForm() {
|
||||
)}
|
||||
/>
|
||||
|
||||
<div className="grid md:grid-cols-2 gap-6">
|
||||
<FormField
|
||||
<div className="grid md:grid-cols-1 gap-6">
|
||||
{/* <FormField
|
||||
control={form.control}
|
||||
name="email"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex flex-col justify-start">
|
||||
<Label>Email</Label>
|
||||
<Label>{t('Email')}</Label>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder="example@email.com"
|
||||
@@ -192,14 +198,14 @@ export function PartnershipForm() {
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
/> */}
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="phone"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex flex-col justify-start">
|
||||
<Label>Telefon raqami</Label>
|
||||
<Label>{t('Telefon raqami')}</Label>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder="+998 90 123 45 67"
|
||||
@@ -219,7 +225,7 @@ export function PartnershipForm() {
|
||||
name="companyFile"
|
||||
render={({ field: { onChange, value, ...field } }) => (
|
||||
<FormItem>
|
||||
<Label>Kompaniya hujjati</Label>
|
||||
<Label>{t('Kompaniya hujjati')}</Label>
|
||||
<FormControl>
|
||||
<div>
|
||||
<Input
|
||||
@@ -239,18 +245,18 @@ export function PartnershipForm() {
|
||||
>
|
||||
<Upload className="size-10 text-muted-foreground" />
|
||||
<p className="text-muted-foreground text-lg">
|
||||
Faylni tanlang
|
||||
{t('Faylni tanlang')}
|
||||
</p>
|
||||
{value && value.length > 0 && (
|
||||
<p className="mt-2 text-sm text-gray-500">
|
||||
Tanlangan fayl: {value[0].name}
|
||||
{t('Tanlangan fayl')}: {value[0].name}
|
||||
</p>
|
||||
)}
|
||||
</FormLabel>
|
||||
</div>
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
PDF yoki Word formatida (maksimal 5MB)
|
||||
{t('PDF yoki Word formatida (maksimal 5MB)')}
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
@@ -260,10 +266,14 @@ export function PartnershipForm() {
|
||||
<Button
|
||||
type="submit"
|
||||
size="lg"
|
||||
className="w-full"
|
||||
disabled={isSubmitting}
|
||||
className="w-full cursor-pointer"
|
||||
disabled={isPending}
|
||||
>
|
||||
{isSubmitting ? 'Yuborilmoqda...' : "So'rov yuborish"}
|
||||
{isPending ? (
|
||||
<Loader2 className="animate-spin" />
|
||||
) : (
|
||||
t("So'rov yuborish")
|
||||
)}
|
||||
</Button>
|
||||
</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';
|
||||
|
||||
import { useRouter } from '@/shared/config/i18n/navigation';
|
||||
import formatPhone from '@/shared/lib/formatPhone';
|
||||
import { Link, useRouter } from '@/shared/config/i18n/navigation';
|
||||
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 { ArrowRight, Check, Lock, Phone } from 'lucide-react';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
|
||||
type Step = 'phone' | 'otp';
|
||||
import { Label } from '@/shared/ui/label';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
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 [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 t = useTranslations();
|
||||
const form = useForm<z.infer<typeof authForm>>({
|
||||
resolver: zodResolver(authForm),
|
||||
defaultValues: {
|
||||
password: '',
|
||||
username: '',
|
||||
},
|
||||
});
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const otpInputs = useRef<Array<HTMLInputElement | null>>([]);
|
||||
|
||||
/* Countdown */
|
||||
useEffect(() => {
|
||||
if (step === 'otp' && countdown > 0) {
|
||||
const timer = setTimeout(() => setCountdown((c) => c - 1), 1000);
|
||||
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');
|
||||
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('/');
|
||||
} else {
|
||||
setError("Noto'g'ri kod. Qayta urinib ko'ring.");
|
||||
setOtp(['', '', '', '', '', '']);
|
||||
otpInputs.current[0]?.focus();
|
||||
queryClient.refetchQueries({ queryKey: ['product_list'] });
|
||||
},
|
||||
onError: () => {
|
||||
toast.error(t('Username yoki parol xato kiritildi'), {
|
||||
richColors: true,
|
||||
position: 'top-center',
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
function onSubmit(values: z.infer<typeof authForm>) {
|
||||
mutate({
|
||||
password: values.password,
|
||||
username: values.username,
|
||||
});
|
||||
}
|
||||
}, 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 (
|
||||
<div className="custom-container flex justify-center items-center h-[85vh]">
|
||||
@@ -152,167 +67,71 @@ const Login = () => {
|
||||
{/* Header */}
|
||||
<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">
|
||||
{step === 'phone' ? (
|
||||
<Phone className="w-10 h-10 text-blue-500" />
|
||||
) : (
|
||||
<Lock className="w-10 h-10 text-blue-500" />
|
||||
)}
|
||||
<User className="w-10 h-10 text-blue-500" />
|
||||
</div>
|
||||
<h1 className="text-2xl font-bold mb-2">
|
||||
{step === 'phone' ? 'Xush kelibsiz!' : 'Kodni tasdiqlang'}
|
||||
</h1>
|
||||
<p className="text-blue-100">
|
||||
{step === 'phone'
|
||||
? 'Telefon raqamingizni kiriting'
|
||||
: `${phoneNumber} raqamiga yuborilgan kodni kiriting`}
|
||||
<p className="text-blue-100 text-2xl font-semibold">
|
||||
{t('Tizimga kirish')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Form */}
|
||||
<div className="p-8">
|
||||
{step === 'phone' ? (
|
||||
// Phone Number Step
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Telefon raqam
|
||||
</label>
|
||||
<div className="relative">
|
||||
<div className="absolute inset-y-0 left-0 pl-4 flex items-center pointer-events-none">
|
||||
<Phone className="w-5 h-5 text-gray-400" />
|
||||
</div>
|
||||
<Form {...form}>
|
||||
<form
|
||||
className="p-8 space-y-4"
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="username"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<Label>{t('Username')}</Label>
|
||||
<FormControl>
|
||||
<Input
|
||||
type="tel"
|
||||
value={formatPhone(phoneNumber)}
|
||||
onChange={(e) => {
|
||||
const value = e.target.value.replace(/\D/g, '');
|
||||
setPhoneNumber(value);
|
||||
setError('');
|
||||
}}
|
||||
placeholder="+998 90 123-45-67"
|
||||
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"
|
||||
placeholder={t('Username')}
|
||||
className="h-12"
|
||||
{...field}
|
||||
/>
|
||||
</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" />
|
||||
</>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
</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) => (
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="password"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<Label>{t('Parol')}</Label>
|
||||
<FormControl>
|
||||
<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"
|
||||
placeholder={t('Parol')}
|
||||
className="h-12"
|
||||
{...field}
|
||||
/>
|
||||
))}
|
||||
</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>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
|
||||
<button
|
||||
onClick={() => handleOtpSubmit()}
|
||||
disabled={isLoading || otp.some((digit) => digit === '')}
|
||||
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"
|
||||
>
|
||||
{isLoading ? (
|
||||
<>
|
||||
<div className="w-5 h-5 border-2 border-white border-t-transparent rounded-full animate-spin" />
|
||||
Tekshirilmoqda...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
Tasdiqlash
|
||||
<Check className="w-5 h-5" />
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
|
||||
{/* Resend OTP */}
|
||||
<div className="mt-6 text-center">
|
||||
{canResend ? (
|
||||
<button
|
||||
onClick={handleResendOtp}
|
||||
disabled={isLoading}
|
||||
className="text-blue-600 hover:text-blue-700 font-semibold hover:underline"
|
||||
>
|
||||
Kodni qayta yuborish
|
||||
</button>
|
||||
) : (
|
||||
<p className="text-gray-500 text-sm">
|
||||
Kodni qayta yuborish ({countdown}s)
|
||||
/>
|
||||
<p className="text-muted-foreground font-semibold mt-5 text-sm">
|
||||
{t(
|
||||
"Agarda sizda kirish uchun login va parol yo'q bolsa iltimos bizga murojat qiling",
|
||||
)}{' '}
|
||||
<Link href={'/about/#contact'} className="text-blue-500">
|
||||
{t('Murojat qilish')}
|
||||
</Link>
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Change Number */}
|
||||
<button
|
||||
onClick={handleChangeNumber}
|
||||
className="w-full mt-4 text-gray-600 hover:text-gray-800 font-medium"
|
||||
<Button
|
||||
disabled={isPending}
|
||||
type="submit"
|
||||
className="w-full h-12 text-md"
|
||||
>
|
||||
{"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>
|
||||
{isPending ? <Loader2 className="animate-spin" /> : t('Kirish')}
|
||||
</Button>
|
||||
</form>
|
||||
</Form>
|
||||
</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' }),
|
||||
long: 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(),
|
||||
});
|
||||
// 998901234567
|
||||
|
||||
@@ -1,8 +1,13 @@
|
||||
'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 { useCartId } from '@/shared/hooks/cartId';
|
||||
import formatPrice from '@/shared/lib/formatPrice';
|
||||
import { Button } from '@/shared/ui/button';
|
||||
import { Input } from '@/shared/ui/input';
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import { AxiosError } from 'axios';
|
||||
import {
|
||||
ArrowLeft,
|
||||
CreditCard,
|
||||
@@ -12,205 +17,198 @@ import {
|
||||
Trash,
|
||||
Truck,
|
||||
} from 'lucide-react';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import Image from 'next/image';
|
||||
import { useState } from 'react';
|
||||
|
||||
interface CartItem {
|
||||
id: number;
|
||||
name: string;
|
||||
price: number;
|
||||
oldPrice: number;
|
||||
image: string;
|
||||
quantity: number | string;
|
||||
inStock: boolean;
|
||||
}
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
const CartPage = () => {
|
||||
const { cart_id } = useCartId();
|
||||
const queryClient = useQueryClient();
|
||||
const router = useRouter();
|
||||
const [cartItems, setCartItems] = useState<CartItem[]>([
|
||||
{
|
||||
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 t = useTranslations();
|
||||
|
||||
const subtotal = cartItems.reduce(
|
||||
(sum, item) => sum + item.price * Number(item.quantity),
|
||||
0,
|
||||
const { data: cartItems, isLoading } = useQuery({
|
||||
queryKey: ['cart_items', cart_id],
|
||||
queryFn: () => cart_api.get_cart_items(cart_id!),
|
||||
enabled: !!cart_id,
|
||||
select: (data) => data.data.cart_item,
|
||||
});
|
||||
|
||||
const [quantities, setQuantities] = useState<Record<string, string>>({});
|
||||
const debounceRef = useRef<Record<string, NodeJS.Timeout | null>>({});
|
||||
|
||||
useEffect(() => {
|
||||
if (!cartItems) return;
|
||||
const initialQuantities: Record<string, string> = {};
|
||||
cartItems.forEach((item) => {
|
||||
initialQuantities[item.id] = String(item.quantity);
|
||||
debounceRef.current[item.id] = null;
|
||||
});
|
||||
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>
|
||||
);
|
||||
const discount = cartItems.reduce((sum, 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') => {
|
||||
setCartItems((prev) =>
|
||||
prev.map((item) => {
|
||||
if (item.id === id) {
|
||||
if (type === 'increase')
|
||||
return { ...item, quantity: Number(item.quantity) + 1 };
|
||||
if (type === 'decrease' && Number(item.quantity) > 1)
|
||||
return { ...item, quantity: Number(item.quantity) - 1 };
|
||||
}
|
||||
return item;
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
// Remove item from cart
|
||||
const handleRemoveItem = (id: number) => {
|
||||
setCartItems((prev) => prev.filter((item) => item.id !== id));
|
||||
};
|
||||
|
||||
const handleCheckout = () => {
|
||||
router.push('/cart/order');
|
||||
};
|
||||
|
||||
if (cartItems.length === 0) {
|
||||
if (!cartItems || cartItems.length === 0)
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center">
|
||||
<div className="text-center">
|
||||
<ShoppingBag className="w-24 h-24 text-gray-300 mx-auto mb-4" />
|
||||
<h2 className="text-2xl font-bold text-gray-800 mb-2">
|
||||
{"Savatingiz bo'sh"}
|
||||
{t("Savatingiz bo'sh")}
|
||||
</h2>
|
||||
<p className="text-gray-600 mb-6">
|
||||
{"Mahsulotlar qo'shish uchun katalogga o'ting"}
|
||||
{t("Mahsulotlar qo'shish uchun katalogga o'ting")}
|
||||
</p>
|
||||
<button
|
||||
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"
|
||||
>
|
||||
<ArrowLeft className="w-5 h-5" /> Xarid qilishni boshlash
|
||||
<ArrowLeft className="w-5 h-5" /> {t('Xarid qilishni boshlash')}
|
||||
</button>
|
||||
</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 (
|
||||
<div className="custom-container mb-6">
|
||||
<>
|
||||
{/* Header */}
|
||||
<div className="mb-6">
|
||||
<h1 className="text-3xl font-bold text-gray-800 mb-2">Savat</h1>
|
||||
<p className="text-gray-600">{cartItems.length} ta mahsulot</p>
|
||||
<h1 className="text-3xl font-bold text-gray-800 mb-2">{t('Savat')}</h1>
|
||||
<p className="text-gray-600">
|
||||
{cartItems.length} {t('ta mahsulot')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||
{/* Cart Items */}
|
||||
<div className="lg:col-span-2">
|
||||
<div className="bg-white rounded-lg shadow-md overflow-hidden">
|
||||
{cartItems.map((item, index) => (
|
||||
<div
|
||||
key={item.id}
|
||||
className={`p-6 flex relative gap-4 ${index !== cartItems.length - 1 ? 'border-b' : ''}`}
|
||||
className={`p-6 flex relative gap-4 ${
|
||||
index !== cartItems.length - 1 ? 'border-b' : ''
|
||||
}`}
|
||||
>
|
||||
{/* Product Image */}
|
||||
<Button
|
||||
variant={'destructive'}
|
||||
size={'icon'}
|
||||
onClick={() => handleRemoveItem(item.id)}
|
||||
variant="destructive"
|
||||
size="icon"
|
||||
onClick={() => deleteCartItem({ cart_item_id: 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
|
||||
src={BASE_URL + item.product_image}
|
||||
alt={item.product_name}
|
||||
width={500}
|
||||
height={500}
|
||||
src={item.image}
|
||||
alt={item.name}
|
||||
className="w-full h-full object-cover"
|
||||
className="object-cover"
|
||||
style={{ width: '100%', height: 'auto' }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Product Info */}
|
||||
<div className="flex-1">
|
||||
<h3 className="font-semibold text-lg mb-1">{item.name}</h3>
|
||||
<h3 className="font-semibold text-lg mb-1">
|
||||
{item.product_name}
|
||||
</h3>
|
||||
<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">
|
||||
{item.price.toLocaleString()} {"so'm"}
|
||||
{formatPrice(item.product_price, true)}
|
||||
</span>
|
||||
{item.oldPrice && (
|
||||
<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">
|
||||
<div className="flex items-center border border-gray-300 rounded-lg w-max">
|
||||
<button
|
||||
onClick={() =>
|
||||
handleQuantityChange(item.id, 'decrease')
|
||||
handleQuantityChange(
|
||||
item.id,
|
||||
Number(quantities[item.id]) - 1,
|
||||
)
|
||||
}
|
||||
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}
|
||||
value={quantities[item.id]}
|
||||
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,
|
||||
),
|
||||
);
|
||||
}
|
||||
const val = e.target.value.replace(/\D/g, ''); // faqat raqam
|
||||
setQuantities((prev) => ({
|
||||
...prev,
|
||||
[item.id]: val,
|
||||
}));
|
||||
|
||||
// Debounce bilan update
|
||||
const valNum = Number(val);
|
||||
if (!isNaN(valNum))
|
||||
handleQuantityChange(item.id, valNum);
|
||||
}}
|
||||
className="w-16 text-center outline-none ring-0 focus-visible:ring-0 border-none font-semibold"
|
||||
type="text"
|
||||
className="w-16 text-center"
|
||||
/>
|
||||
|
||||
<button
|
||||
onClick={() =>
|
||||
handleQuantityChange(item.id, 'increase')
|
||||
handleQuantityChange(
|
||||
item.id,
|
||||
Number(quantities[item.id]) + 1,
|
||||
)
|
||||
}
|
||||
className="p-2 cursor-pointer transition rounded-lg"
|
||||
>
|
||||
@@ -219,63 +217,37 @@ const CartPage = () => {
|
||||
</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>
|
||||
<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>Mahsulotlar narxi:</span>
|
||||
<span>
|
||||
{subtotal.toLocaleString()} {"so'm"}
|
||||
</span>
|
||||
<span>{t('Mahsulotlar narxi')}:</span>
|
||||
<span>{formatPrice(subtotal, true)}</span>
|
||||
</div>
|
||||
|
||||
{discount > 0 && (
|
||||
<div className="flex justify-between text-green-600">
|
||||
<span>Chegirma:</span>
|
||||
<span>
|
||||
-{discount.toLocaleString()} {"so'm"}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex justify-between text-gray-600">
|
||||
<span className="flex items-center gap-1">
|
||||
<Truck className="w-4 h-4" />
|
||||
Yetkazib berish:
|
||||
<Truck className="w-4 h-4" /> {t('Yetkazib berish')}:
|
||||
</span>
|
||||
<span>
|
||||
{deliveryFee === 0 ? (
|
||||
<span className="text-green-600 font-semibold">
|
||||
Bepul
|
||||
{t('Bepul')}
|
||||
</span>
|
||||
) : (
|
||||
`${deliveryFee.toLocaleString()} so'm`
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{deliveryFee > 0 && (
|
||||
<p className="text-sm text-gray-500 bg-blue-50 p-2 rounded">
|
||||
{
|
||||
"50,000 so'mdan ortiq xarid qiling va yetkazib berishni bepul oling!"
|
||||
}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="border-t pt-4 mb-6">
|
||||
<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">
|
||||
{total.toLocaleString()} {"so'm"}
|
||||
{formatPrice(subtotal, true)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -284,34 +256,20 @@ const CartPage = () => {
|
||||
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
|
||||
<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" />
|
||||
Xaridni davom ettirish
|
||||
<ArrowLeft className="w-5 h-5" /> {t('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>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
'use client';
|
||||
|
||||
import { BASE_URL } from '@/shared/config/api/URLs';
|
||||
import { useCartId } from '@/shared/hooks/cartId';
|
||||
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 {
|
||||
Form,
|
||||
@@ -11,6 +15,7 @@ import {
|
||||
} from '@/shared/ui/form';
|
||||
import { Input } from '@/shared/ui/input';
|
||||
import { Label } from '@/shared/ui/label';
|
||||
import { Textarea } from '@/shared/ui/textarea';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import {
|
||||
Map,
|
||||
@@ -19,11 +24,12 @@ import {
|
||||
YMaps,
|
||||
ZoomControl,
|
||||
} from '@pbe/react-yandex-maps';
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import {
|
||||
Building2,
|
||||
CheckCircle2,
|
||||
Clock,
|
||||
CreditCard,
|
||||
Loader2,
|
||||
LocateFixed,
|
||||
MapPin,
|
||||
Package,
|
||||
@@ -31,10 +37,13 @@ import {
|
||||
User,
|
||||
Wallet,
|
||||
} from 'lucide-react';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import Image from 'next/image';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { toast } from 'sonner';
|
||||
import z from 'zod';
|
||||
import { cart_api, OrderCreateBody } from '../lib/api';
|
||||
import { orderForm } from '../lib/form';
|
||||
|
||||
interface CoordsData {
|
||||
@@ -44,53 +53,72 @@ interface CoordsData {
|
||||
}
|
||||
|
||||
const OrderPage = () => {
|
||||
const t = useTranslations();
|
||||
const form = useForm<z.infer<typeof orderForm>>({
|
||||
resolver: zodResolver(orderForm),
|
||||
defaultValues: {
|
||||
firstName: '',
|
||||
comment: '',
|
||||
lastName: '',
|
||||
lat: '',
|
||||
long: '',
|
||||
phone: '+998',
|
||||
},
|
||||
});
|
||||
|
||||
const [paymentMethod, setPaymentMethod] = useState('cash');
|
||||
const [deliveryMethod, setDeliveryMethod] = useState('standard');
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const [cart, setCart] = useState<number | string | null>(null);
|
||||
const { cart_id } = useCartId();
|
||||
const [orderSuccess, setOrderSuccess] = useState(false);
|
||||
const queryClinet = useQueryClient();
|
||||
|
||||
const cartItems = [
|
||||
{
|
||||
id: 5,
|
||||
name: 'Coca-Cola 1.5L',
|
||||
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 { data } = useQuery({
|
||||
queryKey: ['clear_cart', cart],
|
||||
queryFn: () => cart_api.clear_cart(cart!),
|
||||
enabled: !!cart,
|
||||
});
|
||||
|
||||
const subtotal = cartItems.reduce(
|
||||
(sum, item) => sum + item.price * item.quantity,
|
||||
console.log(data);
|
||||
|
||||
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,
|
||||
);
|
||||
|
||||
const deliveryFee =
|
||||
deliveryMethod === 'express' ? 25000 : subtotal > 50000 ? 0 : 15000;
|
||||
const total = subtotal + deliveryFee;
|
||||
deliveryMethod === 'DELIVERY_COURIES'
|
||||
? 25000
|
||||
: subtotal && subtotal > 50000
|
||||
? 0
|
||||
: 15000;
|
||||
const total = subtotal;
|
||||
|
||||
const [coords, setCoords] = useState({
|
||||
latitude: 41.311081,
|
||||
@@ -187,13 +215,27 @@ const OrderPage = () => {
|
||||
}, [cityValue]);
|
||||
|
||||
function onSubmit(value: z.infer<typeof orderForm>) {
|
||||
setIsSubmitting(true);
|
||||
console.log(value);
|
||||
if (!cartItems || cartItems.length === 0) {
|
||||
toast.error('Savatcha bo‘sh', {
|
||||
richColors: true,
|
||||
position: 'top-center',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
setIsSubmitting(false);
|
||||
setOrderSuccess(true);
|
||||
}, 2000);
|
||||
const items = cartItems.map((item) => ({
|
||||
product_id: item.product_id,
|
||||
quantity: item.quantity,
|
||||
}));
|
||||
|
||||
mutate({
|
||||
comment: value.comment,
|
||||
contact_number: onlyNumber(value.phone),
|
||||
delivery_type: deliveryMethod,
|
||||
name: value.firstName + ' ' + value.lastName,
|
||||
payment_type: paymentMethod,
|
||||
items: items,
|
||||
});
|
||||
}
|
||||
|
||||
if (orderSuccess) {
|
||||
@@ -204,28 +246,16 @@ const OrderPage = () => {
|
||||
<CheckCircle2 className="w-12 h-12 text-green-600" />
|
||||
</div>
|
||||
<h2 className="text-2xl font-bold text-gray-800 mb-2">
|
||||
Buyurtma qabul qilindi!
|
||||
{t('Buyurtma qabul qilindi!')}
|
||||
</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">
|
||||
Buyurtmangiz muvaffaqiyatli qabul qilindi. Tez orada sizga aloqaga
|
||||
chiqamiz.
|
||||
{t('Buyurtmangiz muvaffaqiyatli qabul qilindi')}
|
||||
</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
|
||||
onClick={() => (window.location.href = '/')}
|
||||
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>
|
||||
</div>
|
||||
</div>
|
||||
@@ -238,9 +268,9 @@ const OrderPage = () => {
|
||||
{/* Header */}
|
||||
<div className="mb-6">
|
||||
<h1 className="text-3xl font-bold text-gray-800 mb-2">
|
||||
Buyurtmani rasmiylashtirish
|
||||
{t('Buyurtmani rasmiylashtirish')}
|
||||
</h1>
|
||||
<p className="text-gray-600">{"Ma'lumotlaringizni to'ldiring"}</p>
|
||||
<p className="text-gray-600">{t("Ma'lumotlaringizni to'ldiring")}</p>
|
||||
</div>
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)}>
|
||||
@@ -252,7 +282,7 @@ const OrderPage = () => {
|
||||
<div className="flex items-center gap-2 mb-4">
|
||||
<User className="w-5 h-5 text-blue-600" />
|
||||
<h2 className="text-xl font-semibold">
|
||||
{"Shaxsiy ma'lumotlar"}
|
||||
{t("Shaxsiy ma'lumotlar")}
|
||||
</h2>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
@@ -262,13 +292,13 @@ const OrderPage = () => {
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex justify-start flex-col">
|
||||
<Label className="block text-sm font-medium text-gray-700">
|
||||
{'Ism'}
|
||||
{t('Ism')}
|
||||
</Label>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
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>
|
||||
<FormMessage />
|
||||
@@ -282,13 +312,13 @@ const OrderPage = () => {
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex justify-start flex-col">
|
||||
<Label className="block text-sm font-medium text-gray-700">
|
||||
{'Familiya'}
|
||||
{t('Familiya')}
|
||||
</Label>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
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>
|
||||
<FormMessage />
|
||||
@@ -302,7 +332,7 @@ const OrderPage = () => {
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<Label className="block text-sm font-medium text-gray-700">
|
||||
Telefon raqam
|
||||
{t('Telefon raqam')}
|
||||
</Label>
|
||||
<FormControl>
|
||||
<Input
|
||||
@@ -319,6 +349,25 @@ const OrderPage = () => {
|
||||
)}
|
||||
/>
|
||||
</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>
|
||||
|
||||
{/* Delivery Address */}
|
||||
@@ -326,7 +375,7 @@ const OrderPage = () => {
|
||||
<div className="flex items-center gap-2 mb-4">
|
||||
<MapPin className="w-5 h-5 text-blue-600" />
|
||||
<h2 className="text-xl font-semibold">
|
||||
Yetkazib berish manzili
|
||||
{t('Yetkazib berish manzili')}
|
||||
</h2>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
@@ -336,14 +385,14 @@ const OrderPage = () => {
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<Label className="block text-sm font-medium text-gray-700">
|
||||
Manzilni qidirish
|
||||
{t('Manzilni qidirish')}
|
||||
</Label>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
type="text"
|
||||
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>
|
||||
<FormMessage />
|
||||
@@ -392,7 +441,7 @@ const OrderPage = () => {
|
||||
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" />
|
||||
Mening joylashuvim
|
||||
{t('Mening joylashuvim')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -403,7 +452,7 @@ const OrderPage = () => {
|
||||
<div className="flex items-center gap-2 mb-4">
|
||||
<Truck className="w-5 h-5 text-blue-600" />
|
||||
<h2 className="text-xl font-semibold">
|
||||
Yetkazib berish usuli
|
||||
{t('Yetkazib berish usuli')}
|
||||
</h2>
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
@@ -412,8 +461,8 @@ const OrderPage = () => {
|
||||
type="radio"
|
||||
name="delivery"
|
||||
value="standard"
|
||||
checked={deliveryMethod === 'standard'}
|
||||
onChange={(e) => setDeliveryMethod(e.target.value)}
|
||||
checked={deliveryMethod === 'DELIVERY_COURIES'}
|
||||
onChange={() => setDeliveryMethod('DELIVERY_COURIES')}
|
||||
className="w-4 h-4 text-blue-600"
|
||||
/>
|
||||
<div className="ml-4 flex-1">
|
||||
@@ -421,15 +470,17 @@ const OrderPage = () => {
|
||||
<div className="flex items-center gap-2">
|
||||
<Clock className="w-5 h-5 text-gray-600" />
|
||||
<span className="font-semibold">
|
||||
Standart yetkazib berish
|
||||
{t('Standart yetkazib berish')}
|
||||
</span>
|
||||
</div>
|
||||
<span className="font-bold text-blue-600">
|
||||
{subtotal > 50000 ? 'Bepul' : "15,000 so'm"}
|
||||
{subtotal && subtotal > 50000
|
||||
? 'Bepul'
|
||||
: "15,000 so'm"}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-sm text-gray-500 mt-1">
|
||||
2-3 kun ichida
|
||||
{t('2-3 kun ichida')}
|
||||
</p>
|
||||
</div>
|
||||
</label>
|
||||
@@ -439,8 +490,8 @@ const OrderPage = () => {
|
||||
type="radio"
|
||||
name="delivery"
|
||||
value="express"
|
||||
checked={deliveryMethod === 'express'}
|
||||
onChange={(e) => setDeliveryMethod(e.target.value)}
|
||||
checked={deliveryMethod === 'YandexGo'}
|
||||
onChange={() => setDeliveryMethod('YandexGo')}
|
||||
className="w-4 h-4 text-blue-600"
|
||||
/>
|
||||
<div className="ml-4 flex-1">
|
||||
@@ -448,7 +499,7 @@ const OrderPage = () => {
|
||||
<div className="flex items-center gap-2">
|
||||
<Package className="w-5 h-5 text-gray-600" />
|
||||
<span className="font-semibold">
|
||||
Tez yetkazib berish
|
||||
{t('Tez yetkazib berish')}
|
||||
</span>
|
||||
</div>
|
||||
<span className="font-bold text-blue-600">
|
||||
@@ -456,7 +507,7 @@ const OrderPage = () => {
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-sm text-gray-500 mt-1">
|
||||
1 kun ichida
|
||||
{t('1 kun ichida')}
|
||||
</p>
|
||||
</div>
|
||||
</label>
|
||||
@@ -466,7 +517,9 @@ const OrderPage = () => {
|
||||
<div className="bg-white rounded-lg shadow-md p-6">
|
||||
<div className="flex items-center gap-2 mb-4">
|
||||
<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 className="space-y-3">
|
||||
<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"
|
||||
name="payment"
|
||||
value="cash"
|
||||
checked={paymentMethod === 'cash'}
|
||||
onChange={(e) => setPaymentMethod(e.target.value)}
|
||||
checked={paymentMethod === 'CASH'}
|
||||
onChange={() => setPaymentMethod('CASH')}
|
||||
className="w-4 h-4 text-blue-600"
|
||||
/>
|
||||
<div className="ml-4 flex items-center gap-3">
|
||||
<Wallet className="w-6 h-6 text-green-600" />
|
||||
<div>
|
||||
<span className="font-semibold">Naqd pul</span>
|
||||
<span className="font-semibold">{t('Naqd pul')}</span>
|
||||
<p className="text-sm text-gray-500">
|
||||
{"Yetkazib berishda to'lash"}
|
||||
{t("Yetkazib berishda to'lash")}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -494,36 +547,18 @@ const OrderPage = () => {
|
||||
type="radio"
|
||||
name="payment"
|
||||
value="card"
|
||||
checked={paymentMethod === 'card'}
|
||||
onChange={(e) => setPaymentMethod(e.target.value)}
|
||||
checked={paymentMethod === 'ACCOUNT_NUMBER'}
|
||||
onChange={() => setPaymentMethod('ACCOUNT_NUMBER')}
|
||||
className="w-4 h-4 text-blue-600"
|
||||
/>
|
||||
<div className="ml-4 flex items-center gap-3">
|
||||
<CreditCard className="w-6 h-6 text-blue-600" />
|
||||
<div>
|
||||
<span className="font-semibold">Plastik karta</span>
|
||||
<span className="font-semibold">
|
||||
{t('Plastik karta')}
|
||||
</span>
|
||||
<p className="text-sm text-gray-500">
|
||||
{"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
|
||||
{t("Online to'lov")}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -535,28 +570,32 @@ const OrderPage = () => {
|
||||
{/* Right Column - 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">Mahsulotlar</h3>
|
||||
<h3 className="text-xl font-bold mb-4">{t('Mahsulotlar')}</h3>
|
||||
|
||||
{/* Cart Items */}
|
||||
<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">
|
||||
<Image
|
||||
width={500}
|
||||
height={500}
|
||||
src={item.image}
|
||||
alt={item.name}
|
||||
src={BASE_URL + item.product_image}
|
||||
alt={item.product_name}
|
||||
className="w-16 h-16 object-contain bg-gray-100 rounded"
|
||||
/>
|
||||
<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">
|
||||
{item.quantity} x {item.price.toLocaleString()}{' '}
|
||||
{"so'm"}
|
||||
{item.quantity} x{' '}
|
||||
{formatPrice(item.product_price, true)}
|
||||
</p>
|
||||
<p className="font-semibold text-sm">
|
||||
{(item.price * item.quantity).toLocaleString()}{' '}
|
||||
{"so'm"}
|
||||
{formatPrice(
|
||||
item.product_price * item.quantity,
|
||||
true,
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -566,17 +605,15 @@ const OrderPage = () => {
|
||||
{/* Pricing */}
|
||||
<div className="space-y-2 mb-4 pt-4 border-t">
|
||||
<div className="flex justify-between text-gray-600">
|
||||
<span>Mahsulotlar:</span>
|
||||
<span>
|
||||
{subtotal.toLocaleString()} {"so'm"}
|
||||
</span>
|
||||
<span>{t('Mahsulotlar')}:</span>
|
||||
<span>{subtotal && formatPrice(subtotal, true)}</span>
|
||||
</div>
|
||||
<div className="flex justify-between text-gray-600">
|
||||
<span>Yetkazib berish:</span>
|
||||
<span>{t('Yetkazib berish')}:</span>
|
||||
<span>
|
||||
{deliveryFee === 0 ? (
|
||||
<span className="text-green-600 font-semibold">
|
||||
Bepul
|
||||
{t('Bepul')}
|
||||
</span>
|
||||
) : (
|
||||
`${deliveryFee.toLocaleString()} so'm`
|
||||
@@ -587,25 +624,26 @@ const OrderPage = () => {
|
||||
|
||||
<div className="border-t pt-4 mb-6">
|
||||
<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">
|
||||
{total.toLocaleString()} {"so'm"}
|
||||
{total && formatPrice(total, true)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
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"
|
||||
>
|
||||
{isSubmitting ? (
|
||||
{isPending ? (
|
||||
<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" />
|
||||
Yuborilmoqda...
|
||||
<Loader2 className="animate-spin" />
|
||||
</span>
|
||||
) : (
|
||||
'Buyurtmani tasdiqlash'
|
||||
t('Buyurtmani tasdiqlash')
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -1,34 +1,53 @@
|
||||
'use client';
|
||||
import { useRouter } from '@/shared/config/i18n/navigation';
|
||||
import { categoryList, CategoryType } from '@/widgets/welcome/lib/data';
|
||||
import { category_api } from '@/shared/config/api/category/api';
|
||||
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 { useTranslations } from 'next-intl';
|
||||
import Image from 'next/image';
|
||||
|
||||
const Category = () => {
|
||||
const router = useRouter();
|
||||
const handleCategoryClick = (category: CategoryType) => {
|
||||
router.push(`/category/${category.name}`);
|
||||
};
|
||||
const t = useTranslations();
|
||||
const { data: category } = useQuery({
|
||||
queryKey: ['category_list'],
|
||||
queryFn: () => category_api.getCategory({ page: 1, page_size: 99 }),
|
||||
select(data) {
|
||||
return data.data.results;
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50 p-4">
|
||||
<div className="max-w-6xl mx-auto">
|
||||
<div className="custom-container">
|
||||
<>
|
||||
<h1 className="text-2xl font-semibold text-gray-900 mb-6">
|
||||
Kategoriyalar
|
||||
{t('Kategoriyalar')}
|
||||
</h1>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-3">
|
||||
{categoryList.map((category, index) => (
|
||||
<button
|
||||
{category &&
|
||||
category.map((category, index) => (
|
||||
<Link
|
||||
key={index}
|
||||
onClick={() => handleCategoryClick(category)}
|
||||
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>
|
||||
<div className="flex items-center gap-4">
|
||||
<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" />
|
||||
</button>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,45 +1,67 @@
|
||||
'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 { useQuery } from '@tanstack/react-query';
|
||||
import { ArrowLeft } from 'lucide-react';
|
||||
import { useParams } from 'next/navigation';
|
||||
import { useState } from 'react';
|
||||
import { subCategoriesData } from '../lib/data';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { useParams, useSearchParams } from 'next/navigation';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
const PAGE_SIZE = 36;
|
||||
|
||||
const Product = () => {
|
||||
const { subId } = useParams();
|
||||
const { categoryId } = useParams();
|
||||
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 = () => {
|
||||
router.back();
|
||||
};
|
||||
|
||||
const handleRemove = (id: number) => {
|
||||
setProducts((prev) =>
|
||||
prev.map((product) =>
|
||||
product.id === id ? { ...product, liked: false } : product,
|
||||
),
|
||||
);
|
||||
};
|
||||
const handlePageChange = (newPage: number) => {
|
||||
const params = new URLSearchParams(searchParams.toString());
|
||||
params.set('page', newPage.toString());
|
||||
|
||||
const handleLiked = (id: number) => {
|
||||
setProducts((prev) =>
|
||||
prev.map((product) =>
|
||||
product.id === id ? { ...product, liked: true } : product,
|
||||
),
|
||||
);
|
||||
router.push(`${pathname}?${params.toString()}`, {
|
||||
scroll: true,
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="custom-container p-4 mb-5">
|
||||
<div>
|
||||
<div className="custom-container p-4 mb-5 flex flex-col min-h-[calc(85vh)]">
|
||||
<div className="flex-1">
|
||||
{/* Header */}
|
||||
<div className="mb-6">
|
||||
<button
|
||||
@@ -47,28 +69,46 @@ const Product = () => {
|
||||
className="flex items-center gap-2 text-gray-600 hover:text-gray-900 mb-4"
|
||||
>
|
||||
<ArrowLeft className="w-5 h-5" />
|
||||
<span>Orqaga</span>
|
||||
<span>{t('Orqaga')}</span>
|
||||
</button>
|
||||
|
||||
<h1 className="text-2xl font-semibold text-gray-900">
|
||||
{decodedSubId}
|
||||
</h1>
|
||||
<p className="text-gray-600 text-sm mt-1">
|
||||
{subCategory.products.length} ta mahsulot
|
||||
{product?.total} {t('ta mahsulot')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-6 gap-3">
|
||||
{products.map((product) => (
|
||||
<ProductCard
|
||||
key={product.id}
|
||||
product={product}
|
||||
handleRemove={handleRemove}
|
||||
handleLiked={handleLiked}
|
||||
/>
|
||||
{/* Products grid */}
|
||||
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-5 xl:grid-cols-6 gap-3">
|
||||
{isLoading &&
|
||||
Array.from({ length: 6 }).map((_, index) => (
|
||||
<Card className="p-3 space-y-3 rounded-xl" key={index}>
|
||||
<Skeleton className="h-40 sm:h-48 md:h-56 w-full rounded-lg" />
|
||||
<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>
|
||||
|
||||
{/* 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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -17,8 +17,8 @@ const SubCategory = () => {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50 p-4">
|
||||
<div className="max-w-6xl mx-auto">
|
||||
<div className="custom-container">
|
||||
<>
|
||||
<h1 className="text-2xl font-semibold text-gray-900 mb-6">
|
||||
{category.name}
|
||||
</h1>
|
||||
@@ -37,7 +37,7 @@ const SubCategory = () => {
|
||||
</button>
|
||||
))}
|
||||
</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 {
|
||||
Accordion,
|
||||
AccordionContent,
|
||||
@@ -5,153 +7,57 @@ import {
|
||||
AccordionTrigger,
|
||||
} from '@/shared/ui/accordion';
|
||||
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 faqCategories = [
|
||||
{
|
||||
category: 'Umumiy Savollar',
|
||||
questions: [
|
||||
{
|
||||
question: 'Gastro Market nima?',
|
||||
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.",
|
||||
const t = useTranslations();
|
||||
const { data } = useQuery({
|
||||
queryKey: ['faq_list'],
|
||||
queryFn: () => faq_api.list(),
|
||||
select(data) {
|
||||
return data.data;
|
||||
},
|
||||
{
|
||||
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 (
|
||||
<main className="custom-container">
|
||||
<section className="relative py-5 from-accent/5 to-background">
|
||||
<div className="container mx-auto max-w-4xl text-center">
|
||||
<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>
|
||||
<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>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="py-5">
|
||||
<>
|
||||
<div className="space-y-8">
|
||||
{faqCategories.map((category, idx) => (
|
||||
<div className="space-y-2">
|
||||
{data &&
|
||||
data.map((category, idx) => (
|
||||
<div key={idx}>
|
||||
<h2 className="text-xl font-bold mb-4 text-balance">
|
||||
{category.category}
|
||||
</h2>
|
||||
<Card>
|
||||
<Card className="p-0">
|
||||
<CardContent>
|
||||
<Accordion type="single" collapsible className="w-full">
|
||||
{category.questions.map((faq, qIdx) => (
|
||||
<AccordionItem
|
||||
key={qIdx}
|
||||
value={`item-${idx}-${qIdx}`}
|
||||
value={`item-${idx}`}
|
||||
className="border-b last:border-b-0"
|
||||
>
|
||||
<AccordionTrigger className="text-left hover:no-underline py-4">
|
||||
<span className="font-semibold text-lg">
|
||||
{faq.question}
|
||||
{category.question}
|
||||
</span>
|
||||
</AccordionTrigger>
|
||||
<AccordionContent className="text-muted-foreground leading-relaxed pb-4">
|
||||
{faq.answer}
|
||||
{category.answer}
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
))}
|
||||
</Accordion>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
@@ -1,96 +1,41 @@
|
||||
'use client';
|
||||
|
||||
import { product_api } from '@/shared/config/api/product/api';
|
||||
import { useRouter } from '@/shared/config/i18n/navigation';
|
||||
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 { useQuery } from '@tanstack/react-query';
|
||||
import { AxiosError } from 'axios';
|
||||
import { Heart } from 'lucide-react';
|
||||
import { useState } 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,
|
||||
},
|
||||
];
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
export default function Favourite() {
|
||||
const [likedProducts, setLikedProducts] = useState(LIKED_PRODUCTS);
|
||||
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) => {
|
||||
setLikedProducts((prev) => prev.filter((product) => product.id !== id));
|
||||
};
|
||||
useEffect(() => {
|
||||
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 (
|
||||
<div className="min-h-screen py-12">
|
||||
<div className="container mx-auto px-4">
|
||||
@@ -99,18 +44,16 @@ export default function Favourite() {
|
||||
<Heart className="w-16 h-16 text-slate-300" />
|
||||
</div>
|
||||
<h2 className="text-2xl font-bold text-slate-800 mb-2">
|
||||
{"Sevimlilar bo'sh"}
|
||||
{t("Sevimlilar bo'sh")}
|
||||
</h2>
|
||||
<p className="text-slate-500 text-center max-w-md mb-8">
|
||||
{`Hali hech qanday mahsulotni sevimlilarga qo'shmadingiz.
|
||||
Mahsulotlar ro'yxatiga o'ting va yoqqan mahsulotlaringizni
|
||||
saqlang.`}
|
||||
{t(`Hali hech qanday mahsulotni sevimlilarga qo'shmadingiz`)}
|
||||
</p>
|
||||
<Button
|
||||
className="bg-blue-600 text-white px-8 py-3 rounded-xl hover:bg-blue-700"
|
||||
onClick={() => router.push('/')}
|
||||
>
|
||||
Xarid qilishni boshlash
|
||||
{t('Xarid qilishni boshlash')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -118,25 +61,58 @@ 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 (
|
||||
<div className="custom-container">
|
||||
<>
|
||||
<div className="mb-8">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold text-slate-800 mb-2">
|
||||
Sevimli mahsulotlar
|
||||
{t('Sevimli mahsulotlar')}
|
||||
</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 className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 gap-4 mb-30">
|
||||
{likedProducts.map((product) => (
|
||||
<ProductCard
|
||||
key={product.id}
|
||||
product={product}
|
||||
handleRemove={handleRemove}
|
||||
/>
|
||||
{isLoading &&
|
||||
Array.from({ length: 6 }).map((_, index) => (
|
||||
<Card className="p-3 space-y-3 rounded-xl" key={index}>
|
||||
<Skeleton className="h-40 sm:h-48 md:h-56 w-full rounded-lg" />
|
||||
<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>
|
||||
</>
|
||||
|
||||
@@ -1,24 +1,27 @@
|
||||
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 t = useTranslations();
|
||||
return (
|
||||
<main className="custom-container">
|
||||
{/* Hero 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">
|
||||
<Shield className="w-8 h-8 text-primary" />
|
||||
</div>
|
||||
<h1 className="text-4xl md:text-5xl font-bold mb-4 text-balance">
|
||||
Maxfiylik Siyosati
|
||||
</h1>
|
||||
<p className="text-lg text-muted-foreground max-w-2xl mx-auto">
|
||||
{`Gastro Market sizning ma'lumotlaringiz xavfsizligini jiddiy qabul
|
||||
qiladi`}
|
||||
<p className="text-2xl md:text-5xl font-bold mb-4">
|
||||
{t('Maxfiylik Siyosati')}
|
||||
</p>
|
||||
<p className="text-lg text-muted-foreground">
|
||||
{t(
|
||||
`Gastro Market sizning ma'lumotlaringiz xavfsizligini jiddiy qabul qiladi`,
|
||||
)}
|
||||
</p>
|
||||
<p className="text-sm text-muted-foreground mt-4">
|
||||
Oxirgi yangilanish: 16 Dekabr 2025
|
||||
{t('Oxirgi yangilanish: 16 Dekabr 2025')}
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
@@ -29,11 +32,9 @@ const PrivacyPolicy = () => {
|
||||
{/* Introduction */}
|
||||
<div className="prose prose-lg max-w-none mb-12">
|
||||
<p className="text-muted-foreground leading-relaxed">
|
||||
{`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.`}
|
||||
{t(
|
||||
`Ushbu Maxfiylik Siyosati Gastro Market online magazinida siz tomonidan taqdim etilgan shaxsiy ma'lumotlarni qanday to'playmiz`,
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -50,29 +51,30 @@ const PrivacyPolicy = () => {
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<h2 className="text-2xl font-bold mb-4">
|
||||
{"1. Biz To'playdigan Ma'lumotlar"}
|
||||
{t("Biz To'playdigan Ma'lumotlar")}
|
||||
</h2>
|
||||
<div className="space-y-3 text-muted-foreground">
|
||||
<p className="leading-relaxed">
|
||||
{`Sizning tajribangizni yaxshilash uchun biz quyidagi
|
||||
ma'lumotlarni to'playmiz:`}
|
||||
{t(
|
||||
`Sizning tajribangizni yaxshilash uchun biz quyidagi ma'lumotlarni to'playmiz`,
|
||||
)}
|
||||
</p>
|
||||
<ul className="space-y-2 ml-6 list-disc">
|
||||
<li>
|
||||
<strong>{`Shaxsiy Ma'lumotlar`}:</strong> Ism, email
|
||||
manzil, telefon raqami
|
||||
<strong>{t(`Shaxsiy Ma'lumotlar`)}:</strong>{' '}
|
||||
{t('Ism email manzil telefon raqami')}
|
||||
</li>
|
||||
<li>
|
||||
<strong>{"Kompaniya Ma'lumotlari"}:</strong> Kompaniya
|
||||
nomi, website, hamkorlik {"so'rovlari"}
|
||||
<strong>{"Kompaniya Ma'lumotlari"}:</strong>{' '}
|
||||
{t("Kompaniya nomi, website, hamkorlik so'rovlari")}
|
||||
</li>
|
||||
<li>
|
||||
<strong>Fayllar:</strong> Hamkorlik uchun yuklangan
|
||||
hujjatlar
|
||||
<strong>{t('Fayllar:')}</strong>{' '}
|
||||
{t('Hamkorlik uchun yuklangan hujjatlar')}
|
||||
</li>
|
||||
<li>
|
||||
<strong>{"Texnik Ma'lumotlar"}:</strong> IP manzil,
|
||||
brauzer turi, qurilma {"ma'lumotlari"}
|
||||
<strong>{t("Texnik Ma'lumotlar")}:</strong>{' '}
|
||||
{t("IP manzil, brauzer turi, qurilma ma'lumotlari")}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
@@ -92,31 +94,36 @@ const PrivacyPolicy = () => {
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<h2 className="text-2xl font-bold mb-4">
|
||||
2. {"Ma'lumotlardan Foydalanish"}
|
||||
{t("Ma'lumotlardan Foydalanish")}
|
||||
</h2>
|
||||
<div className="space-y-3 text-muted-foreground">
|
||||
<p className="leading-relaxed">
|
||||
{`To'plangan ma'lumotlaringizdan quyidagi maqsadlarda
|
||||
foydalanamiz:`}
|
||||
{t(
|
||||
`To'plangan ma'lumotlaringizdan quyidagi maqsadlarda foydalanamiz:`,
|
||||
)}
|
||||
</p>
|
||||
<ul className="space-y-2 ml-6 list-disc">
|
||||
<li>
|
||||
{
|
||||
"Hamkorlik so'rovlarini qayta ishlash va javob berish"
|
||||
}
|
||||
{t(
|
||||
"Hamkorlik so'rovlarini qayta ishlash va javob berish",
|
||||
)}
|
||||
</li>
|
||||
<li>
|
||||
{`Sizga xizmatlarimiz va yangiliklar haqida ma'lumot
|
||||
berish`}
|
||||
{t(
|
||||
`Sizga xizmatlarimiz va yangiliklar haqida ma'lumot berish`,
|
||||
)}
|
||||
</li>
|
||||
<li>
|
||||
{`Sayt xavfsizligini ta'minlash va firibgarlikka qarshi
|
||||
kurashish`}
|
||||
{t(
|
||||
`Sayt xavfsizligini ta'minlash va firibgarlikka qarshi kurashish`,
|
||||
)}
|
||||
</li>
|
||||
<li>
|
||||
Foydalanuvchi tajribasini tahlil qilish va yaxshilash
|
||||
{t(
|
||||
'Foydalanuvchi tajribasini tahlil qilish va yaxshilash',
|
||||
)}
|
||||
</li>
|
||||
<li>Qonuniy talablarni bajarish</li>
|
||||
<li>{t('Qonuniy talablarni bajarish')}</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
@@ -135,22 +142,29 @@ const PrivacyPolicy = () => {
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<h2 className="text-2xl font-bold mb-4">
|
||||
3. {"Ma'lumotlar Xavfsizligi"}
|
||||
{t("Ma'lumotlar Xavfsizligi")}
|
||||
</h2>
|
||||
<div className="space-y-3 text-muted-foreground">
|
||||
<p className="leading-relaxed">
|
||||
{`Biz sizning shaxsiy ma'lumotlaringizni himoya qilish
|
||||
uchun zamonaviy xavfsizlik choralarini qo'llaymiz:`}
|
||||
{t(
|
||||
`Biz sizning shaxsiy ma'lumotlaringizni himoya qilish uchun zamonaviy xavfsizlik choralarini qo'llaymiz:`,
|
||||
)}
|
||||
</p>
|
||||
<ul className="space-y-2 ml-6 list-disc">
|
||||
<li>
|
||||
{"SSL/TLS shifrlash orqali ma'lumotlar uzatish"}
|
||||
{t("SSL/TLS shifrlash orqali ma'lumotlar uzatish")}
|
||||
</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>Cheklangan kirish huquqlari va autentifikatsiya</li>
|
||||
<li>Doimiy xavfsizlik monitoringi va yangilanishlar</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
@@ -169,22 +183,25 @@ const PrivacyPolicy = () => {
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<h2 className="text-2xl font-bold mb-4">
|
||||
{"4. Ma'lumotlarni Ulashish"}
|
||||
{t("Ma'lumotlarni Ulashish")}
|
||||
</h2>
|
||||
<div className="space-y-3 text-muted-foreground">
|
||||
<p className="leading-relaxed">
|
||||
{`Biz sizning shaxsiy ma'lumotlaringizni uchinchi
|
||||
shaxslarga sotmaymiz. Ma'lumotlaringiz faqat quyidagi
|
||||
hollarda ulashilishi mumkin:`}
|
||||
{t(
|
||||
`Biz sizning shaxsiy ma'lumotlaringizni uchinchi shaxslarga sotmaymiz`,
|
||||
)}
|
||||
</p>
|
||||
<ul className="space-y-2 ml-6 list-disc">
|
||||
<li>Sizning roziligingiz bilan</li>
|
||||
<li>{"Qonuniy talablar bo'yicha"}</li>
|
||||
<li>{t('Sizning roziligingiz bilan')}</li>
|
||||
<li>{t("Qonuniy talablar bo'yicha")}</li>
|
||||
<li>
|
||||
{`Xizmat ko'rsatuvchi ishonchli hamkorlar bilan
|
||||
(maxfiylik shartnomalari ostida)`}
|
||||
{t(
|
||||
`Xizmat ko'rsatuvchi ishonchli hamkorlar bilan (maxfiylik shartnomalari ostida)`,
|
||||
)}
|
||||
</li>
|
||||
<li>
|
||||
{t('Kompaniya birlashuvi yoki sotilishi holatida')}
|
||||
</li>
|
||||
<li>Kompaniya birlashuvi yoki sotilishi holatida</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
@@ -192,131 +209,16 @@ const PrivacyPolicy = () => {
|
||||
</CardContent>
|
||||
</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 */}
|
||||
<Card className="bg-primary/5 border-primary/20">
|
||||
<CardContent className="pt-6">
|
||||
<h2 className="text-2xl font-bold mb-4">
|
||||
9. {"Biz Bilan Bog'lanish"}
|
||||
{t("Biz Bilan Bog'lanish")}
|
||||
</h2>
|
||||
<p className="text-muted-foreground leading-relaxed mb-4">
|
||||
{`Ushbu Maxfiylik Siyosati yoki shaxsiy ma'lumotlaringiz haqida
|
||||
savollaringiz bo'lsa, biz bilan bog'laning:`}
|
||||
{t(
|
||||
`Ushbu Maxfiylik Siyosati yoki shaxsiy ma'lumotlaringiz haqida savollaringiz bo'lsa, biz bilan bog'laning:`,
|
||||
)}
|
||||
</p>
|
||||
<div className="space-y-2 text-muted-foreground">
|
||||
<p>
|
||||
@@ -329,7 +231,7 @@ const PrivacyPolicy = () => {
|
||||
</a>
|
||||
</p>
|
||||
<p>
|
||||
<strong>Telefon:</strong>{' '}
|
||||
<strong>{t('Telefon')}:</strong>{' '}
|
||||
<a
|
||||
href="tel:+998901234567"
|
||||
className="text-primary hover:underline"
|
||||
@@ -338,7 +240,7 @@ const PrivacyPolicy = () => {
|
||||
</a>
|
||||
</p>
|
||||
<p>
|
||||
<strong>Manzil:</strong> {"Toshkent, O'zbekiston"}
|
||||
<strong>{t('Manzil')}:</strong> {t("Toshkent, O'zbekiston")}
|
||||
</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
@@ -346,23 +248,6 @@ const PrivacyPolicy = () => {
|
||||
</div>
|
||||
</div>
|
||||
</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>
|
||||
);
|
||||
};
|
||||
|
||||
0
src/features/product/lib/api.ts
Normal file
0
src/features/product/lib/api.ts
Normal file
@@ -1,5 +1,11 @@
|
||||
'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 {
|
||||
Carousel,
|
||||
CarouselContent,
|
||||
@@ -7,107 +13,112 @@ import {
|
||||
CarouselNext,
|
||||
CarouselPrevious,
|
||||
} 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 {
|
||||
Heart,
|
||||
Minus,
|
||||
Plus,
|
||||
RotateCcw,
|
||||
Shield,
|
||||
ShoppingCart,
|
||||
Star,
|
||||
Truck,
|
||||
} from 'lucide-react';
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import { AxiosError } from 'axios';
|
||||
import { Heart, Minus, Plus, Shield, ShoppingCart, Truck } from 'lucide-react';
|
||||
import { useTranslations } from 'next-intl';
|
||||
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 t = useTranslations();
|
||||
const [quantity, setQuantity] = useState(1);
|
||||
const [selectedImage, setSelectedImage] = useState(0);
|
||||
const [liked, setLiked] = useState(false);
|
||||
const { product } = useParams();
|
||||
const queryClient = useQueryClient();
|
||||
const [selectedImage, setSelectedImage] = useState<number>(0);
|
||||
const debounceRef = useRef<NodeJS.Timeout | null>(null);
|
||||
const { cart_id } = useCartId();
|
||||
|
||||
// Fake product data
|
||||
const product = {
|
||||
id: 5,
|
||||
name: 'Coca-Cola 1.5L',
|
||||
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',
|
||||
const { data: cartItems } = useQuery({
|
||||
queryKey: ['cart_items', cart_id],
|
||||
queryFn: () => cart_api.get_cart_items(cart_id!),
|
||||
enabled: !!cart_id,
|
||||
});
|
||||
|
||||
const { data, isLoading } = useQuery({
|
||||
queryKey: ['product_detail', product],
|
||||
queryFn: () => {
|
||||
if (product) return product_api.detail(product.toString());
|
||||
},
|
||||
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',
|
||||
select(data) {
|
||||
return data?.data;
|
||||
},
|
||||
enabled: !!product,
|
||||
});
|
||||
|
||||
const { data: recomendation, isLoading: proLoad } = useQuery({
|
||||
queryKey: ['product_list'],
|
||||
queryFn: () => product_api.list({ page: 1, page_size: 12 }),
|
||||
select(data) {
|
||||
return data.data.results;
|
||||
},
|
||||
});
|
||||
|
||||
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 [relatedProducts, setRelatedProducts] = useState([
|
||||
{
|
||||
id: 6,
|
||||
name: 'Pepsi 2L',
|
||||
price: 11000,
|
||||
reviews: 342,
|
||||
liked: false,
|
||||
inStock: true,
|
||||
oldPrice: 13000,
|
||||
image: '/pepsi-bottle.jpg',
|
||||
rating: 4.6,
|
||||
discount: 15,
|
||||
const { mutate } = useMutation({
|
||||
mutationFn: (body: { product: string; quantity: number; cart: string }) =>
|
||||
cart_api.cart_item(body),
|
||||
onSuccess: () => {
|
||||
queryClient.refetchQueries({ queryKey: ['cart_items'] });
|
||||
},
|
||||
{
|
||||
id: 8,
|
||||
name: 'Sprite 1.5L',
|
||||
price: 10000,
|
||||
inStock: true,
|
||||
oldPrice: 12000,
|
||||
image: '/clear-soda-bottle.png',
|
||||
rating: 4.5,
|
||||
reviews: 342,
|
||||
liked: false,
|
||||
discount: 17,
|
||||
onError: (err: AxiosError) => {
|
||||
const detail = (err.response?.data as { detail: string }).detail;
|
||||
toast.error(detail || err.message, {
|
||||
richColors: true,
|
||||
position: 'top-center',
|
||||
});
|
||||
},
|
||||
{
|
||||
id: 7,
|
||||
name: 'Fanta Orange 1L',
|
||||
price: 9000,
|
||||
oldPrice: 10000,
|
||||
inStock: true,
|
||||
image: '/fanta-orange-bottle.png',
|
||||
rating: 4.4,
|
||||
reviews: 342,
|
||||
liked: true,
|
||||
discount: 10,
|
||||
});
|
||||
|
||||
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) => {
|
||||
if (type === 'increase') {
|
||||
@@ -117,25 +128,23 @@ const ProductDetail = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const addToCart = () => {
|
||||
alert(`${quantity} ta ${product.name} savatchaga qo'shildi!`);
|
||||
};
|
||||
|
||||
const handleRemove = (id: number) => {
|
||||
setRelatedProducts((prev) =>
|
||||
prev.map((product) =>
|
||||
product.id === id ? { ...product, liked: false } : product,
|
||||
),
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="custom-container pb-5">
|
||||
<div className="w-full max-w-4xl space-y-6">
|
||||
<div className="h-[400px] bg-gray-100 animate-pulse rounded-lg"></div>
|
||||
<div className="h-8 bg-gray-100 animate-pulse rounded w-3/4"></div>
|
||||
<div className="h-6 bg-gray-100 animate-pulse rounded w-1/4"></div>
|
||||
<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 (
|
||||
<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="grid grid-cols-1 md:grid-cols-2 gap-8">
|
||||
<div>
|
||||
<div className="relative bg-gray-100 rounded-lg overflow-hidden mb-4">
|
||||
<div className="relative rounded-lg overflow-hidden mb-4">
|
||||
{data && (
|
||||
<Image
|
||||
width={500}
|
||||
height={500}
|
||||
src={product.images[selectedImage] || '/placeholder.svg'}
|
||||
alt={product.name}
|
||||
className="w-full h-full object-cover"
|
||||
src={
|
||||
data.images.length > 0
|
||||
? data.images[selectedImage].image
|
||||
: data.image || '/placeholder.svg'
|
||||
}
|
||||
alt={data.name}
|
||||
className="w-full h-[400px] object-contain"
|
||||
/>
|
||||
{product.discount > 0 && (
|
||||
<div className="absolute top-4 left-4 bg-red-500 text-white px-3 py-1 rounded-full text-sm font-semibold">
|
||||
-{product.discount}%
|
||||
</div>
|
||||
)}
|
||||
{!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">
|
||||
<span className="text-white text-xl font-bold">
|
||||
Mavjud emas
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
)} */}
|
||||
</div>
|
||||
|
||||
<Carousel
|
||||
@@ -173,9 +188,10 @@ const ProductDetail = () => {
|
||||
className="w-full"
|
||||
>
|
||||
<CarouselContent className="-ml-2 pr-[15%] sm:pr-0">
|
||||
{product.images.map((img, index) => (
|
||||
{data && data.images.length > 0 ? (
|
||||
data.images.map((img, index) => (
|
||||
<CarouselItem
|
||||
key={index}
|
||||
key={img.id}
|
||||
className="pl-2 basis-1/3 sm:basis-1/4 md:basis-1/5 lg:basis-1/6"
|
||||
>
|
||||
<button
|
||||
@@ -187,15 +203,38 @@ const ProductDetail = () => {
|
||||
}`}
|
||||
>
|
||||
<Image
|
||||
src={img || '/placeholder.svg'}
|
||||
alt={`thumb-${index}`}
|
||||
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
|
||||
className={`aspect-square w-full rounded-lg border-2 overflow-hidden transition ${'border-blue-500'}`}
|
||||
>
|
||||
<Image
|
||||
src={
|
||||
data?.image.includes(BASE_URL)
|
||||
? data.image
|
||||
: BASE_URL + data?.image || '/placeholder.svg'
|
||||
}
|
||||
alt={BASE_URL + data?.image}
|
||||
width={150}
|
||||
height={150}
|
||||
className="w-full h-full object-contain"
|
||||
/>
|
||||
</button>
|
||||
</CarouselItem>
|
||||
)}
|
||||
</CarouselContent>
|
||||
</Carousel>
|
||||
</div>
|
||||
@@ -203,83 +242,94 @@ const ProductDetail = () => {
|
||||
{/* Product Info */}
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold text-gray-800 mb-2">
|
||||
{product.name}
|
||||
{data?.name}
|
||||
</h1>
|
||||
|
||||
{/* Rating */}
|
||||
<div className="flex items-center gap-2 mb-4">
|
||||
{/* <div className="flex items-center gap-2 mb-4">
|
||||
<div className="flex items-center">
|
||||
{[...Array(5)].map((_, i) => (
|
||||
<Star
|
||||
key={i}
|
||||
className={`w-5 h-5 ${
|
||||
i < Math.floor(product.rating)
|
||||
i < Math.floor(products.rating)
|
||||
? 'fill-yellow-400 text-yellow-400'
|
||||
: 'text-gray-300'
|
||||
}`}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<span className="text-gray-600">{product.rating}</span>
|
||||
</div>
|
||||
<span className="text-gray-600">{products.rating}</span>
|
||||
</div> */}
|
||||
|
||||
{/* Price */}
|
||||
<div className="mb-6">
|
||||
<div className="flex items-center gap-3 max-lg:flex-col max-lg:items-start">
|
||||
<span className="text-4xl font-bold text-blue-600">
|
||||
{product.price.toLocaleString()} {"so'm"}
|
||||
{data && formatPrice(data.price, true)}
|
||||
</span>
|
||||
{product.oldPrice && (
|
||||
{/* {products.oldPrice && (
|
||||
<span className="text-xl text-gray-400 line-through">
|
||||
{product.oldPrice.toLocaleString()} {"so'm"}
|
||||
{products.oldPrice.toLocaleString()} {"so'm"}
|
||||
</span>
|
||||
)}
|
||||
)} */}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Description */}
|
||||
<p className="text-gray-600 mb-6">{product.description}</p>
|
||||
<p className="text-gray-600 mb-6">{data?.description}</p>
|
||||
|
||||
{/* 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>
|
||||
<span className="text-gray-500">Brand:</span>
|
||||
<p className="font-semibold">{product.brand}</p>
|
||||
<p className="font-semibold">{products.brand}</p>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-gray-500">Kategoriya:</span>
|
||||
<p className="font-semibold">{product.category}</p>
|
||||
</div>
|
||||
<p className="font-semibold">{products.category}</p>
|
||||
</div>
|
||||
</div> */}
|
||||
|
||||
{/* Quantity Selector */}
|
||||
<div className="mb-6">
|
||||
<label className="text-gray-700 font-medium mb-2 block">
|
||||
Miqdor:
|
||||
{t('Miqdor')}:
|
||||
</label>
|
||||
<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">
|
||||
<button
|
||||
onClick={() => handleQuantityChange('decrease')}
|
||||
className="p-3 hover:bg-gray-100 transition"
|
||||
className="p-3 hover:bg-gray-100 transition rounded-lg"
|
||||
disabled={quantity <= 1}
|
||||
>
|
||||
<Minus className="w-5 h-5" />
|
||||
</button>
|
||||
<span className="px-6 font-semibold text-lg">
|
||||
{quantity}
|
||||
</span>
|
||||
<Input
|
||||
value={quantity}
|
||||
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
|
||||
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" />
|
||||
</button>
|
||||
</div>
|
||||
<span className="text-gray-600">
|
||||
Jami:{' '}
|
||||
{t('Jami')}:{' '}
|
||||
<span className="font-bold text-lg">
|
||||
{(product.price * quantity).toLocaleString()} {"so'm"}
|
||||
{data && formatPrice(data.price * quantity, true)}
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
@@ -288,49 +338,74 @@ const ProductDetail = () => {
|
||||
{/* Action Buttons */}
|
||||
<div className="flex gap-4 mb-6">
|
||||
<button
|
||||
onClick={addToCart}
|
||||
disabled={!product.inStock}
|
||||
className={`flex-1 py-4 rounded-lg cursor-pointer font-semibold text-white flex items-center justify-center gap-2 transition ${
|
||||
product.inStock
|
||||
? 'bg-green-600 hover:bg-green-700'
|
||||
: 'bg-gray-400 cursor-not-allowed'
|
||||
}`}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
const cart = cartItems?.data.cart_item.find(
|
||||
(e) => e.product_id === data?.id,
|
||||
);
|
||||
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" />
|
||||
{'Savatga'}
|
||||
{t('Savatga')}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setLiked(!liked)}
|
||||
onClick={() => {
|
||||
if (product) {
|
||||
favouriteMutation.mutate(product.toString());
|
||||
}
|
||||
}}
|
||||
disabled={favouriteMutation.isPending}
|
||||
className={`p-4 rounded-lg border-2 transition ${
|
||||
liked
|
||||
data?.liked
|
||||
? 'border-red-500 bg-red-50'
|
||||
: 'border-gray-300 hover:border-red-500'
|
||||
}`}
|
||||
>
|
||||
<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>
|
||||
</div>
|
||||
|
||||
{/* 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">
|
||||
<Truck className="w-8 h-8 text-blue-600 mb-2" />
|
||||
<span className="text-sm text-gray-600">
|
||||
Bepul yetkazib berish
|
||||
{t('Bepul yetkazib berish')}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex flex-col items-center text-center">
|
||||
<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 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" />
|
||||
<span className="text-sm text-gray-600">
|
||||
14 kun qaytarish
|
||||
</span>
|
||||
</div>
|
||||
</div> */}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -338,35 +413,70 @@ const ProductDetail = () => {
|
||||
|
||||
{/* Specifications */}
|
||||
<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">
|
||||
{Object.entries(product.specifications).map(([key, value]) => (
|
||||
<div
|
||||
key={key}
|
||||
className="flex justify-between border-b pb-2 gap-4"
|
||||
>
|
||||
<span className="text-gray-600">{key}:</span>
|
||||
<span className="font-semibold text-right">{value}</span>
|
||||
<div className="flex justify-between border-b pb-2 gap-4">
|
||||
<span className="text-gray-600">{t('Qadoq turi')}:</span>
|
||||
<span className="font-semibold text-right">
|
||||
{data?.unity.name}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
{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>
|
||||
)}
|
||||
{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>
|
||||
|
||||
{/* Related Products */}
|
||||
<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">
|
||||
<CarouselContent className="pr-[12%] sm:pr-0">
|
||||
{relatedProducts.slice(0, 12).map((product) => (
|
||||
{proLoad &&
|
||||
Array.from({ length: 6 }).map((__, index) => (
|
||||
<CarouselItem
|
||||
key={index}
|
||||
className="basis-1/2 sm:basis-1/3 md:basis-1/4 lg:basis-1/5 xl:basis-1/6 pb-2"
|
||||
>
|
||||
<Card className="p-3 space-y-3 rounded-xl">
|
||||
<Skeleton className="h-40 sm:h-48 md:h-56 w-full rounded-lg" />
|
||||
<Skeleton className="h-4 w-3/4" />
|
||||
<Skeleton className="h-4 w-1/2" />
|
||||
<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/4 lg:basis-1/6 pb-2"
|
||||
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}
|
||||
handleRemove={handleRemove}
|
||||
handleLiked={handleLiked}
|
||||
/>
|
||||
<ProductCard product={product} />
|
||||
</CarouselItem>
|
||||
))}
|
||||
</CarouselContent>
|
||||
|
||||
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 { 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 { useTranslations } from 'next-intl';
|
||||
import Image from 'next/image';
|
||||
import { useState } from 'react';
|
||||
import { order_api } from '../lib/api';
|
||||
import { orders } from '../lib/data';
|
||||
|
||||
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 (
|
||||
<>
|
||||
<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>
|
||||
<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">
|
||||
{orders
|
||||
.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"
|
||||
>
|
||||
<RefreshCw className="w-3 h-3 md:w-4 md:h-4" />
|
||||
Qayta
|
||||
{t('Qayta')}
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
|
||||
@@ -2,12 +2,14 @@ import { Badge } from '@/shared/ui/badge';
|
||||
import { Card, CardContent } from '@/shared/ui/card';
|
||||
import { Tabs, TabsList, TabsTrigger } from '@/shared/ui/tabs';
|
||||
import { CheckCircle, MapPin, Package, Truck } from 'lucide-react';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import Image from 'next/image';
|
||||
import { useState } from 'react';
|
||||
import { orders } from '../lib/data';
|
||||
|
||||
const Orders = () => {
|
||||
const [ordersTab, setOrdersTab] = useState('active');
|
||||
const t = useTranslations();
|
||||
|
||||
const getStatusInfo = (status: string) => {
|
||||
const statusMap: Record<
|
||||
@@ -61,7 +63,7 @@ const Orders = () => {
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-4 md:mb-6">
|
||||
<h2 className="text-xl md:text-2xl font-bold text-foreground">
|
||||
Buyurtmalar
|
||||
{t('Buyurtmalar')}
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
@@ -75,19 +77,21 @@ const Orders = () => {
|
||||
value="active"
|
||||
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
|
||||
value="completed"
|
||||
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
|
||||
value="all"
|
||||
className="data-[state=active]:bg-white text-xs md:text-sm py-2"
|
||||
>
|
||||
Barcha ({orders.length})
|
||||
{t('Barchasi')} ({orders.length})
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
</Tabs>
|
||||
@@ -128,7 +132,7 @@ const Orders = () => {
|
||||
<Badge
|
||||
className={`${statusInfo.bgColor} ${statusInfo.color} border-0 text-xs`}
|
||||
>
|
||||
{statusInfo.text}
|
||||
{t(statusInfo.text)}
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
@@ -168,7 +172,8 @@ const Orders = () => {
|
||||
{"so'm"}
|
||||
</p>
|
||||
<p className="text-[10px] md:text-xs text-muted-foreground">
|
||||
Yetkazish: {order.deliveryFee.toLocaleString()} {"so'm"}
|
||||
{t('Yetkazish')}: {order.deliveryFee.toLocaleString()}{' '}
|
||||
{"so'm"}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,264 +1,34 @@
|
||||
'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 { getMe, removeToken } from '@/shared/lib/token';
|
||||
import { Avatar, AvatarFallback, AvatarImage } from '@/shared/ui/avatar';
|
||||
import { Badge } from '@/shared/ui/badge';
|
||||
import { Button } from '@/shared/ui/button';
|
||||
import { Card, CardContent } from '@/shared/ui/card';
|
||||
import {
|
||||
CheckCircle,
|
||||
ChevronRight,
|
||||
History,
|
||||
Home,
|
||||
LogOut,
|
||||
MapPin,
|
||||
Package,
|
||||
RefreshCw,
|
||||
ShoppingBag,
|
||||
Truck,
|
||||
} from 'lucide-react';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import { Headset, Home, LogOut } from 'lucide-react';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { useState } from 'react';
|
||||
import { orders, user } from '../lib/data';
|
||||
import HistoryTabs from './History';
|
||||
import Orders from './Orders';
|
||||
import CustomerSupport from './Support';
|
||||
|
||||
const Profile = () => {
|
||||
const [activeSection, setActiveSection] = useState('overview');
|
||||
const router = useRouter();
|
||||
|
||||
const getStatusInfo = (status: string) => {
|
||||
const statusMap: Record<
|
||||
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 t = useTranslations();
|
||||
const queryClient = useQueryClient();
|
||||
const user = getMe();
|
||||
|
||||
const menuItems = [
|
||||
{ id: 'overview', label: 'Umumiy', icon: Home },
|
||||
{ id: 'orders', label: 'Buyurtmalar', icon: ShoppingBag },
|
||||
{ id: 'history', label: 'Tarix', icon: History },
|
||||
{ id: 'support', label: "Qo'llab-quvatlash", icon: Headset },
|
||||
];
|
||||
|
||||
const renderContent = () => {
|
||||
switch (activeSection) {
|
||||
case 'orders':
|
||||
return <Orders />;
|
||||
|
||||
case 'history':
|
||||
return <HistoryTabs />;
|
||||
|
||||
case 'favorites':
|
||||
return <Favourite />;
|
||||
|
||||
case 'agency':
|
||||
return <PartnershipForm />;
|
||||
|
||||
case 'faq':
|
||||
return <Faq />;
|
||||
case 'support':
|
||||
return <CustomerSupport />;
|
||||
router.push('https://t.me/web_app_0515_bot');
|
||||
|
||||
default:
|
||||
return (
|
||||
<>
|
||||
{/* 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>
|
||||
</>
|
||||
);
|
||||
return <HistoryTabs />;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -279,7 +49,7 @@ const Profile = () => {
|
||||
}`}
|
||||
>
|
||||
<Icon className="w-4 h-4" />
|
||||
{item.label}
|
||||
{t(item.label)}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
@@ -289,20 +59,18 @@ const Profile = () => {
|
||||
<div>
|
||||
<div className="flex gap-4 md:gap-6">
|
||||
{/* 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">
|
||||
<Avatar className="w-14 h-14 ring-2 ring-emerald-500 ring-offset-2 flex items-center justify-center">
|
||||
<AvatarImage
|
||||
src={user.avatar || '/placeholder.svg'}
|
||||
alt={user.phone}
|
||||
className="h-12 w-12"
|
||||
/>
|
||||
<AvatarFallback className="bg-emerald-500 text-white font-semibold">
|
||||
U
|
||||
<AvatarImage />
|
||||
<AvatarFallback className="text-muted-foreground font-semibold">
|
||||
{user?.slice(0, 1).toUpperCase()}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
<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>
|
||||
|
||||
@@ -319,7 +87,7 @@ const Profile = () => {
|
||||
}`}
|
||||
>
|
||||
<item.icon className="w-5 h-5" />
|
||||
{item.label}
|
||||
{t(item.label)}
|
||||
</button>
|
||||
))}
|
||||
</nav>
|
||||
@@ -328,13 +96,16 @@ const Profile = () => {
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={() => {
|
||||
localStorage.removeItem('user');
|
||||
queryClient.refetchQueries({ queryKey: ['product_list'] });
|
||||
queryClient.refetchQueries({ queryKey: ['favourite_product'] });
|
||||
queryClient.refetchQueries({ queryKey: ['search'] });
|
||||
removeToken();
|
||||
router.push('/');
|
||||
}}
|
||||
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" />
|
||||
Chiqish
|
||||
{t('Chiqish')}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -343,17 +114,14 @@ const Profile = () => {
|
||||
<div className="lg:hidden flex items-center justify-between mb-4 md:mb-6">
|
||||
<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">
|
||||
<AvatarImage
|
||||
src={user.avatar || '/placeholder.svg'}
|
||||
alt={user.phone}
|
||||
/>
|
||||
<AvatarFallback className="bg-emerald-500 text-white text-sm md:text-base">
|
||||
U
|
||||
<AvatarImage />
|
||||
<AvatarFallback className="text-muted-foreground font-semibold">
|
||||
{user?.slice(0, 1).toUpperCase()}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
<div>
|
||||
<p className="text-xs md:text-sm text-muted-foreground">
|
||||
{user.phone}
|
||||
<p className="text-md md:text-xl text-muted-foreground">
|
||||
{user && user.charAt(0).toUpperCase() + user.slice(1)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -361,7 +129,12 @@ const Profile = () => {
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => {
|
||||
localStorage.removeItem('user');
|
||||
queryClient.refetchQueries({ queryKey: ['product_list'] });
|
||||
queryClient.refetchQueries({
|
||||
queryKey: ['favourite_product'],
|
||||
});
|
||||
queryClient.refetchQueries({ queryKey: ['search'] });
|
||||
removeToken();
|
||||
router.push('/');
|
||||
}}
|
||||
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);
|
||||
|
||||
if (!category) {
|
||||
alert('Kategoriya topilmadi');
|
||||
return;
|
||||
@@ -49,19 +48,27 @@ export default function CustomerSupport() {
|
||||
|
||||
const telegramText = `🔔 Yangi murojaat\n\n📋 Kategoriya: ${category.title}\n\n💬 Xabar:\n${message}`;
|
||||
|
||||
const telegramUsername = 'web_app_0515_bot';
|
||||
const telegramUrl = `https://t.me/${telegramUsername}?text=${encodeURIComponent(
|
||||
// Foydalanuvchi ID sini oling (masalan, auth orqali)
|
||||
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,
|
||||
)}`;
|
||||
|
||||
window.open(telegramUrl, '_blank');
|
||||
|
||||
fetch(telegramUrl)
|
||||
.then(() => {
|
||||
setShowSuccess(true);
|
||||
setTimeout(() => {
|
||||
setShowSuccess(false);
|
||||
setSelectedCategory('');
|
||||
setMessage('');
|
||||
}, 3000);
|
||||
})
|
||||
.catch(() => {
|
||||
alert('Xabar yuborishda xatolik yuz berdi');
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,110 +1,85 @@
|
||||
'use client';
|
||||
|
||||
import { Input } from '@/shared/ui/input';
|
||||
import { product_api } from '@/shared/config/api/product/api';
|
||||
import {
|
||||
categories,
|
||||
Product,
|
||||
ProductDetail,
|
||||
} from '@/widgets/categories/lib/data';
|
||||
ProductListResult,
|
||||
SearchDataPro,
|
||||
} from '@/shared/config/api/product/type';
|
||||
import { useRouter } from '@/shared/config/i18n/navigation';
|
||||
import { Input } from '@/shared/ui/input';
|
||||
import { ProductCard } from '@/widgets/categories/ui/product-card';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { Search, X } from 'lucide-react';
|
||||
import { useRouter, useSearchParams } from 'next/navigation';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { useSearchParams } from 'next/navigation';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
const SearchResult: React.FC = () => {
|
||||
const SearchResult = () => {
|
||||
const router = useRouter();
|
||||
const t = useTranslations();
|
||||
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 [loading, setLoading] = useState(false);
|
||||
const [results, setResults] = useState<ProductDetail[]>([]);
|
||||
|
||||
const allProducts = useMemo<ProductDetail[]>(() => {
|
||||
return categories.flatMap((category: Product) =>
|
||||
category.products.map((product) => ({
|
||||
...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);
|
||||
};
|
||||
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(() => {
|
||||
if (queryFromUrl) {
|
||||
handleSearch(queryFromUrl);
|
||||
if (data) {
|
||||
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 = () => {
|
||||
setQuery('');
|
||||
setResults([]);
|
||||
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 (
|
||||
<div className="custom-container justify-center items-center h-screen">
|
||||
<div className="lg:hidden">
|
||||
<div className="custom-container min-h-screen">
|
||||
{/* Search input (mobile) */}
|
||||
<div className="lg:hidden mb-6">
|
||||
<div className="relative">
|
||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-5 h-5 text-slate-400" />
|
||||
|
||||
<Input
|
||||
value={query}
|
||||
placeholder="Mahsulot nomi"
|
||||
onChange={(e) => {
|
||||
setQuery(e.target.value);
|
||||
handleSearch(e.target.value);
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') handleSearch(query);
|
||||
}}
|
||||
className="w-full border rounded-lg pl-10 pr-10 h-12"
|
||||
placeholder={t('Mahsulot nomi')}
|
||||
onChange={(e) => handleSearch(e.target.value)}
|
||||
className="w-full pl-10 pr-10 h-12"
|
||||
/>
|
||||
|
||||
{query && (
|
||||
<button
|
||||
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 />
|
||||
</button>
|
||||
@@ -112,44 +87,20 @@ const SearchResult: React.FC = () => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="px-4 py-8">
|
||||
{loading ? (
|
||||
<div className="text-center py-20">Yuklanmoqda...</div>
|
||||
) : query ? (
|
||||
results.length ? (
|
||||
{isLoading ? (
|
||||
<div className="text-center py-20">{t('Yuklanmoqda')}</div>
|
||||
) : searchRes && searchRes.length > 0 ? (
|
||||
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-6 gap-4">
|
||||
{results.map((product) => (
|
||||
<ProductCard
|
||||
key={product.id}
|
||||
product={product}
|
||||
handleRemove={handleRemove}
|
||||
handleLiked={handleLiked}
|
||||
/>
|
||||
{searchRes
|
||||
.filter((product) => product.is_active)
|
||||
.map((product) => (
|
||||
<ProductCard key={product.id} product={product} />
|
||||
))}
|
||||
</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 className="text-center py-20">{t('Natija topilmadi')}</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,6 +1,24 @@
|
||||
const BASE_URL =
|
||||
process.env.NEXT_PUBLIC_API_URL || 'https://jsonplaceholder.typicode.com';
|
||||
export const BASE_URL =
|
||||
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 axios from 'axios';
|
||||
import { getToken, removeToken } from '@/shared/lib/token';
|
||||
import axios, { AxiosError } from 'axios';
|
||||
import { getLocale } from 'next-intl/server';
|
||||
import { LanguageRoutes } from '../i18n/types';
|
||||
import { BASE_URL } from './URLs';
|
||||
@@ -20,10 +21,10 @@ httpClient.interceptors.request.use(
|
||||
}
|
||||
|
||||
config.headers['Accept-Language'] = language;
|
||||
// const accessToken = localStorage.getItem('accessToken');
|
||||
// if (accessToken) {
|
||||
// config.headers['Authorization'] = `Bearer ${accessToken}`;
|
||||
// }
|
||||
const accessToken = getToken();
|
||||
if (accessToken) {
|
||||
config.headers['Authorization'] = `Bearer ${accessToken}`;
|
||||
}
|
||||
|
||||
return config;
|
||||
},
|
||||
@@ -34,6 +35,11 @@ httpClient.interceptors.response.use(
|
||||
(response) => response,
|
||||
(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);
|
||||
},
|
||||
);
|
||||
|
||||
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": {
|
||||
"title": "Hello world!",
|
||||
"about": "Go to the about page"
|
||||
}
|
||||
"Biz haqimizda": "О нас",
|
||||
"Maxfiylik siyosati": "Политика конфиденциальности",
|
||||
"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
|
||||
|
||||
declare const messages: {
|
||||
HomePage: {
|
||||
title: 'Salom dunyo!';
|
||||
about: 'Go to the about page';
|
||||
};
|
||||
'Biz haqimizda': 'Biz haqimizda';
|
||||
'Maxfiylik siyosati': 'Maxfiylik siyosati';
|
||||
'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;
|
||||
|
||||
@@ -1,6 +1,193 @@
|
||||
{
|
||||
"HomePage": {
|
||||
"title": "Salom dunyo!",
|
||||
"about": "Go to the about page"
|
||||
}
|
||||
"Biz haqimizda": "Biz haqimizda",
|
||||
"Maxfiylik siyosati": "Maxfiylik siyosati",
|
||||
"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';
|
||||
|
||||
import * as React from 'react';
|
||||
import type * as LabelPrimitive from '@radix-ui/react-label';
|
||||
import { Slot } from '@radix-ui/react-slot';
|
||||
import * as React from 'react';
|
||||
import {
|
||||
Controller,
|
||||
FormProvider,
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
|
||||
import { cn } from '@/shared/lib/utils';
|
||||
import { Label } from '@/shared/ui/label';
|
||||
import { useTranslations } from 'next-intl';
|
||||
|
||||
const Form = FormProvider;
|
||||
|
||||
@@ -137,6 +138,7 @@ function FormDescription({ className, ...props }: React.ComponentProps<'p'>) {
|
||||
}
|
||||
|
||||
function FormMessage({ className, ...props }: React.ComponentProps<'p'>) {
|
||||
const t = useTranslations();
|
||||
const { error, formMessageId } = useFormField();
|
||||
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)}
|
||||
{...props}
|
||||
>
|
||||
{body}
|
||||
{error && error.message ? t(error.message) : body}
|
||||
</p>
|
||||
);
|
||||
}
|
||||
|
||||
export {
|
||||
useFormField,
|
||||
Form,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormControl,
|
||||
FormDescription,
|
||||
FormMessage,
|
||||
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';
|
||||
|
||||
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 { cn } from '@/shared/lib/utils';
|
||||
import { Button } from '@/shared/ui/button';
|
||||
import { Card } from '@/shared/ui/card';
|
||||
import {
|
||||
Carousel,
|
||||
CarouselApi,
|
||||
CarouselContent,
|
||||
CarouselItem,
|
||||
CarouselNext,
|
||||
CarouselPrevious,
|
||||
} from '@/shared/ui/carousel';
|
||||
import { ChevronRight } from 'lucide-react';
|
||||
import { useState } from 'react';
|
||||
import { Skeleton } from '@/shared/ui/skeleton';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { ChevronLeft, ChevronRight } from 'lucide-react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { ProductCard } from './product-card';
|
||||
|
||||
export function CategoryCarousel({ category }: { category: SubCategory }) {
|
||||
const [products, setProducts] = useState(category.products);
|
||||
export function CategoryCarousel({ category }: { category: CategoryResult }) {
|
||||
const router = useRouter();
|
||||
const [api, setApi] = useState<CarouselApi>();
|
||||
const [canScrollPrev, setCanScrollPrev] = useState(false);
|
||||
const [canScrollNext, setCanScrollNext] = useState(false);
|
||||
|
||||
const handleRemove = (id: number) => {
|
||||
setProducts((prev) =>
|
||||
prev.map((product) =>
|
||||
product.id === id ? { ...product, liked: false } : product,
|
||||
),
|
||||
);
|
||||
useEffect(() => {
|
||||
if (!api) return;
|
||||
|
||||
const updateButtons = () => {
|
||||
setCanScrollPrev(api.canScrollPrev());
|
||||
setCanScrollNext(api.canScrollNext());
|
||||
};
|
||||
|
||||
const handleLiked = (id: number) => {
|
||||
setProducts((prev) =>
|
||||
prev.map((product) =>
|
||||
product.id === id ? { ...product, liked: true } : product,
|
||||
),
|
||||
);
|
||||
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 scrollNext = () => {
|
||||
if (api) {
|
||||
api?.scrollNext();
|
||||
}
|
||||
};
|
||||
|
||||
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 (
|
||||
<section className="relative custom-container mt-8 justify-center items-center">
|
||||
<div className="flex items-center justify-between mb-6 pb-3 border-b border-slate-200">
|
||||
<section className="relative custom-container mt-5 justify-center items-center border-b border-slate-200">
|
||||
<div className="flex items-center justify-between pb-3">
|
||||
<div
|
||||
className="flex items-center gap-2 group cursor-pointer"
|
||||
onClick={() =>
|
||||
router.push(`/category/${category.category}/${category.name}`)
|
||||
}
|
||||
onClick={() => router.push(`/category/${category.id}/`)}
|
||||
>
|
||||
<h2 className="text-2xl font-bold text-slate-800 group-hover:text-blue-600 transition-colors">
|
||||
{category.name}
|
||||
@@ -51,25 +86,62 @@ export function CategoryCarousel({ category }: { category: SubCategory }) {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Carousel className="w-full">
|
||||
<Carousel className="w-full mt-2" setApi={setApi}>
|
||||
<CarouselContent className="pr-[12%] sm:pr-0">
|
||||
{products.slice(0, 12).map((product) => (
|
||||
{isLoading &&
|
||||
Array.from({ length: 6 }).map((__, index) => (
|
||||
<CarouselItem
|
||||
key={index}
|
||||
className="basis-1/2 sm:basis-1/3 md:basis-1/4 lg:basis-1/5 xl:basis-1/6 pb-2"
|
||||
>
|
||||
<Card className="p-3 space-y-3 rounded-xl">
|
||||
<Skeleton className="h-40 sm:h-48 md:h-56 w-full rounded-lg" />
|
||||
<Skeleton className="h-4 w-3/4" />
|
||||
<Skeleton className="h-4 w-1/2" />
|
||||
<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/6 pb-2"
|
||||
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}
|
||||
handleRemove={handleRemove}
|
||||
handleLiked={handleLiked}
|
||||
/>
|
||||
<ProductCard product={product} />
|
||||
</CarouselItem>
|
||||
))}
|
||||
</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>
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,77 +1,195 @@
|
||||
'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 { useCartId } from '@/shared/hooks/cartId';
|
||||
import formatPrice from '@/shared/lib/formatPrice';
|
||||
import { Alert, AlertDescription, AlertTitle } from '@/shared/ui/alert';
|
||||
import { Button } from '@/shared/ui/button';
|
||||
import { Card, CardContent } from '@/shared/ui/card';
|
||||
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 { MouseEvent, useState } from 'react';
|
||||
|
||||
interface Product {
|
||||
id: number;
|
||||
name: string;
|
||||
price: number;
|
||||
oldPrice: number;
|
||||
image: string;
|
||||
rating: number;
|
||||
reviews: number;
|
||||
discount: number;
|
||||
inStock: boolean;
|
||||
liked: boolean;
|
||||
}
|
||||
import { MouseEvent, useEffect, useRef, useState } from 'react';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
export function ProductCard({
|
||||
product,
|
||||
handleRemove,
|
||||
handleLiked,
|
||||
error,
|
||||
}: {
|
||||
product: Product;
|
||||
handleRemove: (id: number) => void;
|
||||
handleLiked?: (id: number) => void;
|
||||
product: ProductListResult;
|
||||
error?: boolean;
|
||||
}) {
|
||||
const [quantity, setQuantity] = useState<number | ''>(0);
|
||||
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>) => {
|
||||
e.stopPropagation();
|
||||
setQuantity((q) => {
|
||||
if (q === '' || q < 1) return 1;
|
||||
return q >= 999 ? 999 : q + 1;
|
||||
const newQty = (quantity === '' ? 0 : quantity) + 1;
|
||||
setQuantity(newQty);
|
||||
|
||||
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>) => {
|
||||
e.stopPropagation();
|
||||
setQuantity((q) => {
|
||||
if (q === '' || q <= 1) return 0;
|
||||
return q - 1;
|
||||
|
||||
if (!cartItems) return;
|
||||
|
||||
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 (
|
||||
<>
|
||||
<Card
|
||||
onClick={() => router.push(`/product/${product.id}`)}
|
||||
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();
|
||||
router.push(`/product/${product.id}`);
|
||||
}}
|
||||
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">
|
||||
<CardContent className="p-0 flex flex-col h-full">
|
||||
<div className="relative overflow-hidden">
|
||||
{product.discount > 0 && (
|
||||
{/* {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">
|
||||
-{product.discount}%
|
||||
</div>
|
||||
)}
|
||||
)} */}
|
||||
|
||||
<Button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
|
||||
if (product.liked) {
|
||||
handleRemove(product.id);
|
||||
} else if (handleLiked) {
|
||||
handleLiked(product.id);
|
||||
}
|
||||
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"
|
||||
>
|
||||
@@ -84,23 +202,27 @@ export function ProductCard({
|
||||
/>
|
||||
</Button>
|
||||
|
||||
<div className="relative h-40 sm:h-48 md:h-56 bg-slate-50">
|
||||
<div ref={imageRef} className="relative h-40 sm:h-48 md:h-56">
|
||||
<Image
|
||||
fill
|
||||
src={product.image || '/placeholder.svg'}
|
||||
src={
|
||||
product?.image?.includes(BASE_URL)
|
||||
? product.image
|
||||
: BASE_URL + product.image
|
||||
}
|
||||
alt={product.name}
|
||||
className="object-cover group-hover:scale-105 transition-transform"
|
||||
className="object-contain"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="p-3 sm:p-4 space-y-1 sm:space-y-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="p-3 sm:p-4 space-y-1 flex-1">
|
||||
{/* <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" />
|
||||
<span className="text-xs sm:text-sm font-semibold text-orange-600">
|
||||
{product.rating}
|
||||
</span>
|
||||
</div>
|
||||
</div> */}
|
||||
|
||||
<h3 className="text-sm sm:text-base font-semibold text-slate-800 line-clamp-1">
|
||||
{product.name}
|
||||
@@ -111,24 +233,28 @@ export function ProductCard({
|
||||
{formatPrice(product.price, true)}
|
||||
</span>
|
||||
|
||||
{product.oldPrice && (
|
||||
{/* {product. && (
|
||||
<div className="text-xs sm:text-sm text-slate-400 line-through">
|
||||
{formatPrice(product.oldPrice, true)}
|
||||
</div>
|
||||
)}
|
||||
)} */}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div className="p-3 sm:p-4 pt-0">
|
||||
{quantity === 0 ? (
|
||||
<Button
|
||||
disabled={!product.inStock}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setQuantity(1);
|
||||
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 cursor-pointer"
|
||||
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" />
|
||||
Savatga
|
||||
{t('Savatga')}
|
||||
</Button>
|
||||
) : (
|
||||
<div
|
||||
@@ -138,24 +264,52 @@ export function ProductCard({
|
||||
<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);
|
||||
|
||||
setQuantity(num > 999 ? 999 : 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);
|
||||
}}
|
||||
inputMode="numeric"
|
||||
className="w-full border-none text-center text-sm !p-0 focus-visible:ring-0"
|
||||
/>
|
||||
|
||||
<Button size="icon" variant="ghost" onClick={increase}>
|
||||
@@ -166,5 +320,12 @@ export function ProductCard({
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<FlyingAnimationPortal
|
||||
product={product}
|
||||
animated={animated}
|
||||
imageRef={imageRef}
|
||||
setAnimated={setAnimated}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,14 +1,25 @@
|
||||
import { category_api } from '@/shared/config/api/category/api';
|
||||
import { Link } from '@/shared/config/i18n/navigation';
|
||||
import { PRODUCT_INFO } from '@/shared/constants/data';
|
||||
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 { useTranslations } from 'next-intl';
|
||||
import Image from 'next/image';
|
||||
import { Fragment } from 'react';
|
||||
|
||||
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 (
|
||||
<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="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">
|
||||
@@ -27,49 +38,52 @@ const Footer = () => {
|
||||
{PRODUCT_INFO.name}
|
||||
</h2>
|
||||
</div>
|
||||
<p className="text-white max-w-xs leading-relaxed text-sm">
|
||||
Lorem ipsum dolor sit amet consectetur, adipisicing elit. Est,
|
||||
totam?
|
||||
<p className="text-white font-semibold text-md">
|
||||
{t(
|
||||
"Gastronomiya va kulinariya san'ati haqidagi yetakchi onlayn magazin",
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
<div className="grid w-full grid-cols-1 md:grid-cols-3 gap-8 lg:gap-16">
|
||||
<div>
|
||||
<h3 className="mb-2 font-bold text-lg text-muted">
|
||||
Kategoriyalar
|
||||
{t('Kategoriyalar')}
|
||||
</h3>
|
||||
<ul className="space-y-2 text-sm">
|
||||
{categoryList.slice(0, 3).map((link) => (
|
||||
{category?.slice(0, 6)?.map((link) => (
|
||||
<Fragment key={link.name}>
|
||||
{link.subCategories.slice(0, 2).map((e, linkIdx) => (
|
||||
<li
|
||||
key={linkIdx}
|
||||
key={link.id}
|
||||
className="text-white hover:text-gray-300 transition-colors cursor-pointer"
|
||||
>
|
||||
<Link href={`/category/${link.name}/${e.name}`}>
|
||||
{e.name}
|
||||
</Link>
|
||||
<Link href={`/category/${link.id}/`}>{link.name}</Link>
|
||||
</li>
|
||||
))}
|
||||
</Fragment>
|
||||
))}
|
||||
</ul>
|
||||
</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">
|
||||
<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 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 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>
|
||||
</ul>
|
||||
</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">
|
||||
<li className="text-muted hover:text-gray-300 transition-colors cursor-pointer">
|
||||
<a href={'#'} className="flex items-center gap-2">
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from '@/shared/ui/dropdown-menu';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import Image from 'next/image';
|
||||
import { useParams, usePathname, useRouter } from 'next/navigation';
|
||||
import { languages } from '../lib/data';
|
||||
@@ -16,6 +17,7 @@ export function ChangeLang() {
|
||||
const { locale } = useParams();
|
||||
const pathname = usePathname();
|
||||
const router = useRouter();
|
||||
const t = useTranslations();
|
||||
|
||||
const changeLocale = (locale: LanguageRoutes) => {
|
||||
const segments = pathname.split('/');
|
||||
@@ -51,7 +53,7 @@ export function ChangeLang() {
|
||||
)}
|
||||
</div>
|
||||
<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>
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
@@ -65,7 +67,7 @@ export function ChangeLang() {
|
||||
onClick={() => changeLocale(e.key)}
|
||||
className="hover:bg-blue-50 cursor-pointer text-slate-700 hover:text-blue-700 px-3 py-2"
|
||||
>
|
||||
{e.name}
|
||||
{t(e.name)}
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
</DropdownMenuContent>
|
||||
|
||||
@@ -1,24 +1,27 @@
|
||||
'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 { Badge } from '@/shared/ui/badge';
|
||||
import { Button } from '@/shared/ui/button';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
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 { useEffect, useState } from 'react';
|
||||
|
||||
const NavbarMobile = () => {
|
||||
const pathname = usePathname();
|
||||
const [profile, setProfile] = useState<boolean>(false);
|
||||
const user = localStorage.getItem('user');
|
||||
|
||||
const [mounted, setMounted] = useState(false);
|
||||
const t = useTranslations();
|
||||
useEffect(() => {
|
||||
if (user && user === 'true') {
|
||||
setProfile(true);
|
||||
} else {
|
||||
setProfile(false);
|
||||
}
|
||||
}, [user]);
|
||||
setMounted(true);
|
||||
}, []);
|
||||
|
||||
const pathname = usePathname();
|
||||
const token = getToken();
|
||||
|
||||
const navItems = [
|
||||
{ label: 'Asosiy', icon: Home, href: '/' },
|
||||
@@ -28,9 +31,29 @@ const NavbarMobile = () => {
|
||||
{
|
||||
label: 'Profil',
|
||||
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 (
|
||||
<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 (
|
||||
<Link key={item.href} href={item.href}>
|
||||
<Button
|
||||
id={item.href === '/cart' ? 'cart-icon-mobile' : undefined}
|
||||
variant="ghost"
|
||||
className={cn(
|
||||
'h-full w-full flex flex-col items-center justify-center gap-1 rounded-xl',
|
||||
isActive && 'text-green-500',
|
||||
)}
|
||||
>
|
||||
<div className="relative w-full flex justify-center items-center">
|
||||
<item.icon
|
||||
className={cn(
|
||||
'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
|
||||
className={cn(
|
||||
'text-[10px] font-medium',
|
||||
isActive ? 'text-green-500' : 'text-gray-500',
|
||||
)}
|
||||
>
|
||||
{item.label}
|
||||
{t(item.label)}
|
||||
</span>
|
||||
</Button>
|
||||
</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 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 { useTranslations } from 'next-intl';
|
||||
import Image from 'next/image';
|
||||
import { Fragment, useEffect, useState } from 'react';
|
||||
|
||||
@@ -10,37 +18,78 @@ type SearchResultProps = {
|
||||
};
|
||||
|
||||
export const SearchResult = ({ query }: SearchResultProps) => {
|
||||
const [searchProduct, setSearchProduct] = useState<ProductDetail[]>([]);
|
||||
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(() => {
|
||||
setSearchProduct(
|
||||
categories.flatMap((cat) =>
|
||||
cat.products.filter((pro) =>
|
||||
pro.name.toLowerCase().includes(query.toLowerCase()),
|
||||
),
|
||||
),
|
||||
);
|
||||
}, [query]);
|
||||
if (data) {
|
||||
setSearchRes(data);
|
||||
} else if (product && product.length > 0) {
|
||||
setSearchRes(product);
|
||||
} else {
|
||||
setSearchRes([]);
|
||||
}
|
||||
}, [product, data]);
|
||||
|
||||
if (searchProduct.length === 0) {
|
||||
if (searchRes && searchRes.length === 0) {
|
||||
return (
|
||||
<div className="flex flex-col justify-center items-center min-h-[300px] max-h-[600px] gap-2">
|
||||
<PackageOpen className="size-22 text-muted-foreground" />
|
||||
<p className="text-lg text-muted-foreground text-center">
|
||||
Hech narsa topilmadi
|
||||
{t('Hech narsa topilmadi')}
|
||||
</p>
|
||||
</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 (
|
||||
<div className="space-y-3">
|
||||
<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>
|
||||
|
||||
{searchProduct.slice(0, 5).map((product, index) => (
|
||||
{searchRes &&
|
||||
searchRes
|
||||
.filter((product) => product.is_active)
|
||||
.slice(0, 5)
|
||||
.map((product, index) => (
|
||||
<Fragment key={index}>
|
||||
<div
|
||||
className="flex items-center gap-3 p-2 rounded-lg hover:bg-slate-100 cursor-pointer transition"
|
||||
@@ -49,16 +98,19 @@ export const SearchResult = ({ query }: SearchResultProps) => {
|
||||
<Image
|
||||
width={500}
|
||||
height={500}
|
||||
src={product.image}
|
||||
src={
|
||||
product.image.includes(BASE_URL)
|
||||
? product.image
|
||||
: BASE_URL + product.image
|
||||
}
|
||||
alt={product.name}
|
||||
className="w-10 h-10 rounded-md object-cover"
|
||||
className="w-16 h-16 rounded-md object-contain"
|
||||
/>
|
||||
|
||||
<div className="flex-1">
|
||||
<p className="text-sm font-medium text-slate-800">
|
||||
{product.name}
|
||||
</p>
|
||||
<p className="text-xs text-slate-500">{product.rating}</p>
|
||||
<p className="text-xs text-slate-600">
|
||||
{formatPrice(product.price)}
|
||||
</p>
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
'use client';
|
||||
|
||||
import { cart_api } from '@/features/cart/lib/api';
|
||||
import { Link, useRouter } from '@/shared/config/i18n/navigation';
|
||||
import { useCartId } from '@/shared/hooks/cartId';
|
||||
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 { Input } from '@/shared/ui/input';
|
||||
import { Popover, PopoverContent } from '@/shared/ui/popover';
|
||||
@@ -15,12 +19,12 @@ import {
|
||||
} from '@/shared/ui/sheet';
|
||||
import { categoryList } from '@/widgets/welcome/lib/data';
|
||||
import { PopoverTrigger } from '@radix-ui/react-popover';
|
||||
import { useMutation, useQuery } from '@tanstack/react-query';
|
||||
import {
|
||||
ChevronRight,
|
||||
Facebook,
|
||||
Heart,
|
||||
Instagram,
|
||||
LayoutGrid,
|
||||
Mail,
|
||||
MenuIcon,
|
||||
Phone,
|
||||
@@ -29,8 +33,8 @@ import {
|
||||
ShoppingCart,
|
||||
Twitter,
|
||||
User,
|
||||
XIcon,
|
||||
} from 'lucide-react';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import Image from 'next/image';
|
||||
import { useSearchParams } from 'next/navigation';
|
||||
import { useEffect, useState } from 'react';
|
||||
@@ -44,19 +48,43 @@ const Navbar = () => {
|
||||
const [isSticky, setIsSticky] = useState(false);
|
||||
const [query, setQuery] = useState('');
|
||||
const searchParams = useSearchParams();
|
||||
const [user, setUser] = useState<boolean>(false);
|
||||
const users = localStorage.getItem('user');
|
||||
const token = getToken();
|
||||
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(() => {
|
||||
if (users && users === 'true') {
|
||||
setUser(true);
|
||||
} else {
|
||||
setUser(false);
|
||||
if (token) {
|
||||
cart();
|
||||
}
|
||||
}, [users]);
|
||||
}, [token]);
|
||||
|
||||
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(() => {
|
||||
setQuery(queryFromUrl);
|
||||
}, [queryFromUrl]);
|
||||
@@ -89,7 +117,7 @@ const Navbar = () => {
|
||||
href="/about"
|
||||
className="flex items-center gap-1.5 font-medium"
|
||||
>
|
||||
Biz haqimizda
|
||||
{t('Biz haqimizda')}
|
||||
</Link>
|
||||
</li>
|
||||
<li className="text-white hover:text-white/80 transition-colors cursor-pointer">
|
||||
@@ -97,7 +125,7 @@ const Navbar = () => {
|
||||
href="/privacy-policy"
|
||||
className="flex items-center gap-1.5 font-medium"
|
||||
>
|
||||
Maxfiylik siyosati
|
||||
{t('Maxfiylik siyosati')}
|
||||
</Link>
|
||||
</li>
|
||||
<li className="text-white hover:text-white/80 transition-colors cursor-pointer">
|
||||
@@ -105,7 +133,7 @@ const Navbar = () => {
|
||||
href="/faq"
|
||||
className="flex items-center gap-1.5 font-medium"
|
||||
>
|
||||
Savol-javob
|
||||
{t('Savol-javob')}
|
||||
</Link>
|
||||
</li>
|
||||
|
||||
@@ -205,7 +233,7 @@ const Navbar = () => {
|
||||
{/* Asosiy Sahifalar */}
|
||||
<div>
|
||||
<h3 className="text-xs font-semibold text-slate-500 uppercase tracking-wider mb-3 px-2">
|
||||
Sahifalar
|
||||
{t('Sahifalar')}
|
||||
</h3>
|
||||
<nav className="space-y-1">
|
||||
<SheetClose asChild>
|
||||
@@ -215,7 +243,7 @@ const Navbar = () => {
|
||||
>
|
||||
<div className="w-1 h-1 rounded-full bg-slate-400 group-hover:bg-[#57A595]" />
|
||||
<span className="text-sm font-medium">
|
||||
Biz haqimizda
|
||||
{t('Biz haqimizda')}
|
||||
</span>
|
||||
<ChevronRight className="size-4 ml-auto opacity-0 group-hover:opacity-100 transition-opacity" />
|
||||
</Link>
|
||||
@@ -227,7 +255,7 @@ const Navbar = () => {
|
||||
>
|
||||
<div className="w-1 h-1 rounded-full bg-slate-400 group-hover:bg-[#57A595]" />
|
||||
<span className="text-sm font-medium">
|
||||
Maxfiylik siyosati
|
||||
{t('Maxfiylik siyosati')}
|
||||
</span>
|
||||
<ChevronRight className="size-4 ml-auto opacity-0 group-hover:opacity-100 transition-opacity" />
|
||||
</Link>
|
||||
@@ -239,7 +267,7 @@ const Navbar = () => {
|
||||
>
|
||||
<div className="w-1 h-1 rounded-full bg-slate-400 group-hover:bg-[#57A595]" />
|
||||
<span className="text-sm font-medium">
|
||||
Savol va javoblar
|
||||
{t('Savol-javob')}
|
||||
</span>
|
||||
<ChevronRight className="size-4 ml-auto opacity-0 group-hover:opacity-100 transition-opacity" />
|
||||
</Link>
|
||||
@@ -253,7 +281,7 @@ const Navbar = () => {
|
||||
{/* Aloqa */}
|
||||
<div>
|
||||
<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>
|
||||
<nav className="space-y-1">
|
||||
<a
|
||||
@@ -322,7 +350,7 @@ const Navbar = () => {
|
||||
</Sheet>
|
||||
</div>
|
||||
<div className="flex-1 flex gap-3">
|
||||
<Button
|
||||
{/* <Button
|
||||
variant={'outline'}
|
||||
className="h-10 max-lg:hidden cursor-pointer"
|
||||
onClick={() => {
|
||||
@@ -339,11 +367,11 @@ const Navbar = () => {
|
||||
<LayoutGrid className="size-4 text-foreground" />
|
||||
)}
|
||||
<p className="text-foreground">Kataloglar</p>
|
||||
</Button>
|
||||
</Button> */}
|
||||
|
||||
<div className="relative w-full max-lg:hidden">
|
||||
<Input
|
||||
placeholder="Mahsulot nomi"
|
||||
placeholder={t('Mahsulot nomi')}
|
||||
value={query}
|
||||
onFocus={() => setSearchOpen(true)}
|
||||
onBlur={() => setTimeout(() => setSearchOpen(false), 200)}
|
||||
@@ -379,15 +407,19 @@ const Navbar = () => {
|
||||
</Button>
|
||||
<Button
|
||||
variant={'ghost'}
|
||||
id="cart-icon"
|
||||
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" />
|
||||
<Badge className="absolute -top-2 -right-2 line-clamp-1 w-6 flex justify-center items-center">
|
||||
{cartQuenty === 9 ? cartQuenty + '+' : cartQuenty}
|
||||
</Badge>
|
||||
</Button>
|
||||
<Button
|
||||
variant={'ghost'}
|
||||
onClick={() => {
|
||||
if (user) {
|
||||
if (token) {
|
||||
router.push('/profile');
|
||||
} else {
|
||||
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';
|
||||
|
||||
import Banner from '@/assets/gemma-c-stpjHJGqZyw-unsplash.jpg';
|
||||
import Banner_Two from '@/assets/photo-1506617420156-8e4536971650.jpg';
|
||||
import Banner_Three from '@/assets/pngtree-supermarket-aisle-with-empty-shopping-cart-at-grocery-store-retail-business-image_15646095.jpg';
|
||||
import { category_api } from '@/shared/config/api/category/api';
|
||||
import { BASE_URL } from '@/shared/config/api/URLs';
|
||||
import { Link } from '@/shared/config/i18n/navigation';
|
||||
import { AspectRatio } from '@/shared/ui/aspect-ratio';
|
||||
import { Button } from '@/shared/ui/button';
|
||||
import {
|
||||
@@ -11,19 +11,34 @@ import {
|
||||
CarouselItem,
|
||||
type CarouselApi,
|
||||
} from '@/shared/ui/carousel';
|
||||
import useCategoryActive from '@/widgets/navbar/lib/openCategory';
|
||||
import { ChevronLeft, ChevronRight } from 'lucide-react';
|
||||
import { Skeleton } from '@/shared/ui/skeleton';
|
||||
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 { useState } from 'react';
|
||||
import 'swiper/css';
|
||||
import { Swiper, SwiperSlide } from 'swiper/react';
|
||||
import { categoryList } from '../lib/data';
|
||||
|
||||
const banner = [Banner, Banner_Two, Banner_Three];
|
||||
import { banner_api } from '../lib/api';
|
||||
|
||||
const Welcome = () => {
|
||||
const { setActive, setOpenToolbar } = useCategoryActive();
|
||||
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 = () => {
|
||||
if (api?.canScrollPrev()) {
|
||||
@@ -33,6 +48,10 @@ const Welcome = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const scrollPrevCar = () => {
|
||||
apiCat?.scrollPrev();
|
||||
};
|
||||
|
||||
const scrollNext = () => {
|
||||
if (api?.canScrollNext()) {
|
||||
api?.scrollNext();
|
||||
@@ -41,78 +60,110 @@ const Welcome = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const scrollNextCat = () => {
|
||||
apiCat?.scrollNext();
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="custom-container">
|
||||
<Carousel className="w-full" setApi={setApi}>
|
||||
<CarouselContent className="!pr-[15%] lg:!pr-[8%] sm:pr-0">
|
||||
{banner.map((e, index) => (
|
||||
<CarouselContent>
|
||||
{isLoading && (
|
||||
<CarouselItem className="relative">
|
||||
<Skeleton className="w-full h-full" />
|
||||
</CarouselItem>
|
||||
)}
|
||||
{isError && (
|
||||
<CarouselItem className="relative gap-2 bg-gray-300/20 rounded-xl flex flex-col justify-center items-center">
|
||||
<AlertCircle className="size-10 text-red-500" />
|
||||
<p className="text-red-500">Banner yuklanmadi. Xatolik yuz</p>
|
||||
</CarouselItem>
|
||||
)}
|
||||
{data &&
|
||||
data.map((banner, index) => (
|
||||
<CarouselItem key={index} className="relative">
|
||||
<AspectRatio ratio={16 / 8}>
|
||||
<div className="relative w-full h-full">
|
||||
<AspectRatio
|
||||
ratio={16 / 7}
|
||||
className="relative overflow-hidden rounded-2xl"
|
||||
>
|
||||
<Image
|
||||
src={e || '/placeholder.svg'}
|
||||
alt="Banner"
|
||||
src={BASE_URL + banner.banner || '/placeholder.svg'}
|
||||
alt={banner.id}
|
||||
fill
|
||||
className="rounded-2xl object-cover shadow-lg border border-slate-200"
|
||||
priority
|
||||
className="object-cover"
|
||||
priority={index === 0}
|
||||
/>
|
||||
</div>
|
||||
</AspectRatio>
|
||||
<Button
|
||||
onClick={scrollNext}
|
||||
className="absolute bottom-5 right-5 max-lg:hidden cursor-pointer"
|
||||
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" />
|
||||
<ChevronRight className="size-6 max-lg:size-5" />
|
||||
</Button>
|
||||
<Button
|
||||
onClick={scrollPrev}
|
||||
className="absolute bottom-5 right-16 cursor-pointer max-lg:hidden"
|
||||
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" />
|
||||
<ChevronLeft className="size-6 max-lg:size-5" />
|
||||
</Button>
|
||||
</CarouselItem>
|
||||
))}
|
||||
</CarouselContent>
|
||||
</Carousel>
|
||||
|
||||
{/* Category Slider */}
|
||||
<div className="mx-auto mt-5 max-lg:hidden">
|
||||
<Swiper
|
||||
spaceBetween={4}
|
||||
slidesPerView={4}
|
||||
breakpoints={{
|
||||
320: { slidesPerView: 1 },
|
||||
640: { slidesPerView: 2 },
|
||||
1024: { slidesPerView: 4 },
|
||||
}}
|
||||
>
|
||||
{categoryList.map((item, index) => (
|
||||
<SwiperSlide key={index} className="py-3 px-1">
|
||||
<div
|
||||
className="flex gap-1 items-center justify-center bg-gray-100/60 p-3 rounded-lg shadow-sm cursor-pointer space-x-3"
|
||||
onClick={() => {
|
||||
setOpenToolbar(true);
|
||||
setActive(item);
|
||||
}}
|
||||
<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={item.image}
|
||||
alt={item.name}
|
||||
className="w-7 h-7 object-contain"
|
||||
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 truncate line-clamp-2 leading-tight text-slate-700">
|
||||
{item.name}
|
||||
<p className="text-sm font-bold line-clamp-1 leading-tight text-slate-700">
|
||||
{banner.name}
|
||||
</p>
|
||||
</div>
|
||||
</SwiperSlide>
|
||||
</Link>
|
||||
</CarouselItem>
|
||||
))}
|
||||
</Swiper>
|
||||
</div>
|
||||
</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>
|
||||
{category &&
|
||||
category
|
||||
.slice(0, 6)
|
||||
.map((e) => <CategoryCarousel category={e} key={e.id} />)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user