Commit 3d99fb77 authored by Phan Ngọc Quốc Văn's avatar Phan Ngọc Quốc Văn

Msdasderge branch 'feature/home_page' of...

Msdasderge branch 'feature/home_page' of https://gitlab.meu-solutions.com/vanhoangit/vcci-news into feature/home_page
parents 04d56f7d 53828441
......@@ -39,3 +39,6 @@ yarn-error.log*
# typescript
*.tsbuildinfo
next-env.d.ts
#vscode
.vscode/
\ No newline at end of file
......@@ -44,6 +44,9 @@ importers:
dayjs:
specifier: ^1.11.19
version: 1.11.19
html-react-parser:
specifier: ^5.2.7
version: 5.2.7(@types/react@19.2.2)(react@19.2.0)
lucide-react:
specifier: ^0.548.0
version: 0.548.0(react@19.2.0)
......@@ -71,6 +74,9 @@ importers:
tailwindcss-animate:
specifier: ^1.0.7
version: 1.0.7(tailwindcss@4.1.16)
uuid:
specifier: ^13.0.0
version: 13.0.0
zod:
specifier: ^4.1.12
version: 4.1.12
......@@ -1768,6 +1774,19 @@ packages:
resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==}
engines: {node: '>=0.10.0'}
dom-serializer@2.0.0:
resolution: {integrity: sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==}
domelementtype@2.3.0:
resolution: {integrity: sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==}
domhandler@5.0.3:
resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==}
engines: {node: '>= 4'}
domutils@3.2.2:
resolution: {integrity: sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==}
dunder-proto@1.0.1:
resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==}
engines: {node: '>= 0.4'}
......@@ -1793,6 +1812,10 @@ packages:
resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==}
engines: {node: '>=0.12'}
entities@6.0.1:
resolution: {integrity: sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==}
engines: {node: '>=0.12'}
es-abstract@1.24.0:
resolution: {integrity: sha512-WSzPgsdLtTcQwm4CROfS5ju2Wa1QQcVeT37jFjYzdFz1r9ahadC8B8/a4qxJxM+09F18iumCdRmlr96ZYkQvEg==}
engines: {node: '>= 0.4'}
......@@ -2167,6 +2190,21 @@ packages:
hermes-parser@0.25.1:
resolution: {integrity: sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==}
html-dom-parser@5.1.1:
resolution: {integrity: sha512-+o4Y4Z0CLuyemeccvGN4bAO20aauB2N9tFEAep5x4OW34kV4PTarBHm6RL02afYt2BMKcr0D2Agep8S3nJPIBg==}
html-react-parser@5.2.7:
resolution: {integrity: sha512-WzIAcqQoZoF49J9aev8NBDLz9TJvt2RmipeYA+/5+5x0sWCwFxqKiq0lysieiSA/G6dbUZ6KGGy65Cx2fjie5Q==}
peerDependencies:
'@types/react': 0.14 || 15 || 16 || 17 || 18 || 19
react: 0.14 || 15 || 16 || 17 || 18 || 19
peerDependenciesMeta:
'@types/react':
optional: true
htmlparser2@10.0.0:
resolution: {integrity: sha512-TwAZM+zE5Tq3lrEHvOlvwgj1XLWQCtaaibSN11Q+gGBAS7Y1uZSWwXXRe4iF6OXnaq1riyQAPFOBtYc77Mxq0g==}
http2-client@1.3.5:
resolution: {integrity: sha512-EC2utToWl4RKfs5zd36Mxq7nzHHBuomZboI0yYL6Y0RmBgT7Sgkq4rQ0ezFTYoIsSs7Tm9SJe+o2FcAg6GBhGA==}
......@@ -2196,6 +2234,9 @@ packages:
inflected@2.1.0:
resolution: {integrity: sha512-hAEKNxvHf2Iq3H60oMBHkB4wl5jn3TPF3+fXek/sRwAB5gP9xWs4r7aweSF95f99HFoz69pnZTcu8f0SIHV18w==}
inline-style-parser@0.2.4:
resolution: {integrity: sha512-0aO8FkhNZlj/ZIbNi7Lxxr12obT7cL1moPfE4tg1LkX7LlLfC6DeX4l2ZEud1ukP9jNQyNnfzQVqwbwmAATY4Q==}
internal-slot@1.1.0:
resolution: {integrity: sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==}
engines: {node: '>= 0.4'}
......@@ -2830,6 +2871,9 @@ packages:
react-is@16.13.1:
resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==}
react-property@2.0.2:
resolution: {integrity: sha512-+PbtI3VuDV0l6CleQMsx2gtK0JZbZKbpdu5ynr+lbsuvtmgbNcS3VM0tuY2QjFNOcWxvXeHjDpy42RO+4U2rug==}
react-remove-scroll-bar@2.3.8:
resolution: {integrity: sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==}
engines: {node: '>=10'}
......@@ -3065,6 +3109,12 @@ packages:
resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==}
engines: {node: '>=8'}
style-to-js@1.1.18:
resolution: {integrity: sha512-JFPn62D4kJaPTnhFUI244MThx+FEGbi+9dw1b9yBBQ+1CZpV7QAT8kUtJ7b7EUNdHajjF/0x8fT+16oLJoojLg==}
style-to-object@1.0.11:
resolution: {integrity: sha512-5A560JmXr7wDyGLK12Nq/EYS38VkGlglVzkis1JEdbGWSnbQIEhZzTJhzURXN5/8WwwFCs/f/VVcmkTppbXLow==}
styled-jsx@5.1.6:
resolution: {integrity: sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==}
engines: {node: '>= 12.0.0'}
......@@ -3267,6 +3317,10 @@ packages:
resolution: {integrity: sha512-6Z7Ma2aVEWisaL6TvBCy7P8rm2LQoPv6dJ7ecIaIixHcwfbJ0x7mWdbcwlIM5IGQxPZSFYeqRCqlOOeKoJYMkw==}
engines: {node: '>= 4'}
uuid@13.0.0:
resolution: {integrity: sha512-XQegIaBTVUjSHliKqcnFqYypAd4S+WCYt5NIeRs6w/UAry7z8Y9j5ZwRRL4kzq9U3sD6v+85er9FvkEaBpji2w==}
hasBin: true
v8-compile-cache-lib@3.0.1:
resolution: {integrity: sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==}
......@@ -5089,6 +5143,24 @@ snapshots:
dependencies:
esutils: 2.0.3
dom-serializer@2.0.0:
dependencies:
domelementtype: 2.3.0
domhandler: 5.0.3
entities: 4.5.0
domelementtype@2.3.0: {}
domhandler@5.0.3:
dependencies:
domelementtype: 2.3.0
domutils@3.2.2:
dependencies:
dom-serializer: 2.0.0
domelementtype: 2.3.0
domhandler: 5.0.3
dunder-proto@1.0.1:
dependencies:
call-bind-apply-helpers: 1.0.2
......@@ -5113,6 +5185,8 @@ snapshots:
entities@4.5.0: {}
entities@6.0.1: {}
es-abstract@1.24.0:
dependencies:
array-buffer-byte-length: 1.0.2
......@@ -5679,6 +5753,28 @@ snapshots:
dependencies:
hermes-estree: 0.25.1
html-dom-parser@5.1.1:
dependencies:
domhandler: 5.0.3
htmlparser2: 10.0.0
html-react-parser@5.2.7(@types/react@19.2.2)(react@19.2.0):
dependencies:
domhandler: 5.0.3
html-dom-parser: 5.1.1
react: 19.2.0
react-property: 2.0.2
style-to-js: 1.1.18
optionalDependencies:
'@types/react': 19.2.2
htmlparser2@10.0.0:
dependencies:
domelementtype: 2.3.0
domhandler: 5.0.3
domutils: 3.2.2
entities: 6.0.1
http2-client@1.3.5: {}
human-signals@8.0.1: {}
......@@ -5698,6 +5794,8 @@ snapshots:
inflected@2.1.0: {}
inline-style-parser@0.2.4: {}
internal-slot@1.1.0:
dependencies:
es-errors: 1.3.0
......@@ -6326,6 +6424,8 @@ snapshots:
react-is@16.13.1: {}
react-property@2.0.2: {}
react-remove-scroll-bar@2.3.8(@types/react@19.2.2)(react@19.2.0):
dependencies:
react: 19.2.0
......@@ -6629,6 +6729,14 @@ snapshots:
strip-json-comments@3.1.1: {}
style-to-js@1.1.18:
dependencies:
style-to-object: 1.0.11
style-to-object@1.0.11:
dependencies:
inline-style-parser: 0.2.4
styled-jsx@5.1.6(@babel/core@7.28.5)(react@19.2.0):
dependencies:
client-only: 0.0.1
......@@ -6853,6 +6961,8 @@ snapshots:
utility-types@3.11.0: {}
uuid@13.0.0: {}
v8-compile-cache-lib@3.0.1: {}
validator@13.15.20: {}
......
......@@ -27,13 +27,43 @@ AXIOS_INSTANCE.interceptors.response.use(
(error) => Promise.reject(error)
)
const useCustomClient = <T>(config: AxiosRequestConfig, options?: AxiosRequestConfig): Promise<T> => {
// Helper function to convert RequestInit headers to Axios format
const convertHeaders = (headers?: HeadersInit): Record<string, string> | undefined => {
if (!headers) return undefined
if (headers instanceof Headers) {
const result: Record<string, string> = {}
headers.forEach((value, key) => {
result[key] = value
})
return result
}
if (Array.isArray(headers)) {
const result: Record<string, string> = {}
headers.forEach(([key, value]) => {
result[key] = value
})
return result
}
return headers as Record<string, string>
}
const useCustomClient = <T>(url: string, options?: RequestInit): Promise<T> => {
const source = Axios.CancelToken.source()
const promise = AXIOS_INSTANCE({
...config,
...options,
// Convert RequestInit to AxiosRequestConfig
const axiosConfig: AxiosRequestConfig = {
url,
method: options?.method || 'GET',
headers: convertHeaders(options?.headers),
data: options?.body,
signal: options?.signal || undefined,
cancelToken: source.token
}).then(({ data, status }) => {
}
const promise = AXIOS_INSTANCE(axiosConfig).then(({ data, status }) => {
return data instanceof Blob ? data : { ...data, statusCode: status }
})
......
export interface NewsItem {
export interface CategoryAdminItem {
id: string
title: string
thumbnail: string
external_link: string
name: string
description: string
release_at: string
is_active: boolean
created_at: string
created_by: string | null
updated_at: string
updated_by: string | null
mode: 'NOW' | string
category: string
}
export interface NewsResponseData {
export interface CategoryAdminResponseData {
count: number
rows: NewsItem[]
rows: CategoryAdminItem[]
totalPages: number
currentPage: number
}
export interface GetNewsResponseType {
export interface GetCategoryAdminResponseType {
message: string
message_en: string
responseData: NewsResponseData
responseData: CategoryAdminResponseData
status: 'success' | 'error'
timeStamp: string
violations: any | null
}
\ No newline at end of file
}
export interface NewsAdminItem {
id: string
title: string
thumbnail: string
external_link: string
description: string
release_at: string
is_active: boolean
created_at: string
created_by: string | null
updated_at: string
updated_by: string | null
mode: 'NOW' | string
category: string
}
export interface NewsAdminResponseData {
count: number
rows: NewsAdminItem[]
totalPages: number
currentPage: number
}
export interface GetNewsAdminResponseType {
message: string
message_en: string
responseData: NewsAdminResponseData
status: 'success' | 'error'
timeStamp: string
violations: any | null
}
//Core
import dayjs from 'dayjs'
// App
import { Spinner } from '@/components/ui'
import AppEditorContent from '@/components/shared/editor-content'
import BASE_URLS from '@/links'
// import { useGetNewsId } from '#/api/endpoints/news';
import { NewsAdminItem } from '@/api/types/news';
import { Link, CalendarFold, Book } from 'lucide-react';
// import { t } from 'i18next'
// Component
const NewsDetailPage = (params: { id: string }) => {
// server
// const { data, isLoading, isError } = useGetNewsId<NewsAdminItem>(getIdFormUrl)
// const { t, i18n } = useTranslation('newsPage')
// const { routeParams, data } = usePageContext()
// const { newsDetail, fallbackClient } = data as Data
// const lang = i18n.language == 'vi' ? 'vi' : 'vi'
// const infoNews = useMemo(() => {
// if (!fallbackClient) return newsDetail?.responseData
// if (!getNewsIdQuery.data) return
// return getNewsIdQuery.data.responseData
// }, [newsDetail?.responseData, fallbackClient, getNewsIdQuery.data])
// Effects
// useEffect(() => {
// // Update document title
// infoNews?.title && (document.title = infoNews.title)
// }, [infoNews])
const isLoading = false;
// Template
return (
<div className='user-news-detail-page pb-10'>
{isLoading ? (
<Spinner />
) : (
<div>
{/* Banner */}
<img src={`${BASE_URLS.imageEndpoint}${data?.responseData?.thumbnail}`} alt='Banner' />
{/* Breadcrumb */}
<div className='app-container py-10'>
<div className='flex gap-4 items-stretch'>
{/* <Breadcrumbs aria-label='breadcrumb'>
<Link
underline='hover'
color='inherit'
href='/'
className='!text-app-blue-secondary !text-sm !leading-normal'
>
{t('breadcrumbHomePage')}
<p>Trang chủ</p>
</Link>
<Link
underline='hover'
color='inherit'
href='/tin-tuc'
className='!text-app-blue-secondary !text-sm !leading-normal'
>
{t('breadcrumbNewsPage')}
<p>Tin tức</p>
</Link>
<Typography className='!text-sm !text-black !leading-normal'>
{t('breadcrumbNewsDetailPage')}
<p>Chi tiết</p>
</Typography>
</Breadcrumbs> */}
</div>
</div>
<div className='app-container bg-white rounded p-10 flex flex-col gap-10'>
{/* Heading */}
<div className='text-app-blue text-[32px] leading-normal font-medium text-center'>{data?.responseData?.title}</div>
{/* body */}
<div className='flex items-start gap-8 flex-col lg:flex-row'>
{/* Info */}
<div className='lg:w-[332px] bg-white p-4 rounded shadow-app-quaternary flex flex-col gap-2'>
<div className='text-base text-app-grey font-semibold leading-normal text-center'>
{/* {t('information')} */}
</div>
{/* date */}
<div className='flex items-center gap-2 text-app-grey'>
<CalendarFold />
<span className='text-base'>{dayjs(data?.responseData?.created_at).format('DD/MM/YYYY')}</span>
</div>
{/* author */}
{/* <div className='flex items-center gap-2 text-app-grey'>
<PersonIcon />
<span className='text-base'>{data?.responseData?.created_by}</span>
</div> */}
{/* Category */}
<div className='flex items-center gap-2 text-app-grey'>
<Book />
<span className='text-base'>{data?.responseData?.category}</span>
</div>
</div>
{/* Content */}
<div className='flex-1 text-app-grey text-base overflow-hidden'>
<AppEditorContent value={data?.responseData?.description ?? ''} />
</div>
</div>
</div>
{/* Related News */}
{/* <RelatedNews newsQuery={getRelatedNewsQuery} lang={lang} newsId={infoNews.id} /> */}
</div>
)}
</div>
)
}
export default NewsDetailPage
\ No newline at end of file
import { NewsItem } from '../../page.type'
// import BASE_URL from '#/config'
import { NewsAdminItem } from '@/api/types/news'
import BASE_URL from '@/links'
import dayjs from 'dayjs';
// import { AppEditorContent } from '#/components';
import AppEditorContent from '@/components/shared/editor-content';
function NewsContent({ news }: { news: NewsItem }) {
function NewsContent({ news }: { news: NewsAdminItem }) {
return (
<a
href={`/tin-tuc/${news.id}`}
className='flex flex-col sm:flex-row gap-4 mb-3 bg-white rounded-lg shadow-sm p-3'
>
<img
// src={`${BASE_URL.imageEndpoint}${news.thumbnail}`}
src={news.thumbnail}
alt={news.title}
className='w-full sm:w-[120px] h-[80px] object-cover rounded-sm'
/>
<div className='flex-1'>
<p className='text-[#0056b3] font-semibold text-base hover:underline line-clamp-2'>
{news.title}
</p>
<div className='text-gray-500 text-sm my-1'>{dayjs(news.release_at).format('DD/MM/YYYY')}</div>
{/* <AppEditorContent className='line-clamp-4' value={news.description} /> */}
</div>
</a>
);
return (
<a
href={`/tin-tuc/${news.id}`}
className='flex flex-col sm:flex-row gap-4 mb-3 bg-white rounded-lg shadow-sm p-3'
>
<img
// src={`${BASE_URL.imageEndpoint}${news.thumbnail}`}
src={news.thumbnail}
alt={news.title}
className='w-full sm:w-[120px] h-[80px] object-cover rounded-sm'
/>
<div className='flex-1'>
<p className='text-[#0056b3] font-semibold text-base hover:underline line-clamp-2'>
{news.title}
</p>
<div className='text-gray-500 text-sm my-1'>{dayjs(news.release_at).format('DD/MM/YYYY')}</div>
{/* <AppEditorContent className='line-clamp-4' value={news.description} /> */}
</div>
</a>
);
}
export default NewsContent;
\ No newline at end of file
export { default } from './NewsContent';
\ No newline at end of file
This diff is collapsed.
.app-editor-container {
min-height: unset !important;
height: auto !important;
border: none !important;
border-radius: 0px !important;
background-color: transparent !important;
line-height: 1.5;
.jodit-wysiwyg {
padding: 0 !important;
overflow-x: visible !important;
}
}
\ No newline at end of file
import { FC, JSX } from 'react';
import htmlParse, { DOMNode, Element, Text } from 'html-react-parser';
import { AppEditorContentProps } from './AppEditorContent.type';
import './AppEditorContent.css';
const AppEditorContent: FC<AppEditorContentProps> = ({ value = '', className = '' }) => {
const transform = (node: DOMNode): JSX.Element | null => {
if (node instanceof Element && node.tagName === 'strong') {
return (
<strong className="custom-strong">
{node.children && Array.isArray(node.children)
? node.children.map((child, index) => {
if (typeof child === 'string') {
return child;
} else if (child instanceof Text) {
return child.data;
}
return null;
})
: null}
</strong>
);
}
return null;
};
return (
<div className="jodit-container app-editor-container">
<div className={`jodit-wysiwyg ${className}`}>{htmlParse(value, { replace: transform })}</div>
</div>
);
};
export default AppEditorContent;
// Props
interface AppEditorContentProps {
value: string
className?: string
}
export type { AppEditorContentProps }
import AppEditorContent from './AppEditorContent'
export default AppEditorContent
......@@ -3,7 +3,7 @@
@theme inline {
/* Fonts */
--font-reddit-sans: RedditSans, sans-serif;
--default-font-family: 'SF Pro Display', -apple-system;
--default-font-family: 'Roboto', sans-serif;
/* Border Radius */
--radius-lg: hsl(var(--radius));
--radius-md: calc(var(--radius) - 2px);
......@@ -64,4 +64,4 @@
--color-side-bar-ring: hsl(var(--sidebar-ring));
/* Header */
--color-header-primary: hsl(var(--header-primary));
}
}
\ No newline at end of file
......@@ -3,7 +3,7 @@
@apply mr-auto ml-auto px-4;
@media (width >=80rem) {
@apply px-20;
@apply px-40;
}
}
......@@ -12,7 +12,7 @@
}
.sun-editor-editable {
font-family: 'Roboto', sans-serif;
font-family: 'Be Vietnam Pro', sans-serif;
font-size: 16px;
background-color: transparent;
}
......
......@@ -19,7 +19,17 @@
}
],
"paths": {
"@/*": ["./src/*"]
"@/*": ["./src/*"],
"@components/*": ["./src/components/*"],
"@hooks/*": ["./src/hooks/*"],
"@api/*": ["./src/api/*"],
"@lib/*": ["./lib/*"],
"@public/*": ["./public/*"],
"@store/*": ["./src/store/*"],
"@links/*": ["./src/links/*"],
"@app/*": ["./src/app/(main)/*"]
}
},
"include": [
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment