api ulandi

This commit is contained in:
Samandar Turgunboyev
2025-12-22 11:35:55 +05:00
parent 37c7120d1b
commit 9978b4e3fe
75 changed files with 10255 additions and 11924 deletions

12
.npmrc Normal file
View 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
View 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
View File

@@ -0,0 +1,7 @@
{
"i18n-ally.localesPaths": [
"src/shared/config/i18n",
"src/shared/config/i18n/messages"
],
"i18n-ally.sourceLanguage": "ru"
}

View File

@@ -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

File diff suppressed because it is too large Load Diff

View File

@@ -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

File diff suppressed because it is too large Load Diff

View File

@@ -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>
);
};

View File

@@ -20,9 +20,11 @@ export default function LayoutShell({
return (
<Suspense>
<Navbar />
{children}
{!hideFooter && <Footer />}
<div className="flex min-h-screen flex-col">
<Navbar />
<main className="flex-1 max-lg:mb-20">{children}</main>
{!hideFooter && <Footer />}
</div>
</Suspense>
);
}

View File

@@ -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>
);
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 124 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 261 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

View 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;
},
};

View File

@@ -0,0 +1,6 @@
export interface PartnerSendBody {
company_name: string;
full_name: string;
phone_number: string;
file: File;
}

View 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) => (

View File

@@ -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>

View File

@@ -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>

View 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;
},
};

View 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' }),
});

View File

@@ -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>>([]);
const { mutate, isPending } = useMutation({
mutationFn: (body: {
username: string;
password: string;
tg_id?: string;
}) => auth_api.login(body),
onSuccess: (res) => {
setToken(res.data.access);
setUser(form.getValues('username'));
router.push('/');
queryClient.refetchQueries({ queryKey: ['product_list'] });
},
onError: () => {
toast.error(t('Username yoki parol xato kiritildi'), {
richColors: true,
position: 'top-center',
});
},
});
/* Countdown */
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');
router.push('/');
} else {
setError("Noto'g'ri kod. Qayta urinib ko'ring.");
setOtp(['', '', '', '', '', '']);
otpInputs.current[0]?.focus();
}
}, 1500);
};
/* Resend */
const handleResendOtp = (): void => {
if (!canResend) return;
setIsLoading(true);
setTimeout(() => {
setIsLoading(false);
setCountdown(60);
setCanResend(false);
setOtp(['', '', '', '', '', '']);
otpInputs.current[0]?.focus();
alert('Yangi kod yuborildi!');
}, 1000);
};
const handleChangeNumber = (): void => {
setStep('phone');
setPhoneNumber('');
setOtp(['', '', '', '', '', '']);
setError('');
setCountdown(60);
setCanResend(false);
};
function onSubmit(values: z.infer<typeof authForm>) {
mutate({
password: values.password,
username: values.username,
});
}
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>
<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"
/>
</div>
{error && <p className="text-red-500 text-sm mt-2">{error}</p>}
<button
onClick={handlePhoneSubmit}
disabled={isLoading || phoneNumber.length < 9}
className="w-full mt-6 bg-gradient-to-r from-blue-600 to-indigo-600 text-white py-4 rounded-xl font-semibold hover:from-blue-700 hover:to-indigo-700 transition disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center gap-2"
>
{isLoading ? (
<>
<div className="w-5 h-5 border-2 border-white border-t-transparent rounded-full animate-spin" />
Yuborilmoqda...
</>
) : (
<>
Kodni olish
<ArrowRight className="w-5 h-5" />
</>
)}
</button>
<p className="text-center text-sm text-gray-500 mt-6">
Davom etish orqali siz bizning{' '}
<a href="#" className="text-blue-600 hover:underline">
Foydalanish shartlari
</a>{' '}
va{' '}
<a href="#" className="text-blue-600 hover:underline">
Maxfiylik siyosati
</a>
ga rozilik bildirasiz
</p>
</div>
) : (
// OTP Step
<div>
<label className="block text-sm font-medium text-gray-700 mb-4 text-center">
6 raqamli kodni kiriting
</label>
<div
className="flex gap-2 justify-center mb-6"
onPaste={handleOtpPaste}
>
{otp.map((digit, index) => (
<Input
key={index}
ref={(el) => {
otpInputs.current[index] = el;
}}
type="text"
inputMode="numeric"
maxLength={1}
value={digit}
onChange={(e) => handleOtpChange(index, e.target.value)}
onKeyDown={(e) => handleOtpKeyDown(index, e)}
className="w-12 h-14 text-center text-2xl font-bold border-2 border-gray-300 rounded-lg focus:outline-none focus:border-blue-500 transition"
/>
))}
</div>
{error && (
<div className="bg-red-50 border border-red-200 text-red-600 px-4 py-3 rounded-lg mb-4 text-sm text-center">
{error}
</div>
<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
placeholder={t('Username')}
className="h-12"
{...field}
/>
</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>
)}
</div>
{/* Change Number */}
<button
onClick={handleChangeNumber}
className="w-full mt-4 text-gray-600 hover:text-gray-800 font-medium"
>
{"Raqamni o'zgartirish"}
</button>
{/* Demo info */}
<div className="mt-6 bg-blue-50 border border-blue-200 rounded-lg p-4">
<p className="text-sm text-blue-800 text-center">
<strong>Demo uchun:</strong>
{`Kod sifatida "123456" kiriting`}
</p>
</div>
</div>
)}
</div>
<FormField
control={form.control}
name="password"
render={({ field }) => (
<FormItem>
<Label>{t('Parol')}</Label>
<FormControl>
<Input
placeholder={t('Parol')}
className="h-12"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<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>
<Button
disabled={isPending}
type="submit"
className="w-full h-12 text-md"
>
{isPending ? <Loader2 className="animate-spin" /> : t('Kirish')}
</Button>
</form>
</Form>
</div>
</div>
);

View 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;
},
};

