diff --git a/package.json b/package.json index 5518c49..e8522a7 100644 --- a/package.json +++ b/package.json @@ -14,9 +14,12 @@ "axios": "^1.8.4", "critters": "^0.0.23", "formik": "^2.4.6", + "framer-motion": "^12.7.3", "iconsax-react": "^0.0.8", "js-cookie": "^3.0.5", + "moment-jalaali": "^0.10.4", "next": "15.2.3", + "rc-rate": "^2.13.1", "react": "^19.0.0", "react-dom": "^19.0.0", "react-spinners": "^0.15.0", @@ -30,6 +33,7 @@ "@eslint/eslintrc": "^3", "@tailwindcss/postcss": "^4", "@types/js-cookie": "^3.0.6", + "@types/moment-jalaali": "^0.7.9", "@types/node": "^20", "@types/react": "^19", "@types/react-dom": "^19", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ea9b452..9314d41 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -23,15 +23,24 @@ importers: formik: specifier: ^2.4.6 version: 2.4.6(react@19.1.0) + framer-motion: + specifier: ^12.7.3 + version: 12.7.3(react-dom@19.1.0(react@19.1.0))(react@19.1.0) iconsax-react: specifier: ^0.0.8 version: 0.0.8(react@19.1.0) js-cookie: specifier: ^3.0.5 version: 3.0.5 + moment-jalaali: + specifier: ^0.10.4 + version: 0.10.4 next: specifier: 15.2.3 version: 15.2.3(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + rc-rate: + specifier: ^2.13.1 + version: 2.13.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0) react: specifier: ^19.0.0 version: 19.1.0 @@ -66,6 +75,9 @@ importers: '@types/js-cookie': specifier: ^3.0.6 version: 3.0.6 + '@types/moment-jalaali': + specifier: ^0.7.9 + version: 0.7.9 '@types/node': specifier: ^20 version: 20.17.30 @@ -94,6 +106,10 @@ packages: resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==} engines: {node: '>=10'} + '@babel/runtime@7.27.0': + resolution: {integrity: sha512-VtPOkrdPHZsKc/clNqyi9WUA8TINkZ4cGk63UUE3u4pmB2k+ZMQRDuIOagv8UVd6j7k0T3+RRIb7beKTebNbcw==} + engines: {node: '>=6.9.0'} + '@emnapi/core@1.4.3': resolution: {integrity: sha512-4m62DuCE07lw01soJwPiBGC0nAww0Q+RY70VZ+n49yDIO13yyinhbWCeNnaob0lakDtWQzSdtNWzJeOJt2ma+g==} @@ -478,6 +494,9 @@ packages: '@types/json5@0.0.29': resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==} + '@types/moment-jalaali@0.7.9': + resolution: {integrity: sha512-gsDOoAzRnCfQTbfdlUrCvX6R0wIto6CvwfvV2C3j4qJLK+DEiTK8Rl/xlOCBO9C6qeUfX8oyZ2UfjnXJTOvHSA==} + '@types/node@20.17.30': resolution: {integrity: sha512-7zf4YyHA+jvBNfVrk2Gtvs6x7E8V+YDW05bNfG2XkWDJfYRXrTiP/DsB2zSYTaHX0bGIujTBQdMVAhb+j7mwpg==} @@ -740,6 +759,9 @@ packages: resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} engines: {node: '>=10'} + classnames@2.5.1: + resolution: {integrity: sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==} + client-only@0.0.1: resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==} @@ -1093,6 +1115,20 @@ packages: peerDependencies: react: '>=16.8.0' + framer-motion@12.7.3: + resolution: {integrity: sha512-dNT4l5gEnUo2ytXLUBUf6AI21dZ77TMclDKE3ElaIHZ8m90nJ/NCcExW51zdSIaS0RhAS5iXcF7bEIxZe8XG2g==} + peerDependencies: + '@emotion/is-prop-valid': '*' + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@emotion/is-prop-valid': + optional: true + react: + optional: true + react-dom: + optional: true + function-bind@1.1.2: resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} @@ -1310,6 +1346,9 @@ packages: resolution: {integrity: sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==} engines: {node: '>= 0.4'} + jalaali-js@1.2.8: + resolution: {integrity: sha512-Jl/EwY84JwjW2wsWqeU4pNd22VNQ7EkjI36bDuLw31wH98WQW4fPjD0+mG7cdCK+Y8D6s9R3zLiQ3LaKu6bD8A==} + jiti@2.4.2: resolution: {integrity: sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==} hasBin: true @@ -1467,6 +1506,21 @@ packages: minimist@1.2.8: resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + moment-jalaali@0.10.4: + resolution: {integrity: sha512-/eD0HeyvATznb5iE0G1BHjKRZAFEpJ9ZNUkcHwXhNgt1WJJVVzHD7+uDmqzZWVFLdbGme2gvIXKb3ezDYOXcZA==} + + moment-timezone@0.5.48: + resolution: {integrity: sha512-f22b8LV1gbTO2ms2j2z13MuPogNoh5UzxL3nzNAYKGraILnbGc9NEE6dyiiiLv46DGRb8A4kg8UKWLjPthxBHw==} + + moment@2.30.1: + resolution: {integrity: sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==} + + motion-dom@12.7.3: + resolution: {integrity: sha512-IjMt1YJHrvyvruFvmpmd6bGXXGCvmygrnvSb3aZ8KhOzF4H3PulU+cMBzH+U8TBJHjC/mnmJFRIA1Cu4vBfcBA==} + + motion-utils@12.7.2: + resolution: {integrity: sha512-XhZwqctxyJs89oX00zn3OGCuIIpVevbTa+u82usWBC6pSHUd2AoNWiYa7Du8tJxJy9TFbZ82pcn5t7NOm1PHAw==} + ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} @@ -1611,6 +1665,19 @@ packages: queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + rc-rate@2.13.1: + resolution: {integrity: sha512-QUhQ9ivQ8Gy7mtMZPAjLbxBt5y9GRp65VcUyGUMF3N3fhiftivPHdpuDIaWIMOTEprAjZPC08bls1dQB+I1F2Q==} + engines: {node: '>=8.x'} + peerDependencies: + react: '>=16.9.0' + react-dom: '>=16.9.0' + + rc-util@5.44.4: + resolution: {integrity: sha512-resueRJzmHG9Q6rI/DfK6Kdv9/Lfls05vzMs1Sk3M2P+3cJa+MakaZyWY8IPfehVuhPJFKrIY1IK4GqbiaiY5w==} + peerDependencies: + react: '>=16.9.0' + react-dom: '>=16.9.0' + react-dom@19.1.0: resolution: {integrity: sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==} peerDependencies: @@ -1622,6 +1689,9 @@ packages: react-is@16.13.1: resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} + react-is@18.3.1: + resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==} + react-spinners@0.15.0: resolution: {integrity: sha512-ZO3/fNB9Qc+kgpG3SfdlMnvTX6LtLmTnOogb3W6sXIaU/kZ1ydEViPfZ06kSOaEsor58C/tzXw2wROGQu3X2pA==} peerDependencies: @@ -1642,6 +1712,9 @@ packages: resolution: {integrity: sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==} engines: {node: '>= 0.4'} + regenerator-runtime@0.14.1: + resolution: {integrity: sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==} + regexp.prototype.flags@1.5.4: resolution: {integrity: sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==} engines: {node: '>= 0.4'} @@ -1938,6 +2011,10 @@ snapshots: '@alloc/quick-lru@5.2.0': {} + '@babel/runtime@7.27.0': + dependencies: + regenerator-runtime: 0.14.1 + '@emnapi/core@1.4.3': dependencies: '@emnapi/wasi-threads': 1.0.2 @@ -2250,6 +2327,10 @@ snapshots: '@types/json5@0.0.29': {} + '@types/moment-jalaali@0.7.9': + dependencies: + moment: 2.30.1 + '@types/node@20.17.30': dependencies: undici-types: 6.19.8 @@ -2544,6 +2625,8 @@ snapshots: ansi-styles: 4.3.0 supports-color: 7.2.0 + classnames@2.5.1: {} + client-only@0.0.1: {} clsx@2.1.1: {} @@ -3057,6 +3140,15 @@ snapshots: tiny-warning: 1.0.3 tslib: 2.8.1 + framer-motion@12.7.3(react-dom@19.1.0(react@19.1.0))(react@19.1.0): + dependencies: + motion-dom: 12.7.3 + motion-utils: 12.7.2 + tslib: 2.8.1 + optionalDependencies: + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + function-bind@1.1.2: {} function.prototype.name@1.1.8: @@ -3297,6 +3389,8 @@ snapshots: has-symbols: 1.1.0 set-function-name: 2.0.2 + jalaali-js@1.2.8: {} + jiti@2.4.2: {} js-cookie@3.0.5: {} @@ -3423,6 +3517,24 @@ snapshots: minimist@1.2.8: {} + moment-jalaali@0.10.4: + dependencies: + jalaali-js: 1.2.8 + moment: 2.30.1 + moment-timezone: 0.5.48 + + moment-timezone@0.5.48: + dependencies: + moment: 2.30.1 + + moment@2.30.1: {} + + motion-dom@12.7.3: + dependencies: + motion-utils: 12.7.2 + + motion-utils@12.7.2: {} + ms@2.1.3: {} nanoid@3.3.11: {} @@ -3571,6 +3683,21 @@ snapshots: queue-microtask@1.2.3: {} + rc-rate@2.13.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0): + dependencies: + '@babel/runtime': 7.27.0 + classnames: 2.5.1 + rc-util: 5.44.4(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + + rc-util@5.44.4(react-dom@19.1.0(react@19.1.0))(react@19.1.0): + dependencies: + '@babel/runtime': 7.27.0 + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + react-is: 18.3.1 + react-dom@19.1.0(react@19.1.0): dependencies: react: 19.1.0 @@ -3580,6 +3707,8 @@ snapshots: react-is@16.13.1: {} + react-is@18.3.1: {} + react-spinners@0.15.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0): dependencies: react: 19.1.0 @@ -3604,6 +3733,8 @@ snapshots: get-proto: 1.0.1 which-builtin-type: 1.2.1 + regenerator-runtime@0.14.1: {} + regexp.prototype.flags@1.5.4: dependencies: call-bind: 1.0.8 diff --git a/src/app/error.tsx b/src/app/error.tsx new file mode 100644 index 0000000..3f88c49 --- /dev/null +++ b/src/app/error.tsx @@ -0,0 +1,149 @@ +'use client' + +import Link from 'next/link' +import { motion } from 'framer-motion' +import { useEffect, useState } from 'react' + +export default function Error({ + error, + reset, +}: { + error: Error & { digest?: string } + reset: () => void +}) { + const [mounted, setMounted] = useState(false) + + useEffect(() => { + setMounted(true) + // Log the error to an error reporting service + console.error(error) + }, [error]) + + return ( +
+ {/* Animated background elements */} + {mounted && ( + <> + + {[...Array(6)].map((_, i) => ( + + ))} + + + )} + +
+ {mounted && ( + <> + +
خطا
+ + {/* + + */} + +
+ + + یک خطا رخ داده است! + + + + متأسفانه در حین پردازش درخواست شما خطایی رخ داده است. لطفا دوباره تلاش کنید. + + + + + + + + + + + محصولات + + + درباره ما + + + تماس با ما + + + بلاگ + + + + )} +
+
+ ) +} \ No newline at end of file diff --git a/src/app/globals.css b/src/app/globals.css index dd53011..ddc726b 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -99,3 +99,7 @@ tbody tr { direction: rtl; font-family: "irancell" !important; } + +.rc-rate-star { + margin-right: 3px !important; /* فاصله بین ستاره‌ها */ +} diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 9f75ce7..75aa6ea 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -2,6 +2,7 @@ import type { Metadata } from "next"; import { Geist, Geist_Mono } from "next/font/google"; import "swiper/css"; import "./globals.css"; +import 'rc-rate/assets/index.css'; import "@/assets/fonts/irancell/style.css"; import { ToastContainer } from 'react-toastify'; import Header from "@/shared/Header"; diff --git a/src/app/not-found.tsx b/src/app/not-found.tsx new file mode 100644 index 0000000..209133d --- /dev/null +++ b/src/app/not-found.tsx @@ -0,0 +1,136 @@ +'use client' + +import Link from 'next/link' +import { motion } from 'framer-motion' +import { useEffect, useState } from 'react' + +export default function NotFound() { + const [mounted, setMounted] = useState(false) + + useEffect(() => { + setMounted(true) + }, []) + + return ( +
+ {/* Animated background elements */} + {mounted && ( + <> + + {[...Array(6)].map((_, i) => ( + + ))} + + + )} + +
+ {mounted && ( + <> + +
404
+ + + +
+ + + صفحه موردنظر پیدا نشد! + + + + متأسفانه صفحه‌ای که به دنبال آن هستید وجود ندارد یا حذف شده است. + + + + + + + + + + + محصولات + + + درباره ما + + + تماس با ما + + + بلاگ + + + + )} +
+
+ ) +} \ No newline at end of file diff --git a/src/app/products/DetailService.tsx b/src/app/products/DetailService.tsx new file mode 100644 index 0000000..fd6031a --- /dev/null +++ b/src/app/products/DetailService.tsx @@ -0,0 +1,176 @@ +'use client' +import { FC, Fragment } from 'react' +import { useGetDetailService } from './hooks/useProductData' +import ServiceHeader from './components/ServiceHeader' +import moment from 'moment-jalaali' +import ServiceImages from './components/ServiceImages' +import Image from 'next/image' +import Loading from '../loading' +import Rate from 'rc-rate' +import { ReviewItem } from './types/ProductTypes' + +// Default avatar image +const defaultAvatar = '/images/logo.svg' // Using an existing image + + +const DetailService: FC<{ id: string }> = ({ id }) => { + + // const { t } = useTranslation('global') + // const getAdsLeft = useGetAds(AdsDisplayLocation.SINGLE_SERVICE_PAGE) + + const getDetailService = useGetDetailService(id) + + // Show the skeleton loading component while data is loading + if (getDetailService.isLoading) { + return + } + + return ( +
+ + { + +
+ + {getDetailService.data?.data.danakService && ( + + )} +
+
+
+ توسعه شرکت +
+
+ دانک +
+
+
+
+ تعداد کاربر +
+
+ +{getDetailService?.data?.data?.danakService?.userCount} +
+
+
+
+ سال تولید +
+
+ {getDetailService?.data?.data?.danakService?.createdAt ? + moment(getDetailService.data.data.danakService.createdAt).format('jYYYY/jMM/jDD') : ''} +
+
+
+
+ زبان برنامه +
+
+ {getDetailService?.data?.data?.danakService?.serviceLanguage === 'fa' ? 'فارسی' : 'انگلیسی'} +
+
+
+
+ امتیاز +
+
+ +
+
+
+ + {getDetailService?.data?.data?.danakService?.images && ( + + )} + +
+
+ توضیحات کامل +
+ {getDetailService.data?.data?.danakService?.metaDescription && ( +

+

+ )} +
+ +
+ {/* { + getDetailService.data?.data?.purchased && + getDetailService.refetch()} /> + } */} +
+
+ نظرات کاربران +
+ + { + getDetailService.data?.data.danakService?.reviews?.map((item: ReviewItem) => { + return ( +
+
+ avatar +
+ {item.user?.firstName + ' ' + item.user?.lastName} +
+
+ +
+
+ {item.title} +
+
+ {moment(item.createdAt).fromNow()} +
+
+ +
+ +
+ +
+ {item.comment} +
+
+ ) + }) + } +
+
+
+ + + {/* { + getAdsLeft.isSuccess && getAdsLeft.data.data.ads[0] && + + + +
+
+ {getAdsLeft.data.data.ads[0].title} +
+
+
+ } */} +
+ } + + +
+ ) +} + +export default DetailService \ No newline at end of file diff --git a/src/app/products/[id]/loading.tsx b/src/app/products/[id]/loading.tsx new file mode 100644 index 0000000..b44a26c --- /dev/null +++ b/src/app/products/[id]/loading.tsx @@ -0,0 +1,81 @@ +export default function Loading() { + return ( +
+
+ {/* Service Header Skeleton */} +
+
+
+
+
+
+
+ + {/* Stats Skeleton */} +
+ {[1, 2, 3, 4, 5].map((i) => ( +
+
+
+
+ ))} +
+ + {/* Service Images Skeleton */} +
+
+
+
+ + {/* Description Skeleton */} +
+
+
+
+
+
+
+
+
+ + {/* Reviews Skeleton */} +
+
+
+ + {[1, 2].map((i) => ( +
+
+
+
+
+
+
+
+
+
+ {[1, 2, 3, 4, 5].map((star) => ( +
+ ))} +
+
+
+
+
+
+
+ ))} +
+
+
+ + {/* Sidebar Ad Skeleton */} +
+
+
+
+
+
+
+ ) +} \ No newline at end of file diff --git a/src/app/products/[id]/page.tsx b/src/app/products/[id]/page.tsx index 559887a..4e36e06 100644 --- a/src/app/products/[id]/page.tsx +++ b/src/app/products/[id]/page.tsx @@ -1,9 +1,39 @@ -import React from 'react' +import { Suspense } from 'react' +import { dehydrate, HydrationBoundary, QueryClient } from '@tanstack/react-query' +import { detailServiceQueryOptions } from '../hooks/useProductData' +import { NextPage } from 'next' +import DetailService from '../DetailService' +import Loading from './loading' + +interface PageProps { + params: Promise<{ + id: string + }> +} + +const SingleService: NextPage = async ({ params }) => { + + const { id } = await params -const page = () => { return ( -
page
+ }> + + ) } -export default page \ No newline at end of file +async function SingleServicePage({ id }: { id: string }) { + const queryClient = new QueryClient() + + await Promise.all([ + queryClient.prefetchQuery(detailServiceQueryOptions(id)), + ]) + + return ( + + + + ) +} + +export default SingleService \ No newline at end of file diff --git a/src/app/products/components/ServiceHeader.tsx b/src/app/products/components/ServiceHeader.tsx new file mode 100644 index 0000000..1b7dc9c --- /dev/null +++ b/src/app/products/components/ServiceHeader.tsx @@ -0,0 +1,109 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +'use client' +import { FC, useEffect, useState } from 'react' +import Button from '../../../components/Button' +import { ArrowLeft } from 'iconsax-react' +import { NumberFormat } from '../../../config/func' +import Link from 'next/link' +import { DanakService } from '../types/ProductTypes' +import RadioGroup from '@/components/RadioGroup' +import Image from 'next/image' + +type Props = { + data: DanakService, +} + +const ServiceHeader: FC = (props: Props) => { + + const { data } = props + const [planSelected, setPlanSelected] = useState(data?.subscriptionPlans[0]?.id) + const [price, setPrice] = useState(data?.subscriptionPlans[0]?.price) + const [finalPrice, setFinalPrice] = useState(0) + // const buyService = useBuyService() + + useEffect(() => { + + const plan: any = data?.subscriptionPlans.find((item) => item.id === planSelected) + + setFinalPrice(plan.finalPrice) + setPrice(plan?.price) + + }, [data?.subscriptionPlans, planSelected]) + + + return ( +
+
+
+ icon +
+
+
+ {data?.name} +
+ +

+ {data?.description} +

+
+ { + return { + label: item?.name, + value: item?.id + } + })} + onChange={setPlanSelected} + selected={planSelected} + /> +
+
+
+ +
+
+ { + return { + label: item?.name, + value: item?.id + } + })} + onChange={setPlanSelected} + selected={planSelected} + /> +
+
+
+
+ { + finalPrice !== price && +
+ {NumberFormat(Number(price))} +
+ } +
{NumberFormat(Number(price))}
+
+ تومان +
+ + + +
+
+
+ ) +} + +export default ServiceHeader \ No newline at end of file diff --git a/src/app/products/components/ServiceImages.tsx b/src/app/products/components/ServiceImages.tsx new file mode 100644 index 0000000..fb78a2b --- /dev/null +++ b/src/app/products/components/ServiceImages.tsx @@ -0,0 +1,39 @@ +import Image from 'next/image' +import { FC } from 'react' +import { Swiper, SwiperSlide } from 'swiper/react' + +const SWIPER_SLIDE_STYLE = { + overflow: 'visible !important', + // width: 220, +} + +type Props = { + images: { id: string, imageUrl: string }[] +} + +const ServiceImages: FC = ({ images }) => { + return ( +
+ + + { + images.map((item) => { + return ( + +
+ {item.imageUrl} +
+
+ ) + }) + } +
+
+ ) +} + +export default ServiceImages \ No newline at end of file diff --git a/src/app/products/hooks/useProductData.ts b/src/app/products/hooks/useProductData.ts index 7ed21fc..04b7191 100644 --- a/src/app/products/hooks/useProductData.ts +++ b/src/app/products/hooks/useProductData.ts @@ -33,3 +33,14 @@ export const categoriesQueryOptions = { queryKey: ["service-categories"], queryFn: () => api.getCategoriesPublic(), }; + +export const useGetDetailService = (serviceId: string) => + useQuery({ + queryKey: ["service-detail", serviceId], + queryFn: () => api.getDetailService(serviceId), + }); + +export const detailServiceQueryOptions = (serviceId: string) => ({ + queryKey: ["service-detail", serviceId], + queryFn: () => api.getDetailService(serviceId), +}); diff --git a/src/app/products/service/ProductService.ts b/src/app/products/service/ProductService.ts index 261dd03..4f79cb8 100644 --- a/src/app/products/service/ProductService.ts +++ b/src/app/products/service/ProductService.ts @@ -2,6 +2,7 @@ import axios from "@/config/axios"; import { CategoryResponse, DanakServiceSuggestedResponse, + ServiceDetailResponse, ServiceResponse, } from "../types/ProductTypes"; @@ -29,3 +30,10 @@ export const getDanakServiceSuggested = async () => { ); return data; }; + +export const getDetailService = async (serviceId: string) => { + const { data } = await axios.get( + `/danak-services/${serviceId}/public` + ); + return data; +}; diff --git a/src/app/products/types/ProductTypes.ts b/src/app/products/types/ProductTypes.ts index 17de563..05425b9 100644 --- a/src/app/products/types/ProductTypes.ts +++ b/src/app/products/types/ProductTypes.ts @@ -1,6 +1,21 @@ import { DanakSuggestService } from "@/app/home/types/HomeTypes"; import { ApiResponse } from "@/shared/types/SharedTypes"; +export interface ReviewItem { + id: string; + createdAt: string; + title: string; + comment: string; + rating: number; + status: string; + user: { + id: string; + firstName: string; + lastName: string; + profilePic: string; + }; +} + export type ServiceResponse = ApiResponse<{ services: DanakSuggestService[]; nextPage: boolean; @@ -25,3 +40,51 @@ export type ServiceCategoryType = { export type DanakServiceSuggestedResponse = ApiResponse<{ danakServices: DanakSuggestService[]; }>; + +export interface DanakService { + id: string; + createdAt: string; + updatedAt: string; + name: string; + title: string; + isDanakSuggest: boolean; + description: string; + author: string; + createDate: string; + userCount: number; + serviceLanguage: string; + softwareLanguage: string; + metaDescription: string; + isActive: boolean; + link: string; + icon: string; + category: { + id: string; + title: string; + }; + images: { + id: string; + createdAt: string; + updatedAt: string; + imageUrl: string; + }[]; + subscriptionPlans: { + id: string; + createdAt: string; + updatedAt: string; + name: string; + duration: number; + price: number; + isActive: boolean; + }[]; + reviews: ReviewItem[]; + // reviews: any[]; // replace with Review[] if review structure is defined + deletedAt: string | null; +} + +export type ServiceDetailResponse = ApiResponse<{ + danakService: DanakService; + reviews: []; + deletedAt: string | null; + averageRating: number; +}>; diff --git a/src/app/test-error-server/page.tsx b/src/app/test-error-server/page.tsx new file mode 100644 index 0000000..6e0df4a --- /dev/null +++ b/src/app/test-error-server/page.tsx @@ -0,0 +1,12 @@ +export default function TestServerErrorPage() { + // ایجاد خطا در سمت سرور + throw new Error('این یک خطای سمت سرور آزمایشی است') + + // این بخش هرگز اجرا نمی‌شود + return ( +
+

صفحه تست خطای سرور

+

اگر این متن را می‌بینید، خطا رخ نداده است!

+
+ ) +} \ No newline at end of file diff --git a/src/app/test-error/page.tsx b/src/app/test-error/page.tsx new file mode 100644 index 0000000..a1cf841 --- /dev/null +++ b/src/app/test-error/page.tsx @@ -0,0 +1,18 @@ +'use client' + +import { useEffect } from 'react' + +export default function TestErrorPage() { + useEffect(() => { + // این کد عمداً خطا ایجاد می‌کند + throw new Error('این یک خطای آزمایشی برای نمایش صفحه error است') + }, []) + + // این بخش هرگز نمایش داده نمی‌شود چون قبل از آن خطا رخ می‌دهد + return ( +
+

صفحه تست خطا

+

اگر این متن را می‌بینید، خطا رخ نداده است!

+
+ ) +} \ No newline at end of file diff --git a/src/components/Radio.tsx b/src/components/Radio.tsx new file mode 100644 index 0000000..4f7dcdc --- /dev/null +++ b/src/components/Radio.tsx @@ -0,0 +1,20 @@ +import { FC } from 'react' + +type Props = { + isActive: boolean, + value: string, + onChange: (value: string) => void +} + +const Radio: FC = (props: Props) => { + return ( +
props.onChange(props.value)} className='size-4 cursor-pointer rounded-full bg-[#EAEDF5] flex justify-center items-center'> + { + props.isActive && +
+ } +
+ ) +} + +export default Radio \ No newline at end of file diff --git a/src/components/RadioGroup.tsx b/src/components/RadioGroup.tsx new file mode 100644 index 0000000..0421109 --- /dev/null +++ b/src/components/RadioGroup.tsx @@ -0,0 +1,31 @@ +import { FC } from 'react' +import Radio from './Radio' + + +type Props = { + items: { + label: string + value: string + }[] + selected: string + onChange: (value: string) => void +} + +const RadioGroup: FC = (props: Props) => { + return ( +
+ { + props.items.map((item, index) => ( +
+ +
+ {item.label} +
+
+ )) + } +
+ ) +} + +export default RadioGroup \ No newline at end of file diff --git a/src/components/ServiceItem.tsx b/src/components/ServiceItem.tsx index 13fdfa8..04c6dcf 100644 --- a/src/components/ServiceItem.tsx +++ b/src/components/ServiceItem.tsx @@ -1,6 +1,7 @@ import { DanakSuggestService } from '@/app/home/types/HomeTypes' import { ArrowLeft } from 'iconsax-react' import Image from 'next/image' +import Link from 'next/link' import { FC } from 'react' type Props = { @@ -10,7 +11,7 @@ type Props = { const ServiceItem: FC = (props) => { return ( -
+
{props.item.name} @@ -53,7 +54,7 @@ const ServiceItem: FC = (props) => {
} -
+ ) } diff --git a/src/config/axios.ts b/src/config/axios.ts index 6b4fb22..e302827 100644 --- a/src/config/axios.ts +++ b/src/config/axios.ts @@ -4,7 +4,7 @@ import { getToken } from "./func"; const axiosInstance = axios.create({ // baseURL: process.env.NEXT_PUBLIC_BASE_URL, - baseURL: "https://api.danakcorp.com", + baseURL: "http://192.168.1.108:4000", }); axiosInstance.interceptors.response.use( diff --git a/src/config/func.ts b/src/config/func.ts index aeae3e6..e35a716 100644 --- a/src/config/func.ts +++ b/src/config/func.ts @@ -76,6 +76,7 @@ export function toJalaliDate( const parsedDate = new Date(date); if (isNaN(parsedDate.getTime())) { + return ""; throw new Error("Invalid date provided to toJalaliDate"); }