update
This commit is contained in:
19
src/components/Article/HomeArticle/ArticleTopLeft/index.tsx
Normal file
19
src/components/Article/HomeArticle/ArticleTopLeft/index.tsx
Normal file
@@ -0,0 +1,19 @@
|
||||
import ItemArticle from '@/components/Common/ItemArticle';
|
||||
import { DataListArticleNews } from '@/data/article/ListArticleNews';
|
||||
|
||||
export const ArticleTopLeft = () => {
|
||||
return (
|
||||
<div className="flex gap-3">
|
||||
<div className="box-left">
|
||||
{DataListArticleNews.slice(0, 1).map((item, index) => (
|
||||
<ItemArticle item={item} key={index} />
|
||||
))}
|
||||
</div>
|
||||
<div className="box-right flex flex-1 flex-col gap-3">
|
||||
{DataListArticleNews.slice(0, 4).map((item, index) => (
|
||||
<ItemArticle item={item} key={index} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
32
src/components/Article/HomeArticle/ArticleTopRight/index.tsx
Normal file
32
src/components/Article/HomeArticle/ArticleTopRight/index.tsx
Normal file
@@ -0,0 +1,32 @@
|
||||
import { DataListArticleNews } from '@/data/article/ListArticleNews';
|
||||
import Link from 'next/link';
|
||||
|
||||
export const ArticleTopRight = () => {
|
||||
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">
|
||||
{DataListArticleNews.slice(0, 6).map((item, index) => (
|
||||
<li className="item-most-view-article flex items-center gap-2" key={index}>
|
||||
<span className="number flex items-center justify-center font-[600]"></span>
|
||||
<Link href={item.url} className="line-clamp-2 flex-1">
|
||||
{item.title}
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
57
src/components/Article/HomeArticle/BoxArticleMid/index.tsx
Normal file
57
src/components/Article/HomeArticle/BoxArticleMid/index.tsx
Normal file
@@ -0,0 +1,57 @@
|
||||
import ItemArticle from '@/components/Common/ItemArticle';
|
||||
import { DataListArticleNews } from '@/data/article/ListArticleNews';
|
||||
import Link from 'next/link';
|
||||
import Image from 'next/image';
|
||||
|
||||
export const BoxArticleMid = () => {
|
||||
return (
|
||||
<div className="box-article-home-middle flex justify-between gap-2">
|
||||
<div className="box-article-tech col-left-article boder-radius-10 border-box-article">
|
||||
<p className="title-box-article font-[600]">Tin công nghệ</p>
|
||||
<div className="list-article-tech">
|
||||
{DataListArticleNews.slice(0, 9).map((item, index) => (
|
||||
<ItemArticle item={item} key={index} />
|
||||
))}
|
||||
</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">
|
||||
{DataListArticleNews.slice(0, 5).map((item, index) => (
|
||||
<div className="item-article flex gap-4" key={index}>
|
||||
<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="/tuyen-dung-nhan-vien-ky-thuat-1-2" 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,45 @@
|
||||
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 { DataListArticleNews } from '@/data/article/ListArticleNews';
|
||||
|
||||
export const BoxArticleReview = () => {
|
||||
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}
|
||||
>
|
||||
{DataListArticleNews.map((item, index) => (
|
||||
<SwiperSlide key={index}>
|
||||
<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>
|
||||
);
|
||||
};
|
||||
123
src/components/Article/HomeArticle/BoxVideoArticle/index.tsx
Normal file
123
src/components/Article/HomeArticle/BoxVideoArticle/index.tsx
Normal file
@@ -0,0 +1,123 @@
|
||||
import Link from 'next/link';
|
||||
import { FaYoutube } from 'react-icons/fa6';
|
||||
import { DataListArticleVideo } from '@/data/article/ListAricleVideo';
|
||||
import Image from 'next/image';
|
||||
import useFancybox from '@/hooks/useFancybox';
|
||||
|
||||
export const BoxVideoArticle = () => {
|
||||
const getYoutubeEmbedUrl = (url: string): string => {
|
||||
try {
|
||||
const urlObj = new URL(url);
|
||||
// nếu là link youtube dạng watch?v=...
|
||||
if (urlObj.hostname.includes('youtube.com')) {
|
||||
const videoId = urlObj.searchParams.get('v');
|
||||
if (videoId) {
|
||||
return `https://www.youtube.com/embed/${videoId}?autoplay=1`;
|
||||
}
|
||||
}
|
||||
// nếu là link youtu.be/xxxx
|
||||
if (urlObj.hostname.includes('youtu.be')) {
|
||||
const videoId = urlObj.pathname.replace('/', '');
|
||||
if (videoId) {
|
||||
return `https://www.youtube.com/embed/${videoId}?autoplay=1`;
|
||||
}
|
||||
}
|
||||
// fallback: trả về chính url
|
||||
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}>
|
||||
{DataListArticleVideo.slice(0, 1).map((item, index) => (
|
||||
<div className="item-article-video d-flex w-50 gap-10" key={index}>
|
||||
<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">
|
||||
{DataListArticleVideo.slice(1, 7).map((item, index) => (
|
||||
<div className="item-article-video flex w-50 gap-2" key={index}>
|
||||
<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>
|
||||
);
|
||||
};
|
||||
51
src/components/Article/index.tsx
Normal file
51
src/components/Article/index.tsx
Normal file
@@ -0,0 +1,51 @@
|
||||
'use client';
|
||||
import React from 'react';
|
||||
import Link from 'next/link';
|
||||
import { Breadcrumb } from '@components/Common/Breadcrumb';
|
||||
import { DataArticleCategory } from '@/data/article/ListCategory';
|
||||
|
||||
import { ArticleTopLeft } from './HomeArticle/ArticleTopLeft';
|
||||
import { ArticleTopRight } from './HomeArticle/ArticleTopRight';
|
||||
import { BoxVideoArticle } from './HomeArticle/BoxVideoArticle';
|
||||
import { BoxArticleMid } from './HomeArticle/BoxArticleMid';
|
||||
import { BoxArticleReview } from './HomeArticle/BoxArticleReview';
|
||||
|
||||
const ArticleHome = () => {
|
||||
const breadcrumbItems = [{ name: 'Tin tức', url: '/tin-tuc' }];
|
||||
|
||||
return (
|
||||
<section className="page-article pb-10">
|
||||
<div className="container">
|
||||
<Breadcrumb items={breadcrumbItems} />
|
||||
|
||||
<div className="tabs-category-article flex items-center">
|
||||
{DataArticleCategory.map((item, index) => (
|
||||
<Link href={item.url} key={index} className="item-tab-article">
|
||||
<h2 className="title-cate-article font-[400]">{item.title}</h2>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="box-article-home-top flex gap-3">
|
||||
<div className="col-left-article border-box-article box-new-article boder-radius-10">
|
||||
<div className="flex gap-12">
|
||||
<ArticleTopLeft />
|
||||
</div>
|
||||
</div>
|
||||
<ArticleTopRight />
|
||||
</div>
|
||||
|
||||
{/* box video */}
|
||||
<BoxVideoArticle />
|
||||
|
||||
{/* box mid */}
|
||||
<BoxArticleMid />
|
||||
|
||||
{/* review */}
|
||||
<BoxArticleReview />
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default ArticleHome;
|
||||
@@ -1,8 +1,46 @@
|
||||
import { useState } from 'react';
|
||||
'use client';
|
||||
import { useState, forwardRef, useImperativeHandle } from 'react';
|
||||
|
||||
export const FormCart = () => {
|
||||
export interface FormCartRef {
|
||||
validateForm: () => boolean;
|
||||
}
|
||||
export const FormCart = forwardRef<FormCartRef, object>((props, ref) => {
|
||||
const [showTax, setShowTax] = useState(false);
|
||||
|
||||
const validateForm = () => {
|
||||
const name = (document.getElementById('buyer_name') as HTMLInputElement)?.value.trim();
|
||||
const tel = (document.getElementById('buyer_tel') as HTMLInputElement)?.value.trim();
|
||||
const address = (document.getElementById('buyer_address') as HTMLInputElement)?.value.trim();
|
||||
|
||||
// Regex kiểm tra ký tự đặc biệt (chỉ cho phép chữ cái, số, khoảng trắng)
|
||||
const regexNoSpecial = /^[\p{L}\p{N}\s]+$/u;
|
||||
// Regex số điện thoại Việt Nam (10 số, bắt đầu bằng 0)
|
||||
const regexPhone = /^0\d{9}$/;
|
||||
|
||||
// Kiểm tra tên
|
||||
if (!name || name.length <= 4 || !regexNoSpecial.test(name)) {
|
||||
alert('Bạn nhập tên chưa đúng định dạng!');
|
||||
return false;
|
||||
}
|
||||
|
||||
// Kiểm tra số điện thoại
|
||||
if (!tel || !regexPhone.test(tel)) {
|
||||
alert('Số điện thoại không hợp lệ! (Ví dụ: 0912345678)');
|
||||
return false;
|
||||
}
|
||||
|
||||
// Kiểm tra địa chỉ
|
||||
if (!address || address.length <= 4 || !regexNoSpecial.test(address)) {
|
||||
alert('Địa chỉ chưa hợp lệ!');
|
||||
return false;
|
||||
}
|
||||
|
||||
// Nếu hợp lệ thì xử lý đặt hàng
|
||||
return true;
|
||||
};
|
||||
|
||||
useImperativeHandle(ref, () => ({ validateForm }));
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="box-cart-info-customer">
|
||||
@@ -79,4 +117,6 @@ export const FormCart = () => {
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
||||
FormCart.displayName = 'FormCart';
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
'use client';
|
||||
import { useState } from 'react';
|
||||
import { useState, useRef } from 'react';
|
||||
import Image from 'next/image';
|
||||
import Link from 'next/link';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { FaChevronLeft } from 'react-icons/fa6';
|
||||
import { Breadcrumb } from '@/components/Common/Breadcrumb';
|
||||
import { TypeCartItem } from '@/types/cart';
|
||||
import { ItemCart } from './ItemCart';
|
||||
import { FormCart } from './FormCart';
|
||||
import { FormCart, FormCartRef } from './FormCart';
|
||||
import { formatCurrency } from '@/lib/formatPrice';
|
||||
|
||||
const HomeCart = () => {
|
||||
const router = useRouter();
|
||||
const breadcrumbItems = [{ name: 'Giỏ hàng', url: '/cart' }];
|
||||
const [cart, setCart] = useState<TypeCartItem[]>(() => {
|
||||
const storedCart = localStorage.getItem('cart');
|
||||
@@ -18,6 +20,8 @@ const HomeCart = () => {
|
||||
|
||||
const [payMethod, setPayMethod] = useState('2');
|
||||
|
||||
const formRef = useRef<FormCartRef>(null);
|
||||
|
||||
const updateCartItem = (id: string, quantity: number) => {
|
||||
const newCart = cart.map((item) =>
|
||||
item._id === id
|
||||
@@ -57,6 +61,12 @@ const HomeCart = () => {
|
||||
return formatCurrency(cart.reduce((sum, item) => sum + Number(item.in_cart.total_price), 0));
|
||||
};
|
||||
|
||||
const handleClickOrder = () => {
|
||||
if (formRef.current?.validateForm()) {
|
||||
router.push('/send-cart');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="container">
|
||||
@@ -106,7 +116,7 @@ const HomeCart = () => {
|
||||
</div>
|
||||
|
||||
{/* form mua hàng */}
|
||||
<FormCart />
|
||||
<FormCart ref={formRef} />
|
||||
<div className="box-payment">
|
||||
<p className="title-section-cart font-bold">Phương thức thanh toán</p>
|
||||
<div className="list-method-payment">
|
||||
@@ -141,7 +151,7 @@ const HomeCart = () => {
|
||||
</div>
|
||||
|
||||
<div className="list-btn-cart">
|
||||
<button type="submit" className="js-send-cart font-bold">
|
||||
<button type="submit" onClick={handleClickOrder} className="js-send-cart font-bold">
|
||||
Đặt hàng
|
||||
</button>
|
||||
<div className="list-print-cart flex justify-between gap-2">
|
||||
|
||||
56
src/components/common/ItemArticle/index.tsx
Normal file
56
src/components/common/ItemArticle/index.tsx
Normal file
@@ -0,0 +1,56 @@
|
||||
'use client';
|
||||
import React from 'react';
|
||||
import { Article } from '@/types';
|
||||
import Link from 'next/link';
|
||||
import Image from 'next/image';
|
||||
|
||||
type ItemArticleProps = {
|
||||
item: Article;
|
||||
};
|
||||
|
||||
const ItemArticle: React.FC<ItemArticleProps> = ({ item }) => {
|
||||
// chọn link: nếu có external_url thì dùng, ngược lại dùng url
|
||||
const linkHref = item.external_url && item.external_url !== '' ? item.external_url : item.url;
|
||||
|
||||
// chọn ảnh: nếu có original thì dùng, ngược lại ảnh mặc định
|
||||
const imageSrc =
|
||||
item.image?.original && item.image.original !== ''
|
||||
? item.image.original
|
||||
: '/static/assets/nguyencong_2023/images/not-image.png';
|
||||
|
||||
// chọn thời gian: ưu tiên article_time, fallback createDate
|
||||
const timeDisplay =
|
||||
item.article_time && item.article_time !== '' ? item.article_time : item.createDate;
|
||||
|
||||
return (
|
||||
<div className="item-article flex gap-3">
|
||||
<Link href={linkHref} className="img-article boder-radius-10 position-relative">
|
||||
<Image
|
||||
className="boder-radius-10"
|
||||
src={imageSrc}
|
||||
fill
|
||||
alt={item.title}
|
||||
sizes="(max-width: 768px) 100vw, 265px"
|
||||
/>
|
||||
{/* icon video nếu cần */}
|
||||
<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={linkHref} className="title-article">
|
||||
<h3 className="line-clamp-2 font-[400]">{item.title}</h3>
|
||||
</Link>
|
||||
|
||||
<p className="time-article flex items-center gap-1">
|
||||
<i className="sprite sprite-clock-item-article"></i>
|
||||
<span>{timeDisplay}</span>
|
||||
</p>
|
||||
|
||||
<p className="descreption-article line-clamp-2">{item.summary}</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ItemArticle;
|
||||
Reference in New Issue
Block a user