View File

@@ -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

View File

@@ -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,305 +17,258 @@ 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 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 { 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 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;
}),
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>
);
};
// 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>
</div>
<div className="mb-6">
<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' : ''}`}
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
<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' : ''
}`}
>
<Button
variant="destructive"
size="icon"
onClick={() => deleteCartItem({ cart_item_id: item.id })}
className="absolute right-2 w-7 h-7 top-2 cursor-pointer"
>
{/* Product Image */}
<Button
variant={'destructive'}
size={'icon'}
onClick={() => handleRemoveItem(item.id)}
className="absolute right-2 w-7 h-7 top-2 cursor-pointer"
>
<Trash className="size-4" />
</Button>
<div className="w-24 h-40 bg-gray-100 rounded-lg flex-shrink-0 overflow-hidden">
<Image
width={500}
height={500}
src={item.image}
alt={item.name}
className="w-full h-full object-cover"
/>
</div>
<Trash className="size-4" />
</Button>
{/* Product Info */}
<div className="flex-1">
<h3 className="font-semibold text-lg mb-1">{item.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"}
</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">
<button
onClick={() =>
handleQuantityChange(item.id, 'decrease')
}
className="p-2 cursor-pointer transition rounded-lg"
disabled={Number(item.quantity) <= 1}
>
<Minus className="w-4 h-4" />
</button>
<Input
type="text"
min={1}
value={item.quantity}
onChange={(e) => {
const value = e.target.value;
// Bo'sh qiymatga ruxsat berish
if (value === '') {
setCartItems((prev) =>
prev.map((cartItem) =>
cartItem.id === item.id
? { ...cartItem, quantity: '' }
: cartItem,
),
);
return;
}
const number = parseInt(value, 10);
if (!isNaN(number) && number > 0) {
setCartItems((prev) =>
prev.map((cartItem) =>
cartItem.id === item.id
? { ...cartItem, quantity: number }
: cartItem,
),
);
}
}}
className="w-16 text-center outline-none ring-0 focus-visible:ring-0 border-none font-semibold"
/>
<button
onClick={() =>
handleQuantityChange(item.id, 'increase')
}
className="p-2 cursor-pointer transition rounded-lg"
>
<Plus className="w-4 h-4" />
</button>
</div>
</div>
</div>
</div>
))}
</div>
</div>
{/* Order Summary */}
<div className="lg:col-span-1">
<div className="bg-white rounded-lg shadow-md p-6 sticky top-4">
<h3 className="text-xl font-bold mb-4">Buyurtma xulasasi</h3>
<div className="space-y-3 mb-4">
<div className="flex justify-between text-gray-600">
<span>Mahsulotlar narxi:</span>
<span>
{subtotal.toLocaleString()} {"so'm"}
</span>
<div 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}
className="object-cover"
style={{ width: '100%', height: 'auto' }}
/>
</div>
{discount > 0 && (
<div className="flex justify-between text-green-600">
<span>Chegirma:</span>
<span>
-{discount.toLocaleString()} {"so'm"}
<div className="flex-1">
<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">
{formatPrice(item.product_price, true)}
</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:
</span>
<span>
{deliveryFee === 0 ? (
<span className="text-green-600 font-semibold">
Bepul
</span>
) : (
`${deliveryFee.toLocaleString()} so'm`
)}
</span>
</div>
<div className="flex items-center border border-gray-300 rounded-lg w-max">
<button
onClick={() =>
handleQuantityChange(
item.id,
Number(quantities[item.id]) - 1,
)
}
className="p-2 cursor-pointer transition rounded-lg"
>
<Minus className="w-4 h-4" />
</button>
{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>
<Input
value={quantities[item.id]}
onChange={(e) => {
const val = e.target.value.replace(/\D/g, ''); // faqat raqam
setQuantities((prev) => ({
...prev,
[item.id]: val,
}));
<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-2xl font-bold text-blue-600">
{total.toLocaleString()} {"so'm"}
</span>
// Debounce bilan update
const valNum = Number(val);
if (!isNaN(valNum))
handleQuantityChange(item.id, valNum);
}}
type="text"
className="w-16 text-center"
/>
<button
onClick={() =>
handleQuantityChange(
item.id,
Number(quantities[item.id]) + 1,
)
}
className="p-2 cursor-pointer transition rounded-lg"
>
<Plus className="w-4 h-4" />
</button>
</div>
</div>
</div>
<button
onClick={handleCheckout}
className="w-full bg-blue-600 text-white py-4 rounded-lg font-semibold hover:bg-blue-700 transition flex items-center justify-center gap-2 mb-4"
>
<CreditCard className="w-5 h-5" />
Buyurtmani rasmiylashtirish
</button>
<button
onClick={() => router.push('/')}
className="w-full border-2 border-gray-300 cursor-pointer text-gray-700 py-3 rounded-lg font-semibold hover:bg-gray-50 transition flex items-center justify-center gap-2"
>
<ArrowLeft className="w-5 h-5" />
Xaridni davom ettirish
</button>
{/* Additional Info */}
<div className="mt-6 space-y-3 text-sm text-gray-600">
<div className="flex items-start gap-2">
<Truck className="w-5 h-5 text-blue-600 flex-shrink-0 mt-0.5" />
<span>Tez yetkazib berish 1-2 kun ichida</span>
</div>
<div className="flex items-start gap-2">
<CreditCard className="w-5 h-5 text-blue-600 flex-shrink-0 mt-0.5" />
<span>{"Xavfsiz to'lov usullari"}</span>
</div>
</div>
</div>
))}
</div>
</div>
</>
<div className="lg:col-span-1">
<div className="bg-white rounded-lg shadow-md p-6 sticky top-4">
<h3 className="text-xl font-bold mb-4">{t('Buyurtma haqida')}</h3>
<div className="space-y-3 mb-4">
<div className="flex justify-between text-gray-600">
<span>{t('Mahsulotlar narxi')}:</span>
<span>{formatPrice(subtotal, true)}</span>
</div>
<div className="flex justify-between text-gray-600">
<span className="flex items-center gap-1">
<Truck className="w-4 h-4" /> {t('Yetkazib berish')}:
</span>
<span>
<span className="text-green-600 font-semibold">
{t('Bepul')}
</span>
</span>
</div>
</div>
<div className="border-t pt-4 mb-6">
<div className="flex justify-between items-center">
<span className="text-lg font-semibold">{t('Jami')}:</span>
<span className="text-2xl font-bold text-blue-600">
{formatPrice(subtotal, true)}
</span>
</div>
</div>
<button
onClick={handleCheckout}
className="w-full bg-blue-600 text-white py-4 rounded-lg font-semibold hover:bg-blue-700 transition flex items-center justify-center gap-2 mb-4"
>
<CreditCard className="w-5 h-5" />{' '}
{t('Buyurtmani rasmiylashtirish')}
</button>
<button
onClick={() => router.push('/')}
className="w-full border-2 border-gray-300 cursor-pointer text-gray-700 py-3 rounded-lg font-semibold hover:bg-gray-50 transition flex items-center justify-center gap-2"
>
<ArrowLeft className="w-5 h-5" /> {t('Xaridni davom ettirish')}
</button>
</div>
</div>
</div>
</div>
);
};

