banner
This commit is contained in:
@@ -1,6 +1,8 @@
|
||||
import axios from "@/config/axios";
|
||||
import {
|
||||
ApiResponse,
|
||||
AuthenticationType,
|
||||
RefreshTokenResponse,
|
||||
RefreshTokenType,
|
||||
VerifyOtpType,
|
||||
} from "../types/Types";
|
||||
@@ -15,7 +17,9 @@ export const verifyOtp = async (params: VerifyOtpType) => {
|
||||
return data;
|
||||
};
|
||||
|
||||
export const refreshToken = async (params: RefreshTokenType) => {
|
||||
export const refreshToken = async (
|
||||
params: RefreshTokenType
|
||||
): Promise<ApiResponse<RefreshTokenResponse>> => {
|
||||
const { data } = await axios.post(`/user/token`, params);
|
||||
return data;
|
||||
};
|
||||
|
||||
@@ -17,3 +17,22 @@ export type VerifyOtpType = {
|
||||
export type RefreshTokenType = {
|
||||
token: string;
|
||||
};
|
||||
|
||||
export type TokenInfo = {
|
||||
token: string;
|
||||
expires: string;
|
||||
};
|
||||
|
||||
export type RefreshTokenResponse = {
|
||||
tokenType: string;
|
||||
accessToken: TokenInfo;
|
||||
refreshToken: TokenInfo;
|
||||
};
|
||||
|
||||
export type ApiResponse<T> = {
|
||||
status: number;
|
||||
success: boolean;
|
||||
results: {
|
||||
data: T;
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
import { useSharedStore } from "@/share/store/sharedStore";
|
||||
import * as api from "../service/Service";
|
||||
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
|
||||
export const useGetCart = () => {
|
||||
const { isLogin } = useSharedStore();
|
||||
|
||||
return useQuery({
|
||||
queryKey: ["cart"],
|
||||
queryFn: api.getCart,
|
||||
enabled: isLogin,
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import * as api from "../service/Service";
|
||||
|
||||
export const useGetCompare = (productIds: string[]) => {
|
||||
return useQuery({
|
||||
queryKey: ["compare", productIds],
|
||||
queryFn: () => api.getCompare(productIds),
|
||||
enabled: !!productIds,
|
||||
});
|
||||
};
|
||||
|
||||
export const useSearchCompareProduct = (productId?: string) => {
|
||||
return useQuery({
|
||||
queryKey: ["compare", productId],
|
||||
queryFn: () => api.searchComparProduct(productId),
|
||||
enabled: !!productId,
|
||||
});
|
||||
};
|
||||
+87
-39
@@ -1,62 +1,109 @@
|
||||
'use client'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Separator } from '@/components/ui/separator'
|
||||
import Layout from '@/hoc/Layout'
|
||||
import { NextPage } from 'next'
|
||||
import Image from 'next/image'
|
||||
import { useSearchParams } from 'next/navigation'
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useGetCompare } from './hooks/useCompareData'
|
||||
import { useLocalCart } from '@/app/product/hooks/useLocalCart'
|
||||
import { toast } from '@/components/Toast'
|
||||
import { CompareProduct } from './types/Types'
|
||||
import { useSharedStore } from '@/share/store/sharedStore'
|
||||
|
||||
const ComparePage: NextPage = () => {
|
||||
|
||||
const [productIds, setProductIds] = useState<string[]>([]);
|
||||
const search = useSearchParams()
|
||||
const { data, isLoading } = useGetCompare(productIds)
|
||||
const { addToLocalCart } = useLocalCart()
|
||||
const { isLogin } = useSharedStore()
|
||||
|
||||
const products = data?.results?.products || []
|
||||
|
||||
const handleAddToCart = (product: CompareProduct) => {
|
||||
if (!isLogin) {
|
||||
toast('لطفا ابتدا وارد حساب کاربری خود شوید', 'error')
|
||||
return
|
||||
}
|
||||
|
||||
if (!product._id || !product.default_variant?._id) return
|
||||
|
||||
addToLocalCart(product._id, product.default_variant._id, 1)
|
||||
toast('محصول به سبد خرید اضافه شد', 'success')
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const productId = search.get('productId')
|
||||
if (productId) {
|
||||
setProductIds([productId]);
|
||||
}
|
||||
}, [search]);
|
||||
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<Layout>
|
||||
<div className='mt-24 px-20'>
|
||||
<div className='flex justify-center items-center min-h-[400px]'>
|
||||
<div className='text-center'>
|
||||
<div className='animate-spin rounded-full h-12 w-12 border-b-2 border-blue-500 mx-auto'></div>
|
||||
<p className='mt-4 text-gray-600'>در حال بارگذاری...</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Layout>
|
||||
)
|
||||
}
|
||||
|
||||
if (products.length === 0) {
|
||||
return (
|
||||
<Layout>
|
||||
<div className='mt-24 px-20'>
|
||||
<div className='flex justify-center items-center min-h-[400px]'>
|
||||
<div className='text-center'>
|
||||
<p className='text-gray-600'>هیچ محصولی برای مقایسه انتخاب نشده است</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Layout>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Layout>
|
||||
<div className='mt-24 px-20'>
|
||||
<div className='flex border-b border-border'>
|
||||
<div className='w-[300px] flex flex-col justify-center items-center border-l border-border pb-5'>
|
||||
{products.map((product, index) => (
|
||||
<div key={product._id} className={`w-[300px] flex flex-col justify-center items-center ${index < products.length - 1 ? 'border-l border-border' : ''} pb-5`}>
|
||||
<Image
|
||||
src='https://shinan.storage.iran.liara.space/media-file/30661b3bde87834d3feae30953d5fc63232d78f181e7fed0.png'
|
||||
alt='compare'
|
||||
src={product.imagesUrl.cover}
|
||||
alt={product.title_fa}
|
||||
width={150}
|
||||
height={150}
|
||||
className='max-w-[100px] max-h-[100px] object-contain'
|
||||
/>
|
||||
|
||||
<div className='mt-6 font-light text-sm'>
|
||||
رومیزی سندس کد T-2112 طرح فرش
|
||||
<div className='mt-6 font-light text-sm text-center'>
|
||||
{product.title_fa}
|
||||
</div>
|
||||
|
||||
<div className='mt-8 text-sm font-light'>
|
||||
260,000 <span className='text-[10px]'>تومان</span>
|
||||
{product.default_variant.price.selling_price.toLocaleString()} <span className='text-[10px]'>تومان</span>
|
||||
</div>
|
||||
|
||||
<div className='mt-5'>
|
||||
<Button
|
||||
onClick={() => handleAddToCart(product)}
|
||||
disabled={!product.default_variant.stock}
|
||||
className={!product.default_variant.stock ? 'opacity-50 cursor-not-allowed' : ''}
|
||||
>
|
||||
افزودن به سبد
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className='w-[300px] flex flex-col justify-center items-center border-l border-border pb-5'>
|
||||
<Image
|
||||
src='https://shinan.storage.iran.liara.space/media-file/30661b3bde87834d3feae30953d5fc63232d78f181e7fed0.png'
|
||||
alt='compare'
|
||||
width={150}
|
||||
height={150}
|
||||
className='max-w-[100px] max-h-[100px] object-contain'
|
||||
/>
|
||||
|
||||
<div className='mt-6 font-light text-sm'>
|
||||
رومیزی سندس کد T-2112 طرح فرش
|
||||
</div>
|
||||
|
||||
<div className='mt-8 text-sm font-light'>
|
||||
260,000 <span className='text-[10px]'>تومان</span>
|
||||
</div>
|
||||
|
||||
<div className='mt-5'>
|
||||
<Button
|
||||
>
|
||||
افزودن به سبد
|
||||
{product.default_variant.stock > 0 ? 'افزودن به سبد' : 'ناموجود'}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className='mt-8'>
|
||||
@@ -65,19 +112,20 @@ const ComparePage: NextPage = () => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='mt-9 border-b border-border pb-7'>
|
||||
<div className='text-[#999999] text-sm'>وزن</div>
|
||||
{products[0]?.specifications?.map((spec, specIndex) => (
|
||||
<div key={specIndex} className='mt-9 border-b border-border pb-7'>
|
||||
<div className='text-[#999999] text-sm'>{spec.title}</div>
|
||||
|
||||
<div className='flex mt-6 font-light'>
|
||||
<div className='w-[300px]'>
|
||||
350 g
|
||||
</div>
|
||||
<div className='w-[300px] flex gap-6'>
|
||||
<Separator orientation='vertical' />
|
||||
350 g
|
||||
{products.map((product, productIndex) => (
|
||||
<div key={product._id} className='w-[300px] flex items-center'>
|
||||
{product.specifications?.[specIndex]?.values?.join(', ') || '-'}
|
||||
{productIndex < products.length - 1 && <Separator orientation='vertical' className='ml-6' />}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Layout>
|
||||
)
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
import axios from "@/config/axios";
|
||||
|
||||
import { CompareResponse } from "../types/Types";
|
||||
|
||||
export const getCompare = async (
|
||||
productIds: string[]
|
||||
): Promise<CompareResponse> => {
|
||||
const { data } = await axios.get(`/product/compare?productIds=${productIds}`);
|
||||
return data;
|
||||
};
|
||||
|
||||
export const searchComparProduct = async (productId?: string) => {
|
||||
const { data } = await axios.get(
|
||||
`/product/compare/search?productId=${productId}`
|
||||
);
|
||||
return data;
|
||||
};
|
||||
@@ -0,0 +1,94 @@
|
||||
export interface CompareResponse {
|
||||
status: number;
|
||||
success: boolean;
|
||||
results: {
|
||||
products: CompareProduct[];
|
||||
};
|
||||
}
|
||||
|
||||
export interface CompareProduct {
|
||||
_id: number;
|
||||
url: string;
|
||||
title_fa: string;
|
||||
title_en: string;
|
||||
seoTitle: string;
|
||||
seoDescription: string | null;
|
||||
source: string;
|
||||
description: string;
|
||||
metaDescription: string;
|
||||
tags: string[];
|
||||
advantages: string[];
|
||||
disAdvantages: string[];
|
||||
imagesUrl: ImagesUrl;
|
||||
isFake: string;
|
||||
specifications: Specification[];
|
||||
default_variant: Variant;
|
||||
variants: Variant[];
|
||||
}
|
||||
|
||||
export interface ImagesUrl {
|
||||
cover: string;
|
||||
list: string[];
|
||||
}
|
||||
|
||||
export interface Specification {
|
||||
title: string;
|
||||
values: string[];
|
||||
}
|
||||
|
||||
export interface Variant {
|
||||
_id: string;
|
||||
market_status: string;
|
||||
price: Price;
|
||||
stock: number;
|
||||
postingTime: number;
|
||||
isFreeShip: boolean;
|
||||
isWholeSale: boolean;
|
||||
shop: Shop;
|
||||
shipmentMethod: ShipmentMethod[];
|
||||
warranty: Warranty;
|
||||
meterage: Meterage;
|
||||
}
|
||||
|
||||
export interface Price {
|
||||
order_limit: number;
|
||||
retailPrice: number;
|
||||
selling_price: number;
|
||||
is_specialSale: boolean;
|
||||
discount_percent: number;
|
||||
specialSale_order_limit: number | null;
|
||||
specialSale_quantity: number | null;
|
||||
specialSale_endDate: string | null;
|
||||
}
|
||||
|
||||
export interface Shop {
|
||||
_id: string;
|
||||
shopName: string;
|
||||
shopCode: number;
|
||||
owner: string;
|
||||
shopDescription: string;
|
||||
telephoneNumber: string;
|
||||
isChatActive: boolean;
|
||||
shopPostalCode: string;
|
||||
logo: string;
|
||||
}
|
||||
|
||||
export interface ShipmentMethod {
|
||||
_id: number;
|
||||
name: string;
|
||||
description: string;
|
||||
deliveryTime: number;
|
||||
deliveryType: string;
|
||||
}
|
||||
|
||||
export interface Warranty {
|
||||
_id: number;
|
||||
duration: string;
|
||||
logoUrl: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface Meterage {
|
||||
_id: number;
|
||||
value: string;
|
||||
}
|
||||
@@ -1,15 +1,24 @@
|
||||
'use client'
|
||||
import { FC } from 'react'
|
||||
import { useGetLanding } from '../hooks/useHomeData'
|
||||
import Image from 'next/image'
|
||||
|
||||
const Banners3: FC = () => {
|
||||
|
||||
const { data } = useGetLanding()
|
||||
|
||||
return (
|
||||
<div className='flex gap-4 h-[500px]'>
|
||||
<div className='flex flex-col gap-4 h-full flex-1'>
|
||||
<div className='flex-1 bg-red-50 h-full'>
|
||||
<div className='flex gap-4'>
|
||||
<div className='flex flex-col gap-2 h-full flex-1'>
|
||||
<div className='flex-1 h-full'>
|
||||
<Image src={data?.results?.banners?.[1]?.imageUrl as string} height={500} width={1000} className='flex-1 rounded-lg' alt={data?.results?.banners?.[0]?.altText as string} />
|
||||
</div>
|
||||
<div className='flex-1 bg-red-50 h-full'>
|
||||
<div className='flex-1 h-full'>
|
||||
<Image src={data?.results?.banners?.[2]?.imageUrl as string} height={500} width={1000} className='flex-1 rounded-lg' alt={data?.results?.banners?.[2]?.altText as string} />
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex-1 bg-blue-50 h-full'>
|
||||
<div className='flex-1 h-full'>
|
||||
<Image src={data?.results?.banners?.[0]?.imageUrl as string} height={1000} width={1000} className='flex-1 rounded-lg h-full object-cover' alt={data?.results?.banners?.[0]?.altText as string} />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -1,18 +1,28 @@
|
||||
'use client'
|
||||
import { FC } from 'react'
|
||||
import { useGetLanding } from '../hooks/useHomeData'
|
||||
import Image from 'next/image'
|
||||
|
||||
const Banners4: FC = () => {
|
||||
const { data } = useGetLanding()
|
||||
|
||||
return (
|
||||
<div className='flex gap-6 h-[340px]'>
|
||||
<div className='flex-1 bg-blue-100 h-full rounded-[10px]'>
|
||||
<Image src={data?.results?.banners?.[3]?.imageUrl as string} height={340} width={1000} className='flex-1 rounded-lg' alt={data?.results?.banners?.[0]?.altText as string} />
|
||||
</div>
|
||||
|
||||
<div className='w-[158px] flex flex-col gap-6'>
|
||||
<div className='w-[158px] h-[158px] rounded-[10px] bg-slate-100' />
|
||||
<div className='w-[158px] h-[158px] rounded-[10px] bg-slate-200' />
|
||||
<div className='w-[158px] h-[158px] rounded-[10px] bg-slate-100'>
|
||||
<Image src={data?.results?.banners?.[4]?.imageUrl as string} height={158} width={1000} className='flex-1 rounded-lg' alt={data?.results?.banners?.[0]?.altText as string} />
|
||||
</div>
|
||||
<div className='w-[158px] h-[158px] rounded-[10px] bg-slate-200'>
|
||||
<Image src={data?.results?.banners?.[5]?.imageUrl as string} height={158} width={1000} className='flex-1 rounded-lg' alt={data?.results?.banners?.[0]?.altText as string} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='w-[400px] bg-amber-50'>
|
||||
|
||||
<Image src={data?.results?.banners?.[6]?.imageUrl as string} height={400} width={1000} className='flex-1 rounded-lg' alt={data?.results?.banners?.[0]?.altText as string} />
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
'use client'
|
||||
import { Chart2, Heart, Hierarchy2, Notification, RowHorizontal } from 'iconsax-react'
|
||||
import { Heart, Hierarchy2, RowHorizontal } from 'iconsax-react'
|
||||
import { FC, useEffect, useState } from 'react'
|
||||
import { Product } from '@/types/product.types'
|
||||
import { useAddWishlist, useRemoveWishlist } from '@/app/products/hooks/useProductsData'
|
||||
@@ -8,6 +8,7 @@ import { toast } from '@/components/Toast'
|
||||
import { useCheckWishProduct } from '../hooks/useProductData'
|
||||
import { useProductStore } from '../store/Store'
|
||||
import PriceHistory from './PriceHistory'
|
||||
import Link from 'next/link'
|
||||
|
||||
interface ActionProductProps {
|
||||
product: Product
|
||||
@@ -18,7 +19,6 @@ const ActionProduct: FC<ActionProductProps> = ({ product }) => {
|
||||
const { variantId } = useProductStore()
|
||||
const { isLogin } = useSharedStore()
|
||||
const [isWishlist, setIsWishlist] = useState(false)
|
||||
const [isCompare, setIsCompare] = useState(false)
|
||||
const [isWishlistLoading, setIsWishlistLoading] = useState(false)
|
||||
|
||||
const { mutate: addWishlist } = useAddWishlist()
|
||||
@@ -72,11 +72,6 @@ const ActionProduct: FC<ActionProductProps> = ({ product }) => {
|
||||
}
|
||||
}, [data])
|
||||
|
||||
const handleCompare = () => {
|
||||
setIsCompare(!isCompare)
|
||||
// TODO: API call to add/remove from compare
|
||||
}
|
||||
|
||||
const handleShare = () => {
|
||||
if (navigator.share) {
|
||||
navigator.share({
|
||||
@@ -135,12 +130,14 @@ const ActionProduct: FC<ActionProductProps> = ({ product }) => {
|
||||
|
||||
<PriceHistory productId={product._id.toString()} />
|
||||
|
||||
<Link href={`/compare?productId=${product._id}`}>
|
||||
<button
|
||||
className='p-2 rounded-full bg-white/80 backdrop-blur-sm hover:bg-white transition-colors'
|
||||
title='مقایسه کالا'
|
||||
>
|
||||
<RowHorizontal size={20} color='#333333' variant='Bulk' />
|
||||
</button>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
+10
-9
@@ -8,6 +8,7 @@ import {
|
||||
removeRefreshToken,
|
||||
} from "./func";
|
||||
import { refreshToken } from "../app/auth/service/Service";
|
||||
import { ApiResponse, RefreshTokenResponse } from "../app/auth/types/Types";
|
||||
import { BASE_URL } from "./const";
|
||||
|
||||
declare global {
|
||||
@@ -47,32 +48,32 @@ axiosInstance.interceptors.response.use(
|
||||
if (!refreshTokenValue) {
|
||||
await removeRefreshToken();
|
||||
await removeToken();
|
||||
window.location.href = `/auth`;
|
||||
window.location.href = `/`;
|
||||
return;
|
||||
}
|
||||
const { data } = await refreshToken({
|
||||
const data: ApiResponse<RefreshTokenResponse> = await refreshToken({
|
||||
token: refreshTokenValue,
|
||||
});
|
||||
|
||||
if (data?.accessToken?.token) {
|
||||
await setToken(data?.accessToken?.token);
|
||||
if (data.refreshToken?.token) {
|
||||
await setRefreshToken(data.refreshToken?.token);
|
||||
if (data?.results?.data?.accessToken?.token) {
|
||||
await setToken(data?.results?.data?.accessToken?.token);
|
||||
if (data?.results?.data?.refreshToken?.token) {
|
||||
await setRefreshToken(data?.results?.data?.refreshToken?.token);
|
||||
}
|
||||
|
||||
originalRequest.headers.Authorization = `Bearer ${data.accessToken.token}`;
|
||||
originalRequest.headers.Authorization = `Bearer ${data.results.data.accessToken.token}`;
|
||||
return axiosInstance(originalRequest);
|
||||
} else {
|
||||
await removeRefreshToken();
|
||||
await removeToken();
|
||||
window.location.href = `/auth`;
|
||||
window.location.href = `/`;
|
||||
return;
|
||||
}
|
||||
} catch (refreshError) {
|
||||
console.error("Token refresh failed:", refreshError);
|
||||
await removeToken();
|
||||
await removeRefreshToken();
|
||||
window.location.href = `/auth`;
|
||||
window.location.href = `/`;
|
||||
return Promise.reject(refreshError);
|
||||
} finally {
|
||||
window.isRefreshingToken = false;
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
export const TOKEN_NAME = "sh_token";
|
||||
export const REFRESH_TOKEN_NAME = "sh_refresh_token";
|
||||
export const BASE_URL = "https://shop-api.dev.danakcorp.com";
|
||||
// export const BASE_URL = "https://api-sondos.run.danakcorp.com";
|
||||
// export const BASE_URL = "https://api.shinan.ir";
|
||||
|
||||
export const PRIMARY_COLOR = "#015699";
|
||||
|
||||
+1
-1
@@ -38,7 +38,7 @@ export const getToken = async () => {
|
||||
return localStorage.getItem(TOKEN_NAME);
|
||||
};
|
||||
|
||||
export const getRefreshToken = () => {
|
||||
export const getRefreshToken = async () => {
|
||||
if (typeof window === "undefined") {
|
||||
return null;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user