single service + 404 page and 500 page

This commit is contained in:
hamid zarghami
2025-04-17 09:30:33 +03:30
parent a2c226b5a8
commit 1c314972da
21 changed files with 1032 additions and 7 deletions
+4
View File
@@ -14,9 +14,12 @@
"axios": "^1.8.4", "axios": "^1.8.4",
"critters": "^0.0.23", "critters": "^0.0.23",
"formik": "^2.4.6", "formik": "^2.4.6",
"framer-motion": "^12.7.3",
"iconsax-react": "^0.0.8", "iconsax-react": "^0.0.8",
"js-cookie": "^3.0.5", "js-cookie": "^3.0.5",
"moment-jalaali": "^0.10.4",
"next": "15.2.3", "next": "15.2.3",
"rc-rate": "^2.13.1",
"react": "^19.0.0", "react": "^19.0.0",
"react-dom": "^19.0.0", "react-dom": "^19.0.0",
"react-spinners": "^0.15.0", "react-spinners": "^0.15.0",
@@ -30,6 +33,7 @@
"@eslint/eslintrc": "^3", "@eslint/eslintrc": "^3",
"@tailwindcss/postcss": "^4", "@tailwindcss/postcss": "^4",
"@types/js-cookie": "^3.0.6", "@types/js-cookie": "^3.0.6",
"@types/moment-jalaali": "^0.7.9",
"@types/node": "^20", "@types/node": "^20",
"@types/react": "^19", "@types/react": "^19",
"@types/react-dom": "^19", "@types/react-dom": "^19",
+131
View File
@@ -23,15 +23,24 @@ importers:
formik: formik:
specifier: ^2.4.6 specifier: ^2.4.6
version: 2.4.6(react@19.1.0) 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: iconsax-react:
specifier: ^0.0.8 specifier: ^0.0.8
version: 0.0.8(react@19.1.0) version: 0.0.8(react@19.1.0)
js-cookie: js-cookie:
specifier: ^3.0.5 specifier: ^3.0.5
version: 3.0.5 version: 3.0.5
moment-jalaali:
specifier: ^0.10.4
version: 0.10.4
next: next:
specifier: 15.2.3 specifier: 15.2.3
version: 15.2.3(react-dom@19.1.0(react@19.1.0))(react@19.1.0) 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: react:
specifier: ^19.0.0 specifier: ^19.0.0
version: 19.1.0 version: 19.1.0
@@ -66,6 +75,9 @@ importers:
'@types/js-cookie': '@types/js-cookie':
specifier: ^3.0.6 specifier: ^3.0.6
version: 3.0.6 version: 3.0.6
'@types/moment-jalaali':
specifier: ^0.7.9
version: 0.7.9
'@types/node': '@types/node':
specifier: ^20 specifier: ^20
version: 20.17.30 version: 20.17.30
@@ -94,6 +106,10 @@ packages:
resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==} resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==}
engines: {node: '>=10'} 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': '@emnapi/core@1.4.3':
resolution: {integrity: sha512-4m62DuCE07lw01soJwPiBGC0nAww0Q+RY70VZ+n49yDIO13yyinhbWCeNnaob0lakDtWQzSdtNWzJeOJt2ma+g==} resolution: {integrity: sha512-4m62DuCE07lw01soJwPiBGC0nAww0Q+RY70VZ+n49yDIO13yyinhbWCeNnaob0lakDtWQzSdtNWzJeOJt2ma+g==}
@@ -478,6 +494,9 @@ packages:
'@types/json5@0.0.29': '@types/json5@0.0.29':
resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==} resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==}
'@types/moment-jalaali@0.7.9':
resolution: {integrity: sha512-gsDOoAzRnCfQTbfdlUrCvX6R0wIto6CvwfvV2C3j4qJLK+DEiTK8Rl/xlOCBO9C6qeUfX8oyZ2UfjnXJTOvHSA==}
'@types/node@20.17.30': '@types/node@20.17.30':
resolution: {integrity: sha512-7zf4YyHA+jvBNfVrk2Gtvs6x7E8V+YDW05bNfG2XkWDJfYRXrTiP/DsB2zSYTaHX0bGIujTBQdMVAhb+j7mwpg==} resolution: {integrity: sha512-7zf4YyHA+jvBNfVrk2Gtvs6x7E8V+YDW05bNfG2XkWDJfYRXrTiP/DsB2zSYTaHX0bGIujTBQdMVAhb+j7mwpg==}
@@ -740,6 +759,9 @@ packages:
resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==}
engines: {node: '>=10'} engines: {node: '>=10'}
classnames@2.5.1:
resolution: {integrity: sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==}
client-only@0.0.1: client-only@0.0.1:
resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==} resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==}
@@ -1093,6 +1115,20 @@ packages:
peerDependencies: peerDependencies:
react: '>=16.8.0' 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: function-bind@1.1.2:
resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==}
@@ -1310,6 +1346,9 @@ packages:
resolution: {integrity: sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==} resolution: {integrity: sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
jalaali-js@1.2.8:
resolution: {integrity: sha512-Jl/EwY84JwjW2wsWqeU4pNd22VNQ7EkjI36bDuLw31wH98WQW4fPjD0+mG7cdCK+Y8D6s9R3zLiQ3LaKu6bD8A==}
jiti@2.4.2: jiti@2.4.2:
resolution: {integrity: sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==} resolution: {integrity: sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==}
hasBin: true hasBin: true
@@ -1467,6 +1506,21 @@ packages:
minimist@1.2.8: minimist@1.2.8:
resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} 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: ms@2.1.3:
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
@@ -1611,6 +1665,19 @@ packages:
queue-microtask@1.2.3: queue-microtask@1.2.3:
resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} 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: react-dom@19.1.0:
resolution: {integrity: sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==} resolution: {integrity: sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==}
peerDependencies: peerDependencies:
@@ -1622,6 +1689,9 @@ packages:
react-is@16.13.1: react-is@16.13.1:
resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} 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: react-spinners@0.15.0:
resolution: {integrity: sha512-ZO3/fNB9Qc+kgpG3SfdlMnvTX6LtLmTnOogb3W6sXIaU/kZ1ydEViPfZ06kSOaEsor58C/tzXw2wROGQu3X2pA==} resolution: {integrity: sha512-ZO3/fNB9Qc+kgpG3SfdlMnvTX6LtLmTnOogb3W6sXIaU/kZ1ydEViPfZ06kSOaEsor58C/tzXw2wROGQu3X2pA==}
peerDependencies: peerDependencies:
@@ -1642,6 +1712,9 @@ packages:
resolution: {integrity: sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==} resolution: {integrity: sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
regenerator-runtime@0.14.1:
resolution: {integrity: sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==}
regexp.prototype.flags@1.5.4: regexp.prototype.flags@1.5.4:
resolution: {integrity: sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==} resolution: {integrity: sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
@@ -1938,6 +2011,10 @@ snapshots:
'@alloc/quick-lru@5.2.0': {} '@alloc/quick-lru@5.2.0': {}
'@babel/runtime@7.27.0':
dependencies:
regenerator-runtime: 0.14.1
'@emnapi/core@1.4.3': '@emnapi/core@1.4.3':
dependencies: dependencies:
'@emnapi/wasi-threads': 1.0.2 '@emnapi/wasi-threads': 1.0.2
@@ -2250,6 +2327,10 @@ snapshots:
'@types/json5@0.0.29': {} '@types/json5@0.0.29': {}
'@types/moment-jalaali@0.7.9':
dependencies:
moment: 2.30.1
'@types/node@20.17.30': '@types/node@20.17.30':
dependencies: dependencies:
undici-types: 6.19.8 undici-types: 6.19.8
@@ -2544,6 +2625,8 @@ snapshots:
ansi-styles: 4.3.0 ansi-styles: 4.3.0
supports-color: 7.2.0 supports-color: 7.2.0
classnames@2.5.1: {}
client-only@0.0.1: {} client-only@0.0.1: {}
clsx@2.1.1: {} clsx@2.1.1: {}
@@ -3057,6 +3140,15 @@ snapshots:
tiny-warning: 1.0.3 tiny-warning: 1.0.3
tslib: 2.8.1 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-bind@1.1.2: {}
function.prototype.name@1.1.8: function.prototype.name@1.1.8:
@@ -3297,6 +3389,8 @@ snapshots:
has-symbols: 1.1.0 has-symbols: 1.1.0
set-function-name: 2.0.2 set-function-name: 2.0.2
jalaali-js@1.2.8: {}
jiti@2.4.2: {} jiti@2.4.2: {}
js-cookie@3.0.5: {} js-cookie@3.0.5: {}
@@ -3423,6 +3517,24 @@ snapshots:
minimist@1.2.8: {} 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: {} ms@2.1.3: {}
nanoid@3.3.11: {} nanoid@3.3.11: {}
@@ -3571,6 +3683,21 @@ snapshots:
queue-microtask@1.2.3: {} 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): react-dom@19.1.0(react@19.1.0):
dependencies: dependencies:
react: 19.1.0 react: 19.1.0
@@ -3580,6 +3707,8 @@ snapshots:
react-is@16.13.1: {} 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): react-spinners@0.15.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0):
dependencies: dependencies:
react: 19.1.0 react: 19.1.0
@@ -3604,6 +3733,8 @@ snapshots:
get-proto: 1.0.1 get-proto: 1.0.1
which-builtin-type: 1.2.1 which-builtin-type: 1.2.1
regenerator-runtime@0.14.1: {}
regexp.prototype.flags@1.5.4: regexp.prototype.flags@1.5.4:
dependencies: dependencies:
call-bind: 1.0.8 call-bind: 1.0.8
+149
View File
@@ -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 (
<div className="min-h-screen flex flex-col items-center justify-center bg-background p-4 relative overflow-hidden">
{/* Animated background elements */}
{mounted && (
<>
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 0.4 }}
transition={{ duration: 1 }}
className="absolute inset-0 z-0"
>
{[...Array(6)].map((_, i) => (
<motion.div
key={`circle-${i}`}
className="absolute rounded-full bg-[#FF3E39]/5"
initial={{
x: Math.random() * 100 - 50 + '%',
y: Math.random() * 100 - 50 + '%',
scale: Math.random() * 0.5 + 0.5,
}}
animate={{
x: [
Math.random() * 100 - 50 + '%',
Math.random() * 100 - 50 + '%',
Math.random() * 100 - 50 + '%',
],
y: [
Math.random() * 100 - 50 + '%',
Math.random() * 100 - 50 + '%',
Math.random() * 100 - 50 + '%',
],
}}
transition={{
repeat: Infinity,
duration: 20 + Math.random() * 10,
ease: 'easeInOut',
}}
style={{
width: `${Math.random() * 300 + 100}px`,
height: `${Math.random() * 300 + 100}px`,
}}
/>
))}
</motion.div>
</>
)}
<div className="max-w-2xl w-full text-center z-10 relative">
{mounted && (
<>
<motion.div
initial={{ scale: 0.8, opacity: 0 }}
animate={{ scale: 1, opacity: 1 }}
transition={{ duration: 0.5 }}
className="relative"
>
<div className="text-9xl font-bold text-primary/10">خطا</div>
<motion.div
initial={{ y: 0 }}
animate={{ y: [0, -10, 0] }}
transition={{ repeat: Infinity, duration: 2, ease: "easeInOut" }}
className="absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2"
>
{/* <svg width="120" height="120" viewBox="0 0 120 120" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M60 10C32.4 10 10 32.4 10 60C10 87.6 32.4 110 60 110C87.6 110 110 87.6 110 60C110 32.4 87.6 10 60 10ZM60 100C37.9 100 20 82.1 20 60C20 37.9 37.9 20 60 20C82.1 20 100 37.9 100 60C100 82.1 82.1 100 60 100ZM55 75H65V85H55V75ZM55 35H65V65H55V35Z" fill="#000000" />
</svg> */}
</motion.div>
</motion.div>
<motion.h1
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.2, duration: 0.5 }}
className="mt-8 text-2xl md:text-3xl font-bold"
>
یک خطا رخ داده است!
</motion.h1>
<motion.p
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.4, duration: 0.5 }}
className="mt-4 text-description"
>
متأسفانه در حین پردازش درخواست شما خطایی رخ داده است. لطفا دوباره تلاش کنید.
</motion.p>
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.6, duration: 0.5 }}
className="mt-8 flex flex-wrap gap-4 justify-center"
>
<button
onClick={() => reset()}
className="bg-black hover:bg-black/90 transition-colors text-white py-3 px-8 rounded-2xl"
>
تلاش مجدد
</button>
</motion.div>
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: 0.8, duration: 0.5 }}
className="mt-12 flex flex-wrap justify-center gap-6"
>
<Link href="/products" className="text-black hover:underline">
محصولات
</Link>
<Link href="/about" className="text-black hover:underline">
درباره ما
</Link>
<Link href="/contact" className="text-black hover:underline">
تماس با ما
</Link>
<Link href="/blogs" className="text-black hover:underline">
بلاگ
</Link>
</motion.div>
</>
)}
</div>
</div>
)
}
+4
View File
@@ -99,3 +99,7 @@ tbody tr {
direction: rtl; direction: rtl;
font-family: "irancell" !important; font-family: "irancell" !important;
} }
.rc-rate-star {
margin-right: 3px !important; /* فاصله بین ستاره‌ها */
}
+1
View File
@@ -2,6 +2,7 @@ import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google"; import { Geist, Geist_Mono } from "next/font/google";
import "swiper/css"; import "swiper/css";
import "./globals.css"; import "./globals.css";
import 'rc-rate/assets/index.css';
import "@/assets/fonts/irancell/style.css"; import "@/assets/fonts/irancell/style.css";
import { ToastContainer } from 'react-toastify'; import { ToastContainer } from 'react-toastify';
import Header from "@/shared/Header"; import Header from "@/shared/Header";
+136
View File
@@ -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 (
<div className="min-h-screen flex flex-col items-center justify-center bg-background p-4 relative overflow-hidden">
{/* Animated background elements */}
{mounted && (
<>
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 0.4 }}
transition={{ duration: 1 }}
className="absolute inset-0 z-0"
>
{[...Array(6)].map((_, i) => (
<motion.div
key={`circle-${i}`}
className="absolute rounded-full bg-black/5"
initial={{
x: Math.random() * 100 - 50 + '%',
y: Math.random() * 100 - 50 + '%',
scale: Math.random() * 0.5 + 0.5,
}}
animate={{
x: [
Math.random() * 100 - 50 + '%',
Math.random() * 100 - 50 + '%',
Math.random() * 100 - 50 + '%',
],
y: [
Math.random() * 100 - 50 + '%',
Math.random() * 100 - 50 + '%',
Math.random() * 100 - 50 + '%',
],
}}
transition={{
repeat: Infinity,
duration: 20 + Math.random() * 10,
ease: 'easeInOut',
}}
style={{
width: `${Math.random() * 300 + 100}px`,
height: `${Math.random() * 300 + 100}px`,
}}
/>
))}
</motion.div>
</>
)}
<div className="max-w-2xl w-full text-center z-10 relative">
{mounted && (
<>
<motion.div
initial={{ scale: 0.8, opacity: 0 }}
animate={{ scale: 1, opacity: 1 }}
transition={{ duration: 0.5 }}
className="relative"
>
<div className="text-9xl font-bold text-primary/10">404</div>
<motion.div
initial={{ y: 0 }}
animate={{ y: [0, -10, 0] }}
transition={{ repeat: Infinity, duration: 2, ease: "easeInOut" }}
className="absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2"
>
</motion.div>
</motion.div>
<motion.h1
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.2, duration: 0.5 }}
className="mt-8 text-2xl md:text-3xl font-bold"
>
صفحه موردنظر پیدا نشد!
</motion.h1>
<motion.p
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.4, duration: 0.5 }}
className="mt-4 text-description"
>
متأسفانه صفحهای که به دنبال آن هستید وجود ندارد یا حذف شده است.
</motion.p>
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.6, duration: 0.5 }}
className="mt-8"
>
<Link href="/">
<button className="bg-black hover:bg-black/90 transition-colors text-white py-3 px-8 rounded-2xl">
بازگشت به صفحه اصلی
</button>
</Link>
</motion.div>
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: 0.8, duration: 0.5 }}
className="mt-12 flex flex-wrap justify-center gap-6"
>
<Link href="/products" className="text-black hover:underline">
محصولات
</Link>
<Link href="/about" className="text-black hover:underline">
درباره ما
</Link>
<Link href="/contact" className="text-black hover:underline">
تماس با ما
</Link>
<Link href="/blogs" className="text-black hover:underline">
بلاگ
</Link>
</motion.div>
</>
)}
</div>
</div>
)
}
+176
View File
@@ -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 <Loading />
}
return (
<div className='w-full flex gap-6 mt-4'>
{
<Fragment>
<div className='flex-1 max-w-full'>
{getDetailService.data?.data.danakService && (
<ServiceHeader data={getDetailService.data.data.danakService} />
)}
<div className='mt-8 flex xl:flex-row flex-col overflowX justify-between items-center xl:gap-8 gap-5'>
<div className='bg-white flex xl:min-w-[127px] w-full flex-1 flex-col justify-center items-center h-[109px] rounded-2xl py-6 px-4'>
<div className='text-description xl:text-xs text-[11px]'>
توسعه شرکت
</div>
<div className='mt-3 text-sm'>
دانک
</div>
</div>
<div className='bg-white flex xl:min-w-[127px] w-full flex-1 flex-col justify-center items-center h-[109px] rounded-2xl py-6 px-4'>
<div className='text-description xl:text-xs text-[11px]'>
تعداد کاربر
</div>
<div className='mt-3 text-sm'>
+{getDetailService?.data?.data?.danakService?.userCount}
</div>
</div>
<div className='bg-white flex xl:min-w-[127px] w-full flex-1 flex-col justify-center items-center h-[109px] rounded-2xl py-6 px-4'>
<div className='text-description xl:text-xs text-[11px]'>
سال تولید
</div>
<div className='mt-3 text-sm'>
{getDetailService?.data?.data?.danakService?.createdAt ?
moment(getDetailService.data.data.danakService.createdAt).format('jYYYY/jMM/jDD') : ''}
</div>
</div>
<div className='bg-white flex xl:min-w-[127px] w-full flex-1 flex-col justify-center items-center h-[109px] rounded-2xl py-6 px-4'>
<div className='text-description xl:text-xs text-[11px]'>
زبان برنامه
</div>
<div className='mt-3 text-sm'>
{getDetailService?.data?.data?.danakService?.serviceLanguage === 'fa' ? 'فارسی' : 'انگلیسی'}
</div>
</div>
<div className='bg-white flex xl:min-w-[127px] w-full flex-1 flex-col justify-center items-center h-[109px] rounded-2xl py-6 px-4'>
<div className='text-description xl:text-xs text-[11px]'>
امتیاز
</div>
<div className='mt-3 dltr flex gap-1 text-sm'>
<Rate
count={5}
style={{ fontSize: 15 }}
value={getDetailService?.data?.data?.averageRating}
disabled
allowHalf
/>
</div>
</div>
</div>
{getDetailService?.data?.data?.danakService?.images && (
<ServiceImages images={getDetailService.data.data.danakService.images} />
)}
<div className='mt-8 bg-white p-6 rounded-3xl'>
<div>
توضیحات کامل
</div>
{getDetailService.data?.data?.danakService?.metaDescription && (
<p dangerouslySetInnerHTML={{ __html: getDetailService.data.data.danakService.metaDescription }} className='text-[13px] mt-4 leading-6'>
</p>
)}
</div>
<div className='mt-8 flex xl:flex-row flex-col gap-8 items-start'>
{/* {
getDetailService.data?.data?.purchased &&
<CreateReview refetch={() => getDetailService.refetch()} />
} */}
<div className='flex-1 w-full max-w-full bg-white p-6 rounded-3xl'>
<div className='text-sm'>
نظرات کاربران
</div>
{
getDetailService.data?.data.danakService?.reviews?.map((item: ReviewItem) => {
return (
<div key={item.id} className='mt-5 border border-border rounded-3xl p-6'>
<div className='flex gap-2 items-center'>
<Image alt='avatar' width={24} height={24} src={item.user?.profilePic || defaultAvatar} className='size-6 rounded-full object-cover' />
<div className='text-description text-xs'>
{item.user?.firstName + ' ' + item.user?.lastName}
</div>
</div>
<div className='mt-4 flex justify-between'>
<div className='text-sm max-w-[70%] truncate'>
{item.title}
</div>
<div className='text-xs text-description'>
{moment(item.createdAt).fromNow()}
</div>
</div>
<div className='mt-2'>
<Rate
disabled
value={item.rating}
count={5}
style={{ fontSize: 14 }}
/>
</div>
<div className='mt-4 text-[11px] leading-6'>
{item.comment}
</div>
</div>
)
})
}
</div>
</div>
</div>
{/* {
getAdsLeft.isSuccess && getAdsLeft.data.data.ads[0] &&
<a href={getAdsLeft.data?.data?.ads[0].link} target='_blank' className='bg-white w-sidebar h-fit hidden xl:block rounded-3xl overflow-hidden sticky top-4'>
<img
src={getAdsLeft.data.data.ads[0].imageUrl}
className='w-full backdrop-blur-[100px] h-[550px] object-cover'
/>
<div className='absolute flex items-center px-4 w-full bottom-0 h-[86px] bg-black bg-opacity-5 backdrop-blur-[14px]'>
<div className='max-w-[80%] text-sm leading-6 text-white'>
{getAdsLeft.data.data.ads[0].title}
</div>
</div>
</a>
} */}
</Fragment>
}
</div>
)
}
export default DetailService
+81
View File
@@ -0,0 +1,81 @@
export default function Loading() {
return (
<div className='w-full flex gap-6 mt-4'>
<div className='flex-1 max-w-full'>
{/* Service Header Skeleton */}
<div className="bg-white p-6 rounded-3xl animate-pulse">
<div className="h-8 w-60 bg-gray-200 rounded-md"></div>
<div className="mt-4 flex justify-between items-center">
<div className="h-5 w-32 bg-gray-200 rounded-md"></div>
<div className="h-10 w-32 bg-gray-200 rounded-3xl"></div>
</div>
</div>
{/* Stats Skeleton */}
<div className='mt-8 flex xl:flex-row flex-col overflowX justify-between items-center xl:gap-8 gap-5'>
{[1, 2, 3, 4, 5].map((i) => (
<div key={`stat-${i}`} className='bg-white flex xl:min-w-[127px] w-full flex-1 flex-col justify-center items-center h-[109px] rounded-2xl py-6 px-4 animate-pulse'>
<div className='h-4 w-20 bg-gray-200 rounded-md'></div>
<div className='mt-3 h-5 w-16 bg-gray-200 rounded-md'></div>
</div>
))}
</div>
{/* Service Images Skeleton */}
<div className="mt-8 grid grid-cols-2 gap-4 animate-pulse">
<div className="h-64 bg-gray-200 rounded-2xl"></div>
<div className="h-64 bg-gray-200 rounded-2xl"></div>
</div>
{/* Description Skeleton */}
<div className='mt-8 bg-white p-6 rounded-3xl animate-pulse'>
<div className="h-6 w-32 bg-gray-200 rounded-md"></div>
<div className="mt-4 space-y-3">
<div className="h-4 w-full bg-gray-200 rounded-md"></div>
<div className="h-4 w-full bg-gray-200 rounded-md"></div>
<div className="h-4 w-full bg-gray-200 rounded-md"></div>
<div className="h-4 w-3/4 bg-gray-200 rounded-md"></div>
</div>
</div>
{/* Reviews Skeleton */}
<div className='mt-8 flex xl:flex-row flex-col gap-8 items-start'>
<div className='flex-1 w-full max-w-full bg-white p-6 rounded-3xl animate-pulse'>
<div className="h-6 w-32 bg-gray-200 rounded-md"></div>
{[1, 2].map((i) => (
<div key={`review-${i}`} className='mt-5 border border-gray-200 rounded-3xl p-6'>
<div className='flex gap-2 items-center'>
<div className="h-6 w-6 bg-gray-200 rounded-full"></div>
<div className="h-4 w-32 bg-gray-200 rounded-md"></div>
</div>
<div className='mt-4 flex justify-between'>
<div className="h-5 w-3/4 bg-gray-200 rounded-md"></div>
<div className="h-4 w-20 bg-gray-200 rounded-md"></div>
</div>
<div className='mt-2 flex'>
{[1, 2, 3, 4, 5].map((star) => (
<div key={`star-${i}-${star}`} className="h-4 w-4 bg-gray-200 rounded-sm mr-1"></div>
))}
</div>
<div className='mt-4 space-y-2'>
<div className="h-3 w-full bg-gray-200 rounded-md"></div>
<div className="h-3 w-full bg-gray-200 rounded-md"></div>
<div className="h-3 w-1/2 bg-gray-200 rounded-md"></div>
</div>
</div>
))}
</div>
</div>
</div>
{/* Sidebar Ad Skeleton */}
<div className='bg-white w-sidebar h-[550px] hidden xl:block rounded-3xl overflow-hidden sticky top-4 animate-pulse'>
<div className="w-full h-full bg-gray-200"></div>
<div className='absolute flex items-center px-4 w-full bottom-0 h-[86px] bg-gray-100'>
<div className="h-5 w-3/4 bg-gray-200 rounded-md"></div>
</div>
</div>
</div>
)
}
+34 -4
View File
@@ -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<PageProps> = async ({ params }) => {
const { id } = await params
const page = () => {
return ( return (
<div>page</div> <Suspense fallback={<Loading />}>
<SingleServicePage id={id} />
</Suspense>
) )
} }
export default page async function SingleServicePage({ id }: { id: string }) {
const queryClient = new QueryClient()
await Promise.all([
queryClient.prefetchQuery(detailServiceQueryOptions(id)),
])
return (
<HydrationBoundary state={dehydrate(queryClient)}>
<DetailService id={id} />
</HydrationBoundary>
)
}
export default SingleService
@@ -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: Props) => {
const { data } = props
const [planSelected, setPlanSelected] = useState<string>(data?.subscriptionPlans[0]?.id)
const [price, setPrice] = useState<number>(data?.subscriptionPlans[0]?.price)
const [finalPrice, setFinalPrice] = useState<number>(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 (
<div className='w-full p-6 bg-white rounded-3xl flex xl:flex-row flex-col items-end xl:justify-between'>
<div className='flex w-full xl:gap-8 gap-4'>
<div className='xl:size-[70px] xl:min-w-[70px] min-w-[50px] size-[50px] rounded-xl'>
<Image alt='icon' width={70} height={70} src={data?.icon} className='w-full h-full rounded-xl' />
</div>
<div>
<div>
{data?.name}
</div>
<p className='text-description mt-1 text-[11px]'>
{data?.description}
</p>
<div className='hidden xl:flex gap-5 mt-5'>
<RadioGroup
items={data?.subscriptionPlans?.map((item) => {
return {
label: item?.name,
value: item?.id
}
})}
onChange={setPlanSelected}
selected={planSelected}
/>
</div>
</div>
</div>
<div className='flex w-full flex-col justify-between xl:items-end'>
<div className='xl:hidden flex gap-5 mt-5'>
<RadioGroup
items={data?.subscriptionPlans?.map((item) => {
return {
label: item?.name,
value: item?.id
}
})}
onChange={setPlanSelected}
selected={planSelected}
/>
</div>
<div className='flex gap-4 mt-5 xl:mt-0 items-center justify-between xl:justify-normal'>
<div className='text-xs whitespace-nowrap flex gap-1 items-center'>
<div className='flex gap-2'>
{
finalPrice !== price &&
<div className='line-through text-xs text-description'>
{NumberFormat(Number(price))}
</div>
}
<div>{NumberFormat(Number(price))}</div>
</div>
<span>تومان</span>
</div>
<Link target='_blank' href={`https://console.danakcorp.com/services/detail/${data?.id}`}>
<Button
// onClick={handleBuy}
// isLoading={buyService.isPending}
className='bg-description xl:max-w-full max-w-[100px] text-xs h-8 bg-opacity-20 text-black px-4'
>
<div className='flex gap-1 items-center'>
<div>
خرید
</div>
<ArrowLeft className='size-4' color='black' />
</div>
</Button>
</Link>
</div>
</div>
</div>
)
}
export default ServiceHeader
@@ -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<Props> = ({ images }) => {
return (
<div className='w-full mt-8'>
<Swiper
className='w-full'
spaceBetween={20} // فاصله پیش‌فرض بین اسلایدها
slidesPerView='auto'
>
{
images.map((item) => {
return (
<SwiperSlide key={item.id} className='max-w-[38%]' style={SWIPER_SLIDE_STYLE}>
<div className='bg-white bg-opacity-50 w-full p-2 rounded-2xl'>
<Image alt={item.imageUrl} width={2000} height={1000} src={item.imageUrl} className='w-full h-auto rounded-2xl' />
</div>
</SwiperSlide>
)
})
}
</Swiper>
</div>
)
}
export default ServiceImages
+11
View File
@@ -33,3 +33,14 @@ export const categoriesQueryOptions = {
queryKey: ["service-categories"], queryKey: ["service-categories"],
queryFn: () => api.getCategoriesPublic(), 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),
});
@@ -2,6 +2,7 @@ import axios from "@/config/axios";
import { import {
CategoryResponse, CategoryResponse,
DanakServiceSuggestedResponse, DanakServiceSuggestedResponse,
ServiceDetailResponse,
ServiceResponse, ServiceResponse,
} from "../types/ProductTypes"; } from "../types/ProductTypes";
@@ -29,3 +30,10 @@ export const getDanakServiceSuggested = async () => {
); );
return data; return data;
}; };
export const getDetailService = async (serviceId: string) => {
const { data } = await axios.get<ServiceDetailResponse>(
`/danak-services/${serviceId}/public`
);
return data;
};
+63
View File
@@ -1,6 +1,21 @@
import { DanakSuggestService } from "@/app/home/types/HomeTypes"; import { DanakSuggestService } from "@/app/home/types/HomeTypes";
import { ApiResponse } from "@/shared/types/SharedTypes"; 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<{ export type ServiceResponse = ApiResponse<{
services: DanakSuggestService[]; services: DanakSuggestService[];
nextPage: boolean; nextPage: boolean;
@@ -25,3 +40,51 @@ export type ServiceCategoryType = {
export type DanakServiceSuggestedResponse = ApiResponse<{ export type DanakServiceSuggestedResponse = ApiResponse<{
danakServices: DanakSuggestService[]; 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;
}>;
+12
View File
@@ -0,0 +1,12 @@
export default function TestServerErrorPage() {
// ایجاد خطا در سمت سرور
throw new Error('این یک خطای سمت سرور آزمایشی است')
// این بخش هرگز اجرا نمی‌شود
return (
<div>
<h1>صفحه تست خطای سرور</h1>
<p>اگر این متن را میبینید، خطا رخ نداده است!</p>
</div>
)
}
+18
View File
@@ -0,0 +1,18 @@
'use client'
import { useEffect } from 'react'
export default function TestErrorPage() {
useEffect(() => {
// این کد عمداً خطا ایجاد می‌کند
throw new Error('این یک خطای آزمایشی برای نمایش صفحه error است')
}, [])
// این بخش هرگز نمایش داده نمی‌شود چون قبل از آن خطا رخ می‌دهد
return (
<div>
<h1>صفحه تست خطا</h1>
<p>اگر این متن را میبینید، خطا رخ نداده است!</p>
</div>
)
}
+20
View File
@@ -0,0 +1,20 @@
import { FC } from 'react'
type Props = {
isActive: boolean,
value: string,
onChange: (value: string) => void
}
const Radio: FC<Props> = (props: Props) => {
return (
<div onClick={() => props.onChange(props.value)} className='size-4 cursor-pointer rounded-full bg-[#EAEDF5] flex justify-center items-center'>
{
props.isActive &&
<div className='size-2 bg-black rounded-full'></div>
}
</div>
)
}
export default Radio
+31
View File
@@ -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: Props) => {
return (
<div className='flex gap-5 items-center text-xs'>
{
props.items.map((item, index) => (
<div key={index} className='flex gap-2 items-center'>
<Radio value={item.value} onChange={props.onChange} isActive={item.value === props.selected} />
<div className='mt-0.5'>
{item.label}
</div>
</div>
))
}
</div>
)
}
export default RadioGroup
+3 -2
View File
@@ -1,6 +1,7 @@
import { DanakSuggestService } from '@/app/home/types/HomeTypes' import { DanakSuggestService } from '@/app/home/types/HomeTypes'
import { ArrowLeft } from 'iconsax-react' import { ArrowLeft } from 'iconsax-react'
import Image from 'next/image' import Image from 'next/image'
import Link from 'next/link'
import { FC } from 'react' import { FC } from 'react'
type Props = { type Props = {
@@ -10,7 +11,7 @@ type Props = {
const ServiceItem: FC<Props> = (props) => { const ServiceItem: FC<Props> = (props) => {
return ( return (
<div className='flex-1 bg-white rounded-4xl xl:p-7 p-4'> <Link href={`/products/${props.item.id}`} className='flex-1 bg-white rounded-4xl xl:p-7 p-4'>
<div className='flex justify-between'> <div className='flex justify-between'>
<div className='xl:size-[65px] size-[50px] rounded-2xl overflow-hidden'> <div className='xl:size-[65px] size-[50px] rounded-2xl overflow-hidden'>
<Image src={props.item.icon} className='size-full object-cover' alt={props.item.name} width={65} height={65} /> <Image src={props.item.icon} className='size-full object-cover' alt={props.item.name} width={65} height={65} />
@@ -53,7 +54,7 @@ const ServiceItem: FC<Props> = (props) => {
</div> </div>
</div> </div>
} }
</div> </Link>
) )
} }
+1 -1
View File
@@ -4,7 +4,7 @@ import { getToken } from "./func";
const axiosInstance = axios.create({ const axiosInstance = axios.create({
// baseURL: process.env.NEXT_PUBLIC_BASE_URL, // baseURL: process.env.NEXT_PUBLIC_BASE_URL,
baseURL: "https://api.danakcorp.com", baseURL: "http://192.168.1.108:4000",
}); });
axiosInstance.interceptors.response.use( axiosInstance.interceptors.response.use(
+1
View File
@@ -76,6 +76,7 @@ export function toJalaliDate(
const parsedDate = new Date(date); const parsedDate = new Date(date);
if (isNaN(parsedDate.getTime())) { if (isNaN(parsedDate.getTime())) {
return "";
throw new Error("Invalid date provided to toJalaliDate"); throw new Error("Invalid date provided to toJalaliDate");
} }