View File

@@ -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 bosh', {
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>

View File

@@ -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
key={index}
onClick={() => handleCategoryClick(category)}
className="bg-white border border-gray-200 rounded-lg p-4 flex items-center justify-between hover:border-gray-300 transition-colors"
>
<span className="text-gray-900 font-medium">{category.name}</span>
<ChevronRight className="w-5 h-5 text-gray-400" />
</button>
))}
{category &&
category.map((category, index) => (
<Link
key={index}
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"
>
<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" />
</Link>
))}
</div>
</div>
</>
</div>
);
};

View File

@@ -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>
);
};

View File

@@ -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>
);
};

View 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;
},
};

View File

@@ -0,0 +1,5 @@
export interface FaqList {
id: string;
question: string;
answer: string;
}

View File

@@ -1,3 +1,5 @@
'use client';
import {
Accordion,
AccordionContent,
@@ -5,158 +7,62 @@ 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.",
},
{
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.",
},
],
const t = useTranslations();
const { data } = useQuery({
queryKey: ['faq_list'],
queryFn: () => faq_api.list(),
select(data) {
return data.data;
},
{
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 key={idx}>
<h2 className="text-xl font-bold mb-4 text-balance">
{category.category}
</h2>
<Card>
<CardContent>
<Accordion type="single" collapsible className="w-full">
{category.questions.map((faq, qIdx) => (
<div className="space-y-2">
{data &&
data.map((category, idx) => (
<div key={idx}>
<Card className="p-0">
<CardContent>
<Accordion type="single" collapsible className="w-full">
<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>
</div>
))}
</Accordion>
</CardContent>
</Card>
</div>
))}
</div>
</>
</section>

View File

@@ -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,26 +61,59 @@ export default function Favourite() {
);
}
if (isLoading) {
return (
<div className="custom-container">
<div className="mb-8">
<Skeleton className="h-8 w-64 mb-2" />
<Skeleton className="h-4 w-32" />
</div>
{/* Grid */}
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 gap-4">
{Array.from({ length: 10 }).map((_, i) => (
<div key={i} className="rounded-xl border p-3 space-y-3">
<Skeleton className="h-40 w-full rounded-lg" />
<Skeleton className="h-4 w-[80%]" />
<Skeleton className="h-4 w-[60%]" />
</div>
))}
</div>
</div>
);
}
return (
<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>
</>
</div>

View File

@@ -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>
);
};

View File

View 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',
},
images: [
'/classic-coca-cola.png',
'/clear-soda-bottle.png',
'/classic-coca-cola.png',
'/classic-coca-cola.png',
'/classic-coca-cola.png',
'/classic-coca-cola.png',
'/classic-coca-cola.png',
'/classic-coca-cola.png',
'/classic-coca-cola.png',
'/classic-coca-cola.png',
'/classic-coca-cola.png',
],
specifications: {
Hajmi: '1.5 litr',
'Qadoq turi': 'Plastik butilka',
'Ishlab chiqaruvchi': 'Coca-Cola Company',
'Saqlash muddati': '12 oy',
'Energiya qiymati': '180 kJ / 43 kcal',
},
};
const { data: cartItems } = useQuery({
queryKey: ['cart_items', cart_id],
queryFn: () => cart_api.get_cart_items(cart_id!),
enabled: !!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 { data, isLoading } = useQuery({
queryKey: ['product_detail', product],
queryFn: () => {
if (product) return product_api.detail(product.toString());
},
{
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,
select(data) {
return data?.data;
},
{
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,
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 { mutate } = useMutation({
mutationFn: (body: { product: string; quantity: number; cart: string }) =>
cart_api.cart_item(body),
onSuccess: () => {
queryClient.refetchQueries({ queryKey: ['cart_items'] });
},
onError: (err: AxiosError) => {
const detail = (err.response?.data as { detail: string }).detail;
toast.error(detail || err.message, {
richColors: true,
position: 'top-center',
});
},
});
const { mutate: updateCartItem } = useMutation({
mutationFn: ({
body,
cart_item_id,
}: {
body: { quantity: number };
cart_item_id: string;
}) => cart_api.update_cart_item({ body, cart_item_id }),
onSuccess: () => {
queryClient.refetchQueries({ queryKey: ['cart_items'] });
},
onError: (err: AxiosError) => {
toast.error(err.message, { richColors: true, position: 'top-center' });
},
});
const favouriteMutation = useMutation({
mutationFn: (productId: string) => product_api.favourite(productId),
onSuccess: () => {
queryClient.refetchQueries({ queryKey: ['product_list'] });
queryClient.refetchQueries({ queryKey: ['product_detail', product] });
},
});
const handleQuantityChange = (type: string) => {
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">
<Image
width={500}
height={500}
src={product.images[selectedImage] || '/placeholder.svg'}
alt={product.name}
className="w-full h-full object-cover"
/>
{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>
<div className="relative rounded-lg overflow-hidden mb-4">
{data && (
<Image
width={500}
height={500}
src={
data.images.length > 0
? data.images[selectedImage].image
: data.image || '/placeholder.svg'
}
alt={data.name}
className="w-full h-[400px] object-contain"
/>
)}
{!product.inStock && (
{/* {products.discount > 0 && (
<div className="absolute top-4 left-4 bg-red-500 text-white px-3 py-1 rounded-full text-sm font-semibold">
-{products.discount}%
</div>
)} */}
{/* {!products.inStock && (
<div className="absolute inset-0 bg-black bg-opacity-50 flex items-center justify-center">
<span className="text-white text-xl font-bold">
Mavjud emas
</span>
</div>
)}
)} */}
</div>
<Carousel
@@ -173,29 +188,53 @@ const ProductDetail = () => {
className="w-full"
>
<CarouselContent className="-ml-2 pr-[15%] sm:pr-0">
{product.images.map((img, index) => (
<CarouselItem
key={index}
className="pl-2 basis-1/3 sm:basis-1/4 md:basis-1/5 lg:basis-1/6"
>
{data && data.images.length > 0 ? (
data.images.map((img, index) => (
<CarouselItem
key={img.id}
className="pl-2 basis-1/3 sm:basis-1/4 md:basis-1/5 lg:basis-1/6"
>
<button
onClick={() => setSelectedImage(index)}
className={`aspect-square w-full rounded-lg border-2 overflow-hidden transition ${
selectedImage === index
? 'border-blue-500'
: 'border-gray-200 hover:border-blue-300'
}`}
>
<Image
src={
img.image.includes(BASE_URL)
? img.image
: BASE_URL + img.image || '/placeholder.svg'
}
alt={BASE_URL + data?.image}
width={150}
height={150}
className="w-full h-full object-contain"
/>
</button>
</CarouselItem>
))
) : (
<CarouselItem className="pl-2 basis-1/3 sm:basis-1/4 md:basis-1/5 lg:basis-1/6">
<button
onClick={() => setSelectedImage(index)}
className={`aspect-square w-full rounded-lg border-2 overflow-hidden transition ${
selectedImage === index
? 'border-blue-500'
: 'border-gray-200 hover:border-blue-300'
}`}
className={`aspect-square w-full rounded-lg border-2 overflow-hidden transition ${'border-blue-500'}`}
>
<Image
src={img || '/placeholder.svg'}
alt={`thumb-${index}`}
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>
<p className="font-semibold">{products.category}</p>
</div>
</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,37 +413,72 @@ 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) => (
<CarouselItem
key={product.id}
className="basis-1/2 sm:basis-1/3 md:basis-1/4 lg:basis-1/6 pb-2"
>
<ProductCard
product={product}
handleRemove={handleRemove}
handleLiked={handleLiked}
/>
</CarouselItem>
))}
{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/3 lg:basis-1/5 xl:basis-1/6 pb-2"
>
<ProductCard product={product} />
</CarouselItem>
))}
</CarouselContent>
<CarouselPrevious className="hidden lg:flex -top-12 right-12 w-9 h-9 bg-blue-600 text-white border-0" />

View 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;
},
};

View File

@@ -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>

View File

@@ -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>

View File

@@ -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"

View File

@@ -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');
setShowSuccess(true);
setTimeout(() => {
setShowSuccess(false);
setSelectedCategory('');
setMessage('');
}, 3000);
fetch(telegramUrl)
.then(() => {
setShowSuccess(true);
setTimeout(() => {
setShowSuccess(false);
setSelectedCategory('');
setMessage('');
}, 3000);
})
.catch(() => {
alert('Xabar yuborishda xatolik yuz berdi');
});
};
return (

View File

@@ -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,43 +87,19 @@ const SearchResult: React.FC = () => {
</div>
</div>
<div className="px-4 py-8">
{loading ? (
<div className="text-center py-20">Yuklanmoqda...</div>
) : query ? (
results.length ? (
<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}
/>
))}
</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>
{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">
{searchRes
.filter((product) => product.is_active)
.map((product) => (
<ProductCard key={product.id} product={product} />
))}
</div>
) : (
<div className="text-center py-20">{t('Natija topilmadi')}</div>
)}
</div>
);
};

View File

@@ -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/`,
};

View 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;
},
};

View 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;
}

View File

@@ -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);
},
);

View File

View File

View 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;
},
};

View 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 }[];
}

View File

@@ -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 };

View File

@@ -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 bolmadi": "Не удалось загрузить товар. Попробуйте ещё раз.",
"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": "В течение 23 дней",
"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"
}

View File

@@ -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 bolmadi': 'Mahsulotni yuklab bolmadi. Qayta urinib koring.';
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;

View File

@@ -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 bolmadi": "Mahsulotni yuklab bolmadi. Qayta urinib koring.",
"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"
}

View 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 })),
}));

View 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
View 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
View 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 };

View File

@@ -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,
};

View 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>
);
};

View 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,
};

View 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 };

View 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,
);
}

View File

View File

View 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;

View File

@@ -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());
};
updateButtons();
api.on('select', updateButtons);
api.on('reInit', updateButtons);
return () => {
api.off('select', updateButtons);
api.off('reInit', updateButtons);
};
}, [api]);
const scrollPrev = () => {
if (api) {
api?.scrollPrev();
}
};
const handleLiked = (id: number) => {
setProducts((prev) =>
prev.map((product) =>
product.id === id ? { ...product, liked: true } : product,
),
);
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) => (
<CarouselItem
key={product.id}
className="basis-1/2 sm:basis-1/3 md:basis-1/4 lg:basis-1/6 pb-2"
>
<ProductCard
product={product}
handleRemove={handleRemove}
handleLiked={handleLiked}
/>
</CarouselItem>
))}
{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/5 xl:basis-1/6 pb-2"
>
<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>
);
}

View File

@@ -1,170 +1,331 @@
'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 bolmadi')}</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"
>
<CardContent className="p-0">
<div className="relative overflow-hidden">
{product.discount > 0 && (
<>
<Card
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 flex flex-col h-full">
<div className="relative overflow-hidden">
{/* {product. > 0 && (
<div className="absolute top-2 left-2 z-10 bg-orange-500 text-white px-2 py-0.5 rounded-full text-xs sm:text-sm font-bold">
-{product.discount}%
</div>
)}
)} */}
<Button
onClick={(e) => {
e.stopPropagation();
<Button
onClick={(e) => {
e.stopPropagation();
favouriteMutation.mutate(product.id);
}}
className="absolute top-2 right-2 z-10 bg-white hover:bg-gray-200 cursor-pointer rounded-full p-1.5 sm:p-2 shadow hover:scale-110"
>
<Heart
className={`w-4 h-4 sm:w-5 sm:h-5 ${
product.liked
? 'fill-red-500 text-red-500'
: 'text-slate-400 hover:text-red-400'
}`}
/>
</Button>
if (product.liked) {
handleRemove(product.id);
} else if (handleLiked) {
handleLiked(product.id);
}
}}
className="absolute top-2 right-2 z-10 bg-white hover:bg-gray-200 cursor-pointer rounded-full p-1.5 sm:p-2 shadow hover:scale-110"
>
<Heart
className={`w-4 h-4 sm:w-5 sm:h-5 ${
product.liked
? 'fill-red-500 text-red-500'
: 'text-slate-400 hover:text-red-400'
}`}
/>
</Button>
<div className="relative h-40 sm:h-48 md:h-56 bg-slate-50">
<Image
fill
src={product.image || '/placeholder.svg'}
alt={product.name}
className="object-cover group-hover:scale-105 transition-transform"
/>
<div ref={imageRef} className="relative h-40 sm:h-48 md:h-56">
<Image
fill
src={
product?.image?.includes(BASE_URL)
? product.image
: BASE_URL + product.image
}
alt={product.name}
className="object-contain"
/>
</div>
</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}
</h3>
<h3 className="text-sm sm:text-base font-semibold text-slate-800 line-clamp-1">
{product.name}
</h3>
<div>
<span className="text-lg sm:text-xl font-bold text-green-600">
{formatPrice(product.price, true)}
</span>
<div>
<span className="text-lg sm:text-xl font-bold text-green-600">
{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
onClick={(e) => {
e.stopPropagation();
mutate({
product: product.id,
quantity: 1,
cart: cart_id!,
});
}}
className="w-full h-9 sm:h-11 text-sm bg-green-600 hover:bg-green-600/80"
>
<ShoppingCart className="w-4 h-4 mr-1" />
{t('Savatga')}
</Button>
) : (
<div
onClick={(e) => e.stopPropagation()}
className="flex items-center justify-between border border-green-500 rounded-lg h-9 sm:h-11"
>
<Button size="icon" variant="ghost" onClick={decrease}>
<Minus className="w-4 h-4" />
</Button>
<Input
value={quantity}
className="border-none text-center focus-visible:border-none focus-visible:ring-0"
onChange={(e) => {
const v = e.target.value;
// ❌ faqat raqam
if (!/^\d*$/.test(v)) return;
// ⛔ oldingi debounce'ni tozalaymiz
if (debounceRef.current) {
clearTimeout(debounceRef.current);
}
// bosh input — faqat UI
if (v === '') {
setQuantity('');
return;
}
const num = Number(v);
setQuantity(num);
if (!cartItems) return;
const cartItemId = cartItems.data.cart_item.find(
(item) => item.product_id === product.id,
)?.id;
if (!cartItemId) return;
// ❗ 0 bolsa — DELETE (darhol)
if (num === 0) {
deleteCartItem({ cart_item_id: cartItemId });
return;
}
// 🕒 debounce bilan UPDATE
debounceRef.current = setTimeout(() => {
updateCartItem({
body: { quantity: num },
cart_item_id: cartItemId,
});
}, 500);
}}
/>
<Button size="icon" variant="ghost" onClick={increase}>
<Plus className="w-4 h-4" />
</Button>
</div>
)}
</div>
{quantity === 0 ? (
<Button
disabled={!product.inStock}
onClick={(e) => {
e.stopPropagation();
setQuantity(1);
}}
className="w-full h-9 sm:h-11 text-sm bg-green-600 hover:bg-green-600/80 cursor-pointer"
>
<ShoppingCart className="w-4 h-4 mr-1" />
Savatga
</Button>
) : (
<div
onClick={(e) => e.stopPropagation()}
className="flex items-center justify-between border border-green-500 rounded-lg h-9 sm:h-11"
>
<Button size="icon" variant="ghost" onClick={decrease}>
<Minus className="w-4 h-4" />
</Button>
<Input
value={quantity}
onChange={(e) => {
const v = e.target.value;
if (!/^\d*$/.test(v)) return;
if (v === '') {
setQuantity('');
return;
}
const num = Number(v);
setQuantity(num > 999 ? 999 : num);
}}
inputMode="numeric"
className="w-full border-none text-center text-sm !p-0 focus-visible:ring-0"
/>
<Button size="icon" variant="ghost" onClick={increase}>
<Plus className="w-4 h-4" />
</Button>
</div>
)}
</div>
</CardContent>
</Card>
</CardContent>
</Card>
<FlyingAnimationPortal
product={product}
animated={animated}
imageRef={imageRef}
setAnimated={setAnimated}
/>
</>
);
}

View File

@@ -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}
className="text-white hover:text-gray-300 transition-colors cursor-pointer"
>
<Link href={`/category/${link.name}/${e.name}`}>
{e.name}
</Link>
</li>
))}
<li
key={link.id}
className="text-white hover:text-gray-300 transition-colors cursor-pointer"
>
<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">

View File

@@ -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>

View File

@@ -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',
)}
>
<item.icon
className={cn(
'size-6 transition-colors',
isActive ? 'text-green-500' : 'text-gray-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>

View File

@@ -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,62 +18,106 @@ 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) => (
<Fragment key={index}>
<div
className="flex items-center gap-3 p-2 rounded-lg hover:bg-slate-100 cursor-pointer transition"
onClick={() => router.push(`/product/${product.id}`)}
>
<Image
width={500}
height={500}
src={product.image}
alt={product.name}
className="w-10 h-10 rounded-md object-cover"
/>
{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"
onClick={() => router.push(`/product/${product.id}`)}
>
<Image
width={500}
height={500}
src={
product.image.includes(BASE_URL)
? product.image
: BASE_URL + product.image
}
alt={product.name}
className="w-16 h-16 rounded-md object-contain"
/>
<div className="flex-1">
<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>
</div>
</div>
</Fragment>
))}
<div className="flex-1">
<p className="text-sm font-medium text-slate-800">
{product.name}
</p>
<p className="text-xs text-slate-600">
{formatPrice(product.price)}
</p>
</div>
</div>
</Fragment>
))}
</div>
);
};

View File

@@ -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');

View 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;
},
};

View File

@@ -0,0 +1,4 @@
export interface BannerRes {
id: string;
banner: string;
}

View File

@@ -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 = () => {
}
};
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) => (
<CarouselItem key={index} className="relative">
<AspectRatio ratio={16 / 8}>
<div className="relative w-full h-full">
<Image
src={e || '/placeholder.svg'}
alt="Banner"
fill
className="rounded-2xl object-cover shadow-lg border border-slate-200"
priority
/>
</div>
</AspectRatio>
<Button
onClick={scrollNext}
className="absolute bottom-5 right-5 max-lg:hidden cursor-pointer"
variant={'secondary'}
size={'icon'}
>
<ChevronRight className="size-6" />
</Button>
<Button
onClick={scrollPrev}
className="absolute bottom-5 right-16 cursor-pointer max-lg:hidden"
variant={'secondary'}
size={'icon'}
>
<ChevronLeft className="size-6" />
</Button>
</CarouselItem>
))}
</CarouselContent>
</Carousel>
const scrollNextCat = () => {
apiCat?.scrollNext();
};
{/* 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);
}}
>
<Image
src={item.image}
alt={item.name}
className="w-7 h-7 object-contain"
/>
<p className="text-sm font-bold truncate line-clamp-2 leading-tight text-slate-700">
{item.name}
</p>
</div>
</SwiperSlide>
))}
</Swiper>
return (
<>
<div className="custom-container">
<Carousel className="w-full" setApi={setApi}>
<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 / 7}
className="relative overflow-hidden rounded-2xl"
>
<Image
src={BASE_URL + banner.banner || '/placeholder.svg'}
alt={banner.id}
fill
className="object-cover"
priority={index === 0}
/>
</AspectRatio>
<Button
onClick={scrollNext}
className="absolute max-lg:w-6 max-lg:h-6 top-1/2 -translate-y-1/2 right-2 cursor-pointer"
variant={'secondary'}
size={'icon'}
>
<ChevronRight className="size-6 max-lg:size-5" />
</Button>
<Button
onClick={scrollPrev}
className="absolute max-lg:w-6 max-lg:h-6 top-1/2 -translate-y-1/2 left-5 cursor-pointer"
variant={'secondary'}
size={'icon'}
>
<ChevronLeft className="size-6 max-lg:size-5" />
</Button>
</CarouselItem>
))}
</CarouselContent>
</Carousel>
<Carousel className="w-full mt-5" setApi={setApiCat}>
<CarouselContent className="py-2 px-1 pr-[12%]">
{category &&
category.map((banner, index) => (
<CarouselItem
key={index}
className="basis-1/5 max-lg:basis-1/3 max-md:basis-1/2 max-xs:basis-1/1"
>
<Link href={`/category/${banner.id}`}>
<div className="flex flex-col gap-1 items-center justify-start bg-white p-3 rounded-lg shadow-md cursor-pointer space-x-3">
<Image
src={BASE_URL + banner.image}
alt={banner.name}
width={500}
height={500}
className="w-full h-16 object-contain"
/>
<p className="text-sm font-bold line-clamp-1 leading-tight text-slate-700">
{banner.name}
</p>
</div>
</Link>
</CarouselItem>
))}
</CarouselContent>
<Button
onClick={scrollNextCat}
className="absolute max-lg:w-8 max-lg:h-8 top-1/2 -translate-y-1/2 -right-2 cursor-pointer"
variant={'secondary'}
size={'icon'}
>
<ChevronRight className="size-6 max-lg:size-6" />
</Button>
<Button
onClick={scrollPrevCar}
className="absolute max-lg:w-8 max-lg:h-8 top-1/2 -translate-y-1/2 -left-2 cursor-pointer"
variant={'secondary'}
size={'icon'}
>
<ChevronLeft className="size-6 max-lg:size-6" />
</Button>
</Carousel>
</div>
</div>
{category &&
category
.slice(0, 6)
.map((e) => <CategoryCarousel category={e} key={e.id} />)}
</>
);
};