diff --git a/package-lock.json b/package-lock.json index 39ebca5..ecd4ae3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,6 +14,7 @@ "@radix-ui/react-separator": "^1.1.7", "@radix-ui/react-slot": "^1.2.3", "@tanstack/react-query": "^5.84.1", + "@tanstack/react-query-devtools": "^5.85.5", "axios": "^1.11.0", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", @@ -1711,9 +1712,19 @@ } }, "node_modules/@tanstack/query-core": { - "version": "5.83.1", - "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.83.1.tgz", - "integrity": "sha512-OG69LQgT7jSp+5pPuCfzltq/+7l2xoweggjme9vlbCPa/d7D7zaqv5vN/S82SzSYZ4EDLTxNO1PWrv49RAS64Q==", + "version": "5.85.5", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.85.5.tgz", + "integrity": "sha512-KO0WTob4JEApv69iYp1eGvfMSUkgw//IpMnq+//cORBzXf0smyRwPLrUvEe5qtAEGjwZTXrjxg+oJNP/C00t6w==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/query-devtools": { + "version": "5.84.0", + "resolved": "https://registry.npmjs.org/@tanstack/query-devtools/-/query-devtools-5.84.0.tgz", + "integrity": "sha512-fbF3n+z1rqhvd9EoGp5knHkv3p5B2Zml1yNRjh7sNXklngYI5RVIWUrUjZ1RIcEoscarUb0+bOvIs5x9dwzOXQ==", "license": "MIT", "funding": { "type": "github", @@ -1721,12 +1732,12 @@ } }, "node_modules/@tanstack/react-query": { - "version": "5.84.1", - "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.84.1.tgz", - "integrity": "sha512-zo7EUygcWJMQfFNWDSG7CBhy8irje/XY0RDVKKV4IQJAysb+ZJkkJPcnQi+KboyGUgT+SQebRFoTqLuTtfoDLw==", + "version": "5.85.5", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.85.5.tgz", + "integrity": "sha512-/X4EFNcnPiSs8wM2v+b6DqS5mmGeuJQvxBglmDxl6ZQb5V26ouD2SJYAcC3VjbNwqhY2zjxVD15rDA5nGbMn3A==", "license": "MIT", "dependencies": { - "@tanstack/query-core": "5.83.1" + "@tanstack/query-core": "5.85.5" }, "funding": { "type": "github", @@ -1736,6 +1747,23 @@ "react": "^18 || ^19" } }, + "node_modules/@tanstack/react-query-devtools": { + "version": "5.85.5", + "resolved": "https://registry.npmjs.org/@tanstack/react-query-devtools/-/react-query-devtools-5.85.5.tgz", + "integrity": "sha512-6Ol6Q+LxrCZlQR4NoI5181r+ptTwnlPG2t7H9Sp3klxTBhYGunONqcgBn2YKRPsaKiYM8pItpKMdMXMEINntMQ==", + "license": "MIT", + "dependencies": { + "@tanstack/query-devtools": "5.84.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "@tanstack/react-query": "^5.85.5", + "react": "^18 || ^19" + } + }, "node_modules/@tybys/wasm-util": { "version": "0.10.0", "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.0.tgz", diff --git a/package.json b/package.json index b8ae233..179f473 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,7 @@ "@radix-ui/react-separator": "^1.1.7", "@radix-ui/react-slot": "^1.2.3", "@tanstack/react-query": "^5.84.1", + "@tanstack/react-query-devtools": "^5.85.5", "axios": "^1.11.0", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", diff --git a/src/app/blogs/[id]/page.tsx b/src/app/blogs/[id]/page.tsx index e158b37..a632af5 100644 --- a/src/app/blogs/[id]/page.tsx +++ b/src/app/blogs/[id]/page.tsx @@ -2,7 +2,7 @@ import Image from 'next/image' import { Calendar, Eye, User } from 'iconsax-react' import LastPost from '../components/LastPost' import { NextPage } from 'next' -import withLayout from '@/hoc/withLayout' +import Layout from '@/hoc/Layout' const BlogDetail: NextPage = () => { return ( @@ -127,4 +127,10 @@ const BlogDetail: NextPage = () => { ) } -export default withLayout(BlogDetail) \ No newline at end of file +export default function BlogDetailWithLayout() { + return ( + + + + ) +} \ No newline at end of file diff --git a/src/app/blogs/page.tsx b/src/app/blogs/page.tsx index 1423bd5..780e82d 100644 --- a/src/app/blogs/page.tsx +++ b/src/app/blogs/page.tsx @@ -1,6 +1,6 @@ import { NextPage } from 'next' +import Layout from '@/hoc/Layout' import Categories from './components/Categories' -import withLayout from '@/hoc/withLayout' import BlogItem from './components/BlogItem' import LastPost from './components/LastPost' @@ -21,4 +21,10 @@ const BlogsPage: NextPage = () => { ) } -export default withLayout(BlogsPage) \ No newline at end of file +export default function BlogsPageWithLayout() { + return ( + + + + ) +} \ No newline at end of file diff --git a/src/app/cart/page.tsx b/src/app/cart/page.tsx index 6bc019f..07d7c40 100644 --- a/src/app/cart/page.tsx +++ b/src/app/cart/page.tsx @@ -1,7 +1,7 @@ import { Button } from "@/components/ui/button" import CartItem from "@/app/cart/components/CartItem" import CartSummary from "@/components/CartSummary" -import withLayout from "@/hoc/withLayout" +import Layout from "@/hoc/Layout" import { Trash } from "iconsax-react" import { NextPage } from "next" @@ -47,4 +47,10 @@ const Cart: NextPage = () => { ) } -export default withLayout(Cart) \ No newline at end of file +export default function CartWithLayout() { + return ( + + + + ) +} \ No newline at end of file diff --git a/src/app/cart/payment/page.tsx b/src/app/cart/payment/page.tsx index 01012ef..c795f1a 100644 --- a/src/app/cart/payment/page.tsx +++ b/src/app/cart/payment/page.tsx @@ -1,5 +1,5 @@ import CartSummary from '@/components/CartSummary' -import withLayout from '@/hoc/withLayout' +import Layout from '@/hoc/Layout' import { NextPage } from 'next' import TitleBack from '../components/TitleBack' import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group" @@ -45,4 +45,10 @@ const CartPayment: NextPage = () => { ) } -export default withLayout(CartPayment) \ No newline at end of file +export default function CartPaymentWithLayout() { + return ( + + + + ) +} \ No newline at end of file diff --git a/src/app/cart/shipping/page.tsx b/src/app/cart/shipping/page.tsx index 0aa20ae..aa69588 100644 --- a/src/app/cart/shipping/page.tsx +++ b/src/app/cart/shipping/page.tsx @@ -1,5 +1,5 @@ import CartSummary from '@/components/CartSummary' -import withLayout from '@/hoc/withLayout' +import Layout from '@/hoc/Layout' import { NextPage } from 'next' import CartItem from '../components/CartItem' import TitleBack from '../components/TitleBack' @@ -46,4 +46,10 @@ const CartShipping: NextPage = () => { ) } -export default withLayout(CartShipping) \ No newline at end of file +export default function CartShippingWithLayout() { + return ( + + + + ) +} \ No newline at end of file diff --git a/src/app/home/components/Carousel.tsx b/src/app/home/components/Carousel.tsx index 9443cb1..788aaf0 100644 --- a/src/app/home/components/Carousel.tsx +++ b/src/app/home/components/Carousel.tsx @@ -2,21 +2,35 @@ import { FC } from 'react' import { Swiper, SwiperSlide } from 'swiper/react'; import { Pagination } from 'swiper/modules'; +import Image from 'next/image'; +import { useGetLanding } from '../hooks/useHomeData'; const Carousel: FC = () => { + const { data } = useGetLanding() + + if (!data?.results?.sliders?.desktop || data.results.sliders.desktop.length === 0) { + return ( +
+

اسلایدری موجود نیست

+
+ ); + } + return (
- -
- 1 -
-
- -
- 1 -
-
+ {data.results.sliders.desktop.map((slider) => ( + +
+ {slider.altText} +
+
+ ))}
) diff --git a/src/app/home/components/LatestProducts.tsx b/src/app/home/components/LatestProducts.tsx index ca444ad..e3f4c37 100644 --- a/src/app/home/components/LatestProducts.tsx +++ b/src/app/home/components/LatestProducts.tsx @@ -1,3 +1,4 @@ +'use client' import { FC } from 'react' import { Carousel, @@ -7,9 +8,17 @@ import { CarouselPrevious, } from "@/components/ui/carousel" import ProductCard from '@/components/ProductCard' +import { useGetLanding } from '../hooks/useHomeData' const NewestProducts: FC = () => { + + const { data: landing } = useGetLanding() + + console.log('landing', landing?.results?. + latestProducts); + + return (

جدیدترین محصولات

@@ -25,12 +34,14 @@ const NewestProducts: FC = () => { }} className="w-full" > - - {Array.from({ length: 5 }).map((_, index) => ( - - - - ))} + + { + landing?.results?.latestProducts?.map((product) => ( + + + + )) + } diff --git a/src/app/home/hooks/useHomeData.ts b/src/app/home/hooks/useHomeData.ts new file mode 100644 index 0000000..5214fef --- /dev/null +++ b/src/app/home/hooks/useHomeData.ts @@ -0,0 +1,14 @@ +import { useQuery } from "@tanstack/react-query"; +import * as api from "../service/Service"; + +export const useGetLanding = () => { + return useQuery({ + queryKey: ["landing"], + queryFn: api.getLanding, + }); +}; + +export const getLandingQueryOptions = { + queryKey: ["landing"], + queryFn: api.getLanding, +}; diff --git a/src/app/home/service/Service.ts b/src/app/home/service/Service.ts new file mode 100644 index 0000000..f748b52 --- /dev/null +++ b/src/app/home/service/Service.ts @@ -0,0 +1,7 @@ +import axios from "@/config/axios"; +import { ILandingResponse } from "@/types/landing.types"; + +export const getLanding = async (): Promise => { + const { data } = await axios.get("/landing"); + return data; +}; diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 1361b44..d676530 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -4,6 +4,8 @@ import "../../public/css/fontiran.css"; import 'swiper/css'; import QueryProvider from "@/config/QueryProvider"; import ToastContainer from "@/components/Toast"; +import { ReactQueryDevtools } from '@tanstack/react-query-devtools' + export const metadata: Metadata = { title: "Create Next App", @@ -21,6 +23,7 @@ export default function RootLayout({ {children} + diff --git a/src/app/page.tsx b/src/app/page.tsx index 006b73b..dccffe6 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,5 +1,5 @@ import { NextPage } from 'next' -import withLayout from '@/hoc/withLayout' +import Layout from '@/hoc/Layout' import Carousel from './home/components/Carousel' import HotOffer from './home/components/HotOffer' import Categories from './home/components/Categories' @@ -8,8 +8,13 @@ import Blogs from './home/components/Blogs' import NewestProducts from './home/components/LatestProducts' import Banners4 from './home/components/Banners4' import PopularProducts from './home/components/PopularProducts' +import { HydrationBoundary, QueryClient, dehydrate } from '@tanstack/react-query' +import { Suspense } from 'react' +import PageLoading from '@/components/PageLoading' +import { getLandingQueryOptions } from './home/hooks/useHomeData' + +const HomePage: NextPage = () => { -const Home: NextPage = () => { return (
@@ -19,8 +24,7 @@ const Home: NextPage = () => {
- +
@@ -41,4 +45,20 @@ const Home: NextPage = () => { ) } -export default withLayout(Home) \ No newline at end of file +export default async function HomePageWithLayout() { + const queryClient = new QueryClient() + + await queryClient.prefetchQuery(getLandingQueryOptions) + + const dehydratedState = dehydrate(queryClient) + + return ( + + }> + + + + + + ) +} \ No newline at end of file diff --git a/src/app/product/[id]/page.tsx b/src/app/product/[id]/page.tsx index 6108e9d..08c0c55 100644 --- a/src/app/product/[id]/page.tsx +++ b/src/app/product/[id]/page.tsx @@ -1,5 +1,5 @@ import { NextPage } from 'next' -import withLayout from '@/hoc/withLayout' +import Layout from '@/hoc/Layout' import { Breadcrumb, BreadcrumbItem, @@ -60,4 +60,10 @@ const SingleProduct: NextPage = () => { ) } -export default withLayout(SingleProduct) \ No newline at end of file +export default function SingleProductWithLayout() { + return ( + + + + ) +} \ No newline at end of file diff --git a/src/app/products/[id]/page.tsx b/src/app/products/[id]/page.tsx index 3caade2..aa85e0e 100644 --- a/src/app/products/[id]/page.tsx +++ b/src/app/products/[id]/page.tsx @@ -1,4 +1,4 @@ -import withLayout from '@/hoc/withLayout' +import Layout from '@/hoc/Layout' import { NextPage } from 'next' import { Breadcrumb, @@ -51,4 +51,10 @@ const Products: NextPage = () => { ) } -export default withLayout(Products) \ No newline at end of file +export default function ProductsWithLayout() { + return ( + + + + ) +} \ No newline at end of file diff --git a/src/app/profile/comments/page.tsx b/src/app/profile/comments/page.tsx index 421033d..4b098f8 100644 --- a/src/app/profile/comments/page.tsx +++ b/src/app/profile/comments/page.tsx @@ -1,4 +1,4 @@ -import withLayout from '@/hoc/withLayout' +import Layout from '@/hoc/Layout' import { NextPage } from 'next' import Menu from '../components/Menu' import Image from 'next/image' @@ -86,4 +86,10 @@ const Comments: NextPage = () => { ) } -export default withLayout(Comments) \ No newline at end of file +export default function CommentsWithLayout() { + return ( + + + + ) +} \ No newline at end of file diff --git a/src/app/profile/favorite/page.tsx b/src/app/profile/favorite/page.tsx index fbac9a7..c076d72 100644 --- a/src/app/profile/favorite/page.tsx +++ b/src/app/profile/favorite/page.tsx @@ -1,4 +1,4 @@ -import withLayout from "@/hoc/withLayout" +import Layout from "@/hoc/Layout" import { NextPage } from "next" import Menu from "../components/Menu" import GridWrapper from "@/components/GridWrapper" @@ -20,4 +20,10 @@ const FavoritePage: NextPage = () => { ) } -export default withLayout(FavoritePage) \ No newline at end of file +export default function FavoritePageWithLayout() { + return ( + + + + ) +} \ No newline at end of file diff --git a/src/app/profile/orders/[id]/page.tsx b/src/app/profile/orders/[id]/page.tsx index b8d78ee..65f849d 100644 --- a/src/app/profile/orders/[id]/page.tsx +++ b/src/app/profile/orders/[id]/page.tsx @@ -11,7 +11,7 @@ import { Button } from '@/components/ui/button' import Modal from '@/components/Modal' import Select from '@/components/Select' import Input from '@/components/Input' -import withLayout from '@/hoc/withLayout' +import Layout from '@/hoc/Layout' import Image from 'next/image' import Menu from '../../components/Menu' @@ -418,4 +418,10 @@ const OrderDetail: FC = () => { ) } -export default withLayout(OrderDetail) \ No newline at end of file +export default function OrderDetailWithLayout() { + return ( + + + + ) +} \ No newline at end of file diff --git a/src/app/profile/orders/page.tsx b/src/app/profile/orders/page.tsx index ff2b149..d15654e 100644 --- a/src/app/profile/orders/page.tsx +++ b/src/app/profile/orders/page.tsx @@ -1,5 +1,5 @@ 'use client' -import withLayout from '@/hoc/withLayout' +import Layout from '@/hoc/Layout' import { NextPage } from 'next' import React, { useState } from 'react' import Menu from '../components/Menu' @@ -143,4 +143,10 @@ const Orders: NextPage = () => { ) } -export default withLayout(Orders) \ No newline at end of file +export default function OrdersWithLayout() { + return ( + + + + ) +} \ No newline at end of file diff --git a/src/app/profile/page.tsx b/src/app/profile/page.tsx index 339ab77..0db0ba8 100644 --- a/src/app/profile/page.tsx +++ b/src/app/profile/page.tsx @@ -3,7 +3,7 @@ import React, { useState } from 'react' import { Button } from '@/components/ui/button' import Input from '@/components/Input' import { Edit2 } from 'lucide-react' -import withLayout from '@/hoc/withLayout' +import Layout from '@/hoc/Layout' import { Heart, ShoppingCart } from 'iconsax-react' import Menu from './components/Menu' @@ -177,4 +177,10 @@ const ProfilePage = () => { ) } -export default withLayout(ProfilePage) \ No newline at end of file +export default function ProfilePageWithLayout() { + return ( + + + + ) +} \ No newline at end of file diff --git a/src/components/ProductCard.tsx b/src/components/ProductCard.tsx index cd8dd1f..d257f50 100644 --- a/src/components/ProductCard.tsx +++ b/src/components/ProductCard.tsx @@ -1,8 +1,49 @@ +import { IProduct } from '@/types/landing.types' +import { NumberFormat } from '@/config/func' import { Heart } from 'iconsax-react' import Image from 'next/image' import { FC } from 'react' -const ProductCard: FC = () => { +type Props = { + item?: IProduct +} + + +const ProductCard: FC = ({ item }) => { + if (item) return ( +
+
+ +
+ +

+ {item.title_fa} +

+ +
+
+
+
+
+ +
+ {NumberFormat(item.default_variant.price.selling_price)} تومان +
+
+ {NumberFormat(item.default_variant.price.retailPrice)} +
+ +
+ +
+
+ ) return (
diff --git a/src/config/QueryProvider.tsx b/src/config/QueryProvider.tsx index f7d3220..2cb2501 100644 --- a/src/config/QueryProvider.tsx +++ b/src/config/QueryProvider.tsx @@ -21,7 +21,8 @@ export default function QueryProvider({ children }: { children: ReactNode }) { defaultOptions: { queries: { staleTime: 1000 * 60 * 5, // داده‌ها برای 5 دقیقه معتبر هستند - + refetchOnMount: false, + refetchOnWindowFocus: false, } } })) diff --git a/src/config/func.ts b/src/config/func.ts index e2332a4..d1875c4 100644 --- a/src/config/func.ts +++ b/src/config/func.ts @@ -32,26 +32,44 @@ export const timeAgo = (date: string | Date): string => { return "همین الان"; }; export const getToken = async () => { + if (typeof window === "undefined") { + return null; // در سرور token موجود نیست + } return localStorage.getItem(TOKEN_NAME); }; export const getRefreshToken = () => { + if (typeof window === "undefined") { + return null; + } return localStorage.getItem(REFRESH_TOKEN_NAME); }; export const setToken = (token: string) => { + if (typeof window === "undefined") { + return; + } localStorage.setItem(TOKEN_NAME, token); }; export const setRefreshToken = (refreshToken: string) => { + if (typeof window === "undefined") { + return; + } localStorage.setItem(REFRESH_TOKEN_NAME, refreshToken); }; export const removeToken = () => { + if (typeof window === "undefined") { + return; + } localStorage.removeItem(TOKEN_NAME); }; export const removeRefreshToken = () => { + if (typeof window === "undefined") { + return; + } localStorage.removeItem(REFRESH_TOKEN_NAME); }; diff --git a/src/hoc/Layout.tsx b/src/hoc/Layout.tsx new file mode 100644 index 0000000..f2620e6 --- /dev/null +++ b/src/hoc/Layout.tsx @@ -0,0 +1,21 @@ +import Footer from '@/share/Footer' +import Header from '@/share/Header' +import { FC } from 'react' + +interface LayoutProps { + children: React.ReactNode +} + +const Layout: FC = ({ children }) => { + return ( +
+
+
+ {children} +
+
+
+ ) +} + +export default Layout diff --git a/src/hoc/withLayout.tsx b/src/hoc/withLayout.tsx deleted file mode 100644 index 331882e..0000000 --- a/src/hoc/withLayout.tsx +++ /dev/null @@ -1,39 +0,0 @@ - -import Footer from '@/share/Footer' -import Header from '@/share/Header' -import { FC, ComponentType } from 'react' -// import Header from '@/share/Header' - -interface LayoutProps { - children: React.ReactNode -} - -const Layout: FC = ({ children }) => { - return ( -
-
-
- {children} -
-
-
- ) -} - -// HOC function -function withLayout

(WrappedComponent: ComponentType

) { - const WithLayoutComponent: FC

= (props) => { - return ( - - - - ) - } - - WithLayoutComponent.displayName = `withLayout(${WrappedComponent.displayName || WrappedComponent.name})` - - return WithLayoutComponent -} - -export default withLayout -export { Layout } diff --git a/src/types/landing.types.ts b/src/types/landing.types.ts new file mode 100644 index 0000000..8947a8d --- /dev/null +++ b/src/types/landing.types.ts @@ -0,0 +1,102 @@ +export interface IShop { + _id: string; + shopName: string; + shopDescription: string; + logo: string; +} + +export interface IPrice { + order_limit: number; + retailPrice: number; + is_specialSale: boolean; + discount_percent: number; + specialSale_order_limit: number | null; + specialSale_quantity: number | null; + specialSale_endDate: string | null; + selling_price: number; +} + +export interface IDefaultVariant { + _id: string; + market_status: string; + price: IPrice; + stock: number; + isFreeShip: boolean; + isWholeSale: boolean; +} + +export interface IImagesUrl { + cover: string; + list: string[]; +} + +export interface IProduct { + _id: number; + title_fa: string; + title_en: string; + seoTitle: string | null; + seoDescription: string | null; + model: string; + description: string; + tags: string[]; + shop: IShop; + status: string; + imagesUrl: IImagesUrl; + url: string; + default_variant: IDefaultVariant; +} + +export interface ICategory { + _id: string; + title_fa: string; + title_en: string; + icon: string; + imageUrl: string; + description: string; + url: string; +} + +export interface ISlider { + _id: string; + displayLocation: "desktop" | "mobile"; + imageUrl: string; + altText: string; + title: string; + linkUrl: string; + isActive: boolean; + createdAt: string; + updatedAt: string; + order: number; +} + +export interface ISliders { + desktop: ISlider[]; + mobile: ISlider[]; +} + +export interface IBanner { + _id: string; + imageUrl: string; + altText: string; + title: string; + linkUrl: string; + isActive: boolean; + order: number; + createdAt: string; + updatedAt: string; +} + +export interface ILandingResults { + popularProducts: IProduct[]; + incredibleOffers: IProduct[]; + latestProducts: IProduct[]; + topCategory: ICategory[]; + sliders: ISliders; + banners: IBanner[]; +} + +export interface ILandingResponse { + status: number; + success: boolean; + results: ILandingResults; +} diff --git a/src/types/response.types.ts b/src/types/response.types.ts index be1b704..f246926 100644 --- a/src/types/response.types.ts +++ b/src/types/response.types.ts @@ -5,3 +5,6 @@ export interface IBaseResponse { export interface IResponse extends IBaseResponse { data: T; } + +// Landing types +export * from "./landing.types";