update 15/01
This commit is contained in:
BIN
public/images/404-page-1.png
Normal file
BIN
public/images/404-page-1.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 92 KiB |
@@ -11,13 +11,18 @@ import ArticleHome from "@/components/article/home";
|
|||||||
export default async function SlugPage({
|
export default async function SlugPage({
|
||||||
params,
|
params,
|
||||||
}: {
|
}: {
|
||||||
params: { slug: string };
|
params: Promise<{ slug: string }>;
|
||||||
}) {
|
}) {
|
||||||
const { slug } = await params;
|
const { slug } = await params;
|
||||||
if (!slug) return notFound();
|
if (!slug) {
|
||||||
|
notFound();
|
||||||
|
}
|
||||||
|
|
||||||
const result = findBySlug(slug);
|
const result = findBySlug(slug);
|
||||||
if (!result) return notFound();
|
|
||||||
|
if (!result) {
|
||||||
|
notFound();
|
||||||
|
}
|
||||||
|
|
||||||
switch (result.type) {
|
switch (result.type) {
|
||||||
case "product_category":
|
case "product_category":
|
||||||
@@ -36,6 +41,7 @@ export default async function SlugPage({
|
|||||||
return <ArticleDetail slug={result.data.slug} />;
|
return <ArticleDetail slug={result.data.slug} />;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return notFound();
|
const _exhaustive: never = result;
|
||||||
|
notFound();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,21 @@
|
|||||||
|
import Link from "next/link"
|
||||||
|
|
||||||
export default function NotFound() {
|
export default function NotFound() {
|
||||||
return (
|
return (
|
||||||
<div style={{ padding: 40 }}>
|
<div className="error-page bg-white py-10">
|
||||||
<h1>404</h1>
|
<div className="container">
|
||||||
<p>Trang không tồn tại</p>
|
<div className="text-18 text-dark text-center pt-4 pb-5">
|
||||||
|
<img src="/images/404-page-1.png" alt="Không tìm thấy" width={1} height={1} className="block w-auto h-auto m-auto" style={{ maxWidth: 600 }} />
|
||||||
|
<h1 className="text-36 mt-5"> Không Tìm Thấy </h1>
|
||||||
|
<p className="py-4">
|
||||||
|
Xin lỗi, nhưng trang bạn yêu cầu không tìm thấy hoặc đã bị xóa bỏ. Vui lòng thử lại.
|
||||||
|
</p>
|
||||||
|
<Link className="red" href="https://hoanghapc.vn">
|
||||||
|
<b>← Quay lại trang chủ</b>
|
||||||
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
34
src/components/home/Product/CategoryIcon.tsx
Normal file
34
src/components/home/Product/CategoryIcon.tsx
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
export default function CategoryIcon({item}:any) {
|
||||||
|
return(
|
||||||
|
<>
|
||||||
|
{item == 1 &&
|
||||||
|
<p className="m-0">
|
||||||
|
<i className="icons icon-wallet"></i>
|
||||||
|
<span> Trả góp 0% </span>
|
||||||
|
</p>
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
{item == 91 || item == 27 &&
|
||||||
|
<p className="m-0">
|
||||||
|
<i className="icons icon-truck"></i>
|
||||||
|
<span> Miễn phí giao hàng </span>
|
||||||
|
</p>
|
||||||
|
}
|
||||||
|
|
||||||
|
{item == 103 &&
|
||||||
|
<p className="m-0">
|
||||||
|
<i className="icons icon-world"></i>
|
||||||
|
<span> Giao hàng toàn quốc </span>
|
||||||
|
</p>
|
||||||
|
}
|
||||||
|
|
||||||
|
{item == 92 &&
|
||||||
|
<p className="m-0">
|
||||||
|
<i className="icons icon-medal"></i>
|
||||||
|
<span> Bảo hành tận nơi </span>
|
||||||
|
</p>
|
||||||
|
}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
File diff suppressed because one or more lines are too long
@@ -3,19 +3,38 @@ import { categories } from "@/data/categories";
|
|||||||
export default function FeaturedProductCategories() {
|
export default function FeaturedProductCategories() {
|
||||||
const {all_category} = categories.product;
|
const {all_category} = categories.product;
|
||||||
|
|
||||||
|
const getAllCategoriesFlat = (cats: any[]): any[] => {
|
||||||
|
const result: any[] = [];
|
||||||
|
|
||||||
|
cats.forEach((cat) => {
|
||||||
|
result.push(cat);
|
||||||
|
|
||||||
|
if (cat.children && cat.children.length > 0) {
|
||||||
|
result.push(...getAllCategoriesFlat(cat.children));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
|
const allCategoriesFlat = getAllCategoriesFlat(all_category);
|
||||||
|
// Lọc các danh mục nổi bật
|
||||||
|
const featuredCategories = allCategoriesFlat.filter(
|
||||||
|
(item: any) => item.is_featured === 1
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{all_category &&
|
{featuredCategories.length &&
|
||||||
<div className="home-categories-container bg-white rounded-[24px] my-10 p-6 pb-8">
|
<div className="home-categories-container bg-white rounded-[24px] my-10 p-6 pb-8">
|
||||||
<h2 className="group-title font-600 text-[28px] text-[#004BA4] mb-5 leading-9"> Danh mục nổi bật </h2>
|
<h2 className="group-title font-600 text-[28px] text-[#004BA4] mb-5 leading-9"> Danh mục nổi bật </h2>
|
||||||
|
|
||||||
<div className="grid grid-cols-10 gap-6">
|
<div className="grid grid-cols-10 gap-6">
|
||||||
{all_category
|
{featuredCategories
|
||||||
.filter( (item:any) => item.is_featured === 1 )
|
|
||||||
.map( (item:any) =>
|
.map( (item:any) =>
|
||||||
<a href={item.url} className="item" key={item.id}>
|
<a href={item.url} className="item" key={item.id}>
|
||||||
<i className="image lazy"
|
<i className="image lazy"
|
||||||
style={{ backgroundImage: `url(${item.thumnail})` }}
|
style={{ backgroundImage: `url(${item.thumnail ? item.thumnail : '/images/avatar-admin.png'})` }}
|
||||||
></i>
|
></i>
|
||||||
|
|
||||||
<span className="block">
|
<span className="block">
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import Slider from "./slider";
|
import Slider from "./slider";
|
||||||
import Deal from "./deal";
|
import Deal from "./deal";
|
||||||
import FeaturedProductCategories from "./category";
|
import FeaturedProductCategories from "./featured-category";
|
||||||
import ProductCategories from "./product";
|
import ProductCategories from "./product";
|
||||||
import Article from "./article";
|
import Article from "./article";
|
||||||
|
|
||||||
|
|||||||
@@ -1,18 +1,47 @@
|
|||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useCart } from "@/hooks/useCart";
|
import { useCart } from "@/hooks/useCart";
|
||||||
import { getAllProducts, formatPrice } from "@/lib/utils"
|
import { getAllProducts } from "@/lib/utils"
|
||||||
|
import CartItem from "@/components/other/header/cart/index"
|
||||||
|
|
||||||
export default function Cart() {
|
export default function Cart() {
|
||||||
|
const {
|
||||||
|
cartItems,
|
||||||
|
cartCount,
|
||||||
|
totalItems,
|
||||||
|
loading,
|
||||||
|
} = useCart();
|
||||||
|
|
||||||
const { cartCount, cartIds, loading } = useCart();
|
|
||||||
const allProducts = getAllProducts();
|
const allProducts = getAllProducts();
|
||||||
|
|
||||||
const productsInCart = allProducts.filter(p => cartIds.includes(p.id) );
|
// Lọc sản phẩm có trong giỏ hàng với quantity
|
||||||
console.log(productsInCart);
|
const productsInCart = cartItems
|
||||||
|
.map(cartItem => {
|
||||||
|
const product = allProducts.find(p => p.id === cartItem.id);
|
||||||
|
if (product) {
|
||||||
|
return {
|
||||||
|
...product,
|
||||||
|
cartQuantity: cartItem.cartQuantity,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
})
|
||||||
|
.filter(item => item !== null);
|
||||||
|
|
||||||
|
|
||||||
|
// Tính tổng tiền
|
||||||
|
const totalPrice = productsInCart.reduce((sum, item) => {
|
||||||
|
if (item && item.price) {
|
||||||
|
return sum + (item.price * item.quantity);
|
||||||
|
}
|
||||||
|
return sum;
|
||||||
|
}, 0);
|
||||||
|
|
||||||
|
const hasProducts = cartCount > 0;
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="header-cart-item" id="js-header-cart">
|
<div className="header-cart-item">
|
||||||
<Link href="/cart" rel="nofollow" className="flex items-center">
|
<Link href="/cart" rel="nofollow" className="flex items-center">
|
||||||
<i className="icon-cart relative mr-[10px]">
|
<i className="icon-cart relative mr-[10px]">
|
||||||
<b className="js-cart-count cart-count">{cartCount}</b>
|
<b className="js-cart-count cart-count">{cartCount}</b>
|
||||||
@@ -20,39 +49,24 @@ export default function Cart() {
|
|||||||
<span className="text"> Giỏ <br />Hàng </span>
|
<span className="text"> Giỏ <br />Hàng </span>
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
{productsInCart.length > 0 &&
|
{!loading && hasProducts &&
|
||||||
<div className="header-cart-hover">
|
<div className="header-cart-hover">
|
||||||
<div className="cart-items-holder">
|
<div className="cart-items-holder">
|
||||||
|
<p className="text-center underline font-600 red border-b" style={{ margin: '0', padding: "10px" }}>
|
||||||
|
1 số sp thêm từ DEAL sẽ không có trong DB tĩnh nên không hiển thị
|
||||||
|
</p>
|
||||||
|
|
||||||
{productsInCart.map((item:any) =>
|
{productsInCart.map((item:any) =>
|
||||||
<div className="cart-item" key={item.id}>
|
<CartItem key={item.id} item={item} />
|
||||||
<Link href={item.productUrl} className="cart-img">
|
|
||||||
<img src={item.productImage.small} alt="image" width={100} height={100} />
|
|
||||||
</Link>
|
|
||||||
|
|
||||||
<div className="cart-text">
|
|
||||||
<Link href={item.productUrl} className="d-block font-700" style={{ marginBottom: "5px" }}>
|
|
||||||
{item.productName}
|
|
||||||
</Link>
|
|
||||||
|
|
||||||
<p className="m-0 d-flex justify-content-between">
|
|
||||||
<b>x1</b>
|
|
||||||
<b className="red">
|
|
||||||
{item.price > 0 ? formatPrice(item.price) +'đ' : 'Liên hệ'}
|
|
||||||
</b>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<p className="text-center underline font-600 red" style={{ margin: '0', padding: "10px" }}> 1 số sp không có trong DB tĩnh </p>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="cart-price-hover">
|
<div className="cart-price-hover">
|
||||||
<p className="grey m-0 text-right">
|
<p className="grey m-0 text-right">
|
||||||
Tổng tiền hàng
|
Tổng tiền hàng
|
||||||
(<span className="red"><span className="js-cart-count">{cartCount}</span> sản phẩm</span>):
|
(<span className="red"><span className="js-cart-count">{totalItems}</span> sản phẩm</span>):
|
||||||
<span className="red text-18 font-600" style={{ verticalAlign: "top" }}> 10.770.000đ </span>
|
<span className="red text-18 font-600" style={{ verticalAlign: "top" }}> {totalPrice}đ </span>
|
||||||
</p>
|
</p>
|
||||||
<Link href="/cart" className="d-block text-center text-white btn-goCart"> THANH TOÁN NGAY </Link>
|
<Link href="/cart" className="d-block text-center text-white btn-goCart"> THANH TOÁN NGAY </Link>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
30
src/components/other/Header/cart/index.tsx
Normal file
30
src/components/other/Header/cart/index.tsx
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import Link from "next/link";
|
||||||
|
import { formatPrice } from "@/lib/utils";
|
||||||
|
|
||||||
|
export default function CartItem({item}:any) {
|
||||||
|
return(
|
||||||
|
<div className="cart-item">
|
||||||
|
<Link href={item.productUrl} className="cart-img">
|
||||||
|
<img
|
||||||
|
src={item.productImage.small}
|
||||||
|
alt={item.productName}
|
||||||
|
width={100}
|
||||||
|
height={100}
|
||||||
|
/>
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
<div className="cart-text">
|
||||||
|
<Link href={item.productUrl} className="d-block font-700" style={{ marginBottom: "5px" }}>
|
||||||
|
{item.productName}
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
<p className="m-0 d-flex justify-content-between">
|
||||||
|
<b>x{item.cartQuantity}</b>
|
||||||
|
<b className="red">
|
||||||
|
{item.price > 0 ? formatPrice(item.price) +'đ' : 'Liên hệ'}
|
||||||
|
</b>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -26,6 +26,7 @@ export default function DealItem( {item} : any) {
|
|||||||
<span className="deal-discount">-{discount}%</span>
|
<span className="deal-discount">-{discount}%</span>
|
||||||
</>
|
</>
|
||||||
) : null;
|
) : null;
|
||||||
|
const checkIncart = isInCart(productInfo.id);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="deal-item">
|
<div className="deal-item">
|
||||||
@@ -51,9 +52,12 @@ export default function DealItem( {item} : any) {
|
|||||||
{discountView}
|
{discountView}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button className="deal-btn bx bx-plus" type="button" aria-label="Mua"
|
<button className={`deal-btn bx ${checkIncart ? 'bx-check' : 'bx-plus' }`}
|
||||||
|
style={{ background: `${checkIncart ? '#ccc' : ''}` }}
|
||||||
|
disabled={checkIncart}
|
||||||
|
type="button" aria-label="Mua"
|
||||||
onClick={() => addToCart(productInfo.id)}
|
onClick={() => addToCart(productInfo.id)}
|
||||||
disabled={isInCart(productInfo.id)}
|
|
||||||
></button>
|
></button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
109
src/components/shared/ProductItem.tsx
Normal file
109
src/components/shared/ProductItem.tsx
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
'use client';
|
||||||
|
import Link from "next/link";
|
||||||
|
import { formatPrice } from "@/lib/utils";
|
||||||
|
|
||||||
|
export default function ProductItem({item}:any){
|
||||||
|
console.log('ProductItem: ', item)
|
||||||
|
return (
|
||||||
|
<div className="p-item">
|
||||||
|
<Link href={item.productUrl} className="p-img">
|
||||||
|
<img
|
||||||
|
src={item.productImage.large}
|
||||||
|
alt={item.productName}
|
||||||
|
width={250}
|
||||||
|
height={250}
|
||||||
|
/>
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
<div className="p-text">
|
||||||
|
<div className="p-price-group">
|
||||||
|
<del>52.000.000 đ</del>
|
||||||
|
<span className="p-discount">-10%</span>
|
||||||
|
<p className="p-price"> 22.000.000 đ </p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Link href={item.productUrl} className="p-name">
|
||||||
|
<h3>{item.productName}</h3>
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
<div className="p-btn-group flex items-center justify-between text-16 font-500 leading-[23px]">
|
||||||
|
<div>
|
||||||
|
<p className="m-0 text-[#00AD4F] flex items-center gap-1">
|
||||||
|
<i className="bx bx-check-circle text-18 w-[18px]"></i>
|
||||||
|
<span> Sẵn hàng </span>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{/* <p className="m-0 red flex items-center gap-1">
|
||||||
|
<i className="bx bxs-phone text-18 w-[18px]"></i>
|
||||||
|
<span> Liên hệ </span>
|
||||||
|
</p> */}
|
||||||
|
|
||||||
|
<p className="m-0 text-[#E16B10] flex items-center gap-1">
|
||||||
|
<i className="bx bx-gift text-18 w-[18px]"></i>
|
||||||
|
<span> Quà tặng </span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button type="button" className="" aria-label="Mua"></button>
|
||||||
|
<button className={`p-btn bx bg-btn text-white rounded-full w-9 h-9 text-20 ${checkIncart ? 'bx-check' : 'bx-plus' }`}
|
||||||
|
style={{ background: `${checkIncart ? '#ccc' : ''}` }}
|
||||||
|
disabled={checkIncart}
|
||||||
|
type="button" aria-label="Mua"
|
||||||
|
onClick={() => addToCart(productInfo.id)}
|
||||||
|
|
||||||
|
></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="p-tooltip hidden">
|
||||||
|
<div className="bg-white rounded-[20px] overflow-hidden border-2 border-[#EAECF0] shadow-[0px_6px_8px_-2px_#10182814]">
|
||||||
|
<p className="tooltip-name px-5 py-4 text-white font-600 text-16 leading-[21px] bg-[linear-gradient(180.3deg,#259AFF_-18.56%,_#114CDD_100.92%)] m-0">
|
||||||
|
[Tặng bàn phím] HHPC ULTRA 7 265K | 32GB DDR5 | NVIDIA RTX 3060 12GB
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="p-4 tooltip-content">
|
||||||
|
<div className="last:border-0 last:p-0 last:m-0 text-16 border-b border-[#DEE4EC] mb-4 pb-4">
|
||||||
|
<p className="m-0 flex items-center flex-wrap gap-1">
|
||||||
|
<b className="font-600"> Giá bán: </b>
|
||||||
|
<b className="font-600 text-[#FF4E2A] text-18"> 48.990.000 đ </b>
|
||||||
|
<del className="text-[#A0A5AC] font-300"> 52.000.000 đ </del>
|
||||||
|
<span className="bg-[#FA354A] text-white text-11 px-[6px] leading-[18px] rounded-[20px]"> -6% </span>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p className="m-0">
|
||||||
|
<b className="font-600"> Bảo hành: </b>
|
||||||
|
<span> Theo từng linh kiện </span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="last:mb-0 mb-6 px-1">
|
||||||
|
<p className="text-16 font-600 flex items-center leading-6 mb-2">
|
||||||
|
<i className="icons icon-screen"></i>
|
||||||
|
<span> Thông số sản phẩm </span>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="tooltip-spec">
|
||||||
|
<div className="item"> CPU: INTEL CORE i5 13400F up 4.6GHz | 10 CORE | 16 THREAD </div>
|
||||||
|
<div className="item"> RAM: DDR4 16GB (1x16G) 3200 MHz </div>
|
||||||
|
<div className="item"> VGA: NVIDIA RTX 3060 12GB GDDR6 </div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="rounded-[12px] bg-[linear-gradient(182.15deg,#FFA480_-18.44%,#EB0C23_60.76%)] p-1 pt-2">
|
||||||
|
<p className="px-2 text-16 font-600 flex items-center leading-5 mb-2 text-white">
|
||||||
|
<i className="icons icon-gift"></i>
|
||||||
|
<span> Khuyến mại hấp dẫn </span>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="tooltip-offer rounded-[8px] bg-[#FEF2F2] px-2 py-4">
|
||||||
|
<div className="item"> <p><span style={{ color: '#ff0000', fontSize: '12pt' }}><strong>⭐ Bảo Hành Tại Nơi Sử Dụng (Áp Dụng Nội Thành Hà Nội và Hồ Chí Minh)</strong></span></p> </div>
|
||||||
|
<div className="item"><p><span style={{ color: '#ff0000', fontSize: '12pt' }}><strong>⭐ Bảo Hành Siêu Tốc 1 Đổi 1 Trong 24h </strong></span></p></div> <div className="item"><p><span style={{ color: '#ff0000', fontSize: '12pt' }}><strong>⭐ Miễn Phí 100% Vận Chuyển Toàn Quốc </strong></span></p></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -478,7 +478,7 @@ export const categories = {
|
|||||||
id : 27,
|
id : 27,
|
||||||
parentId : 0,
|
parentId : 0,
|
||||||
isParent : 1,
|
isParent : 1,
|
||||||
is_featured : 1,
|
is_featured : 0,
|
||||||
type: "product",
|
type: "product",
|
||||||
title : 'PC Gaming, Học Tập',
|
title : 'PC Gaming, Học Tập',
|
||||||
url : '/pc-gaming',
|
url : '/pc-gaming',
|
||||||
|
|||||||
@@ -1288,8 +1288,8 @@ export const productList = [
|
|||||||
"total": 7,
|
"total": 7,
|
||||||
"list": [
|
"list": [
|
||||||
{
|
{
|
||||||
"id": 5965,
|
"id": 6667,
|
||||||
"productId": 5965,
|
"productId": 6667,
|
||||||
"priceUnit": "chi\u1ebfc",
|
"priceUnit": "chi\u1ebfc",
|
||||||
"marketPrice": 11000000,
|
"marketPrice": 11000000,
|
||||||
"price": 10600000,
|
"price": 10600000,
|
||||||
@@ -1407,8 +1407,8 @@ export const productList = [
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 5210,
|
"id": 6111,
|
||||||
"productId": 5210,
|
"productId": 6111,
|
||||||
"priceUnit": "chi\u1ebfc",
|
"priceUnit": "chi\u1ebfc",
|
||||||
"marketPrice": 23000000,
|
"marketPrice": 23000000,
|
||||||
"price": 21810000,
|
"price": 21810000,
|
||||||
@@ -2095,8 +2095,8 @@ export const productList = [
|
|||||||
"total": 10,
|
"total": 10,
|
||||||
"list": [
|
"list": [
|
||||||
{
|
{
|
||||||
"id": 5401,
|
"id": 6110,
|
||||||
"productId": 5401,
|
"productId": 6110,
|
||||||
"priceUnit": "chi\u1ebfc",
|
"priceUnit": "chi\u1ebfc",
|
||||||
"marketPrice": 0,
|
"marketPrice": 0,
|
||||||
"price": 12990000,
|
"price": 12990000,
|
||||||
|
|||||||
@@ -2,52 +2,120 @@
|
|||||||
|
|
||||||
import { useEffect, useState, useCallback } from 'react';
|
import { useEffect, useState, useCallback } from 'react';
|
||||||
import {
|
import {
|
||||||
getCartProductIds,
|
getCartItems,
|
||||||
addProductToCart,
|
addProductToCart,
|
||||||
removeProductFromCart,
|
removeProductFromCart,
|
||||||
clearCart,
|
clearCart,
|
||||||
|
increaseQuantity,
|
||||||
|
decreaseQuantity,
|
||||||
|
updateQuantity,
|
||||||
|
getProductQuantity,
|
||||||
|
isProductInCart,
|
||||||
|
CART_CHANGE_EVENT,
|
||||||
|
type CartItem,
|
||||||
} from '../services/cart';
|
} from '../services/cart';
|
||||||
|
|
||||||
export function useCart() {
|
export function useCart() {
|
||||||
const [cartIds, setCartIds] = useState<number[] | null>(null);
|
const [cartItems, setCartItems] = useState<CartItem[] | null>(null);
|
||||||
|
|
||||||
// Load cart lần đầu
|
// Load cart lần đầu
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setCartIds(getCartProductIds());
|
setCartItems(getCartItems());
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Listen to cart changes from ANY source
|
||||||
|
useEffect(() => {
|
||||||
|
const handleCartChange = () => {
|
||||||
|
setCartItems(getCartItems());
|
||||||
|
};
|
||||||
|
|
||||||
|
// Listen to custom event
|
||||||
|
window.addEventListener(CART_CHANGE_EVENT, handleCartChange);
|
||||||
|
|
||||||
|
// Also listen to storage event (for changes from other tabs)
|
||||||
|
window.addEventListener('storage', (e) => {
|
||||||
|
if (e.key === 'cart_products') {
|
||||||
|
handleCartChange();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener(CART_CHANGE_EVENT, handleCartChange);
|
||||||
|
window.removeEventListener('storage', handleCartChange);
|
||||||
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const refresh = useCallback(() => {
|
const refresh = useCallback(() => {
|
||||||
setCartIds(getCartProductIds());
|
setCartItems(getCartItems());
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const addToCart = useCallback((productId: number) => {
|
const addToCart = useCallback((productId: number, quantity: number = 1) => {
|
||||||
addProductToCart(productId);
|
const result = addProductToCart(productId, quantity);
|
||||||
refresh();
|
// Không cần refresh() nữa vì event listener sẽ tự động update
|
||||||
}, [refresh]);
|
return result;
|
||||||
|
}, []);
|
||||||
|
|
||||||
const removeFromCart = useCallback((productId: number) => {
|
const removeFromCart = useCallback((productId: number) => {
|
||||||
removeProductFromCart(productId);
|
removeProductFromCart(productId);
|
||||||
refresh();
|
// Không cần refresh()
|
||||||
}, [refresh]);
|
}, []);
|
||||||
|
|
||||||
|
const increase = useCallback((productId: number, amount: number = 1) => {
|
||||||
|
const success = increaseQuantity(productId, amount);
|
||||||
|
// Không cần refresh()
|
||||||
|
return success;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const decrease = useCallback((productId: number, amount: number = 1) => {
|
||||||
|
const success = decreaseQuantity(productId, amount);
|
||||||
|
if (!success) {
|
||||||
|
console.log('Số lượng tối thiểu: 1');
|
||||||
|
}
|
||||||
|
// Không cần refresh()
|
||||||
|
return success;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const updateQty = useCallback((productId: number, cartQuantity: number) => {
|
||||||
|
const success = updateQuantity(productId, cartQuantity);
|
||||||
|
if (!success) {
|
||||||
|
console.log('Số lượng phải lớn hơn hoặc bằng 1');
|
||||||
|
}
|
||||||
|
// Không cần refresh()
|
||||||
|
return success;
|
||||||
|
}, []);
|
||||||
|
|
||||||
const clear = useCallback(() => {
|
const clear = useCallback(() => {
|
||||||
clearCart();
|
clearCart();
|
||||||
setCartIds([]);
|
// Không cần setCartItems([]) nữa, event sẽ tự động update
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const isInCart = useCallback(
|
const isInCart = useCallback(
|
||||||
(productId: number) => cartIds?.includes(productId) ?? false,
|
(productId: number) => cartItems?.some(item => item.id === productId) ?? false,
|
||||||
[cartIds]
|
[cartItems]
|
||||||
);
|
);
|
||||||
|
|
||||||
return {
|
const getQuantity = useCallback(
|
||||||
cartIds: cartIds ?? [],
|
(productId: number) => {
|
||||||
cartCount: cartIds?.length ?? 0,
|
return cartItems?.find(item => item.id === productId)?.cartQuantity ?? 0;
|
||||||
loading: cartIds === null,
|
},
|
||||||
|
[cartItems]
|
||||||
|
);
|
||||||
|
|
||||||
|
const totalItems = cartItems?.reduce((sum, item) => sum + item.cartQuantity, 0) ?? 0;
|
||||||
|
|
||||||
|
return {
|
||||||
|
cartItems: cartItems ?? [],
|
||||||
|
cartCount: cartItems?.length ?? 0,
|
||||||
|
totalItems,
|
||||||
|
loading: cartItems === null,
|
||||||
addToCart,
|
addToCart,
|
||||||
removeFromCart,
|
removeFromCart,
|
||||||
|
increaseQuantity: increase,
|
||||||
|
decreaseQuantity: decrease,
|
||||||
|
updateQuantity: updateQty,
|
||||||
clear,
|
clear,
|
||||||
isInCart,
|
isInCart,
|
||||||
|
getQuantity,
|
||||||
|
refresh,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
28
src/hooks/useProductItem.ts
Normal file
28
src/hooks/useProductItem.ts
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import { calculateDiscount } from "../lib/utils";
|
||||||
|
import type { ProductItemProps } from "../types/product"
|
||||||
|
|
||||||
|
export function useDealItem(item: ProductItemProps) {
|
||||||
|
const productUrl = item.productUrl;
|
||||||
|
const productImage = item.productImage.large;
|
||||||
|
const productName = item.productName;
|
||||||
|
const price = Number(item.price);
|
||||||
|
const marketPrice = Number(item.marketPrice);
|
||||||
|
const discount = calculateDiscount(price, marketPrice);
|
||||||
|
const quantity = item.quantity;
|
||||||
|
const specialOffer = item.specialOffer.all?.[0]?.title ?? '';
|
||||||
|
const warranty = item.warranty;
|
||||||
|
const productSummary= item.productSummary;
|
||||||
|
|
||||||
|
return {
|
||||||
|
productUrl,
|
||||||
|
productImage,
|
||||||
|
productName,
|
||||||
|
quantity,
|
||||||
|
price,
|
||||||
|
marketPrice,
|
||||||
|
discount,
|
||||||
|
specialOffer,
|
||||||
|
warranty,
|
||||||
|
productSummary
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
// src/lib/articlePage.ts
|
// src/lib/articlePage.ts
|
||||||
import { categories } from "../../data/categories";
|
import { categories } from "@/data/categories";
|
||||||
|
|
||||||
export type ArticleResult =
|
export type ArticleResult =
|
||||||
| { type: "article_home"; data: any }
|
| { type: "article_home"; data: any }
|
||||||
@@ -29,5 +29,8 @@ export function resolveArticlePage(slug: string): ArticleResult | null {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// DETAIL
|
// DETAIL
|
||||||
return { type: "article_detail", data: { slug } };
|
const isValidSlugFormat = slug.includes('-') && slug.length > 5;
|
||||||
|
if (!isValidSlugFormat) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
// hoanghapc/src/lib/productPage.ts
|
// hoanghapc/src/lib/productPage.ts
|
||||||
import { categories } from "../../data/categories";
|
import { categories } from "@/data/categories";
|
||||||
import { productList } from "../../data/product-list";
|
import { productList } from "@/data/products";
|
||||||
|
|
||||||
export type ProductResult =
|
export type ProductResult =
|
||||||
| { type: "product_category"; data: any }
|
| { type: "product_category"; data: any }
|
||||||
@@ -23,7 +23,7 @@ export function resolveProductPage(slug: string): ProductResult | null {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// DETAIL
|
// DETAIL
|
||||||
const product = productList.find(p => p.productUrl === url);
|
const product = productList.list?.find(p => p.productUrl === url);
|
||||||
if (product) {
|
if (product) {
|
||||||
return { type: "product_detail", data: product };
|
return { type: "product_detail", data: product };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { notFound } from "next/navigation";
|
||||||
import { resolveArticlePage } from "./resolveArticlePage";
|
import { resolveArticlePage } from "./resolveArticlePage";
|
||||||
import { resolveProductPage } from "./resolveProductPage";
|
import { resolveProductPage } from "./resolveProductPage";
|
||||||
|
|
||||||
@@ -6,12 +7,18 @@ export type SlugResult =
|
|||||||
| ReturnType<typeof resolveProductPage>;
|
| ReturnType<typeof resolveProductPage>;
|
||||||
|
|
||||||
export function findBySlug(slug?: string): SlugResult | null {
|
export function findBySlug(slug?: string): SlugResult | null {
|
||||||
if (!slug) return null;
|
if (!slug || slug.trim() === '') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
// PRODUCT
|
// PRODUCT
|
||||||
const product = resolveProductPage(slug);
|
const product = resolveProductPage(slug);
|
||||||
if (product) return product;
|
if (product) return product;
|
||||||
|
|
||||||
// ARTICLE
|
// ARTICLE
|
||||||
return resolveArticlePage(slug);
|
const articler = resolveArticlePage(slug);
|
||||||
|
if (articler) return articler;
|
||||||
|
|
||||||
|
// 404
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
@@ -1,48 +1,143 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
const CART_KEY = 'cart_product_ids';
|
export interface CartItem {
|
||||||
|
id: number;
|
||||||
|
cartQuantity: number;
|
||||||
|
}
|
||||||
|
|
||||||
// 1. Lấy danh sách ID
|
const CART_KEY = 'cart_products';
|
||||||
export function getCartProductIds(): number[] {
|
const CART_CHANGE_EVENT = 'cart-changed';
|
||||||
|
|
||||||
|
// Helper để dispatch event khi cart thay đổi
|
||||||
|
function notifyCartChange() {
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
window.dispatchEvent(new Event(CART_CHANGE_EVENT));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. Lấy danh sách sản phẩm trong giỏ
|
||||||
|
export function getCartItems(): CartItem[] {
|
||||||
if (typeof window === 'undefined') return [];
|
if (typeof window === 'undefined') return [];
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const raw = localStorage.getItem(CART_KEY);
|
const raw = localStorage.getItem(CART_KEY);
|
||||||
return raw ? (JSON.parse(raw) as number[]) : [];
|
return raw ? (JSON.parse(raw) as CartItem[]) : [];
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Invalid cart data', error);
|
console.error('Invalid cart data', error);
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Thêm sản phẩm
|
// 2. Thêm sản phẩm vào giỏ
|
||||||
export function addProductToCart(productId: number) {
|
export function addProductToCart(productId: number, quantity: number = 1): { success: boolean; message: string; isNew: boolean } {
|
||||||
if (typeof window === 'undefined') return;
|
if (typeof window === 'undefined') {
|
||||||
|
return { success: false, message: 'Window is undefined', isNew: false };
|
||||||
const ids = getCartProductIds();
|
|
||||||
|
|
||||||
if (ids.includes(productId)) {
|
|
||||||
console.log('Product đã tồn tại:', productId);
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ids.push(productId);
|
const items = getCartItems();
|
||||||
localStorage.setItem(CART_KEY, JSON.stringify(ids));
|
const existingItem = items.find(item => item.id === productId);
|
||||||
|
|
||||||
|
if (existingItem) {
|
||||||
|
existingItem.cartQuantity += quantity;
|
||||||
|
localStorage.setItem(CART_KEY, JSON.stringify(items));
|
||||||
|
notifyCartChange(); // Notify change
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
message: `Đã cập nhật số lượng thành ${existingItem.cartQuantity}`,
|
||||||
|
isNew: false
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. Xóa 1 sản phẩm
|
items.push({ id: productId, cartQuantity: quantity });
|
||||||
|
localStorage.setItem(CART_KEY, JSON.stringify(items));
|
||||||
|
notifyCartChange(); // Notify change
|
||||||
|
return { success: true, message: 'Đã thêm sản phẩm vào giỏ hàng', isNew: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Tăng số lượng sản phẩm
|
||||||
|
export function increaseQuantity(productId: number, amount: number = 1): boolean {
|
||||||
|
if (typeof window === 'undefined') return false;
|
||||||
|
|
||||||
|
const items = getCartItems();
|
||||||
|
const item = items.find(item => item.id === productId);
|
||||||
|
|
||||||
|
if (item) {
|
||||||
|
item.cartQuantity += amount;
|
||||||
|
localStorage.setItem(CART_KEY, JSON.stringify(items));
|
||||||
|
notifyCartChange(); // Notify change
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Giảm số lượng sản phẩm
|
||||||
|
export function decreaseQuantity(productId: number, amount: number = 1): boolean {
|
||||||
|
if (typeof window === 'undefined') return false;
|
||||||
|
|
||||||
|
const items = getCartItems();
|
||||||
|
const item = items.find(item => item.id === productId);
|
||||||
|
|
||||||
|
if (item) {
|
||||||
|
const newQuantity = item.cartQuantity - amount;
|
||||||
|
if (newQuantity < 1) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
item.cartQuantity = newQuantity;
|
||||||
|
localStorage.setItem(CART_KEY, JSON.stringify(items));
|
||||||
|
notifyCartChange(); // Notify change
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. Update số lượng trực tiếp
|
||||||
|
export function updateQuantity(productId: number, cartQuantity: number): boolean {
|
||||||
|
if (typeof window === 'undefined') return false;
|
||||||
|
|
||||||
|
if (cartQuantity < 1) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const items = getCartItems();
|
||||||
|
const item = items.find(item => item.id === productId);
|
||||||
|
|
||||||
|
if (item) {
|
||||||
|
item.cartQuantity = cartQuantity;
|
||||||
|
localStorage.setItem(CART_KEY, JSON.stringify(items));
|
||||||
|
notifyCartChange(); // Notify change
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 6. Xóa 1 sản phẩm
|
||||||
export function removeProductFromCart(productId: number) {
|
export function removeProductFromCart(productId: number) {
|
||||||
if (typeof window === 'undefined') return;
|
if (typeof window === 'undefined') return;
|
||||||
|
|
||||||
const ids = getCartProductIds();
|
const items = getCartItems();
|
||||||
const newIds = ids.filter(id => id !== productId);
|
const newItems = items.filter(item => item.id !== productId);
|
||||||
|
localStorage.setItem(CART_KEY, JSON.stringify(newItems));
|
||||||
localStorage.setItem(CART_KEY, JSON.stringify(newIds));
|
notifyCartChange(); // Notify change
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. Xóa giỏ hàng
|
// 7. Xóa toàn bộ giỏ hàng
|
||||||
export function clearCart() {
|
export function clearCart() {
|
||||||
if (typeof window === 'undefined') return;
|
if (typeof window === 'undefined') return;
|
||||||
|
|
||||||
localStorage.removeItem(CART_KEY);
|
localStorage.removeItem(CART_KEY);
|
||||||
|
notifyCartChange(); // Notify change
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 8. Lấy cartQuantity của 1 sản phẩm
|
||||||
|
export function getProductQuantity(productId: number): number {
|
||||||
|
const items = getCartItems();
|
||||||
|
const item = items.find(item => item.id === productId);
|
||||||
|
return item?.cartQuantity ?? 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 9. Kiểm tra sản phẩm có trong giỏ hàng không
|
||||||
|
export function isProductInCart(productId: number): boolean {
|
||||||
|
const items = getCartItems();
|
||||||
|
return items.some(item => item.id === productId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 10. Export event name để hooks sử dụng
|
||||||
|
export { CART_CHANGE_EVENT };
|
||||||
12
src/types/product.ts
Normal file
12
src/types/product.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
// src/types/deal.ts
|
||||||
|
export type ProductItemProps = {
|
||||||
|
price: number;
|
||||||
|
quantity: number;
|
||||||
|
productUrl: string;
|
||||||
|
productImage: string;
|
||||||
|
productName: string;
|
||||||
|
marketPrice : number;
|
||||||
|
specialOffer : string;
|
||||||
|
warranty: string;
|
||||||
|
productSummary: string;
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user