api ulandi
This commit is contained in:
9
src/features/about/lib/api.ts
Normal file
9
src/features/about/lib/api.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import httpClient from '@/shared/config/api/httpClient';
|
||||
import { API_URLS } from '@/shared/config/api/URLs';
|
||||
|
||||
export const partner_api = {
|
||||
async send(body: FormData) {
|
||||
const res = httpClient.post(API_URLS.Partners, body);
|
||||
return res;
|
||||
},
|
||||
};
|
||||
6
src/features/about/lib/type.ts
Normal file
6
src/features/about/lib/type.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export interface PartnerSendBody {
|
||||
company_name: string;
|
||||
full_name: string;
|
||||
phone_number: string;
|
||||
file: File;
|
||||
}
|
||||
@@ -1,7 +1,9 @@
|
||||
import { Card } from '@/shared/ui/card';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import Image from 'next/image';
|
||||
|
||||
export function AboutContent() {
|
||||
const t = useTranslations();
|
||||
const features = [
|
||||
{
|
||||
number: '1',
|
||||
@@ -44,7 +46,7 @@ export function AboutContent() {
|
||||
{/* Mission Section */}
|
||||
<div className="mb-20">
|
||||
<h2 className="text-4xl md:text-5xl font-bold text-center mb-16 text-balance">
|
||||
Bizning maqsadimiz
|
||||
{t('Bizning maqsadimiz')}
|
||||
</h2>
|
||||
<div className="grid md:grid-cols-3 gap-8">
|
||||
{features.map((feature) => (
|
||||
@@ -55,9 +57,11 @@ export function AboutContent() {
|
||||
<div className="text-6xl font-bold text-primary mb-4">
|
||||
{feature.number}
|
||||
</div>
|
||||
<h3 className="text-2xl font-semibold mb-4">{feature.title}</h3>
|
||||
<h3 className="text-2xl font-semibold mb-4">
|
||||
{t(feature.title)}
|
||||
</h3>
|
||||
<p className="text-muted-foreground leading-relaxed">
|
||||
{feature.description}
|
||||
{t(feature.description)}
|
||||
</p>
|
||||
</Card>
|
||||
))}
|
||||
@@ -67,25 +71,22 @@ export function AboutContent() {
|
||||
{/* About Text */}
|
||||
<div className="mb-20 max-w-4xl mx-auto text-center">
|
||||
<h2 className="text-3xl md:text-4xl font-bold mb-8 text-balance">
|
||||
Innovatsiya, sifat va professionallik
|
||||
{t('Innovatsiya, sifat va professionallik')}
|
||||
</h2>
|
||||
<p className="text-lg text-muted-foreground leading-relaxed mb-6">
|
||||
{`Gastro Market – bu gastronomiya dunyosidagi eng so'nggi
|
||||
yangiliklarni, retseptlarni va tendentsiyalarni taqdim etuvchi
|
||||
onlayn platforma. Biz o'quvchilarimizga sifatli va qiziqarli kontent
|
||||
taqdim etishga intilamiz.`}
|
||||
{t(
|
||||
`Gastro Market – bu gastronomiya dunyosidagi eng so'nggi yangiliklarni`,
|
||||
)}
|
||||
</p>
|
||||
<p className="text-lg text-muted-foreground leading-relaxed">
|
||||
{`Bizning jamoamiz tajribali kulinariya mutaxassislari, oshpazlar va
|
||||
gastronomiya sohasidagi ekspertlardan iborat. Biz har bir maqolada
|
||||
sifat va professionallikka e'tibor qaratamiz.`}
|
||||
{t(`Bizning jamoamiz tajribali kulinariya mutaxassislari`)}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Image Gallery */}
|
||||
<div className="mb-20">
|
||||
<h3 className="text-3xl font-bold text-center mb-12 text-balance">
|
||||
Bizning dunyo
|
||||
{t('Bizning dunyo')}
|
||||
</h3>
|
||||
<div className="grid md:grid-cols-3 gap-6">
|
||||
{images.map((image, idx) => (
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { useTranslations } from 'next-intl';
|
||||
import Image from 'next/image';
|
||||
|
||||
export function AboutHero() {
|
||||
const t = useTranslations();
|
||||
return (
|
||||
<section className="relative h-[60vh] min-h-[500px] flex items-center justify-center overflow-hidden">
|
||||
<div className="absolute inset-0 z-0">
|
||||
@@ -17,9 +19,9 @@ export function AboutHero() {
|
||||
Gastro Market
|
||||
</h1>
|
||||
<p className="text-xl md:text-2xl text-white/90 font-light leading-relaxed text-balance">
|
||||
{
|
||||
"Gastronomiya va kulinariya san'ati haqidagi yetakchi onlayn magazin"
|
||||
}
|
||||
{t(
|
||||
"Gastronomiya va kulinariya san'ati haqidagi yetakchi onlayn magazin",
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import formatPhone from '@/shared/lib/formatPhone';
|
||||
import onlyNumber from '@/shared/lib/onlyNumber';
|
||||
import { Button } from '@/shared/ui/button';
|
||||
import { Card } from '@/shared/ui/card';
|
||||
import {
|
||||
@@ -15,11 +16,13 @@ import {
|
||||
import { Input } from '@/shared/ui/input';
|
||||
import { Label } from '@/shared/ui/label';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { Upload } from 'lucide-react';
|
||||
import { useState } from 'react';
|
||||
import { useMutation } from '@tanstack/react-query';
|
||||
import { Loader2, Upload } from 'lucide-react';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { toast } from 'sonner';
|
||||
import * as z from 'zod';
|
||||
import { partner_api } from '../lib/api';
|
||||
|
||||
const MAX_FILE_SIZE = 5 * 1024 * 1024; // 5MB
|
||||
const ACCEPTED_FILE_TYPES = [
|
||||
@@ -32,18 +35,18 @@ const partnershipFormSchema = z.object({
|
||||
companyName: z.string().min(2, {
|
||||
message: "Kompaniya nomi kamida 2 ta belgidan iborat bo'lishi kerak",
|
||||
}),
|
||||
website: z
|
||||
.string()
|
||||
.url({ message: "To'g'ri website manzilini kiriting" })
|
||||
.optional()
|
||||
.or(z.literal('')),
|
||||
// website: z
|
||||
// .string()
|
||||
// .url({ message: "To'g'ri website manzilini kiriting" })
|
||||
// .optional()
|
||||
// .or(z.literal('')),
|
||||
contactPerson: z
|
||||
.string()
|
||||
.min(2, { message: "Ism kamida 2 ta belgidan iborat bo'lishi kerak" }),
|
||||
email: z
|
||||
.string()
|
||||
.email({ message: "To'g'ri email manzilini kiriting" })
|
||||
.optional(),
|
||||
// email: z
|
||||
// .string()
|
||||
// .email({ message: "To'g'ri email manzilini kiriting" })
|
||||
// .optional(),
|
||||
phone: z
|
||||
.string()
|
||||
.min(9, { message: "To'g'ri telefon raqamini kiriting" })
|
||||
@@ -66,53 +69,56 @@ const partnershipFormSchema = z.object({
|
||||
type PartnershipFormValues = z.infer<typeof partnershipFormSchema>;
|
||||
|
||||
export function PartnershipForm() {
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const t = useTranslations();
|
||||
|
||||
const form = useForm<PartnershipFormValues>({
|
||||
resolver: zodResolver(partnershipFormSchema),
|
||||
defaultValues: {
|
||||
companyName: '',
|
||||
website: '',
|
||||
contactPerson: '',
|
||||
email: '',
|
||||
phone: '+998',
|
||||
},
|
||||
});
|
||||
|
||||
async function onSubmit(data: PartnershipFormValues) {
|
||||
console.log(data);
|
||||
|
||||
setIsSubmitting(true);
|
||||
|
||||
try {
|
||||
await new Promise((resolve) => setTimeout(resolve, 2000));
|
||||
|
||||
toast.success("So'rov yuborildi!", {
|
||||
const { mutate, isPending } = useMutation({
|
||||
mutationFn: (body: FormData) => partner_api.send(body),
|
||||
onSuccess: () => {
|
||||
toast.success(t("So'rov yuborildi!"), {
|
||||
richColors: true,
|
||||
position: 'top-center',
|
||||
});
|
||||
|
||||
form.reset();
|
||||
} catch {
|
||||
},
|
||||
onError: () => {
|
||||
toast.error('Xatolik yuz berdi', {
|
||||
richColors: true,
|
||||
position: 'top-center',
|
||||
});
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
},
|
||||
});
|
||||
|
||||
async function onSubmit(data: PartnershipFormValues) {
|
||||
const formData = new FormData();
|
||||
formData.append('company_name', data.companyName);
|
||||
formData.append('full_name', data.contactPerson);
|
||||
formData.append('phone_number', onlyNumber(data.phone));
|
||||
|
||||
if (data.companyFile && data.companyFile.length > 0) {
|
||||
formData.append('file', data.companyFile[0]);
|
||||
}
|
||||
|
||||
mutate(formData);
|
||||
}
|
||||
|
||||
return (
|
||||
<section className="px-4 mb-5">
|
||||
<section className="px-4 mb-5" id="contact">
|
||||
<div className="max-w-3xl mx-auto">
|
||||
<div className="text-center mb-5">
|
||||
<h2 className="text-2xl md:text-5xl font-bold mb-2 text-balance">
|
||||
{`Hamkor bo'ling`}
|
||||
{t(`Hamkor bo'ling`)}
|
||||
</h2>
|
||||
<p className="text-md text-muted-foreground leading-relaxed">
|
||||
{`Gastro Market bilan hamkorlik qilishni xohlaysizmi? Quyidagi formani
|
||||
to'ldiring va biz siz bilan tez orada bog'lanamiz.`}
|
||||
{t(`Gastro Market bilan hamkorlik qilishni xohlaysizmi?`)}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -124,7 +130,7 @@ export function PartnershipForm() {
|
||||
name="companyName"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<Label>Kompaniya nomi</Label>
|
||||
<Label>{t('Kompaniya nomi')}</Label>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder="Kompaniyangiz nomini kiriting"
|
||||
@@ -137,12 +143,12 @@ export function PartnershipForm() {
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
{/* <FormField
|
||||
control={form.control}
|
||||
name="website"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<Label>Website</Label>
|
||||
<Label>{t('Website')}</Label>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder="https://example.com"
|
||||
@@ -154,14 +160,14 @@ export function PartnershipForm() {
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
/> */}
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="contactPerson"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<Label>Ism Familiya</Label>
|
||||
<Label>{t('Ism Familiya')}</Label>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder="Ism va familiya"
|
||||
@@ -174,13 +180,13 @@ export function PartnershipForm() {
|
||||
)}
|
||||
/>
|
||||
|
||||
<div className="grid md:grid-cols-2 gap-6">
|
||||
<FormField
|
||||
<div className="grid md:grid-cols-1 gap-6">
|
||||
{/* <FormField
|
||||
control={form.control}
|
||||
name="email"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex flex-col justify-start">
|
||||
<Label>Email</Label>
|
||||
<Label>{t('Email')}</Label>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder="example@email.com"
|
||||
@@ -192,14 +198,14 @@ export function PartnershipForm() {
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
/> */}
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="phone"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex flex-col justify-start">
|
||||
<Label>Telefon raqami</Label>
|
||||
<Label>{t('Telefon raqami')}</Label>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder="+998 90 123 45 67"
|
||||
@@ -219,7 +225,7 @@ export function PartnershipForm() {
|
||||
name="companyFile"
|
||||
render={({ field: { onChange, value, ...field } }) => (
|
||||
<FormItem>
|
||||
<Label>Kompaniya hujjati</Label>
|
||||
<Label>{t('Kompaniya hujjati')}</Label>
|
||||
<FormControl>
|
||||
<div>
|
||||
<Input
|
||||
@@ -239,18 +245,18 @@ export function PartnershipForm() {
|
||||
>
|
||||
<Upload className="size-10 text-muted-foreground" />
|
||||
<p className="text-muted-foreground text-lg">
|
||||
Faylni tanlang
|
||||
{t('Faylni tanlang')}
|
||||
</p>
|
||||
{value && value.length > 0 && (
|
||||
<p className="mt-2 text-sm text-gray-500">
|
||||
Tanlangan fayl: {value[0].name}
|
||||
{t('Tanlangan fayl')}: {value[0].name}
|
||||
</p>
|
||||
)}
|
||||
</FormLabel>
|
||||
</div>
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
PDF yoki Word formatida (maksimal 5MB)
|
||||
{t('PDF yoki Word formatida (maksimal 5MB)')}
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
@@ -260,10 +266,14 @@ export function PartnershipForm() {
|
||||
<Button
|
||||
type="submit"
|
||||
size="lg"
|
||||
className="w-full"
|
||||
disabled={isSubmitting}
|
||||
className="w-full cursor-pointer"
|
||||
disabled={isPending}
|
||||
>
|
||||
{isSubmitting ? 'Yuborilmoqda...' : "So'rov yuborish"}
|
||||
{isPending ? (
|
||||
<Loader2 className="animate-spin" />
|
||||
) : (
|
||||
t("So'rov yuborish")
|
||||
)}
|
||||
</Button>
|
||||
</form>
|
||||
</Form>
|
||||
|
||||
9
src/features/auth/lib/api.ts
Normal file
9
src/features/auth/lib/api.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import httpClient from '@/shared/config/api/httpClient';
|
||||
import { API_URLS } from '@/shared/config/api/URLs';
|
||||
|
||||
export const auth_api = {
|
||||
async login(body: { username: string; password: string; tg_id?: string }) {
|
||||
const res = await httpClient.post(API_URLS.Login, body);
|
||||
return res;
|
||||
},
|
||||
};
|
||||
6
src/features/auth/lib/form.ts
Normal file
6
src/features/auth/lib/form.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import z from 'zod';
|
||||
|
||||
export const authForm = z.object({
|
||||
username: z.string().min(1, { message: 'Majburiy maydon' }),
|
||||
password: z.string().min(1, { message: 'Majburiy maydon' }),
|
||||
});
|
||||
@@ -1,150 +1,65 @@
|
||||
'use client';
|
||||
|
||||
import { useRouter } from '@/shared/config/i18n/navigation';
|
||||
import formatPhone from '@/shared/lib/formatPhone';
|
||||
import { Link, useRouter } from '@/shared/config/i18n/navigation';
|
||||
import { setToken, setUser } from '@/shared/lib/token';
|
||||
import { Button } from '@/shared/ui/button';
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormMessage,
|
||||
} from '@/shared/ui/form';
|
||||
import { Input } from '@/shared/ui/input';
|
||||
import { ArrowRight, Check, Lock, Phone } from 'lucide-react';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
|
||||
type Step = 'phone' | 'otp';
|
||||
import { Label } from '@/shared/ui/label';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { Loader2, User } from 'lucide-react';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { toast } from 'sonner';
|
||||
import z from 'zod';
|
||||
import { auth_api } from '../lib/api';
|
||||
import { authForm } from '../lib/form';
|
||||
|
||||
const Login = () => {
|
||||
const [step, setStep] = useState<Step>('phone');
|
||||
const [phoneNumber, setPhoneNumber] = useState<string>('+998');
|
||||
const [otp, setOtp] = useState<string[]>(['', '', '', '', '', '']);
|
||||
const [isLoading, setIsLoading] = useState<boolean>(false);
|
||||
const [error, setError] = useState<string>('');
|
||||
const [countdown, setCountdown] = useState<number>(60);
|
||||
const [canResend, setCanResend] = useState<boolean>(false);
|
||||
const router = useRouter();
|
||||
const t = useTranslations();
|
||||
const form = useForm<z.infer<typeof authForm>>({
|
||||
resolver: zodResolver(authForm),
|
||||
defaultValues: {
|
||||
password: '',
|
||||
username: '',
|
||||
},
|
||||
});
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const otpInputs = useRef<Array<HTMLInputElement | null>>([]);
|
||||
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>
|
||||
);
|
||||
|
||||
75
src/features/cart/lib/api.ts
Normal file
75
src/features/cart/lib/api.ts
Normal file
@@ -0,0 +1,75 @@
|
||||
import httpClient from '@/shared/config/api/httpClient';
|
||||
import { API_URLS } from '@/shared/config/api/URLs';
|
||||
import { AxiosResponse } from 'axios';
|
||||
|
||||
interface CartItem {
|
||||
id: string;
|
||||
cart_item: {
|
||||
id: string;
|
||||
product_name: string;
|
||||
product_id: string;
|
||||
product_image: string;
|
||||
quantity: number;
|
||||
product_price: number;
|
||||
}[];
|
||||
}
|
||||
|
||||
export interface OrderCreateBody {
|
||||
items: {
|
||||
product_id: string;
|
||||
quantity: number;
|
||||
}[];
|
||||
payment_type: 'CASH' | 'ACCOUNT_NUMBER';
|
||||
delivery_type: 'YandexGo' | 'DELIVERY_COURIES' | 'PICKUP';
|
||||
contact_number: string;
|
||||
comment: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export const cart_api = {
|
||||
async create(): Promise<AxiosResponse<{ cart_id: string }>> {
|
||||
const res = await httpClient.post(API_URLS.CartCrate);
|
||||
return res;
|
||||
},
|
||||
|
||||
async cart_item(body: { product: string; quantity: number; cart: string }) {
|
||||
const res = await httpClient.post(API_URLS.CartItem, body);
|
||||
return res;
|
||||
},
|
||||
|
||||
async get_cart_items(cart_id: string): Promise<AxiosResponse<CartItem>> {
|
||||
const res = await httpClient.get(`${API_URLS.CartItemList(cart_id)}`);
|
||||
return res;
|
||||
},
|
||||
|
||||
async update_cart_item({
|
||||
body,
|
||||
cart_item_id,
|
||||
}: {
|
||||
body: { quantity: number };
|
||||
cart_item_id: string;
|
||||
}): Promise<AxiosResponse> {
|
||||
const res = await httpClient.patch(
|
||||
`${API_URLS.CartItemUpdate(cart_item_id)}`,
|
||||
body,
|
||||
);
|
||||
return res;
|
||||
},
|
||||
|
||||
async delete_cart_item(cart_item_id: string): Promise<AxiosResponse> {
|
||||
const res = await httpClient.delete(
|
||||
`${API_URLS.CartItemDelete(cart_item_id)}`,
|
||||
);
|
||||
return res;
|
||||
},
|
||||
|
||||
async createOrder(body: OrderCreateBody) {
|
||||
const res = await httpClient.post(`${API_URLS.CreateOrder}`, body);
|
||||
return res;
|
||||
},
|
||||
|
||||
async clear_cart(id: number | string) {
|
||||
const res = await httpClient.get(API_URLS.CartClear(id));
|
||||
return res;
|
||||
},
|
||||
};
|
||||
@@ -6,6 +6,6 @@ export const orderForm = z.object({
|
||||
phone: z.string().min(12, { message: 'Xato raqam kiritildi' }),
|
||||
long: z.string().min(1, { message: 'Majburiy maydon' }),
|
||||
lat: z.string().min(1, { message: 'Majburiy maydon' }),
|
||||
comment: z.string().min(1, { message: 'Majburiy maydon' }),
|
||||
city: z.string().optional(),
|
||||
});
|
||||
// 998901234567
|
||||
|
||||
@@ -1,8 +1,13 @@
|
||||
'use client';
|
||||
|
||||
import { cart_api } from '@/features/cart/lib/api';
|
||||
import { BASE_URL } from '@/shared/config/api/URLs';
|
||||
import { useRouter } from '@/shared/config/i18n/navigation';
|
||||
import { useCartId } from '@/shared/hooks/cartId';
|
||||
import formatPrice from '@/shared/lib/formatPrice';
|
||||
import { Button } from '@/shared/ui/button';
|
||||
import { Input } from '@/shared/ui/input';
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import { AxiosError } from 'axios';
|
||||
import {
|
||||
ArrowLeft,
|
||||
CreditCard,
|
||||
@@ -12,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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
'use client';
|
||||
|
||||
import { BASE_URL } from '@/shared/config/api/URLs';
|
||||
import { useCartId } from '@/shared/hooks/cartId';
|
||||
import formatPhone from '@/shared/lib/formatPhone';
|
||||
import formatPrice from '@/shared/lib/formatPrice';
|
||||
import onlyNumber from '@/shared/lib/onlyNumber';
|
||||
import { Button } from '@/shared/ui/button';
|
||||
import {
|
||||
Form,
|
||||
@@ -11,6 +15,7 @@ import {
|
||||
} from '@/shared/ui/form';
|
||||
import { Input } from '@/shared/ui/input';
|
||||
import { Label } from '@/shared/ui/label';
|
||||
import { Textarea } from '@/shared/ui/textarea';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import {
|
||||
Map,
|
||||
@@ -19,11 +24,12 @@ import {
|
||||
YMaps,
|
||||
ZoomControl,
|
||||
} from '@pbe/react-yandex-maps';
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import {
|
||||
Building2,
|
||||
CheckCircle2,
|
||||
Clock,
|
||||
CreditCard,
|
||||
Loader2,
|
||||
LocateFixed,
|
||||
MapPin,
|
||||
Package,
|
||||
@@ -31,10 +37,13 @@ import {
|
||||
User,
|
||||
Wallet,
|
||||
} from 'lucide-react';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import Image from 'next/image';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { toast } from 'sonner';
|
||||
import z from 'zod';
|
||||
import { cart_api, OrderCreateBody } from '../lib/api';
|
||||
import { orderForm } from '../lib/form';
|
||||
|
||||
interface CoordsData {
|
||||
@@ -44,53 +53,72 @@ interface CoordsData {
|
||||
}
|
||||
|
||||
const OrderPage = () => {
|
||||
const t = useTranslations();
|
||||
const form = useForm<z.infer<typeof orderForm>>({
|
||||
resolver: zodResolver(orderForm),
|
||||
defaultValues: {
|
||||
firstName: '',
|
||||
comment: '',
|
||||
lastName: '',
|
||||
lat: '',
|
||||
long: '',
|
||||
phone: '+998',
|
||||
},
|
||||
});
|
||||
|
||||
const [paymentMethod, setPaymentMethod] = useState('cash');
|
||||
const [deliveryMethod, setDeliveryMethod] = useState('standard');
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const [cart, setCart] = useState<number | string | null>(null);
|
||||
const { cart_id } = useCartId();
|
||||
const [orderSuccess, setOrderSuccess] = useState(false);
|
||||
const queryClinet = useQueryClient();
|
||||
|
||||
const cartItems = [
|
||||
{
|
||||
id: 5,
|
||||
name: 'Coca-Cola 1.5L',
|
||||
price: 12000,
|
||||
quantity: 2,
|
||||
image: '/classic-coca-cola.png',
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
name: 'Pepsi 2L',
|
||||
price: 11000,
|
||||
quantity: 1,
|
||||
image: '/pepsi-bottle.jpg',
|
||||
},
|
||||
{
|
||||
id: 8,
|
||||
name: 'Sprite 1.5L',
|
||||
price: 10000,
|
||||
quantity: 3,
|
||||
image: '/clear-soda-bottle.png',
|
||||
},
|
||||
];
|
||||
const { data } = useQuery({
|
||||
queryKey: ['clear_cart', cart],
|
||||
queryFn: () => cart_api.clear_cart(cart!),
|
||||
enabled: !!cart,
|
||||
});
|
||||
|
||||
const subtotal = cartItems.reduce(
|
||||
(sum, item) => sum + item.price * item.quantity,
|
||||
console.log(data);
|
||||
|
||||
const { mutate, isPending } = useMutation({
|
||||
mutationFn: (body: OrderCreateBody) => cart_api.createOrder(body),
|
||||
onSuccess: () => {
|
||||
setOrderSuccess(true);
|
||||
setCart(cart_id);
|
||||
queryClinet.refetchQueries({ queryKey: ['cart_items'] });
|
||||
},
|
||||
onError: () => {
|
||||
toast.error('Xatolik yuz berdi', {
|
||||
richColors: true,
|
||||
position: 'top-center',
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const { data: cartItems } = useQuery({
|
||||
queryKey: ['cart_items', cart_id],
|
||||
queryFn: () => cart_api.get_cart_items(cart_id!),
|
||||
enabled: !!cart_id,
|
||||
select: (data) => data.data.cart_item,
|
||||
});
|
||||
|
||||
const [paymentMethod, setPaymentMethod] = useState<'CASH' | 'ACCOUNT_NUMBER'>(
|
||||
'CASH',
|
||||
);
|
||||
const [deliveryMethod, setDeliveryMethod] = useState<
|
||||
'YandexGo' | 'DELIVERY_COURIES' | 'PICKUP'
|
||||
>('DELIVERY_COURIES');
|
||||
|
||||
const subtotal = cartItems?.reduce(
|
||||
(sum, item) => sum + item.product_price * item.quantity,
|
||||
0,
|
||||
);
|
||||
|
||||
const deliveryFee =
|
||||
deliveryMethod === 'express' ? 25000 : subtotal > 50000 ? 0 : 15000;
|
||||
const total = subtotal + deliveryFee;
|
||||
deliveryMethod === 'DELIVERY_COURIES'
|
||||
? 25000
|
||||
: subtotal && subtotal > 50000
|
||||
? 0
|
||||
: 15000;
|
||||
const total = subtotal;
|
||||
|
||||
const [coords, setCoords] = useState({
|
||||
latitude: 41.311081,
|
||||
@@ -187,13 +215,27 @@ const OrderPage = () => {
|
||||
}, [cityValue]);
|
||||
|
||||
function onSubmit(value: z.infer<typeof orderForm>) {
|
||||
setIsSubmitting(true);
|
||||
console.log(value);
|
||||
if (!cartItems || cartItems.length === 0) {
|
||||
toast.error('Savatcha bo‘sh', {
|
||||
richColors: true,
|
||||
position: 'top-center',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
setIsSubmitting(false);
|
||||
setOrderSuccess(true);
|
||||
}, 2000);
|
||||
const items = cartItems.map((item) => ({
|
||||
product_id: item.product_id,
|
||||
quantity: item.quantity,
|
||||
}));
|
||||
|
||||
mutate({
|
||||
comment: value.comment,
|
||||
contact_number: onlyNumber(value.phone),
|
||||
delivery_type: deliveryMethod,
|
||||
name: value.firstName + ' ' + value.lastName,
|
||||
payment_type: paymentMethod,
|
||||
items: items,
|
||||
});
|
||||
}
|
||||
|
||||
if (orderSuccess) {
|
||||
@@ -204,28 +246,16 @@ const OrderPage = () => {
|
||||
<CheckCircle2 className="w-12 h-12 text-green-600" />
|
||||
</div>
|
||||
<h2 className="text-2xl font-bold text-gray-800 mb-2">
|
||||
Buyurtma qabul qilindi!
|
||||
{t('Buyurtma qabul qilindi!')}
|
||||
</h2>
|
||||
<p className="text-gray-600 mb-4">
|
||||
Buyurtma raqami:{' '}
|
||||
<span className="font-bold">
|
||||
#ORD-{Math.floor(Math.random() * 10000)}
|
||||
</span>
|
||||
</p>
|
||||
<p className="text-gray-500 mb-6">
|
||||
Buyurtmangiz muvaffaqiyatli qabul qilindi. Tez orada sizga aloqaga
|
||||
chiqamiz.
|
||||
{t('Buyurtmangiz muvaffaqiyatli qabul qilindi')}
|
||||
</p>
|
||||
<div className="bg-blue-50 p-4 rounded-lg mb-6">
|
||||
<p className="text-sm text-gray-700">
|
||||
Buyurtma holati haqida SMS orqali xabardor qilinasiz
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => (window.location.href = '/')}
|
||||
className="w-full bg-blue-600 text-white py-3 rounded-lg font-semibold hover:bg-blue-700 transition"
|
||||
>
|
||||
Bosh sahifaga qaytish
|
||||
{t('Bosh sahifaga qaytish')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -238,9 +268,9 @@ const OrderPage = () => {
|
||||
{/* Header */}
|
||||
<div className="mb-6">
|
||||
<h1 className="text-3xl font-bold text-gray-800 mb-2">
|
||||
Buyurtmani rasmiylashtirish
|
||||
{t('Buyurtmani rasmiylashtirish')}
|
||||
</h1>
|
||||
<p className="text-gray-600">{"Ma'lumotlaringizni to'ldiring"}</p>
|
||||
<p className="text-gray-600">{t("Ma'lumotlaringizni to'ldiring")}</p>
|
||||
</div>
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)}>
|
||||
@@ -252,7 +282,7 @@ const OrderPage = () => {
|
||||
<div className="flex items-center gap-2 mb-4">
|
||||
<User className="w-5 h-5 text-blue-600" />
|
||||
<h2 className="text-xl font-semibold">
|
||||
{"Shaxsiy ma'lumotlar"}
|
||||
{t("Shaxsiy ma'lumotlar")}
|
||||
</h2>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
@@ -262,13 +292,13 @@ const OrderPage = () => {
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex justify-start flex-col">
|
||||
<Label className="block text-sm font-medium text-gray-700">
|
||||
{'Ism'}
|
||||
{t('Ism')}
|
||||
</Label>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
className="w-full border h-12 border-gray-300 rounded-lg px-4 py-3 focus:outline-none focus:border-blue-500"
|
||||
placeholder="Ismingiz"
|
||||
placeholder={t('Ismingiz')}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
@@ -282,13 +312,13 @@ const OrderPage = () => {
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex justify-start flex-col">
|
||||
<Label className="block text-sm font-medium text-gray-700">
|
||||
{'Familiya'}
|
||||
{t('Familiya')}
|
||||
</Label>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
className="w-full border h-12 border-gray-300 rounded-lg px-4 py-3 focus:outline-none focus:border-blue-500"
|
||||
placeholder="Familiyangiz"
|
||||
placeholder={t('Familiyangiz')}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
@@ -302,7 +332,7 @@ const OrderPage = () => {
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<Label className="block text-sm font-medium text-gray-700">
|
||||
Telefon raqam
|
||||
{t('Telefon raqam')}
|
||||
</Label>
|
||||
<FormControl>
|
||||
<Input
|
||||
@@ -319,6 +349,25 @@ const OrderPage = () => {
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="comment"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<Label className="block mt-3 text-sm font-medium text-gray-700">
|
||||
{t('Izoh')}
|
||||
</Label>
|
||||
<FormControl>
|
||||
<Textarea
|
||||
{...field}
|
||||
className="w-full min-h-42 max-h-64 border border-gray-300 rounded-lg px-4 py-3 focus:outline-none focus:border-blue-500"
|
||||
placeholder={t('Izoh')}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Delivery Address */}
|
||||
@@ -326,7 +375,7 @@ const OrderPage = () => {
|
||||
<div className="flex items-center gap-2 mb-4">
|
||||
<MapPin className="w-5 h-5 text-blue-600" />
|
||||
<h2 className="text-xl font-semibold">
|
||||
Yetkazib berish manzili
|
||||
{t('Yetkazib berish manzili')}
|
||||
</h2>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
@@ -336,14 +385,14 @@ const OrderPage = () => {
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<Label className="block text-sm font-medium text-gray-700">
|
||||
Manzilni qidirish
|
||||
{t('Manzilni qidirish')}
|
||||
</Label>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
type="text"
|
||||
className="w-full border h-12 border-gray-300 rounded-lg px-4 py-3 focus:outline-none focus:border-blue-500"
|
||||
placeholder="Toshkent"
|
||||
placeholder={t('Toshkent')}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
@@ -392,7 +441,7 @@ const OrderPage = () => {
|
||||
className="absolute bottom-3 right-2.5 shadow-md bg-white text-black hover:bg-gray-100"
|
||||
>
|
||||
<LocateFixed className="w-4 h-4 mr-1" />
|
||||
Mening joylashuvim
|
||||
{t('Mening joylashuvim')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -403,7 +452,7 @@ const OrderPage = () => {
|
||||
<div className="flex items-center gap-2 mb-4">
|
||||
<Truck className="w-5 h-5 text-blue-600" />
|
||||
<h2 className="text-xl font-semibold">
|
||||
Yetkazib berish usuli
|
||||
{t('Yetkazib berish usuli')}
|
||||
</h2>
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
@@ -412,8 +461,8 @@ const OrderPage = () => {
|
||||
type="radio"
|
||||
name="delivery"
|
||||
value="standard"
|
||||
checked={deliveryMethod === 'standard'}
|
||||
onChange={(e) => setDeliveryMethod(e.target.value)}
|
||||
checked={deliveryMethod === 'DELIVERY_COURIES'}
|
||||
onChange={() => setDeliveryMethod('DELIVERY_COURIES')}
|
||||
className="w-4 h-4 text-blue-600"
|
||||
/>
|
||||
<div className="ml-4 flex-1">
|
||||
@@ -421,15 +470,17 @@ const OrderPage = () => {
|
||||
<div className="flex items-center gap-2">
|
||||
<Clock className="w-5 h-5 text-gray-600" />
|
||||
<span className="font-semibold">
|
||||
Standart yetkazib berish
|
||||
{t('Standart yetkazib berish')}
|
||||
</span>
|
||||
</div>
|
||||
<span className="font-bold text-blue-600">
|
||||
{subtotal > 50000 ? 'Bepul' : "15,000 so'm"}
|
||||
{subtotal && subtotal > 50000
|
||||
? 'Bepul'
|
||||
: "15,000 so'm"}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-sm text-gray-500 mt-1">
|
||||
2-3 kun ichida
|
||||
{t('2-3 kun ichida')}
|
||||
</p>
|
||||
</div>
|
||||
</label>
|
||||
@@ -439,8 +490,8 @@ const OrderPage = () => {
|
||||
type="radio"
|
||||
name="delivery"
|
||||
value="express"
|
||||
checked={deliveryMethod === 'express'}
|
||||
onChange={(e) => setDeliveryMethod(e.target.value)}
|
||||
checked={deliveryMethod === 'YandexGo'}
|
||||
onChange={() => setDeliveryMethod('YandexGo')}
|
||||
className="w-4 h-4 text-blue-600"
|
||||
/>
|
||||
<div className="ml-4 flex-1">
|
||||
@@ -448,7 +499,7 @@ const OrderPage = () => {
|
||||
<div className="flex items-center gap-2">
|
||||
<Package className="w-5 h-5 text-gray-600" />
|
||||
<span className="font-semibold">
|
||||
Tez yetkazib berish
|
||||
{t('Tez yetkazib berish')}
|
||||
</span>
|
||||
</div>
|
||||
<span className="font-bold text-blue-600">
|
||||
@@ -456,7 +507,7 @@ const OrderPage = () => {
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-sm text-gray-500 mt-1">
|
||||
1 kun ichida
|
||||
{t('1 kun ichida')}
|
||||
</p>
|
||||
</div>
|
||||
</label>
|
||||
@@ -466,7 +517,9 @@ const OrderPage = () => {
|
||||
<div className="bg-white rounded-lg shadow-md p-6">
|
||||
<div className="flex items-center gap-2 mb-4">
|
||||
<CreditCard className="w-5 h-5 text-blue-600" />
|
||||
<h2 className="text-xl font-semibold">{"To'lov usuli"}</h2>
|
||||
<h2 className="text-xl font-semibold">
|
||||
{t("To'lov usuli")}
|
||||
</h2>
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
<Label className="flex items-center p-4 border-2 rounded-lg cursor-pointer transition hover:bg-gray-50">
|
||||
@@ -474,16 +527,16 @@ const OrderPage = () => {
|
||||
type="radio"
|
||||
name="payment"
|
||||
value="cash"
|
||||
checked={paymentMethod === 'cash'}
|
||||
onChange={(e) => setPaymentMethod(e.target.value)}
|
||||
checked={paymentMethod === 'CASH'}
|
||||
onChange={() => setPaymentMethod('CASH')}
|
||||
className="w-4 h-4 text-blue-600"
|
||||
/>
|
||||
<div className="ml-4 flex items-center gap-3">
|
||||
<Wallet className="w-6 h-6 text-green-600" />
|
||||
<div>
|
||||
<span className="font-semibold">Naqd pul</span>
|
||||
<span className="font-semibold">{t('Naqd pul')}</span>
|
||||
<p className="text-sm text-gray-500">
|
||||
{"Yetkazib berishda to'lash"}
|
||||
{t("Yetkazib berishda to'lash")}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -494,36 +547,18 @@ const OrderPage = () => {
|
||||
type="radio"
|
||||
name="payment"
|
||||
value="card"
|
||||
checked={paymentMethod === 'card'}
|
||||
onChange={(e) => setPaymentMethod(e.target.value)}
|
||||
checked={paymentMethod === 'ACCOUNT_NUMBER'}
|
||||
onChange={() => setPaymentMethod('ACCOUNT_NUMBER')}
|
||||
className="w-4 h-4 text-blue-600"
|
||||
/>
|
||||
<div className="ml-4 flex items-center gap-3">
|
||||
<CreditCard className="w-6 h-6 text-blue-600" />
|
||||
<div>
|
||||
<span className="font-semibold">Plastik karta</span>
|
||||
<span className="font-semibold">
|
||||
{t('Plastik karta')}
|
||||
</span>
|
||||
<p className="text-sm text-gray-500">
|
||||
{"Online to'lov"}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</Label>
|
||||
|
||||
<Label className="flex items-center p-4 border-2 rounded-lg cursor-pointer transition hover:bg-gray-50">
|
||||
<Input
|
||||
type="radio"
|
||||
name="payment"
|
||||
value="terminal"
|
||||
checked={paymentMethod === 'terminal'}
|
||||
onChange={(e) => setPaymentMethod(e.target.value)}
|
||||
className="w-4 h-4 text-blue-600"
|
||||
/>
|
||||
<div className="ml-4 flex items-center gap-3">
|
||||
<Building2 className="w-6 h-6 text-purple-600" />
|
||||
<div>
|
||||
<span className="font-semibold">Terminal orqali</span>
|
||||
<p className="text-sm text-gray-500">
|
||||
Yetkazib berishda terminal
|
||||
{t("Online to'lov")}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -535,28 +570,32 @@ const OrderPage = () => {
|
||||
{/* Right Column - Order Summary */}
|
||||
<div className="lg:col-span-1">
|
||||
<div className="bg-white rounded-lg shadow-md p-6 sticky top-4">
|
||||
<h3 className="text-xl font-bold mb-4">Mahsulotlar</h3>
|
||||
<h3 className="text-xl font-bold mb-4">{t('Mahsulotlar')}</h3>
|
||||
|
||||
{/* Cart Items */}
|
||||
<div className="space-y-3 mb-4 max-h-60 overflow-y-auto">
|
||||
{cartItems.map((item) => (
|
||||
{cartItems?.map((item) => (
|
||||
<div key={item.id} className="flex gap-3 pb-3 border-b">
|
||||
<Image
|
||||
width={500}
|
||||
height={500}
|
||||
src={item.image}
|
||||
alt={item.name}
|
||||
src={BASE_URL + item.product_image}
|
||||
alt={item.product_name}
|
||||
className="w-16 h-16 object-contain bg-gray-100 rounded"
|
||||
/>
|
||||
<div className="flex-1">
|
||||
<h4 className="font-medium text-sm">{item.name}</h4>
|
||||
<h4 className="font-medium text-sm">
|
||||
{item.product_name}
|
||||
</h4>
|
||||
<p className="text-sm text-gray-500">
|
||||
{item.quantity} x {item.price.toLocaleString()}{' '}
|
||||
{"so'm"}
|
||||
{item.quantity} x{' '}
|
||||
{formatPrice(item.product_price, true)}
|
||||
</p>
|
||||
<p className="font-semibold text-sm">
|
||||
{(item.price * item.quantity).toLocaleString()}{' '}
|
||||
{"so'm"}
|
||||
{formatPrice(
|
||||
item.product_price * item.quantity,
|
||||
true,
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -566,17 +605,15 @@ const OrderPage = () => {
|
||||
{/* Pricing */}
|
||||
<div className="space-y-2 mb-4 pt-4 border-t">
|
||||
<div className="flex justify-between text-gray-600">
|
||||
<span>Mahsulotlar:</span>
|
||||
<span>
|
||||
{subtotal.toLocaleString()} {"so'm"}
|
||||
</span>
|
||||
<span>{t('Mahsulotlar')}:</span>
|
||||
<span>{subtotal && formatPrice(subtotal, true)}</span>
|
||||
</div>
|
||||
<div className="flex justify-between text-gray-600">
|
||||
<span>Yetkazib berish:</span>
|
||||
<span>{t('Yetkazib berish')}:</span>
|
||||
<span>
|
||||
{deliveryFee === 0 ? (
|
||||
<span className="text-green-600 font-semibold">
|
||||
Bepul
|
||||
{t('Bepul')}
|
||||
</span>
|
||||
) : (
|
||||
`${deliveryFee.toLocaleString()} so'm`
|
||||
@@ -587,25 +624,26 @@ const OrderPage = () => {
|
||||
|
||||
<div className="border-t pt-4 mb-6">
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-lg font-semibold">Jami:</span>
|
||||
<span className="text-lg font-semibold">
|
||||
{t('Jami')}:
|
||||
</span>
|
||||
<span className="text-2xl font-bold text-blue-600">
|
||||
{total.toLocaleString()} {"so'm"}
|
||||
{total && formatPrice(total, true)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
disabled={isSubmitting}
|
||||
disabled={isPending}
|
||||
className="w-full bg-blue-600 text-white py-4 rounded-lg font-semibold hover:bg-blue-700 transition disabled:bg-gray-400 disabled:cursor-not-allowed"
|
||||
>
|
||||
{isSubmitting ? (
|
||||
{isPending ? (
|
||||
<span className="flex items-center justify-center gap-2">
|
||||
<div className="w-5 h-5 border-2 border-white border-t-transparent rounded-full animate-spin" />
|
||||
Yuborilmoqda...
|
||||
<Loader2 className="animate-spin" />
|
||||
</span>
|
||||
) : (
|
||||
'Buyurtmani tasdiqlash'
|
||||
t('Buyurtmani tasdiqlash')
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -1,34 +1,53 @@
|
||||
'use client';
|
||||
import { useRouter } from '@/shared/config/i18n/navigation';
|
||||
import { categoryList, CategoryType } from '@/widgets/welcome/lib/data';
|
||||
import { category_api } from '@/shared/config/api/category/api';
|
||||
import { BASE_URL } from '@/shared/config/api/URLs';
|
||||
import { Link } from '@/shared/config/i18n/navigation';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { ChevronRight } from 'lucide-react';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import Image from 'next/image';
|
||||
|
||||
const Category = () => {
|
||||
const router = useRouter();
|
||||
const handleCategoryClick = (category: CategoryType) => {
|
||||
router.push(`/category/${category.name}`);
|
||||
};
|
||||
const t = useTranslations();
|
||||
const { data: category } = useQuery({
|
||||
queryKey: ['category_list'],
|
||||
queryFn: () => category_api.getCategory({ page: 1, page_size: 99 }),
|
||||
select(data) {
|
||||
return data.data.results;
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50 p-4">
|
||||
<div className="max-w-6xl mx-auto">
|
||||
<div className="custom-container">
|
||||
<>
|
||||
<h1 className="text-2xl font-semibold text-gray-900 mb-6">
|
||||
Kategoriyalar
|
||||
{t('Kategoriyalar')}
|
||||
</h1>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-3">
|
||||
{categoryList.map((category, index) => (
|
||||
<button
|
||||
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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,45 +1,67 @@
|
||||
'use client';
|
||||
|
||||
import { useRouter } from '@/shared/config/i18n/navigation';
|
||||
import { product_api } from '@/shared/config/api/product/api';
|
||||
import { usePathname, useRouter } from '@/shared/config/i18n/navigation';
|
||||
import { Card } from '@/shared/ui/card';
|
||||
import { GlobalPagination } from '@/shared/ui/global-pagination';
|
||||
import { Skeleton } from '@/shared/ui/skeleton';
|
||||
import { ProductCard } from '@/widgets/categories/ui/product-card';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { ArrowLeft } from 'lucide-react';
|
||||
import { useParams } from 'next/navigation';
|
||||
import { useState } from 'react';
|
||||
import { subCategoriesData } from '../lib/data';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { useParams, useSearchParams } from 'next/navigation';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
const PAGE_SIZE = 36;
|
||||
|
||||
const Product = () => {
|
||||
const { subId } = useParams();
|
||||
const { categoryId } = useParams();
|
||||
const router = useRouter();
|
||||
const pathname = usePathname();
|
||||
const searchParams = useSearchParams();
|
||||
const [page, setPage] = useState(1);
|
||||
const t = useTranslations();
|
||||
|
||||
const decodedSubId = decodeURIComponent(subId as string);
|
||||
useEffect(() => {
|
||||
const urlPage = Number(searchParams.get('page')) || 1;
|
||||
setPage(urlPage);
|
||||
}, [searchParams]);
|
||||
|
||||
const {
|
||||
data: product,
|
||||
isLoading,
|
||||
isError,
|
||||
} = useQuery({
|
||||
queryKey: ['product_list', categoryId, page],
|
||||
queryFn: () => {
|
||||
if (!categoryId) throw new Error('Category ID is required');
|
||||
return product_api.listGetCategoryId({
|
||||
category_id: categoryId.toString(),
|
||||
params: { page, page_size: PAGE_SIZE },
|
||||
});
|
||||
},
|
||||
select(data) {
|
||||
return data.data;
|
||||
},
|
||||
enabled: !!categoryId,
|
||||
});
|
||||
|
||||
const subCategory =
|
||||
subCategoriesData.find((cat) => cat.name === decodedSubId) ||
|
||||
subCategoriesData[0];
|
||||
const [products, setProducts] = useState(subCategory.products);
|
||||
const handleBack = () => {
|
||||
router.back();
|
||||
};
|
||||
|
||||
const handleRemove = (id: number) => {
|
||||
setProducts((prev) =>
|
||||
prev.map((product) =>
|
||||
product.id === id ? { ...product, liked: false } : product,
|
||||
),
|
||||
);
|
||||
};
|
||||
const handlePageChange = (newPage: number) => {
|
||||
const params = new URLSearchParams(searchParams.toString());
|
||||
params.set('page', newPage.toString());
|
||||
|
||||
const handleLiked = (id: number) => {
|
||||
setProducts((prev) =>
|
||||
prev.map((product) =>
|
||||
product.id === id ? { ...product, liked: true } : product,
|
||||
),
|
||||
);
|
||||
router.push(`${pathname}?${params.toString()}`, {
|
||||
scroll: true,
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="custom-container p-4 mb-5">
|
||||
<div>
|
||||
<div className="custom-container p-4 mb-5 flex flex-col min-h-[calc(85vh)]">
|
||||
<div className="flex-1">
|
||||
{/* Header */}
|
||||
<div className="mb-6">
|
||||
<button
|
||||
@@ -47,28 +69,46 @@ const Product = () => {
|
||||
className="flex items-center gap-2 text-gray-600 hover:text-gray-900 mb-4"
|
||||
>
|
||||
<ArrowLeft className="w-5 h-5" />
|
||||
<span>Orqaga</span>
|
||||
<span>{t('Orqaga')}</span>
|
||||
</button>
|
||||
|
||||
<h1 className="text-2xl font-semibold text-gray-900">
|
||||
{decodedSubId}
|
||||
</h1>
|
||||
<p className="text-gray-600 text-sm mt-1">
|
||||
{subCategory.products.length} ta mahsulot
|
||||
{product?.total} {t('ta mahsulot')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-6 gap-3">
|
||||
{products.map((product) => (
|
||||
<ProductCard
|
||||
key={product.id}
|
||||
product={product}
|
||||
handleRemove={handleRemove}
|
||||
handleLiked={handleLiked}
|
||||
/>
|
||||
))}
|
||||
{/* Products grid */}
|
||||
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-5 xl:grid-cols-6 gap-3">
|
||||
{isLoading &&
|
||||
Array.from({ length: 6 }).map((_, index) => (
|
||||
<Card className="p-3 space-y-3 rounded-xl" key={index}>
|
||||
<Skeleton className="h-40 sm:h-48 md:h-56 w-full rounded-lg" />
|
||||
<Skeleton className="h-4 w-3/4" />
|
||||
<Skeleton className="h-4 w-1/2" />
|
||||
<Skeleton className="h-10 w-full rounded-lg" />
|
||||
</Card>
|
||||
))}
|
||||
{product &&
|
||||
!isLoading &&
|
||||
product.results
|
||||
.filter((product) => product.is_active)
|
||||
.map((item) => (
|
||||
<ProductCard key={item.id} product={item} error={isError} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Pagination at the bottom */}
|
||||
{product && (
|
||||
<div className="w-full mt-5 flex justify-end">
|
||||
<GlobalPagination
|
||||
page={page}
|
||||
total={product.total ?? 0}
|
||||
pageSize={PAGE_SIZE}
|
||||
onChange={handlePageChange}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -17,8 +17,8 @@ const SubCategory = () => {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50 p-4">
|
||||
<div className="max-w-6xl mx-auto">
|
||||
<div className="custom-container">
|
||||
<>
|
||||
<h1 className="text-2xl font-semibold text-gray-900 mb-6">
|
||||
{category.name}
|
||||
</h1>
|
||||
@@ -37,7 +37,7 @@ const SubCategory = () => {
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
11
src/features/faq/lib/api.ts
Normal file
11
src/features/faq/lib/api.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import httpClient from '@/shared/config/api/httpClient';
|
||||
import { API_URLS } from '@/shared/config/api/URLs';
|
||||
import { AxiosResponse } from 'axios';
|
||||
import { FaqList } from './types';
|
||||
|
||||
export const faq_api = {
|
||||
async list(): Promise<AxiosResponse<FaqList[]>> {
|
||||
const res = await httpClient.get(API_URLS.Faq);
|
||||
return res;
|
||||
},
|
||||
};
|
||||
5
src/features/faq/lib/types.ts
Normal file
5
src/features/faq/lib/types.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export interface FaqList {
|
||||
id: string;
|
||||
question: string;
|
||||
answer: string;
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
'use client';
|
||||
|
||||
import {
|
||||
Accordion,
|
||||
AccordionContent,
|
||||
@@ -5,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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -1,24 +1,27 @@
|
||||
import { Card, CardContent } from '@/shared/ui/card';
|
||||
import { Database, Eye, FileText, Lock, Shield, UserCheck } from 'lucide-react';
|
||||
import { Database, Eye, FileText, Lock, Shield } from 'lucide-react';
|
||||
import { useTranslations } from 'next-intl';
|
||||
|
||||
const PrivacyPolicy = () => {
|
||||
const t = useTranslations();
|
||||
return (
|
||||
<main className="custom-container">
|
||||
{/* Hero Section */}
|
||||
<section>
|
||||
<div className="container mx-auto max-w-4xl text-center">
|
||||
<div className="text-center">
|
||||
<div className="inline-flex items-center justify-center w-16 h-16 rounded-full bg-primary/10 mb-6">
|
||||
<Shield className="w-8 h-8 text-primary" />
|
||||
</div>
|
||||
<h1 className="text-4xl md:text-5xl font-bold mb-4 text-balance">
|
||||
Maxfiylik Siyosati
|
||||
</h1>
|
||||
<p className="text-lg text-muted-foreground max-w-2xl mx-auto">
|
||||
{`Gastro Market sizning ma'lumotlaringiz xavfsizligini jiddiy qabul
|
||||
qiladi`}
|
||||
<p className="text-2xl md:text-5xl font-bold mb-4">
|
||||
{t('Maxfiylik Siyosati')}
|
||||
</p>
|
||||
<p className="text-lg text-muted-foreground">
|
||||
{t(
|
||||
`Gastro Market sizning ma'lumotlaringiz xavfsizligini jiddiy qabul qiladi`,
|
||||
)}
|
||||
</p>
|
||||
<p className="text-sm text-muted-foreground mt-4">
|
||||
Oxirgi yangilanish: 16 Dekabr 2025
|
||||
{t('Oxirgi yangilanish: 16 Dekabr 2025')}
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
@@ -29,11 +32,9 @@ const PrivacyPolicy = () => {
|
||||
{/* Introduction */}
|
||||
<div className="prose prose-lg max-w-none mb-12">
|
||||
<p className="text-muted-foreground leading-relaxed">
|
||||
{`Ushbu Maxfiylik Siyosati Gastro Market online magazinida siz
|
||||
tomonidan taqdim etilgan shaxsiy ma'lumotlarni qanday to'playmiz,
|
||||
ishlatamiz va himoya qilishimizni tushuntiradi. Xizmatlarimizdan
|
||||
foydalanish orqali siz ushbu siyosatda tasvirlangan amaliyotlarga
|
||||
rozilik bildirasiz.`}
|
||||
{t(
|
||||
`Ushbu Maxfiylik Siyosati Gastro Market online magazinida siz tomonidan taqdim etilgan shaxsiy ma'lumotlarni qanday to'playmiz`,
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -50,29 +51,30 @@ const PrivacyPolicy = () => {
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<h2 className="text-2xl font-bold mb-4">
|
||||
{"1. Biz To'playdigan Ma'lumotlar"}
|
||||
{t("Biz To'playdigan Ma'lumotlar")}
|
||||
</h2>
|
||||
<div className="space-y-3 text-muted-foreground">
|
||||
<p className="leading-relaxed">
|
||||
{`Sizning tajribangizni yaxshilash uchun biz quyidagi
|
||||
ma'lumotlarni to'playmiz:`}
|
||||
{t(
|
||||
`Sizning tajribangizni yaxshilash uchun biz quyidagi ma'lumotlarni to'playmiz`,
|
||||
)}
|
||||
</p>
|
||||
<ul className="space-y-2 ml-6 list-disc">
|
||||
<li>
|
||||
<strong>{`Shaxsiy Ma'lumotlar`}:</strong> Ism, email
|
||||
manzil, telefon raqami
|
||||
<strong>{t(`Shaxsiy Ma'lumotlar`)}:</strong>{' '}
|
||||
{t('Ism email manzil telefon raqami')}
|
||||
</li>
|
||||
<li>
|
||||
<strong>{"Kompaniya Ma'lumotlari"}:</strong> Kompaniya
|
||||
nomi, website, hamkorlik {"so'rovlari"}
|
||||
<strong>{"Kompaniya Ma'lumotlari"}:</strong>{' '}
|
||||
{t("Kompaniya nomi, website, hamkorlik so'rovlari")}
|
||||
</li>
|
||||
<li>
|
||||
<strong>Fayllar:</strong> Hamkorlik uchun yuklangan
|
||||
hujjatlar
|
||||
<strong>{t('Fayllar:')}</strong>{' '}
|
||||
{t('Hamkorlik uchun yuklangan hujjatlar')}
|
||||
</li>
|
||||
<li>
|
||||
<strong>{"Texnik Ma'lumotlar"}:</strong> IP manzil,
|
||||
brauzer turi, qurilma {"ma'lumotlari"}
|
||||
<strong>{t("Texnik Ma'lumotlar")}:</strong>{' '}
|
||||
{t("IP manzil, brauzer turi, qurilma ma'lumotlari")}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
@@ -92,31 +94,36 @@ const PrivacyPolicy = () => {
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<h2 className="text-2xl font-bold mb-4">
|
||||
2. {"Ma'lumotlardan Foydalanish"}
|
||||
{t("Ma'lumotlardan Foydalanish")}
|
||||
</h2>
|
||||
<div className="space-y-3 text-muted-foreground">
|
||||
<p className="leading-relaxed">
|
||||
{`To'plangan ma'lumotlaringizdan quyidagi maqsadlarda
|
||||
foydalanamiz:`}
|
||||
{t(
|
||||
`To'plangan ma'lumotlaringizdan quyidagi maqsadlarda foydalanamiz:`,
|
||||
)}
|
||||
</p>
|
||||
<ul className="space-y-2 ml-6 list-disc">
|
||||
<li>
|
||||
{
|
||||
"Hamkorlik so'rovlarini qayta ishlash va javob berish"
|
||||
}
|
||||
{t(
|
||||
"Hamkorlik so'rovlarini qayta ishlash va javob berish",
|
||||
)}
|
||||
</li>
|
||||
<li>
|
||||
{`Sizga xizmatlarimiz va yangiliklar haqida ma'lumot
|
||||
berish`}
|
||||
{t(
|
||||
`Sizga xizmatlarimiz va yangiliklar haqida ma'lumot berish`,
|
||||
)}
|
||||
</li>
|
||||
<li>
|
||||
{`Sayt xavfsizligini ta'minlash va firibgarlikka qarshi
|
||||
kurashish`}
|
||||
{t(
|
||||
`Sayt xavfsizligini ta'minlash va firibgarlikka qarshi kurashish`,
|
||||
)}
|
||||
</li>
|
||||
<li>
|
||||
Foydalanuvchi tajribasini tahlil qilish va yaxshilash
|
||||
{t(
|
||||
'Foydalanuvchi tajribasini tahlil qilish va yaxshilash',
|
||||
)}
|
||||
</li>
|
||||
<li>Qonuniy talablarni bajarish</li>
|
||||
<li>{t('Qonuniy talablarni bajarish')}</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
@@ -135,22 +142,29 @@ const PrivacyPolicy = () => {
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<h2 className="text-2xl font-bold mb-4">
|
||||
3. {"Ma'lumotlar Xavfsizligi"}
|
||||
{t("Ma'lumotlar Xavfsizligi")}
|
||||
</h2>
|
||||
<div className="space-y-3 text-muted-foreground">
|
||||
<p className="leading-relaxed">
|
||||
{`Biz sizning shaxsiy ma'lumotlaringizni himoya qilish
|
||||
uchun zamonaviy xavfsizlik choralarini qo'llaymiz:`}
|
||||
{t(
|
||||
`Biz sizning shaxsiy ma'lumotlaringizni himoya qilish uchun zamonaviy xavfsizlik choralarini qo'llaymiz:`,
|
||||
)}
|
||||
</p>
|
||||
<ul className="space-y-2 ml-6 list-disc">
|
||||
<li>
|
||||
{"SSL/TLS shifrlash orqali ma'lumotlar uzatish"}
|
||||
{t("SSL/TLS shifrlash orqali ma'lumotlar uzatish")}
|
||||
</li>
|
||||
<li>
|
||||
{"Xavfsiz serverlar va ma'lumotlar bazasida saqlash"}
|
||||
{t(
|
||||
"Xavfsiz serverlar va ma'lumotlar bazasida saqlash",
|
||||
)}
|
||||
</li>
|
||||
<li>
|
||||
{t('Cheklangan kirish huquqlari va autentifikatsiya')}
|
||||
</li>
|
||||
<li>
|
||||
{t('Doimiy xavfsizlik monitoringi va yangilanishlar')}
|
||||
</li>
|
||||
<li>Cheklangan kirish huquqlari va autentifikatsiya</li>
|
||||
<li>Doimiy xavfsizlik monitoringi va yangilanishlar</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
@@ -169,22 +183,25 @@ const PrivacyPolicy = () => {
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<h2 className="text-2xl font-bold mb-4">
|
||||
{"4. Ma'lumotlarni Ulashish"}
|
||||
{t("Ma'lumotlarni Ulashish")}
|
||||
</h2>
|
||||
<div className="space-y-3 text-muted-foreground">
|
||||
<p className="leading-relaxed">
|
||||
{`Biz sizning shaxsiy ma'lumotlaringizni uchinchi
|
||||
shaxslarga sotmaymiz. Ma'lumotlaringiz faqat quyidagi
|
||||
hollarda ulashilishi mumkin:`}
|
||||
{t(
|
||||
`Biz sizning shaxsiy ma'lumotlaringizni uchinchi shaxslarga sotmaymiz`,
|
||||
)}
|
||||
</p>
|
||||
<ul className="space-y-2 ml-6 list-disc">
|
||||
<li>Sizning roziligingiz bilan</li>
|
||||
<li>{"Qonuniy talablar bo'yicha"}</li>
|
||||
<li>{t('Sizning roziligingiz bilan')}</li>
|
||||
<li>{t("Qonuniy talablar bo'yicha")}</li>
|
||||
<li>
|
||||
{`Xizmat ko'rsatuvchi ishonchli hamkorlar bilan
|
||||
(maxfiylik shartnomalari ostida)`}
|
||||
{t(
|
||||
`Xizmat ko'rsatuvchi ishonchli hamkorlar bilan (maxfiylik shartnomalari ostida)`,
|
||||
)}
|
||||
</li>
|
||||
<li>
|
||||
{t('Kompaniya birlashuvi yoki sotilishi holatida')}
|
||||
</li>
|
||||
<li>Kompaniya birlashuvi yoki sotilishi holatida</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
@@ -192,131 +209,16 @@ const PrivacyPolicy = () => {
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Section 5 */}
|
||||
<Card className="border-l-4 border-l-chart-4">
|
||||
<CardContent className="pt-6">
|
||||
<div className="flex items-start gap-4">
|
||||
<div className="flex-shrink-0">
|
||||
<div className="w-12 h-12 rounded-lg bg-chart-4/10 flex items-center justify-center">
|
||||
<UserCheck className="w-6 h-6 text-chart-4" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<h2 className="text-2xl font-bold mb-4">
|
||||
5. Sizning Huquqlaringiz
|
||||
</h2>
|
||||
<div className="space-y-3 text-muted-foreground">
|
||||
<p className="leading-relaxed">
|
||||
Sizda quyidagi huquqlar mavjud:
|
||||
</p>
|
||||
<ul className="space-y-2 ml-6 list-disc">
|
||||
<li>
|
||||
<strong>Kirish Huquqi:</strong> {"O'zingiz haqidagi"}
|
||||
{"ma'lumotlarni ko'rish"}
|
||||
</li>
|
||||
<li>
|
||||
<strong>Tuzatish Huquqi:</strong>{' '}
|
||||
{`Noto'g'ri
|
||||
ma'lumotlarni tuzatish`}
|
||||
</li>
|
||||
<li>
|
||||
<strong>{"O'chirish Huquqi"}:</strong>{' '}
|
||||
{`Ma'lumotlaringizni
|
||||
o'chirishni so'rash`}
|
||||
</li>
|
||||
<li>
|
||||
<strong>Rad Etish Huquqi:</strong> Marketing
|
||||
xabarlaridan voz kechish
|
||||
</li>
|
||||
<li>
|
||||
<strong>Portativlik:</strong>{' '}
|
||||
{`Ma'lumotlaringizni
|
||||
boshqa joyga ko'chirish`}
|
||||
</li>
|
||||
</ul>
|
||||
<p className="mt-4 leading-relaxed">
|
||||
Ushbu huquqlardan foydalanish uchun biz bilan{' '}
|
||||
<a
|
||||
href="mailto:info@gastromarket.uz"
|
||||
className="text-primary hover:underline"
|
||||
>
|
||||
info@gastromarket.uz
|
||||
</a>{' '}
|
||||
{"orqali bog'laning."}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Section 6 - Cookies */}
|
||||
<Card>
|
||||
<CardContent className="pt-6">
|
||||
<h2 className="text-2xl font-bold mb-4">
|
||||
6. Cookies va Kuzatish Texnologiyalari
|
||||
</h2>
|
||||
<div className="space-y-3 text-muted-foreground">
|
||||
<p className="leading-relaxed">
|
||||
{`Saytimiz cookies va shunga o'xshash texnologiyalardan
|
||||
foydalanadi. Ulardan foydalanish maqsadi:`}
|
||||
</p>
|
||||
<ul className="space-y-2 ml-6 list-disc">
|
||||
<li>Siz tanlagan parametrlarni eslab qolish</li>
|
||||
<li>
|
||||
Sayt traffic va foydalanuvchi xatti-harakatlarini tahlil
|
||||
qilish
|
||||
</li>
|
||||
<li>
|
||||
{"Marketing kampaniyalarining samaradorligini o'lchash"}
|
||||
</li>
|
||||
</ul>
|
||||
<p className="mt-4 leading-relaxed">
|
||||
Brauzer sozlamalaridan cookies-ni boshqarishingiz mumkin.
|
||||
</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Section 7 - Children */}
|
||||
<Card>
|
||||
<CardContent className="pt-6">
|
||||
<h2 className="text-2xl font-bold mb-4">
|
||||
7. Bolalar Maxfiyligi
|
||||
</h2>
|
||||
<p className="text-muted-foreground leading-relaxed">
|
||||
{`Bizning xizmatlarimiz 16 yoshdan kichik bolalarga
|
||||
mo'ljallanmagan. Agar siz 16 yoshdan kichik bo'lsangiz,
|
||||
iltimos, shaxsiy ma'lumotlaringizni taqdim etishdan oldin
|
||||
ota-onangiz yoki vasiyingizning roziligini oling.`}
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Section 8 - Changes */}
|
||||
<Card>
|
||||
<CardContent className="pt-6">
|
||||
<h2 className="text-2xl font-bold mb-4">
|
||||
8. {"Siyosatdagi O'zgarishlar"}
|
||||
</h2>
|
||||
<p className="text-muted-foreground leading-relaxed">
|
||||
{`Biz vaqti-vaqti bilan ushbu Maxfiylik Siyosatini yangilashimiz
|
||||
mumkin. Barcha o'zgarishlar ushbu sahifada e'lon qilinadi va
|
||||
yuqorida "Oxirgi yangilanish" sanasi ko'rsatiladi. Muhim
|
||||
o'zgarishlar bo'lsa, sizni email orqali xabardor qilamiz.`}
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Contact Section */}
|
||||
<Card className="bg-primary/5 border-primary/20">
|
||||
<CardContent className="pt-6">
|
||||
<h2 className="text-2xl font-bold mb-4">
|
||||
9. {"Biz Bilan Bog'lanish"}
|
||||
{t("Biz Bilan Bog'lanish")}
|
||||
</h2>
|
||||
<p className="text-muted-foreground leading-relaxed mb-4">
|
||||
{`Ushbu Maxfiylik Siyosati yoki shaxsiy ma'lumotlaringiz haqida
|
||||
savollaringiz bo'lsa, biz bilan bog'laning:`}
|
||||
{t(
|
||||
`Ushbu Maxfiylik Siyosati yoki shaxsiy ma'lumotlaringiz haqida savollaringiz bo'lsa, biz bilan bog'laning:`,
|
||||
)}
|
||||
</p>
|
||||
<div className="space-y-2 text-muted-foreground">
|
||||
<p>
|
||||
@@ -329,7 +231,7 @@ const PrivacyPolicy = () => {
|
||||
</a>
|
||||
</p>
|
||||
<p>
|
||||
<strong>Telefon:</strong>{' '}
|
||||
<strong>{t('Telefon')}:</strong>{' '}
|
||||
<a
|
||||
href="tel:+998901234567"
|
||||
className="text-primary hover:underline"
|
||||
@@ -338,7 +240,7 @@ const PrivacyPolicy = () => {
|
||||
</a>
|
||||
</p>
|
||||
<p>
|
||||
<strong>Manzil:</strong> {"Toshkent, O'zbekiston"}
|
||||
<strong>{t('Manzil')}:</strong> {t("Toshkent, O'zbekiston")}
|
||||
</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
@@ -346,23 +248,6 @@ const PrivacyPolicy = () => {
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Bottom CTA */}
|
||||
<section className="py-16 px-4 bg-muted/30">
|
||||
<div className="container mx-auto max-w-4xl text-center">
|
||||
<h2 className="text-3xl font-bold mb-4">Savollaringiz Bormi?</h2>
|
||||
<p className="text-muted-foreground mb-6 max-w-2xl mx-auto">
|
||||
{`Maxfiylik siyosati yoki ma'lumotlaringiz xavfsizligi haqida
|
||||
qo'shimcha ma'lumot olish uchun bizga murojaat qiling.`}
|
||||
</p>
|
||||
<a
|
||||
href="mailto:privacy@gastromarket.uz"
|
||||
className="inline-flex items-center justify-center px-6 py-3 bg-primary text-primary-foreground rounded-lg hover:bg-primary/90 transition-colors font-medium"
|
||||
>
|
||||
{"Biz Bilan Bog'laning"}
|
||||
</a>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
);
|
||||
};
|
||||
|
||||
0
src/features/product/lib/api.ts
Normal file
0
src/features/product/lib/api.ts
Normal file
@@ -1,5 +1,11 @@
|
||||
'use client';
|
||||
|
||||
import { cart_api } from '@/features/cart/lib/api';
|
||||
import { product_api } from '@/shared/config/api/product/api';
|
||||
import { BASE_URL } from '@/shared/config/api/URLs';
|
||||
import { useCartId } from '@/shared/hooks/cartId';
|
||||
import formatPrice from '@/shared/lib/formatPrice';
|
||||
import { Card } from '@/shared/ui/card';
|
||||
import {
|
||||
Carousel,
|
||||
CarouselContent,
|
||||
@@ -7,107 +13,112 @@ import {
|
||||
CarouselNext,
|
||||
CarouselPrevious,
|
||||
} from '@/shared/ui/carousel';
|
||||
import { Input } from '@/shared/ui/input';
|
||||
import { Skeleton } from '@/shared/ui/skeleton';
|
||||
import { ProductCard } from '@/widgets/categories/ui/product-card';
|
||||
import {
|
||||
Heart,
|
||||
Minus,
|
||||
Plus,
|
||||
RotateCcw,
|
||||
Shield,
|
||||
ShoppingCart,
|
||||
Star,
|
||||
Truck,
|
||||
} from 'lucide-react';
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import { AxiosError } from 'axios';
|
||||
import { Heart, Minus, Plus, Shield, ShoppingCart, Truck } from 'lucide-react';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import Image from 'next/image';
|
||||
import { useState } from 'react';
|
||||
import { useParams } from 'next/navigation';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
const ProductDetail = () => {
|
||||
const t = useTranslations();
|
||||
const [quantity, setQuantity] = useState(1);
|
||||
const [selectedImage, setSelectedImage] = useState(0);
|
||||
const [liked, setLiked] = useState(false);
|
||||
const { product } = useParams();
|
||||
const queryClient = useQueryClient();
|
||||
const [selectedImage, setSelectedImage] = useState<number>(0);
|
||||
const debounceRef = useRef<NodeJS.Timeout | null>(null);
|
||||
const { cart_id } = useCartId();
|
||||
|
||||
// Fake product data
|
||||
const product = {
|
||||
id: 5,
|
||||
name: 'Coca-Cola 1.5L',
|
||||
price: 12000,
|
||||
oldPrice: 14000,
|
||||
image: '/classic-coca-cola.png',
|
||||
rating: 4.8,
|
||||
reviews: 342,
|
||||
discount: 14,
|
||||
inStock: true,
|
||||
description:
|
||||
"Coca-Cola klassik ta'mi bilan ajoyib gazlangan ichimlik. 1.5 litrlik shisha butilkada. Sovuq holda iste'mol qilish tavsiya etiladi.",
|
||||
category: 'Ichimliklar',
|
||||
brand: 'Coca-Cola',
|
||||
volume: '1.5L',
|
||||
supplier: {
|
||||
name: 'Global Trade LLC',
|
||||
logo: '/generic-company-logo.png',
|
||||
phone: '+998 90 123 45 67',
|
||||
email: 'info@globaltrade.uz',
|
||||
},
|
||||
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" />
|
||||
|
||||
64
src/features/profile/lib/api.ts
Normal file
64
src/features/profile/lib/api.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
import httpClient from '@/shared/config/api/httpClient';
|
||||
import { API_URLS } from '@/shared/config/api/URLs';
|
||||
import { AxiosResponse } from 'axios';
|
||||
|
||||
export interface OrderList {
|
||||
count: number;
|
||||
next: string;
|
||||
previous: string;
|
||||
results: OrderListRes[];
|
||||
}
|
||||
|
||||
export interface OrderListRes {
|
||||
id: string;
|
||||
order_number: number;
|
||||
status: 'NEW' | 'DONE';
|
||||
total_price: number;
|
||||
payment_type: 'CASH' | 'ACCOUNT_NUMBER';
|
||||
delivery_type: 'YandexGo' | 'DELIVERY_COURIES' | 'PICKUP';
|
||||
delivery_price: number;
|
||||
contact_number: string;
|
||||
comment: string;
|
||||
name: string;
|
||||
items: [
|
||||
{
|
||||
id: string;
|
||||
product: {
|
||||
id: string;
|
||||
name: string;
|
||||
image: string;
|
||||
price: number;
|
||||
description: string;
|
||||
unity: string;
|
||||
min_quantity: number;
|
||||
is_active: true;
|
||||
liked: string;
|
||||
brand: string;
|
||||
return_date: string;
|
||||
expires_date: string;
|
||||
manufacturer: string;
|
||||
volume: string;
|
||||
images: [
|
||||
{
|
||||
id: string;
|
||||
image: string;
|
||||
},
|
||||
];
|
||||
};
|
||||
price: number;
|
||||
quantity: number;
|
||||
created_at: string;
|
||||
},
|
||||
];
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
export const order_api = {
|
||||
async list(params: {
|
||||
page: number;
|
||||
page_size: number;
|
||||
}): Promise<AxiosResponse<OrderList>> {
|
||||
const res = await httpClient.get(API_URLS.OrderList, { params });
|
||||
return res;
|
||||
},
|
||||
};
|
||||
@@ -1,46 +1,29 @@
|
||||
import { Button } from '@/shared/ui/button';
|
||||
import { Card, CardContent } from '@/shared/ui/card';
|
||||
import { Tabs, TabsList, TabsTrigger } from '@/shared/ui/tabs';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { Calendar, CheckCircle, Clock, RefreshCw } from 'lucide-react';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import Image from 'next/image';
|
||||
import { useState } from 'react';
|
||||
import { order_api } from '../lib/api';
|
||||
import { orders } from '../lib/data';
|
||||
|
||||
const HistoryTabs = () => {
|
||||
const [historyTab, setHistoryTab] = useState('all');
|
||||
const t = useTranslations();
|
||||
|
||||
const { data } = useQuery({
|
||||
queryKey: ['order_list'],
|
||||
queryFn: () => order_api.list({ page: 1, page_size: 1 }),
|
||||
});
|
||||
|
||||
console.log(data);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex items-center justify-between mb-4 md:mb-6">
|
||||
<h2 className="text-xl md:text-2xl font-bold text-foreground">Tarix</h2>
|
||||
<h2 className="text-xl md:text-2xl font-bold text-foreground">
|
||||
{t('Tarix')}
|
||||
</h2>
|
||||
</div>
|
||||
<Tabs
|
||||
value={historyTab}
|
||||
onValueChange={setHistoryTab}
|
||||
className="mb-4 md:mb-6"
|
||||
>
|
||||
<TabsList className="bg-slate-100 w-full grid grid-cols-3 h-auto p-1">
|
||||
<TabsTrigger
|
||||
value="all"
|
||||
className="data-[state=active]:bg-white text-xs md:text-sm py-2"
|
||||
>
|
||||
Barchasi
|
||||
</TabsTrigger>
|
||||
<TabsTrigger
|
||||
value="week"
|
||||
className="data-[state=active]:bg-white text-xs md:text-sm py-2"
|
||||
>
|
||||
Bu hafta
|
||||
</TabsTrigger>
|
||||
<TabsTrigger
|
||||
value="month"
|
||||
className="data-[state=active]:bg-white text-xs md:text-sm py-2"
|
||||
>
|
||||
Bu oy
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
</Tabs>
|
||||
|
||||
<div className="space-y-3 md:space-y-4">
|
||||
{orders
|
||||
.filter((o) => o.status === 'delivered')
|
||||
@@ -100,7 +83,7 @@ const HistoryTabs = () => {
|
||||
className="bg-transparent gap-1 md:gap-2 text-xs md:text-sm h-8 md:h-9 px-2 md:px-3"
|
||||
>
|
||||
<RefreshCw className="w-3 h-3 md:w-4 md:h-4" />
|
||||
Qayta
|
||||
{t('Qayta')}
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
|
||||
@@ -2,12 +2,14 @@ import { Badge } from '@/shared/ui/badge';
|
||||
import { Card, CardContent } from '@/shared/ui/card';
|
||||
import { Tabs, TabsList, TabsTrigger } from '@/shared/ui/tabs';
|
||||
import { CheckCircle, MapPin, Package, Truck } from 'lucide-react';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import Image from 'next/image';
|
||||
import { useState } from 'react';
|
||||
import { orders } from '../lib/data';
|
||||
|
||||
const Orders = () => {
|
||||
const [ordersTab, setOrdersTab] = useState('active');
|
||||
const t = useTranslations();
|
||||
|
||||
const getStatusInfo = (status: string) => {
|
||||
const statusMap: Record<
|
||||
@@ -61,7 +63,7 @@ const Orders = () => {
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-4 md:mb-6">
|
||||
<h2 className="text-xl md:text-2xl font-bold text-foreground">
|
||||
Buyurtmalar
|
||||
{t('Buyurtmalar')}
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
@@ -75,19 +77,21 @@ const Orders = () => {
|
||||
value="active"
|
||||
className="data-[state=active]:bg-white text-xs md:text-sm py-2"
|
||||
>
|
||||
Faol ({orders.filter((o) => o.status !== 'delivered').length})
|
||||
{t('Faol')} ({orders.filter((o) => o.status !== 'delivered').length}
|
||||
)
|
||||
</TabsTrigger>
|
||||
<TabsTrigger
|
||||
value="completed"
|
||||
className="data-[state=active]:bg-white text-xs md:text-sm py-2"
|
||||
>
|
||||
Tugadi ({orders.filter((o) => o.status === 'delivered').length})
|
||||
{t('Tugadi')} (
|
||||
{orders.filter((o) => o.status === 'delivered').length})
|
||||
</TabsTrigger>
|
||||
<TabsTrigger
|
||||
value="all"
|
||||
className="data-[state=active]:bg-white text-xs md:text-sm py-2"
|
||||
>
|
||||
Barcha ({orders.length})
|
||||
{t('Barchasi')} ({orders.length})
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
</Tabs>
|
||||
@@ -128,7 +132,7 @@ const Orders = () => {
|
||||
<Badge
|
||||
className={`${statusInfo.bgColor} ${statusInfo.color} border-0 text-xs`}
|
||||
>
|
||||
{statusInfo.text}
|
||||
{t(statusInfo.text)}
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
@@ -168,7 +172,8 @@ const Orders = () => {
|
||||
{"so'm"}
|
||||
</p>
|
||||
<p className="text-[10px] md:text-xs text-muted-foreground">
|
||||
Yetkazish: {order.deliveryFee.toLocaleString()} {"so'm"}
|
||||
{t('Yetkazish')}: {order.deliveryFee.toLocaleString()}{' '}
|
||||
{"so'm"}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,264 +1,34 @@
|
||||
'use client';
|
||||
|
||||
import { PartnershipForm } from '@/features/about/ui/AboutPage';
|
||||
import Faq from '@/features/faq/ui/Faq';
|
||||
import Favourite from '@/features/favourite/ui/Favourite';
|
||||
import { useRouter } from '@/shared/config/i18n/navigation';
|
||||
import { getMe, removeToken } from '@/shared/lib/token';
|
||||
import { Avatar, AvatarFallback, AvatarImage } from '@/shared/ui/avatar';
|
||||
import { Badge } from '@/shared/ui/badge';
|
||||
import { Button } from '@/shared/ui/button';
|
||||
import { Card, CardContent } from '@/shared/ui/card';
|
||||
import {
|
||||
CheckCircle,
|
||||
ChevronRight,
|
||||
History,
|
||||
Home,
|
||||
LogOut,
|
||||
MapPin,
|
||||
Package,
|
||||
RefreshCw,
|
||||
ShoppingBag,
|
||||
Truck,
|
||||
} from 'lucide-react';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import { Headset, Home, LogOut } from 'lucide-react';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { useState } from 'react';
|
||||
import { orders, user } from '../lib/data';
|
||||
import HistoryTabs from './History';
|
||||
import Orders from './Orders';
|
||||
import CustomerSupport from './Support';
|
||||
|
||||
const Profile = () => {
|
||||
const [activeSection, setActiveSection] = useState('overview');
|
||||
const router = useRouter();
|
||||
|
||||
const getStatusInfo = (status: string) => {
|
||||
const statusMap: Record<
|
||||
string,
|
||||
{ text: string; color: string; bgColor: string }
|
||||
> = {
|
||||
inTransit: {
|
||||
text: "Yo'lda",
|
||||
color: 'text-blue-600',
|
||||
bgColor: 'bg-blue-100',
|
||||
},
|
||||
atPickup: {
|
||||
text: 'Punktda',
|
||||
color: 'text-amber-600',
|
||||
bgColor: 'bg-amber-100',
|
||||
},
|
||||
delivered: {
|
||||
text: 'Yetkazildi',
|
||||
color: 'text-emerald-600',
|
||||
bgColor: 'bg-emerald-100',
|
||||
},
|
||||
cancelled: {
|
||||
text: 'Bekor qilindi',
|
||||
color: 'text-red-600',
|
||||
bgColor: 'bg-red-100',
|
||||
},
|
||||
};
|
||||
return (
|
||||
statusMap[status] || {
|
||||
text: status,
|
||||
color: 'text-muted-foreground',
|
||||
bgColor: 'bg-muted',
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
const getStatusIcon = (status: string) => {
|
||||
switch (status) {
|
||||
case 'inTransit':
|
||||
return <Truck className="w-4 h-4" />;
|
||||
case 'atPickup':
|
||||
return <MapPin className="w-4 h-4" />;
|
||||
case 'delivered':
|
||||
return <CheckCircle className="w-4 h-4" />;
|
||||
default:
|
||||
return <Package className="w-4 h-4" />;
|
||||
}
|
||||
};
|
||||
const t = useTranslations();
|
||||
const queryClient = useQueryClient();
|
||||
const user = getMe();
|
||||
|
||||
const menuItems = [
|
||||
{ id: 'overview', label: 'Umumiy', icon: Home },
|
||||
{ id: 'orders', label: 'Buyurtmalar', icon: ShoppingBag },
|
||||
{ id: 'history', label: 'Tarix', icon: History },
|
||||
{ id: 'support', label: "Qo'llab-quvatlash", icon: Headset },
|
||||
];
|
||||
|
||||
const renderContent = () => {
|
||||
switch (activeSection) {
|
||||
case 'orders':
|
||||
return <Orders />;
|
||||
|
||||
case 'history':
|
||||
return <HistoryTabs />;
|
||||
|
||||
case 'favorites':
|
||||
return <Favourite />;
|
||||
|
||||
case 'agency':
|
||||
return <PartnershipForm />;
|
||||
|
||||
case 'faq':
|
||||
return <Faq />;
|
||||
case 'support':
|
||||
return <CustomerSupport />;
|
||||
router.push('https://t.me/web_app_0515_bot');
|
||||
|
||||
default:
|
||||
return (
|
||||
<>
|
||||
{/* Active Orders Section */}
|
||||
<div className="mb-6 md:mb-8">
|
||||
<div className="flex items-center justify-between mb-3 md:mb-4">
|
||||
<h3 className="text-base md:text-lg font-semibold text-foreground">
|
||||
Faol buyurtmalar
|
||||
</h3>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="text-emerald-600 hover:text-emerald-700 h-8 text-xs md:text-sm"
|
||||
onClick={() => setActiveSection('orders')}
|
||||
>
|
||||
Barchasi
|
||||
<ChevronRight className="w-3 h-3 md:w-4 md:h-4 ml-1" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
{orders
|
||||
.filter((o) => o.status !== 'delivered')
|
||||
.slice(0, 2)
|
||||
.map((order) => {
|
||||
const statusInfo = getStatusInfo(order.status);
|
||||
return (
|
||||
<Card
|
||||
key={order.id}
|
||||
className="border-0 shadow-sm hover:shadow-md transition-shadow"
|
||||
>
|
||||
<CardContent className="p-3 md:p-5">
|
||||
<div className="flex items-start justify-between mb-3 md:mb-4">
|
||||
<div className="flex items-center gap-2 md:gap-3">
|
||||
<div
|
||||
className={`w-10 h-10 md:w-12 md:h-12 rounded-xl ${statusInfo.bgColor} flex items-center justify-center shrink-0`}
|
||||
>
|
||||
<span className={statusInfo.color}>
|
||||
{getStatusIcon(order.status)}
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-semibold text-sm md:text-base text-foreground">
|
||||
{order.id}
|
||||
</p>
|
||||
<p className="text-xs md:text-sm text-muted-foreground">
|
||||
{order.date} • {order.time}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<Badge
|
||||
className={`${statusInfo.bgColor} ${statusInfo.color} border-0 text-xs`}
|
||||
>
|
||||
{statusInfo.text}
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap gap-1.5 md:gap-2 mb-3 md:mb-4">
|
||||
{order.items.map((item, idx) => (
|
||||
<span
|
||||
key={idx}
|
||||
className="px-2 md:px-3 py-1 bg-slate-100 rounded-full text-xs text-muted-foreground"
|
||||
>
|
||||
{item.name} ×{item.quantity}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col md:flex-row md:items-center justify-between pt-3 md:pt-4 border-t border-slate-100 gap-2">
|
||||
<div className="flex items-center gap-2 text-xs md:text-sm text-muted-foreground">
|
||||
<MapPin className="w-3 h-3 md:w-4 md:h-4 shrink-0" />
|
||||
<span className="truncate">{order.address}</span>
|
||||
</div>
|
||||
<p className="font-bold text-sm md:text-base text-foreground">
|
||||
{(
|
||||
order.total + order.deliveryFee
|
||||
).toLocaleString()}{' '}
|
||||
{"so'm"}
|
||||
</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Order History */}
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-3 md:mb-4">
|
||||
<h3 className="text-base md:text-lg font-semibold text-foreground">
|
||||
Buyurtmalar tarixi
|
||||
</h3>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="text-emerald-600 hover:text-emerald-700 h-8 text-xs md:text-sm"
|
||||
onClick={() => setActiveSection('history')}
|
||||
>
|
||||
Barchasi{' '}
|
||||
<ChevronRight className="w-3 h-3 md:w-4 md:h-4 ml-1" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
{orders
|
||||
.filter((o) => o.status === 'delivered')
|
||||
.slice(0, 2)
|
||||
.map((order) => (
|
||||
<Card
|
||||
key={order.id}
|
||||
className="border-0 shadow-sm hover:shadow-md transition-shadow"
|
||||
>
|
||||
<CardContent className="p-3 md:p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3 md:gap-4">
|
||||
<div className="w-10 h-10 md:w-12 md:h-12 rounded-xl bg-emerald-100 flex items-center justify-center shrink-0">
|
||||
<CheckCircle className="w-4 h-4 md:w-5 md:h-5 text-emerald-600" />
|
||||
</div>
|
||||
<div className="min-w-0">
|
||||
<p className="font-semibold text-sm md:text-base text-foreground">
|
||||
{order.id}
|
||||
</p>
|
||||
<p className="text-xs md:text-sm text-muted-foreground truncate">
|
||||
{order.items.map((i) => i.name).join(', ')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-right shrink-0 ml-2">
|
||||
<p className="font-bold text-sm md:text-base text-foreground">
|
||||
{(
|
||||
order.total + order.deliveryFee
|
||||
).toLocaleString()}
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{order.date}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-2 mt-3 md:mt-4">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="flex-1 gap-1 md:gap-2 bg-transparent h-8 md:h-9 text-xs md:text-sm"
|
||||
>
|
||||
<RefreshCw className="w-3 h-3 md:w-4 md:h-4" />
|
||||
Qayta
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
return <HistoryTabs />;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -279,7 +49,7 @@ const Profile = () => {
|
||||
}`}
|
||||
>
|
||||
<Icon className="w-4 h-4" />
|
||||
{item.label}
|
||||
{t(item.label)}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
@@ -289,20 +59,18 @@ const Profile = () => {
|
||||
<div>
|
||||
<div className="flex gap-4 md:gap-6">
|
||||
{/* Desktop Sidebar - hidden on mobile */}
|
||||
<div className="hidden md:block w-80 shrink-0">
|
||||
<div className="hidden lg:block w-80 shrink-0">
|
||||
<div className="flex items-center gap-4 mb-8">
|
||||
<Avatar className="w-14 h-14 ring-2 ring-emerald-500 ring-offset-2 flex items-center justify-center">
|
||||
<AvatarImage
|
||||
src={user.avatar || '/placeholder.svg'}
|
||||
alt={user.phone}
|
||||
className="h-12 w-12"
|
||||
/>
|
||||
<AvatarFallback className="bg-emerald-500 text-white font-semibold">
|
||||
U
|
||||
<AvatarImage />
|
||||
<AvatarFallback className="text-muted-foreground font-semibold">
|
||||
{user?.slice(0, 1).toUpperCase()}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
<div>
|
||||
<p className="text-sm text-muted-foreground">{user.phone}</p>
|
||||
<p className="text-lg text-muted-foreground font-medium">
|
||||
{user && user.charAt(0).toUpperCase() + user.slice(1)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -319,7 +87,7 @@ const Profile = () => {
|
||||
}`}
|
||||
>
|
||||
<item.icon className="w-5 h-5" />
|
||||
{item.label}
|
||||
{t(item.label)}
|
||||
</button>
|
||||
))}
|
||||
</nav>
|
||||
@@ -328,13 +96,16 @@ const Profile = () => {
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={() => {
|
||||
localStorage.removeItem('user');
|
||||
queryClient.refetchQueries({ queryKey: ['product_list'] });
|
||||
queryClient.refetchQueries({ queryKey: ['favourite_product'] });
|
||||
queryClient.refetchQueries({ queryKey: ['search'] });
|
||||
removeToken();
|
||||
router.push('/');
|
||||
}}
|
||||
className="w-full justify-start gap-3 text-red-500 hover:text-red-600 hover:bg-red-50 mt-4"
|
||||
>
|
||||
<LogOut className="w-5 h-5" />
|
||||
Chiqish
|
||||
{t('Chiqish')}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -343,17 +114,14 @@ const Profile = () => {
|
||||
<div className="lg:hidden flex items-center justify-between mb-4 md:mb-6">
|
||||
<div className="flex items-center gap-2 md:gap-3">
|
||||
<Avatar className="w-10 h-10 md:w-12 md:h-12 ring-2 ring-emerald-500 ring-offset-2">
|
||||
<AvatarImage
|
||||
src={user.avatar || '/placeholder.svg'}
|
||||
alt={user.phone}
|
||||
/>
|
||||
<AvatarFallback className="bg-emerald-500 text-white text-sm md:text-base">
|
||||
U
|
||||
<AvatarImage />
|
||||
<AvatarFallback className="text-muted-foreground font-semibold">
|
||||
{user?.slice(0, 1).toUpperCase()}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
<div>
|
||||
<p className="text-xs md:text-sm text-muted-foreground">
|
||||
{user.phone}
|
||||
<p className="text-md md:text-xl text-muted-foreground">
|
||||
{user && user.charAt(0).toUpperCase() + user.slice(1)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -361,7 +129,12 @@ const Profile = () => {
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => {
|
||||
localStorage.removeItem('user');
|
||||
queryClient.refetchQueries({ queryKey: ['product_list'] });
|
||||
queryClient.refetchQueries({
|
||||
queryKey: ['favourite_product'],
|
||||
});
|
||||
queryClient.refetchQueries({ queryKey: ['search'] });
|
||||
removeToken();
|
||||
router.push('/');
|
||||
}}
|
||||
className="w-9 h-9 md:w-10 md:h-10"
|
||||
|
||||
@@ -41,7 +41,6 @@ export default function CustomerSupport() {
|
||||
}
|
||||
|
||||
const category = categories.find((c) => c.id === selectedCategory);
|
||||
|
||||
if (!category) {
|
||||
alert('Kategoriya topilmadi');
|
||||
return;
|
||||
@@ -49,19 +48,27 @@ export default function CustomerSupport() {
|
||||
|
||||
const telegramText = `🔔 Yangi murojaat\n\n📋 Kategoriya: ${category.title}\n\n💬 Xabar:\n${message}`;
|
||||
|
||||
const telegramUsername = 'web_app_0515_bot';
|
||||
const telegramUrl = `https://t.me/${telegramUsername}?text=${encodeURIComponent(
|
||||
// Foydalanuvchi ID sini oling (masalan, auth orqali)
|
||||
const userId = '6487794662'; // <-- bu yerda dinamik ID bo'lishi kerak
|
||||
|
||||
// Telegram link bot orqali yuborish
|
||||
const botToken = 'web_app_0515_bot';
|
||||
const telegramUrl = `https://api.telegram.org/bot${botToken}/sendMessage?chat_id=${userId}&text=${encodeURIComponent(
|
||||
telegramText,
|
||||
)}`;
|
||||
|
||||
window.open(telegramUrl, '_blank');
|
||||
|
||||
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 (
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user