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 && (
+ <>
+
+ خطا
+
+ {/*
+
+ */}
+
+
+
+
+ یک خطا رخ داده است!
+
+
+
+ متأسفانه در حین پردازش درخواست شما خطایی رخ داده است. لطفا دوباره تلاش کنید.
+
+
+
+ reset()}
+ className="bg-black hover:bg-black/90 transition-colors text-white py-3 px-8 rounded-2xl"
+ >
+ تلاش مجدد
+
+
+
+
+
+
+
+ محصولات
+
+
+ درباره ما
+
+
+ تماس با ما
+
+
+ بلاگ
+
+
+ >
+ )}
+
+
+ )
+}
\ 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 (
+
+
+
+
+ {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 (
+
+
+
+
+
+
+
+ {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 (
+
+
+
+
+
+ )
+ })
+ }
+
+
+ )
+}
+
+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) => (
+
+ ))
+ }
+
+ )
+}
+
+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 (
-
+
@@ -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");
}