This commit is contained in:
2026-03-13 17:23:37 +07:00
parent 25111ff10e
commit 2d2bf85f83
43 changed files with 2094 additions and 448 deletions

View File

@@ -1,6 +1,7 @@
'use client';
import Link from 'next/link';
import { FaHouse, FaAngleRight } from 'react-icons/fa6';
import { FaAngleRight, FaHouse } from 'react-icons/fa6';
interface BreadcrumbItem {
name: string | undefined;
@@ -19,25 +20,27 @@ export const Breadcrumb = ({ items }: { items: BreadcrumbItem[] }) => {
>
<Link href="/" itemProp="item">
<span itemProp="name" className="flex items-center gap-2">
<span style={{ fontSize: 0 }}>Trang chủ</span> <FaHouse className="text-gray-700" />
<span style={{ fontSize: 0 }}>Trang chủ</span>
<FaHouse className="text-gray-700" />
</span>
</Link>{' '}
</Link>
<FaAngleRight className="text-gray-700" />
<meta itemProp="position" content="1" />
</li>
{items.map((item, idx) => (
{items.map((item, index) => (
<li
key={idx}
key={`${item.url}-${index}`}
itemProp="itemListElement"
itemScope
itemType="http://schema.org/ListItem"
className="flex items-center gap-2"
>
<Link href={item.url ?? '/'} itemProp="item">
<span itemProp="name">{item?.name}</span>
<span itemProp="name">{item.name}</span>
</Link>
{idx < items.length - 1 && <FaAngleRight className="text-gray-700" />}
<meta itemProp="position" content={(idx + 1).toString()} />
{index < items.length - 1 && <FaAngleRight className="text-gray-700" />}
<meta itemProp="position" content={(index + 2).toString()} />
</li>
))}
</ol>

View File

@@ -1,14 +1,13 @@
'use client';
import { useEffect, useState } from 'react';
import { MSWContext } from '@/contexts/MSWContext';
/**
* Khởi động MSW browser worker trong development.
* Block render cho đến khi worker ready để tránh race condition
* (requests gửi trước khi MSW sẵn sàng sẽ bị passthrough → "Failed to fetch").
* Cung cấp trạng thái ready qua MSWContext để các hook fetch
* (useApiData) tự chờ MSW sẵn sàng trước khi gọi API.
*/
const MSWProvider = ({ children }: { children: React.ReactNode }) => {
// Production: render ngay (NODE_ENV !== 'development' → ready = true)
// Development: chờ worker.start() xong rồi mới render
const [ready, setReady] = useState(process.env.NODE_ENV !== 'development');
useEffect(() => {
@@ -23,12 +22,11 @@ const MSWProvider = ({ children }: { children: React.ReactNode }) => {
.then(() => setReady(true))
.catch((err) => {
console.error('[MSW] Failed to start worker:', err);
setReady(true); // vẫn render dù worker fail
setReady(true);
});
}, []);
if (!ready) return null;
return <>{children}</>;
return <MSWContext.Provider value={ready}>{children}</MSWContext.Provider>;
};
export default MSWProvider;

View File

@@ -0,0 +1,5 @@
const Skeleton = ({ className = '' }: { className?: string }) => (
<div className={`animate-pulse rounded-lg bg-gray-200 ${className}`} />
);
export default Skeleton;

View File

@@ -10,7 +10,6 @@ export const ErrorLink = () => {
transition={{ duration: 0.4 }}
className="w-full max-w-md rounded-3xl bg-white p-8 text-center shadow-xl"
>
{/* Icon lỗi link */}
<motion.div
animate={{ y: [0, -4, 0] }}
transition={{ repeat: Infinity, duration: 1.8 }}
@@ -34,23 +33,22 @@ export const ErrorLink = () => {
<h1 className="text-2xl font-bold text-gray-800">Đưng dẫn không hợp lệ</h1>
<p className="mt-3 text-sm text-gray-600">
Bạn truy cập không tồn tại hoặc đưng dẫn đã bị thay đi.
Nội dung bạn truy cập không tồn tại hoặc đưng dẫn đã đưc thay đi.
</p>
{/* CTA */}
<div className="mt-8 flex flex-col gap-3">
<Link
href="/"
className="rounded-xl bg-blue-600 px-6 py-3 text-sm font-semibold text-white transition hover:bg-blue-700"
>
Về trang chủ
Về trang chủ
</Link>
<Link
href="/products"
href="/pc-gaming"
className="rounded-xl border border-gray-300 px-6 py-3 text-sm font-medium text-gray-700 transition hover:bg-gray-100"
>
Xem tất cả sản phẩm
Xem danh mục PC Gaming
</Link>
</div>
</motion.div>