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

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