This commit is contained in:
hamid zarghami
2025-04-15 16:37:25 +03:30
parent 9bba9156a3
commit afacc7c488
26 changed files with 458 additions and 103 deletions
+144 -2
View File
@@ -11,14 +11,17 @@
"@tanstack/react-query": "^5.73.3", "@tanstack/react-query": "^5.73.3",
"@tanstack/react-query-devtools": "^5.74.3", "@tanstack/react-query-devtools": "^5.74.3",
"axios": "^1.8.4", "axios": "^1.8.4",
"formik": "^2.4.6",
"iconsax-react": "^0.0.8", "iconsax-react": "^0.0.8",
"js-cookie": "^3.0.5", "js-cookie": "^3.0.5",
"next": "15.2.3", "next": "15.2.3",
"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",
"react-toastify": "^11.0.5",
"swiper": "^11.2.6", "swiper": "^11.2.6",
"tailwind-merge": "^3.0.2", "tailwind-merge": "^3.0.2",
"yup": "^1.6.1",
"zustand": "^5.0.3" "zustand": "^5.0.3"
}, },
"devDependencies": { "devDependencies": {
@@ -1189,6 +1192,16 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/@types/hoist-non-react-statics": {
"version": "3.3.6",
"resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.6.tgz",
"integrity": "sha512-lPByRJUer/iN/xa4qpyL0qmL11DqNW81iU/IG1S3uvRUq4oKagz8VCxZjiWkumgt66YT3vOdDgZ0o32sGKtCEw==",
"license": "MIT",
"dependencies": {
"@types/react": "*",
"hoist-non-react-statics": "^3.3.0"
}
},
"node_modules/@types/js-cookie": { "node_modules/@types/js-cookie": {
"version": "3.0.6", "version": "3.0.6",
"resolved": "https://registry.npmjs.org/@types/js-cookie/-/js-cookie-3.0.6.tgz", "resolved": "https://registry.npmjs.org/@types/js-cookie/-/js-cookie-3.0.6.tgz",
@@ -1224,7 +1237,6 @@
"version": "19.0.12", "version": "19.0.12",
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.0.12.tgz", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.0.12.tgz",
"integrity": "sha512-V6Ar115dBDrjbtXSrS+/Oruobc+qVbbUxDFC1RSbRqLt5SYvxxyIDrSC85RWml54g+jfNeEMZhEj7wW07ONQhA==", "integrity": "sha512-V6Ar115dBDrjbtXSrS+/Oruobc+qVbbUxDFC1RSbRqLt5SYvxxyIDrSC85RWml54g+jfNeEMZhEj7wW07ONQhA==",
"devOptional": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"csstype": "^3.0.2" "csstype": "^3.0.2"
@@ -2078,6 +2090,15 @@
"integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==", "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/clsx": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
"integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==",
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/color": { "node_modules/color": {
"version": "4.2.3", "version": "4.2.3",
"resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz", "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz",
@@ -2161,7 +2182,6 @@
"version": "3.1.3", "version": "3.1.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
"devOptional": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/damerau-levenshtein": { "node_modules/damerau-levenshtein": {
@@ -2250,6 +2270,15 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/deepmerge": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-2.2.1.tgz",
"integrity": "sha512-R9hc1Xa/NOBi9WRVUWg19rl1UB7Tt4kuPd+thNJgFZoxXsTz7ncaPaeIm+40oSGuP33DfMb4sZt1QIGiJzC4EA==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/define-data-property": { "node_modules/define-data-property": {
"version": "1.1.4", "version": "1.1.4",
"resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz",
@@ -3138,6 +3167,31 @@
"node": ">= 6" "node": ">= 6"
} }
}, },
"node_modules/formik": {
"version": "2.4.6",
"resolved": "https://registry.npmjs.org/formik/-/formik-2.4.6.tgz",
"integrity": "sha512-A+2EI7U7aG296q2TLGvNapDNTZp1khVt5Vk0Q/fyfSROss0V/V6+txt2aJnwEos44IxTCW/LYAi/zgWzlevj+g==",
"funding": [
{
"type": "individual",
"url": "https://opencollective.com/formik"
}
],
"license": "Apache-2.0",
"dependencies": {
"@types/hoist-non-react-statics": "^3.3.1",
"deepmerge": "^2.1.1",
"hoist-non-react-statics": "^3.3.0",
"lodash": "^4.17.21",
"lodash-es": "^4.17.21",
"react-fast-compare": "^2.0.1",
"tiny-warning": "^1.0.2",
"tslib": "^2.0.0"
},
"peerDependencies": {
"react": ">=16.8.0"
}
},
"node_modules/function-bind": { "node_modules/function-bind": {
"version": "1.1.2", "version": "1.1.2",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
@@ -3406,6 +3460,15 @@
"node": ">= 0.4" "node": ">= 0.4"
} }
}, },
"node_modules/hoist-non-react-statics": {
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz",
"integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==",
"license": "BSD-3-Clause",
"dependencies": {
"react-is": "^16.7.0"
}
},
"node_modules/i": { "node_modules/i": {
"version": "0.3.7", "version": "0.3.7",
"resolved": "https://registry.npmjs.org/i/-/i-0.3.7.tgz", "resolved": "https://registry.npmjs.org/i/-/i-0.3.7.tgz",
@@ -4293,6 +4356,18 @@
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
}, },
"node_modules/lodash": {
"version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
"license": "MIT"
},
"node_modules/lodash-es": {
"version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz",
"integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==",
"license": "MIT"
},
"node_modules/lodash.merge": { "node_modules/lodash.merge": {
"version": "4.6.2", "version": "4.6.2",
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
@@ -7486,6 +7561,12 @@
"react-is": "^16.13.1" "react-is": "^16.13.1"
} }
}, },
"node_modules/property-expr": {
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/property-expr/-/property-expr-2.0.6.tgz",
"integrity": "sha512-SVtmxhRE/CGkn3eZY1T6pC8Nln6Fr/lu1mKSgRud0eC73whjGfoAogbn78LkD8aFL0zz3bAFerKSnOl7NlErBA==",
"license": "MIT"
},
"node_modules/proxy-from-env": { "node_modules/proxy-from-env": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
@@ -7544,6 +7625,12 @@
"react": "^19.0.0" "react": "^19.0.0"
} }
}, },
"node_modules/react-fast-compare": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-2.0.4.tgz",
"integrity": "sha512-suNP+J1VU1MWFKcyt7RtjiSWUjvidmQSlqu+eHslq+342xCbGTYmC0mEhPCOHxlW0CywylOC1u2DFAT+bv4dBw==",
"license": "MIT"
},
"node_modules/react-is": { "node_modules/react-is": {
"version": "16.13.1", "version": "16.13.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
@@ -7560,6 +7647,19 @@
"react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
} }
}, },
"node_modules/react-toastify": {
"version": "11.0.5",
"resolved": "https://registry.npmjs.org/react-toastify/-/react-toastify-11.0.5.tgz",
"integrity": "sha512-EpqHBGvnSTtHYhCPLxML05NLY2ZX0JURbAdNYa6BUkk+amz4wbKBQvoKQAB0ardvSarUBuY4Q4s1sluAzZwkmA==",
"license": "MIT",
"dependencies": {
"clsx": "^2.1.1"
},
"peerDependencies": {
"react": "^18 || ^19",
"react-dom": "^18 || ^19"
}
},
"node_modules/reflect.getprototypeof": { "node_modules/reflect.getprototypeof": {
"version": "1.0.10", "version": "1.0.10",
"resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz",
@@ -8230,6 +8330,18 @@
"node": ">=6" "node": ">=6"
} }
}, },
"node_modules/tiny-case": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/tiny-case/-/tiny-case-1.0.3.tgz",
"integrity": "sha512-Eet/eeMhkO6TX8mnUteS9zgPbUMQa4I6Kkp5ORiBD5476/m+PIRiumP5tmh5ioJpH7k51Kehawy2UDfsnxxY8Q==",
"license": "MIT"
},
"node_modules/tiny-warning": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz",
"integrity": "sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==",
"license": "MIT"
},
"node_modules/tinyglobby": { "node_modules/tinyglobby": {
"version": "0.2.12", "version": "0.2.12",
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.12.tgz", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.12.tgz",
@@ -8288,6 +8400,12 @@
"node": ">=8.0" "node": ">=8.0"
} }
}, },
"node_modules/toposort": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/toposort/-/toposort-2.0.2.tgz",
"integrity": "sha512-0a5EOkAUp8D4moMi2W8ZF8jcga7BgZd91O/yabJCFY8az+XSzeGyTKs0Aoo897iV1Nj6guFq8orWDS96z91oGg==",
"license": "MIT"
},
"node_modules/ts-api-utils": { "node_modules/ts-api-utils": {
"version": "2.1.0", "version": "2.1.0",
"resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz", "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz",
@@ -8333,6 +8451,18 @@
"node": ">= 0.8.0" "node": ">= 0.8.0"
} }
}, },
"node_modules/type-fest": {
"version": "2.19.0",
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz",
"integrity": "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==",
"license": "(MIT OR CC0-1.0)",
"engines": {
"node": ">=12.20"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/typed-array-buffer": { "node_modules/typed-array-buffer": {
"version": "1.0.3", "version": "1.0.3",
"resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz",
@@ -8589,6 +8719,18 @@
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
}, },
"node_modules/yup": {
"version": "1.6.1",
"resolved": "https://registry.npmjs.org/yup/-/yup-1.6.1.tgz",
"integrity": "sha512-JED8pB50qbA4FOkDol0bYF/p60qSEDQqBD0/qeIrUCG1KbPBIQ776fCUNb9ldbPcSTxA69g/47XTo4TqWiuXOA==",
"license": "MIT",
"dependencies": {
"property-expr": "^2.0.5",
"tiny-case": "^1.0.3",
"toposort": "^2.0.2",
"type-fest": "^2.19.0"
}
},
"node_modules/zustand": { "node_modules/zustand": {
"version": "5.0.3", "version": "5.0.3",
"resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.3.tgz", "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.3.tgz",
+3
View File
@@ -12,14 +12,17 @@
"@tanstack/react-query": "^5.73.3", "@tanstack/react-query": "^5.73.3",
"@tanstack/react-query-devtools": "^5.74.3", "@tanstack/react-query-devtools": "^5.74.3",
"axios": "^1.8.4", "axios": "^1.8.4",
"formik": "^2.4.6",
"iconsax-react": "^0.0.8", "iconsax-react": "^0.0.8",
"js-cookie": "^3.0.5", "js-cookie": "^3.0.5",
"next": "15.2.3", "next": "15.2.3",
"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",
"react-toastify": "^11.0.5",
"swiper": "^11.2.6", "swiper": "^11.2.6",
"tailwind-merge": "^3.0.2", "tailwind-merge": "^3.0.2",
"yup": "^1.6.1",
"zustand": "^5.0.3" "zustand": "^5.0.3"
}, },
"devDependencies": { "devDependencies": {
File diff suppressed because one or more lines are too long
+11 -9
View File
@@ -3,6 +3,7 @@ import Image from 'next/image'
import { ArrowLeft, Calendar2, Profile } from 'iconsax-react' import { ArrowLeft, Calendar2, Profile } from 'iconsax-react'
import { Blog } from '../types/BlogTypes' import { Blog } from '../types/BlogTypes'
import { toJalaliDate } from '@/config/func' import { toJalaliDate } from '@/config/func'
import Link from 'next/link'
interface BigBlogItemProps { interface BigBlogItemProps {
item: Blog item: Blog
@@ -38,17 +39,18 @@ const BigBlogItem: FC<BigBlogItemProps> = ({ item }) => {
</div> </div>
</div> </div>
<p className='mt-3 text-sm leading-5'> <p dangerouslySetInnerHTML={{ __html: item.previewContent }} className='mt-3 text-sm leading-5'>
{item.previewContent}
</p> </p>
<div className='mt-3 flex gap-1 text-sm text-[#8C90A3]'> <Link href={`/blogs/${item.id}`}>
<div>ادامه ی مطلب</div> <div className='mt-3 flex gap-1 text-sm text-[#8C90A3]'>
<ArrowLeft <div>ادامه ی مطلب</div>
size={18} <ArrowLeft
color='#8C90A3' size={18}
/> color='#8C90A3'
</div> />
</div>
</Link>
</div> </div>
</div> </div>
) )
+1 -2
View File
@@ -27,8 +27,7 @@ const HeroSection: FC = () => {
{item.title} {item.title}
</h4> </h4>
<p className="text-xs xl:text-sm mt-4 leading-7 truncate xl:text-clip xl:whitespace-normal"> <p dangerouslySetInnerHTML={{ __html: item.previewContent }} className="text-xs xl:text-sm mt-4 leading-7 truncate xl:text-clip xl:whitespace-normal">
{item.previewContent}
</p> </p>
</div> </div>
</div> </div>
+3 -3
View File
@@ -1,9 +1,9 @@
import { FC } from 'react' import { FC } from 'react'
import MiniBlogItem from './MiniBlogItem' import MiniBlogItem from './MiniBlogItem'
import { useGetBlogCombined } from '../hooks/useBlogsData' import { useGetMostVisitedBlogs } from '../hooks/useBlogsData'
const MostVisited: FC = () => { const MostVisited: FC = () => {
const { data } = useGetBlogCombined() const { data } = useGetMostVisitedBlogs()
return ( return (
<div className='w-full rounded-4xl bg-white p-4'> <div className='w-full rounded-4xl bg-white p-4'>
<h4> <h4>
@@ -12,7 +12,7 @@ const MostVisited: FC = () => {
<div className='flex flex-col gap-6 mt-7 text-sm'> <div className='flex flex-col gap-6 mt-7 text-sm'>
{ {
data?.data?.mostVisitedBlogs?.map((item) => { data?.data?.blogs?.map((item) => {
return <MiniBlogItem key={item.id} item={item} /> return <MiniBlogItem key={item.id} item={item} />
}) })
} }
+102
View File
@@ -0,0 +1,102 @@
'use client'
import { Profile, Calendar } from 'iconsax-react'
import { NextPage } from 'next'
import Image from 'next/image'
// import MostVisited from '../components/MostVisited'
import ContactUs from '../components/ContactUs'
import { useGetBlogSingle } from '../hooks/useBlogsData'
import { toJalaliDate } from '@/config/func'
import MostVisited from './MostVisited'
type Props = {
id: string
}
const SingleBlog: NextPage<Props> = ({ id }) => {
const { data } = useGetBlogSingle(id)
if (!data) return <div></div>
return (
<div className='mt-24 max-w-maxWidth mx-auto'>
<h1 className='text-2xl font-bold'>
{data?.data?.blog?.title}
</h1>
<div className='mt-10 flex gap-8'>
<div className='flex-1 bg-white rounded-4xl p-10'>
<div className='flex gap-4'>
<div className='flex gap-1'>
<div className='size-5 overflow-hidden rounded-full'>
<Image
src={data?.data?.blog?.category?.iconUrl}
alt={data?.data?.blog?.category?.title}
width={20}
height={20}
/>
</div>
<h6 className='text-[#7D818C] text-sm'>{data?.data?.blog?.category?.title}</h6>
</div>
<div className='flex gap-1'>
<Profile size={20} color='#7D818C' />
<h6 className='text-[#7D818C] text-sm'>{data?.data?.blog?.author.firstName} {data?.data?.blog?.author.lastName}</h6>
</div>
<div className='flex gap-1'>
<Calendar size={20} color='#7D818C' />
<h6 className='text-[#7D818C] text-sm'>{toJalaliDate(data?.data?.blog?.createdAt)}</h6>
</div>
</div>
<div className='mt-6'>
<Image
src={data?.data?.blog?.imageUrl}
alt={data?.data?.blog?.title}
width={1000}
height={1000}
className='w-full rounded-4xl max-h-[400px] object-cover'
/>
</div>
<p dangerouslySetInnerHTML={{ __html: data?.data?.blog?.content }} className='mt-8 text-sm leading-6'>
</p>
<div className='mt-6 flex gap-4 border-y border-[#E5E5E5] py-4'>
<div className='text-sm'>اشتراک گذاری</div>
<Image
src='/images/instagram.svg'
alt='share'
width={20}
height={20}
/>
<Image
src='/images/instagram.svg'
alt='share'
width={20}
height={20}
/>
<Image
src='/images/instagram.svg'
alt='share'
width={20}
height={20}
/>
</div>
</div>
<div className='w-[261px]'>
<MostVisited />
<div className='mt-6'>
<ContactUs />
</div>
<div className='mt-6'>
{/* <MostVisited /> */}
</div>
</div>
</div>
</div>
)
}
export default SingleBlog
+22
View File
@@ -8,6 +8,12 @@ export const useGetBlogs = () =>
queryFn: api.getBlogs, queryFn: api.getBlogs,
}); });
export const useGetMostVisitedBlogs = () =>
useQuery({
queryKey: ["most-visited-blogs"],
queryFn: api.getMostVisitedBlogs,
});
export const useGetBlogsPinned = () => export const useGetBlogsPinned = () =>
useQuery({ useQuery({
queryKey: ["blogs-pinned"], queryKey: ["blogs-pinned"],
@@ -26,12 +32,28 @@ export const useGetBlogCombined = () =>
queryFn: api.getCombinedBlogs, queryFn: api.getCombinedBlogs,
}); });
export const useGetBlogSingle = (id: string) =>
useQuery({
queryKey: ["blog-single", id],
queryFn: () => api.getBlogSingle(id),
});
// برای server prefetch // برای server prefetch
export const blogsQueryOptions = { export const blogsQueryOptions = {
queryKey: ["blogs"], queryKey: ["blogs"],
queryFn: api.getBlogs, queryFn: api.getBlogs,
}; };
export const blogSingleQueryOptions = (id: string) => ({
queryKey: ["blog-single", id],
queryFn: () => api.getBlogSingle(id),
});
export const mostVisitedBlogsQueryOptions = {
queryKey: ["most-visited-blogs"],
queryFn: api.getMostVisitedBlogs,
};
export const blogsPinnedQueryOptions = { export const blogsPinnedQueryOptions = {
queryKey: ["blogs-pinned"], queryKey: ["blogs-pinned"],
queryFn: api.getBlogsPinned, queryFn: api.getBlogsPinned,
+2 -1
View File
@@ -1,7 +1,7 @@
import { HydrationBoundary, dehydrate } from '@tanstack/react-query' import { HydrationBoundary, dehydrate } from '@tanstack/react-query'
import { QueryClient } from '@tanstack/react-query' import { QueryClient } from '@tanstack/react-query'
import BlogList from './components/BlogList' import BlogList from './components/BlogList'
import { blogCategoriesQueryOptions, blogsQueryOptions, blogCombinedQueryOptions } from './hooks/useBlogsData' import { blogCategoriesQueryOptions, blogsQueryOptions, blogCombinedQueryOptions, mostVisitedBlogsQueryOptions } from './hooks/useBlogsData'
export const metadata = { export const metadata = {
title: 'مجله داناک | مقالات و آموزش‌ها', title: 'مجله داناک | مقالات و آموزش‌ها',
@@ -14,6 +14,7 @@ export default async function BlogsPage() {
await queryClient.prefetchQuery(blogsQueryOptions) await queryClient.prefetchQuery(blogsQueryOptions)
await queryClient.prefetchQuery(blogCategoriesQueryOptions) await queryClient.prefetchQuery(blogCategoriesQueryOptions)
await queryClient.prefetchQuery(blogCombinedQueryOptions) await queryClient.prefetchQuery(blogCombinedQueryOptions)
await queryClient.prefetchQuery(mostVisitedBlogsQueryOptions)
return ( return (
<HydrationBoundary state={dehydrate(queryClient)}> <HydrationBoundary state={dehydrate(queryClient)}>
+11
View File
@@ -3,6 +3,7 @@ import {
BlogCategoryResponse, BlogCategoryResponse,
BlogCombinedResponse, BlogCombinedResponse,
BlogResponse, BlogResponse,
BlogSingleResponse,
} from "../types/BlogTypes"; } from "../types/BlogTypes";
export const getBlogs = async () => { export const getBlogs = async () => {
@@ -10,6 +11,11 @@ export const getBlogs = async () => {
return data; return data;
}; };
export const getMostVisitedBlogs = async () => {
const { data } = await axios.get<BlogResponse>(`/blogs/most-visited`);
return data;
};
export const getBlogCategories = async () => { export const getBlogCategories = async () => {
const { data } = await axios.get<BlogCategoryResponse>( const { data } = await axios.get<BlogCategoryResponse>(
`/blogs/categories/public` `/blogs/categories/public`
@@ -26,3 +32,8 @@ export const getCombinedBlogs = async () => {
const { data } = await axios.get<BlogCombinedResponse>(`/blogs/combined`); const { data } = await axios.get<BlogCombinedResponse>(`/blogs/combined`);
return data; return data;
}; };
export const getBlogSingle = async (id: string) => {
const { data } = await axios.get<BlogSingleResponse>(`/blogs/${id}`);
return data;
};
+5
View File
@@ -24,6 +24,7 @@ export interface Blog {
viewCount: number; viewCount: number;
author: Author; author: Author;
category: Category; category: Category;
content: string;
} }
export interface Pager { export interface Pager {
@@ -57,3 +58,7 @@ export type BlogStoreType = {
selectedCategory: string; selectedCategory: string;
setSelectedCategory: (category: string) => void; setSelectedCategory: (category: string) => void;
}; };
export type BlogSingleResponse = ApiResponse<{
blog: Blog;
}>;
+8
View File
@@ -0,0 +1,8 @@
import { useMutation } from "@tanstack/react-query";
import * as api from "../service/ContactService";
export const useCreateContact = () => {
return useMutation({
mutationFn: api.createContact,
});
};
+49 -1
View File
@@ -1,11 +1,47 @@
'use client'
import Button from '@/components/Button' import Button from '@/components/Button'
import Input from '@/components/Input' import Input from '@/components/Input'
import Textarea from '@/components/Textarea' import Textarea from '@/components/Textarea'
import { CallCalling, Location, SmsTracking } from 'iconsax-react' import { CallCalling, Location, SmsTracking } from 'iconsax-react'
import { NextPage } from 'next' import { NextPage } from 'next'
import React from 'react' import React from 'react'
import { CreateContactType } from './types/ContactTypes'
import { useFormik } from 'formik'
import * as Yup from 'yup'
import { useCreateContact } from './hooks/useContactData'
import { toast } from 'react-toastify'
import { ErrorType } from '@/helpers/types'
const Contact: NextPage = () => { const Contact: NextPage = () => {
const createContact = useCreateContact()
const formik = useFormik<CreateContactType>({
initialValues: {
fullName: '',
email: '',
phone: '',
businessName: '',
content: '',
},
validationSchema: Yup.object({
fullName: Yup.string().required('نام وارد کنید'),
email: Yup.string().email('ایمیل معتبر وارد کنید').required('ایمیل وارد کنید'),
phone: Yup.string().required('شماره تماس وارد کنید'),
businessName: Yup.string().required('نام شرکت وارد کنید'),
content: Yup.string().required('پیام وارد کنید'),
}),
onSubmit: (values) => {
createContact.mutate(values, {
onSuccess: () => {
toast.success('پیام شما با موفقیت ثبت شد')
formik.resetForm()
},
onError: (error: ErrorType) => {
toast.error(error.response?.data?.error.message[0])
}
})
},
})
return ( return (
<div className='mt-24 max-w-maxWidth mx-auto'> <div className='mt-24 max-w-maxWidth mx-auto'>
<h1 className='text-2xl font-bold'> <h1 className='text-2xl font-bold'>
@@ -62,23 +98,33 @@ const Contact: NextPage = () => {
<div className='rowTwoInput mt-8'> <div className='rowTwoInput mt-8'>
<Input <Input
placeholder='نام و نام خانوادگی' placeholder='نام و نام خانوادگی'
{...formik.getFieldProps('fullName')}
error_text={formik.touched.fullName && formik.errors.fullName ? formik.errors.fullName : undefined}
/> />
<Input <Input
placeholder='نام شرکت ' placeholder='نام شرکت '
{...formik.getFieldProps('businessName')}
error_text={formik.touched.businessName && formik.errors.businessName ? formik.errors.businessName : undefined}
/> />
</div> </div>
<div className='rowTwoInput mt-8'> <div className='rowTwoInput mt-8'>
<Input <Input
placeholder='شماره تماس' placeholder='شماره تماس'
{...formik.getFieldProps('phone')}
error_text={formik.touched.phone && formik.errors.phone ? formik.errors.phone : undefined}
/> />
<Input <Input
placeholder='ایمیل' placeholder='ایمیل'
{...formik.getFieldProps('email')}
error_text={formik.touched.email && formik.errors.email ? formik.errors.email : undefined}
/> />
</div> </div>
<div className='mt-8'> <div className='mt-8'>
<Textarea <Textarea
label='پیام' label='پیام'
{...formik.getFieldProps('content')}
error_text={formik.touched.content && formik.errors.content ? formik.errors.content : undefined}
/> />
</div> </div>
@@ -86,6 +132,8 @@ const Contact: NextPage = () => {
<Button <Button
label='ارسال' label='ارسال'
className='w-fit px-8' className='w-fit px-8'
onClick={() => formik.handleSubmit()}
isLoading={createContact.isPending}
/> />
</div> </div>
</div> </div>
@@ -0,0 +1,7 @@
import axios from "@/config/axios";
import { CreateContactType } from "../types/ContactTypes";
export const createContact = async (params: CreateContactType) => {
const { data } = await axios.post("/contact-us", params);
return data;
};
+7
View File
@@ -0,0 +1,7 @@
export type CreateContactType = {
fullName: string;
email: string;
phone: string;
businessName: string;
content: string;
};
+5
View File
@@ -94,3 +94,8 @@ tbody tr {
.swiper-button-prev { .swiper-button-prev {
color: white !important; color: white !important;
} }
.Toastify__toast {
direction: rtl;
font-family: "irancell" !important;
}
+1 -1
View File
@@ -9,7 +9,7 @@ const Blog: FC = () => {
return ( return (
<div className='xl:mt-24 mt-14 max-w-maxWidth mx-auto'> <div className='xl:mt-24 mt-14 max-w-maxWidth mx-auto'>
<SectionHeader <SectionHeader
title='بلاگ داناک' title='مجله داناک'
/> />
<div className='flex xl:flex-row flex-col gap-8 xl:mt-16 mt-10 items-center'> <div className='flex xl:flex-row flex-col gap-8 xl:mt-16 mt-10 items-center'>
+2
View File
@@ -3,6 +3,7 @@ import { Geist, Geist_Mono } from "next/font/google";
import "swiper/css"; import "swiper/css";
import "./globals.css"; import "./globals.css";
import "@/assets/fonts/irancell/style.css"; import "@/assets/fonts/irancell/style.css";
import { ToastContainer } from 'react-toastify';
import Header from "@/shared/Header"; import Header from "@/shared/Header";
import Footer from "@/shared/Footer"; import Footer from "@/shared/Footer";
import SideBar from "@/shared/SideBar"; import SideBar from "@/shared/SideBar";
@@ -43,6 +44,7 @@ export default function RootLayout({
<Footer /> <Footer />
</div> </div>
<ReactQueryDevtools initialIsOpen={false} /> <ReactQueryDevtools initialIsOpen={false} />
<ToastContainer />
</QueryProvider> </QueryProvider>
</body> </body>
@@ -0,0 +1,30 @@
'use client'
import { FC } from 'react'
import ServiceItem from '@/components/ServiceItem'
import { useGetDanakServiceSuggested } from './hooks/useProductData'
const DanakSuggestedService: FC = () => {
const { data } = useGetDanakServiceSuggested()
return (
<div className='max-w-maxWidth mx-auto xl:mt-20 mt-12'>
<h2 className='xl:text-2xl text-sm font-bold text-center xl:text-right'>
سرویس های پیشنهادی داناک
</h2>
<div className='xl:mt-14 mt-10 flex xl:flex-row flex-col gap-8'>
{
data?.data?.danakServices?.map((item) => {
return (
<ServiceItem key={item.id} item={item} />
)
})
}
</div>
</div>
)
}
export default DanakSuggestedService
@@ -2,7 +2,7 @@
import { ArrowLeft } from 'iconsax-react' import { ArrowLeft } from 'iconsax-react'
import Image from 'next/image' import Image from 'next/image'
import { Swiper, SwiperSlide } from 'swiper/react' import { Swiper, SwiperSlide } from 'swiper/react'
import DanakSuggestedService from '../home/DanakSuggestedService' import DanakSuggestedService from './DanakSuggestedService'
import { FC } from 'react' import { FC } from 'react'
import DanakServices from './DanakServices' import DanakServices from './DanakServices'
+11
View File
@@ -7,11 +7,22 @@ export const useGetServicesByCategory = (categoryId?: string) =>
queryFn: () => api.getServicesByCategory(categoryId), queryFn: () => api.getServicesByCategory(categoryId),
}); });
export const useGetDanakServiceSuggested = () =>
useQuery({
queryKey: ["danak-service-suggested"],
queryFn: () => api.getDanakServiceSuggested(),
});
export const servicesQueryOptions = { export const servicesQueryOptions = {
queryKey: ["services", ""], queryKey: ["services", ""],
queryFn: () => api.getServicesByCategory(undefined), queryFn: () => api.getServicesByCategory(undefined),
}; };
export const danakServiceSuggestedQueryOptions = {
queryKey: ["danak-service-suggested"],
queryFn: () => api.getDanakServiceSuggested(),
};
export const useGetCategories = () => export const useGetCategories = () =>
useQuery({ useQuery({
queryKey: ["service-categories"], queryKey: ["service-categories"],
+3 -2
View File
@@ -2,12 +2,13 @@ import { NextPage } from 'next'
import { HydrationBoundary } from '@tanstack/react-query' import { HydrationBoundary } from '@tanstack/react-query'
import { dehydrate } from '@tanstack/react-query' import { dehydrate } from '@tanstack/react-query'
import { QueryClient } from '@tanstack/react-query' import { QueryClient } from '@tanstack/react-query'
import { categoriesQueryOptions, servicesQueryOptions } from './hooks/useProductData' import { categoriesQueryOptions, danakServiceSuggestedQueryOptions, servicesQueryOptions } from './hooks/useProductData'
import Products from './Products' import Products from './ProductsPage'
const ProductsPage: NextPage = async () => { const ProductsPage: NextPage = async () => {
const queryClient = new QueryClient() const queryClient = new QueryClient()
await queryClient.prefetchQuery(servicesQueryOptions) await queryClient.prefetchQuery(servicesQueryOptions)
await queryClient.prefetchQuery(categoriesQueryOptions) await queryClient.prefetchQuery(categoriesQueryOptions)
await queryClient.prefetchQuery(danakServiceSuggestedQueryOptions)
return ( return (
<HydrationBoundary state={dehydrate(queryClient)}> <HydrationBoundary state={dehydrate(queryClient)}>
<Products /> <Products />
+12 -1
View File
@@ -1,5 +1,9 @@
import axios from "@/config/axios"; import axios from "@/config/axios";
import { CategoryResponse, ServiceResponse } from "../types/ProductTypes"; import {
CategoryResponse,
DanakServiceSuggestedResponse,
ServiceResponse,
} from "../types/ProductTypes";
export const getServicesByCategory = async (categoryId?: string) => { export const getServicesByCategory = async (categoryId?: string) => {
let query = ``; let query = ``;
@@ -18,3 +22,10 @@ export const getCategoriesPublic = async () => {
); );
return data; return data;
}; };
export const getDanakServiceSuggested = async () => {
const { data } = await axios.get<DanakServiceSuggestedResponse>(
`/danak-services/suggested`
);
return data;
};
+4
View File
@@ -21,3 +21,7 @@ export type ServiceCategoryType = {
parentId: string | null; parentId: string | null;
deletedAt: string | null; deletedAt: string | null;
}; };
export type DanakServiceSuggestedResponse = ApiResponse<{
danakServices: DanakSuggestService[];
}>;
+1 -2
View File
@@ -25,8 +25,7 @@ const BlogItem: FC<Props> = ({ item }) => {
{item.title} {item.title}
</h6> </h6>
<p className='mt-4 text-sm'> <p className='mt-4 text-sm' dangerouslySetInnerHTML={{ __html: item.previewContent }}>
{item.previewContent}
</p> </p>
</div> </div>
</div> </div>
+1 -1
View File
@@ -85,7 +85,7 @@ const SideBar: FC = () => {
<SideBarItem <SideBarItem
icon={<DocumentText variant={isActive('blogs') ? 'Bold' : 'Outline'} color={isActive('blogs') ? 'black' : '#8C90A3'} size={iconSizeSideBar} />} icon={<DocumentText variant={isActive('blogs') ? 'Bold' : 'Outline'} color={isActive('blogs') ? 'black' : '#8C90A3'} size={iconSizeSideBar} />}
title={'بلاگ'} title={'مجله'}
isActive={isActive('blogs')} isActive={isActive('blogs')}
link={'/blogs'} link={'/blogs'}
/> />