update
This commit is contained in:
29
src/features/Article/ArticleTopLeft/index.tsx
Normal file
29
src/features/Article/ArticleTopLeft/index.tsx
Normal file
@@ -0,0 +1,29 @@
|
||||
'use client';
|
||||
|
||||
import ItemArticle from '@/components/Common/ItemArticle';
|
||||
import { getArticles } from '@/lib/api/article';
|
||||
import { useApiData } from '@/hooks/useApiData';
|
||||
import type { ListArticle } from '@/types/article/TypeListArticle';
|
||||
|
||||
export const ArticleTopLeft = () => {
|
||||
const { data: articles } = useApiData(
|
||||
() => getArticles(),
|
||||
[],
|
||||
{ initialData: [] as ListArticle },
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="flex gap-3">
|
||||
<div className="box-left">
|
||||
{articles.slice(0, 1).map((item) => (
|
||||
<ItemArticle item={item} key={item.id} />
|
||||
))}
|
||||
</div>
|
||||
<div className="box-right flex flex-1 flex-col gap-3">
|
||||
{articles.slice(0, 4).map((item) => (
|
||||
<ItemArticle item={item} key={item.id} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
42
src/features/Article/ArticleTopRight/index.tsx
Normal file
42
src/features/Article/ArticleTopRight/index.tsx
Normal file
@@ -0,0 +1,42 @@
|
||||
'use client';
|
||||
|
||||
import Link from 'next/link';
|
||||
import { getArticles } from '@/lib/api/article';
|
||||
import { useApiData } from '@/hooks/useApiData';
|
||||
import type { ListArticle } from '@/types/article/TypeListArticle';
|
||||
|
||||
export const ArticleTopRight = () => {
|
||||
const { data: articles } = useApiData(
|
||||
() => getArticles(),
|
||||
[],
|
||||
{ initialData: [] as ListArticle },
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="col-right-article box-view-article flex-1">
|
||||
<form
|
||||
method="get"
|
||||
action="/tim-bai"
|
||||
name="search"
|
||||
className="boder-radius-10 border-box-article article-search-container"
|
||||
>
|
||||
<input type="text" name="q" placeholder="Tìm kiếm bài viết" defaultValue="" />
|
||||
<button type="submit" className="fas fa-search"></button>
|
||||
</form>
|
||||
|
||||
<div className="boder-radius-10 border-box-article">
|
||||
<div className="title-box-article font-bold">Xem nhiều</div>
|
||||
<ul className="list-most-view-article flex flex-col gap-4">
|
||||
{articles.slice(0, 6).map((item, index) => (
|
||||
<li className="item-most-view-article flex items-center gap-2" key={item.id}>
|
||||
<span className="number flex items-center justify-center font-[600]">{index + 1}</span>
|
||||
<Link href={item.url} className="line-clamp-2 flex-1">
|
||||
{item.title}
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
132
src/features/Article/CategoryPage/index.tsx
Normal file
132
src/features/Article/CategoryPage/index.tsx
Normal file
@@ -0,0 +1,132 @@
|
||||
'use client';
|
||||
|
||||
import Link from 'next/link';
|
||||
import Image from 'next/image';
|
||||
|
||||
import type { TypeArticleCatePage } from '@/types/article/TypeArticleCatePage';
|
||||
|
||||
import { Breadcrumb } from '@components/Common/Breadcrumb';
|
||||
import { ErrorLink } from '@components/Common/Error';
|
||||
import { ArticleTopLeft } from '../ArticleTopLeft';
|
||||
import { ArticleTopRight } from '../ArticleTopRight';
|
||||
import ItemArticle from '@/components/Common/ItemArticle';
|
||||
import PreLoader from '@/components/Common/PreLoader';
|
||||
import { getArticleCategories, getArticleCategoryDetail, getArticles } from '@/lib/api/article';
|
||||
import { useApiData } from '@/hooks/useApiData';
|
||||
import type { TypeArticleCategory } from '@/types/article/ListCategoryArticle';
|
||||
import type { ListArticle } from '@/types/article/TypeListArticle';
|
||||
|
||||
interface CategoryPageProps {
|
||||
slug: string;
|
||||
}
|
||||
|
||||
const ArticleCategoryPage: React.FC<CategoryPageProps> = ({ slug }) => {
|
||||
const { data: currentCategory, isLoading } = useApiData(
|
||||
() => getArticleCategoryDetail(slug),
|
||||
[slug],
|
||||
{ initialData: null as TypeArticleCatePage | null },
|
||||
);
|
||||
const { data: categories } = useApiData(
|
||||
() => getArticleCategories(),
|
||||
[],
|
||||
{ initialData: [] as TypeArticleCategory[] },
|
||||
);
|
||||
const { data: articles } = useApiData(
|
||||
() => getArticles(),
|
||||
[],
|
||||
{ initialData: [] as ListArticle },
|
||||
);
|
||||
|
||||
if (isLoading) {
|
||||
return <PreLoader />;
|
||||
}
|
||||
|
||||
if (!currentCategory) {
|
||||
return <ErrorLink />;
|
||||
}
|
||||
|
||||
const breadcrumbItems = [
|
||||
{ name: 'Tin tức', url: '/tin-tuc' },
|
||||
{ name: currentCategory.category_info.name, url: currentCategory.category_info.request_path },
|
||||
];
|
||||
|
||||
const articleList = Object.values(currentCategory.article_list);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="container">
|
||||
<Breadcrumb items={breadcrumbItems} />
|
||||
</div>
|
||||
<section className="page-article page-article-category container">
|
||||
<div className="tabs-category-article flex items-center">
|
||||
{categories.map((item, index) => (
|
||||
<Link
|
||||
href={item.url}
|
||||
key={`${item.id}-${index}`}
|
||||
className={`item-tab-article ${currentCategory.title === item.title ? 'active' : ''}`}
|
||||
>
|
||||
<h2 className="title-cate-article font-[400]">{item.title}</h2>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
<div className="box-article-home-top grid grid-cols-3 gap-3">
|
||||
<div className="col-left-article border-box-article box-new-article boder-radius-10 col-span-2">
|
||||
<ArticleTopLeft />
|
||||
</div>
|
||||
<ArticleTopRight />
|
||||
</div>
|
||||
|
||||
<div className="box-article-home-middle mt-5 grid grid-cols-3 gap-3">
|
||||
<div className="box-article-tech col-left-article boder-radius-10 border-box-article col-span-2">
|
||||
<p className="title-box-article font-[600]">{currentCategory.title}</p>
|
||||
<div className="list-article-tech">
|
||||
{articleList.slice(0, 9).map((item) => (
|
||||
<ItemArticle item={item} key={item.id} />
|
||||
))}
|
||||
</div>
|
||||
<Link
|
||||
href="/tin-cong-nghe"
|
||||
className="btn-article-col flex items-center justify-center gap-2 font-[500]"
|
||||
>
|
||||
Xem tất cả
|
||||
</Link>
|
||||
</div>
|
||||
<div className="col-right-article page-hompage flex-1">
|
||||
<div className="box-article-global border-box-article boder-radius-10">
|
||||
<p className="title-box-article font-bold">Tin nổi bật</p>
|
||||
<div className="list-article-global flex flex-col gap-2">
|
||||
{articles.slice(0, 5).map((item) => (
|
||||
<div className="item-article flex gap-4" key={item.id}>
|
||||
<Link href={item.url} className="img-article boder-radius-10 relative">
|
||||
<Image
|
||||
className="boder-radius-10"
|
||||
src={item.image.original}
|
||||
fill
|
||||
alt={item.title}
|
||||
/>
|
||||
|
||||
<i className="sprite sprite-icon-play-video-detail icon-video-feature incon-play-youtube"></i>
|
||||
<i className="sprite sprite-play-youtube incon-play-youtube"></i>
|
||||
</Link>
|
||||
<div className="content-article content-article-item flex flex-1 flex-col">
|
||||
<Link href={item.url} className="title-article">
|
||||
<h3 className="line-clamp-2 font-[400]">{item.title}</h3>
|
||||
</Link>
|
||||
<p className="time-article flex items-center gap-2">
|
||||
<i className="sprite sprite-clock-item-article"></i>
|
||||
<span>{item.createDate}</span>
|
||||
</p>
|
||||
<p className="descreption-article line-clamp-2">{item.summary}</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ArticleCategoryPage;
|
||||
113
src/features/Article/DetailPage/TocBox/index.tsx
Normal file
113
src/features/Article/DetailPage/TocBox/index.tsx
Normal file
@@ -0,0 +1,113 @@
|
||||
'use client';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
type HeadingItem = {
|
||||
id: string;
|
||||
text: string;
|
||||
level: number;
|
||||
children?: HeadingItem[];
|
||||
};
|
||||
|
||||
function convertToSlug(text: string) {
|
||||
return text
|
||||
.toLowerCase()
|
||||
.normalize('NFD')
|
||||
.replace(/[\u0300-\u036f]/g, '')
|
||||
.replace(/đ/g, 'd')
|
||||
.replace(/[^\w ]+/g, '')
|
||||
.trim()
|
||||
.replace(/\s+/g, '-');
|
||||
}
|
||||
|
||||
// Hàm xây dựng cây TOC từ danh sách heading
|
||||
function buildTree(headings: HeadingItem[]): HeadingItem[] {
|
||||
const root: HeadingItem[] = [];
|
||||
const stack: HeadingItem[] = [];
|
||||
|
||||
headings.forEach((h) => {
|
||||
const node = { ...h, children: [] };
|
||||
|
||||
while (stack.length && stack[stack.length - 1].level >= node.level) {
|
||||
stack.pop();
|
||||
}
|
||||
|
||||
if (stack.length === 0) {
|
||||
root.push(node);
|
||||
} else {
|
||||
stack[stack.length - 1].children!.push(node);
|
||||
}
|
||||
|
||||
stack.push(node);
|
||||
});
|
||||
|
||||
return root;
|
||||
}
|
||||
|
||||
function renderTree(nodes: HeadingItem[]) {
|
||||
return (
|
||||
<ol>
|
||||
{nodes.map((n) => (
|
||||
<li key={n.id}>
|
||||
<a
|
||||
href={`#${n.id}`}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
const el = document.getElementById(n.id);
|
||||
if (el) {
|
||||
const y = el.getBoundingClientRect().top + window.scrollY - 120;
|
||||
window.scrollTo({ top: y, behavior: 'smooth' });
|
||||
}
|
||||
}}
|
||||
className="text-blue-600 hover:underline"
|
||||
>
|
||||
{n.text}
|
||||
</a>
|
||||
{n.children && n.children.length > 0 && renderTree(n.children)}
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
);
|
||||
}
|
||||
|
||||
export default function TocBox({ htmlContent }: { htmlContent: string }) {
|
||||
const { headingsTree, contentWithIds } = useMemo(() => {
|
||||
const parser = new DOMParser();
|
||||
const doc = parser.parseFromString(htmlContent, 'text/html');
|
||||
const nodes = doc.querySelectorAll('h1,h2,h3,h4,h5,h6');
|
||||
|
||||
const flat: HeadingItem[] = Array.from(nodes).map((node) => {
|
||||
const text = node.textContent || '';
|
||||
const id = convertToSlug(text);
|
||||
node.setAttribute('id', id);
|
||||
return {
|
||||
id,
|
||||
text,
|
||||
level: parseInt(node.tagName.substring(1)),
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
headingsTree: buildTree(flat),
|
||||
contentWithIds: doc.body.innerHTML,
|
||||
};
|
||||
}, [htmlContent]);
|
||||
|
||||
if (!headingsTree.length) return null;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="archor-text-group">
|
||||
<div className="toc_title flex items-center justify-between gap-2">
|
||||
<b className="text-fint-toc flex items-center text-base font-bold">
|
||||
<span>Nội dung chính</span>
|
||||
</b>
|
||||
</div>
|
||||
<div id="js-outp">{renderTree(headingsTree)}</div>
|
||||
</div>
|
||||
<div
|
||||
className="box-article-detail-ct nd js_find"
|
||||
dangerouslySetInnerHTML={{ __html: contentWithIds }}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
117
src/features/Article/DetailPage/index.tsx
Normal file
117
src/features/Article/DetailPage/index.tsx
Normal file
@@ -0,0 +1,117 @@
|
||||
'use client';
|
||||
|
||||
import Link from 'next/link';
|
||||
import Image from 'next/image';
|
||||
|
||||
import type { TypeArticleDetailPage } from '@/types/article/TypeArticleDetailPage';
|
||||
import { ErrorLink } from '@components/Common/Error';
|
||||
|
||||
import { Breadcrumb } from '@components/Common/Breadcrumb';
|
||||
import TocBox from './TocBox';
|
||||
import PreLoader from '@/components/Common/PreLoader';
|
||||
import { getArticleCategories, getArticleDetail } from '@/lib/api/article';
|
||||
import { useApiData } from '@/hooks/useApiData';
|
||||
import type { TypeArticleCategory } from '@/types/article/ListCategoryArticle';
|
||||
|
||||
interface DetailPageProps {
|
||||
slug: string;
|
||||
}
|
||||
|
||||
const ArticleDetailPage: React.FC<DetailPageProps> = ({ slug }) => {
|
||||
const { data: page, isLoading } = useApiData(
|
||||
() => getArticleDetail(slug),
|
||||
[slug],
|
||||
{ initialData: null as TypeArticleDetailPage | null },
|
||||
);
|
||||
const { data: categories } = useApiData(
|
||||
() => getArticleCategories(),
|
||||
[],
|
||||
{ initialData: [] as TypeArticleCategory[] },
|
||||
);
|
||||
|
||||
if (isLoading) {
|
||||
return <PreLoader />;
|
||||
}
|
||||
|
||||
if (!page) {
|
||||
return <ErrorLink />;
|
||||
}
|
||||
|
||||
const breadcrumbItems = [
|
||||
{ name: 'Tin tức', url: '/tin-tuc' },
|
||||
{ name: page.article_detail.title, url: page.article_detail.url },
|
||||
];
|
||||
|
||||
const listRelayNew = Object.values(page.article_other_same_category.new);
|
||||
const listRelayOld = Object.values(page.article_other_same_category.old);
|
||||
const combinedList = [...listRelayNew.slice(0, 6), ...listRelayOld.slice(0, 6)];
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="container">
|
||||
<Breadcrumb items={breadcrumbItems} />
|
||||
</div>
|
||||
<section className="page-article box-article-detail container">
|
||||
<div className="tabs-category-article flex items-center">
|
||||
{categories.map((item, index) => (
|
||||
<Link
|
||||
href={item.url}
|
||||
key={`${item.id}-${index}`}
|
||||
className={`item-tab-article ${page.article_detail.categoryInfo[0].id === item.id ? 'active' : ''}`}
|
||||
>
|
||||
<h2 className="title-cate-article font-[400]">{item.title}</h2>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
<div className="row article-detail-page mt-5">
|
||||
<div className="col-md-8">
|
||||
<div className="box-article-detail-title">
|
||||
<h1 className="font-weight-700">{page.article_detail.title}</h1>
|
||||
<div className="post__user border-bottom my-5 flex items-center gap-2">
|
||||
<span className="author-name">{page.article_detail.author}</span>
|
||||
<span className="post-time">{page.article_detail.createDate}</span>
|
||||
</div>
|
||||
<TocBox htmlContent={page.article_detail.content} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{page.article_other_same_category && (
|
||||
<div className="col-md-4">
|
||||
<div className="box-article-relay">
|
||||
<p className="title-ar">
|
||||
Bài viết <span>liên quan</span>
|
||||
</p>
|
||||
<div className="article-list list-article-relative flex flex-wrap gap-3">
|
||||
{combinedList.map((item) => (
|
||||
<div className="item-article d-flex flex-column gap-12" key={item.id}>
|
||||
<Link href={item.url} className="img-article boder-radius-10">
|
||||
<Image
|
||||
className="boder-radius-10"
|
||||
src={item.image.original}
|
||||
fill
|
||||
alt={item.title}
|
||||
/>
|
||||
</Link>
|
||||
<div className="content-article flex-1">
|
||||
<a href={item.url} className="title-article">
|
||||
<h3 className="font-weight-400 line-clamp-2">{item.title}</h3>
|
||||
</a>
|
||||
<p className="time-article d-flex align-items-center gap-4">
|
||||
<i className="sprite sprite-clock-item-article"></i>
|
||||
<span>{item.createDate}</span>
|
||||
</p>
|
||||
<p className="descreption-article line-clamp-2">{item.summary}</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ArticleDetailPage;
|
||||
67
src/features/Article/HomeArticlePage/BoxArticleMid/index.tsx
Normal file
67
src/features/Article/HomeArticlePage/BoxArticleMid/index.tsx
Normal file
@@ -0,0 +1,67 @@
|
||||
'use client';
|
||||
|
||||
import ItemArticle from '@/components/Common/ItemArticle';
|
||||
import Link from 'next/link';
|
||||
import Image from 'next/image';
|
||||
import { getArticles } from '@/lib/api/article';
|
||||
import { useApiData } from '@/hooks/useApiData';
|
||||
import type { ListArticle } from '@/types/article/TypeListArticle';
|
||||
|
||||
export const BoxArticleMid = () => {
|
||||
const { data: articles } = useApiData(
|
||||
() => getArticles(),
|
||||
[],
|
||||
{ initialData: [] as ListArticle },
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="box-article-home-middle grid grid-cols-3 gap-2">
|
||||
<div className="box-article-tech col-left-article boder-radius-10 border-box-article col-span-2">
|
||||
<p className="title-box-article font-[600]">Tin công nghệ</p>
|
||||
<div className="list-article-tech">
|
||||
{articles.slice(0, 9).map((item) => (
|
||||
<ItemArticle item={item} key={item.id} />
|
||||
))}
|
||||
</div>
|
||||
<Link
|
||||
href="/tin-cong-nghe"
|
||||
className="btn-article-col flex items-center justify-center gap-2 font-[500]"
|
||||
>
|
||||
Xem tất cả
|
||||
</Link>
|
||||
</div>
|
||||
<div className="col-right-article flex-1">
|
||||
<div className="box-article-hot border-box-article boder-radius-10">
|
||||
<p className="title-box-article font-bold">Tin nổi bật</p>
|
||||
<div className="list-article-hot">
|
||||
{articles.slice(0, 5).map((item) => (
|
||||
<div className="item-article flex gap-4" key={item.id}>
|
||||
<Link href={item.url} className="img-article boder-radius-10 relative">
|
||||
<Image
|
||||
className="boder-radius-10"
|
||||
src={item.image.original}
|
||||
fill
|
||||
alt={item.title}
|
||||
/>
|
||||
|
||||
<i className="sprite sprite-icon-play-video-detail icon-video-feature incon-play-youtube"></i>
|
||||
<i className="sprite sprite-play-youtube incon-play-youtube"></i>
|
||||
</Link>
|
||||
<div className="content-article content-article-item flex flex-1 flex-col">
|
||||
<Link href={item.url} className="title-article">
|
||||
<h3 className="line-clamp-2 font-[400]">{item.title}</h3>
|
||||
</Link>
|
||||
<p className="time-article flex items-center gap-2">
|
||||
<i className="sprite sprite-clock-item-article"></i>
|
||||
<span>{item.createDate}</span>
|
||||
</p>
|
||||
<p className="descreption-article line-clamp-2">{item.summary}</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,55 @@
|
||||
'use client';
|
||||
|
||||
import { Swiper, SwiperSlide } from 'swiper/react';
|
||||
import { Autoplay, Navigation, Pagination, Thumbs } from 'swiper/modules';
|
||||
import Image from 'next/image';
|
||||
import Link from 'next/link';
|
||||
import { getArticles } from '@/lib/api/article';
|
||||
import { useApiData } from '@/hooks/useApiData';
|
||||
import type { ListArticle } from '@/types/article/TypeListArticle';
|
||||
|
||||
export const BoxArticleReview = () => {
|
||||
const { data: articles } = useApiData(
|
||||
() => getArticles(),
|
||||
[],
|
||||
{ initialData: [] as ListArticle },
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="box-article-category page-hompage">
|
||||
<div className="box-article-global box-artice-review">
|
||||
<div className="title-box-product-home mb-5 flex flex-col items-center">
|
||||
<p className="title font-[500]">Review sản phẩm</p>
|
||||
<p className="border-title"></p>
|
||||
</div>
|
||||
<div className="list-article-global">
|
||||
<Swiper
|
||||
modules={[Autoplay, Navigation, Pagination, Thumbs]}
|
||||
spaceBetween={15}
|
||||
slidesPerView={3}
|
||||
loop={true}
|
||||
>
|
||||
{articles.map((item) => (
|
||||
<SwiperSlide key={item.id}>
|
||||
<div className="item-article">
|
||||
<Link href={item.url} className="img-article">
|
||||
<Image src={item.image.original} fill alt={item.title} />
|
||||
</Link>
|
||||
<div className="content-article-item">
|
||||
<Link href={item.url} className="title font-weight-500 line-clamp-2">
|
||||
{item.title}
|
||||
</Link>
|
||||
<div className="time-aricle-item flex items-center">
|
||||
<i className="sprite sprite-clock-item-article"></i>
|
||||
<span>{item.createDate}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</SwiperSlide>
|
||||
))}
|
||||
</Swiper>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
130
src/features/Article/HomeArticlePage/BoxVideoArticle/index.tsx
Normal file
130
src/features/Article/HomeArticlePage/BoxVideoArticle/index.tsx
Normal file
@@ -0,0 +1,130 @@
|
||||
'use client';
|
||||
|
||||
import Link from 'next/link';
|
||||
import { FaYoutube } from 'react-icons/fa6';
|
||||
import Image from 'next/image';
|
||||
import useFancybox from '@/hooks/useFancybox';
|
||||
import { getArticleVideos } from '@/lib/api/article';
|
||||
import { useApiData } from '@/hooks/useApiData';
|
||||
import type { ListArticle } from '@/types/article/TypeListArticle';
|
||||
|
||||
export const BoxVideoArticle = () => {
|
||||
const { data: videos } = useApiData(
|
||||
() => getArticleVideos(),
|
||||
[],
|
||||
{ initialData: [] as ListArticle },
|
||||
);
|
||||
|
||||
const getYoutubeEmbedUrl = (url: string): string => {
|
||||
try {
|
||||
const urlObj = new URL(url);
|
||||
if (urlObj.hostname.includes('youtube.com')) {
|
||||
const videoId = urlObj.searchParams.get('v');
|
||||
if (videoId) {
|
||||
return `https://www.youtube.com/embed/${videoId}?autoplay=1`;
|
||||
}
|
||||
}
|
||||
if (urlObj.hostname.includes('youtu.be')) {
|
||||
const videoId = urlObj.pathname.replace('/', '');
|
||||
if (videoId) {
|
||||
return `https://www.youtube.com/embed/${videoId}?autoplay=1`;
|
||||
}
|
||||
}
|
||||
return url;
|
||||
} catch {
|
||||
return url;
|
||||
}
|
||||
};
|
||||
|
||||
const [fancyboxRef] = useFancybox({
|
||||
closeButton: 'auto',
|
||||
dragToClose: true,
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="box-video-article boder-radius-10">
|
||||
<div className="title-video-article flex justify-between">
|
||||
<p className="title font-bold">Youtube channel</p>
|
||||
<Link
|
||||
href="https://www.youtube.com/c/NGUYENCONGPC"
|
||||
className="follow-youtube flex items-center gap-2"
|
||||
>
|
||||
<FaYoutube />
|
||||
<span className="font-bold">Theo dõi trên YouTube</span>
|
||||
</Link>
|
||||
</div>
|
||||
<div className="list-video-article flex justify-between gap-2">
|
||||
<div className="box-left" ref={fancyboxRef}>
|
||||
{videos.slice(0, 1).map((item) => (
|
||||
<div className="item-article-video d-flex w-50 gap-10" key={item.id}>
|
||||
<Link
|
||||
href={getYoutubeEmbedUrl(item.external_url)}
|
||||
className="img-article img-article-video boder-radius-10 relative"
|
||||
data-fancybox
|
||||
>
|
||||
<Image
|
||||
src={item.image.original}
|
||||
width={430}
|
||||
height={310}
|
||||
className="boder-radius-10"
|
||||
alt={item.title}
|
||||
/>
|
||||
|
||||
<i className="sprite sprite-big-play-video-article icon-play"></i>
|
||||
<Image
|
||||
className="icon-play-small"
|
||||
src="https://nguyencongpc.vn/static/assets/nguyencong_2023/images/small-play-youtube.png"
|
||||
alt="play"
|
||||
width={58}
|
||||
height={41}
|
||||
/>
|
||||
</Link>
|
||||
<Link
|
||||
href={getYoutubeEmbedUrl(item.external_url)}
|
||||
className="title-article-video flex-1"
|
||||
data-fancybox
|
||||
>
|
||||
{item.title}
|
||||
</Link>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="box-right grid grid-cols-2 gap-2">
|
||||
{videos.slice(1, 7).map((item) => (
|
||||
<div className="item-article-video flex w-50 gap-2" key={item.id}>
|
||||
<Link
|
||||
href={getYoutubeEmbedUrl(item.external_url)}
|
||||
className="img-article img-article-video boder-radius-10 relative"
|
||||
data-fancybox
|
||||
>
|
||||
<Image
|
||||
src={item.image.original}
|
||||
width={430}
|
||||
height={310}
|
||||
className="boder-radius-10"
|
||||
alt={item.title}
|
||||
/>
|
||||
|
||||
<i className="sprite sprite-big-play-video-article icon-play"></i>
|
||||
<Image
|
||||
className="icon-play-small"
|
||||
src="https://nguyencongpc.vn/static/assets/nguyencong_2023/images/small-play-youtube.png"
|
||||
alt="play"
|
||||
width={58}
|
||||
height={41}
|
||||
/>
|
||||
</Link>
|
||||
<Link
|
||||
href={getYoutubeEmbedUrl(item.external_url)}
|
||||
className="title-article-video flex-1"
|
||||
data-fancybox
|
||||
>
|
||||
{item.title}
|
||||
</Link>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
52
src/features/Article/HomeArticlePage/index.tsx
Normal file
52
src/features/Article/HomeArticlePage/index.tsx
Normal file
@@ -0,0 +1,52 @@
|
||||
'use client';
|
||||
|
||||
import React from 'react';
|
||||
import Link from 'next/link';
|
||||
import { Breadcrumb } from '@components/Common/Breadcrumb';
|
||||
|
||||
import { ArticleTopLeft } from '../ArticleTopLeft';
|
||||
import { ArticleTopRight } from '../ArticleTopRight';
|
||||
import { BoxVideoArticle } from './BoxVideoArticle';
|
||||
import { BoxArticleMid } from './BoxArticleMid';
|
||||
import { BoxArticleReview } from './BoxArticleReview';
|
||||
import { getArticleCategories } from '@/lib/api/article';
|
||||
import { useApiData } from '@/hooks/useApiData';
|
||||
import type { TypeArticleCategory } from '@/types/article/ListCategoryArticle';
|
||||
|
||||
const ArticleHome = () => {
|
||||
const breadcrumbItems = [{ name: 'Tin tức', url: '/tin-tuc' }];
|
||||
const { data: categories } = useApiData(
|
||||
() => getArticleCategories(),
|
||||
[],
|
||||
{ initialData: [] as TypeArticleCategory[] },
|
||||
);
|
||||
|
||||
return (
|
||||
<section className="page-article pb-10">
|
||||
<div className="container">
|
||||
<Breadcrumb items={breadcrumbItems} />
|
||||
|
||||
<div className="tabs-category-article flex items-center">
|
||||
{categories.map((item, index) => (
|
||||
<Link href={item.url} key={`${item.id}-${index}`} className="item-tab-article">
|
||||
<h2 className="title-cate-article font-[400]">{item.title}</h2>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="box-article-home-top grid grid-cols-3 gap-3">
|
||||
<div className="col-left-article border-box-article box-new-article boder-radius-10 col-span-2">
|
||||
<ArticleTopLeft />
|
||||
</div>
|
||||
<ArticleTopRight />
|
||||
</div>
|
||||
|
||||
<BoxVideoArticle />
|
||||
<BoxArticleMid />
|
||||
<BoxArticleReview />
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default ArticleHome;
|
||||
Reference in New Issue
Block a user