update
This commit is contained in:
95
src/components/Deal/ItemDeal.tsx
Normal file
95
src/components/Deal/ItemDeal.tsx
Normal file
@@ -0,0 +1,95 @@
|
||||
'use client';
|
||||
import React, { useState } from 'react';
|
||||
import { parse } from 'date-fns';
|
||||
import Link from 'next/link';
|
||||
import Image from 'next/image';
|
||||
import CounDown from '@/components/Common/CounDown';
|
||||
import { DealType } from '@/types';
|
||||
import { formatCurrency } from '@/lib/formatPrice';
|
||||
|
||||
type ItemDealProps = {
|
||||
Item: DealType;
|
||||
};
|
||||
|
||||
const ItemDeal: React.FC<ItemDealProps> = ({ Item }) => {
|
||||
const [now] = useState(() => Date.now());
|
||||
|
||||
// ép kiểu to_time sang số (timestamp) hoặc Date
|
||||
const deadline = parse(Item.to_time, 'dd-MM-yyyy, h:mm a', new Date()).getTime();
|
||||
|
||||
// chỉ hiển thị nếu deadline còn lớn hơn thời gian hiện tại
|
||||
if (deadline <= now) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="product-item">
|
||||
<div className="item-deal">
|
||||
<Link href={Item.product_info.productUrl} className="product-image position-relative">
|
||||
<Image
|
||||
src={Item.product_info.productImage.large}
|
||||
width={250}
|
||||
height={250}
|
||||
alt={Item.product_info.productName}
|
||||
/>
|
||||
</Link>
|
||||
<div className="product-info flex-1">
|
||||
<Link href={Item.product_info.productUrl}>
|
||||
<h3 className="product-title line-clamp-3">{Item.product_info.productName}</h3>
|
||||
</Link>
|
||||
<div className="product-martket-main flex items-center">
|
||||
{Item.product_info.marketPrice > 0 && (
|
||||
<>
|
||||
<p className="product-market-price">
|
||||
{Item.product_info.marketPrice.toLocaleString()} ₫
|
||||
</p>
|
||||
<div className="product-percent-price">-{Item.product_info.price_off || 0}%</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<div className="product-price-main font-bold">
|
||||
{Item.product_info.price > '0'
|
||||
? `${formatCurrency(Item.product_info.price)}đ`
|
||||
: 'Liên hệ'}
|
||||
</div>
|
||||
<div className="p-quantity-sale">
|
||||
<i className="sprite sprite-fire-deal"></i>
|
||||
<div className="bg-gradient"></div>
|
||||
{(() => {
|
||||
const percentRemaining =
|
||||
((Number(Item.quantity) - Number(Item.sale_quantity)) / Number(Item.quantity)) *
|
||||
100;
|
||||
|
||||
return (
|
||||
<>
|
||||
<p className="js-line-deal-left" style={{ width: `${percentRemaining}%` }}></p>
|
||||
</>
|
||||
);
|
||||
})()}
|
||||
<span>
|
||||
Còn {Number(Item.quantity) - Number(Item.sale_quantity)}/{Number(Item.quantity)} sản
|
||||
phẩm
|
||||
</span>
|
||||
</div>
|
||||
<div className="js-item-deal-time js-item-time-25404">
|
||||
<div className="time-deal-page flex items-center justify-center gap-2">
|
||||
<div>Kết thúc sau: </div>
|
||||
<CounDown deadline={Item.to_time} />
|
||||
</div>
|
||||
</div>
|
||||
<a href="javascript:buyNow(25404)" className="buy-now-deal">
|
||||
Mua giá sốc
|
||||
</a>
|
||||
<Link
|
||||
href="/bts-gaming-02"
|
||||
className="text-deal-item color-primary mt-3 hidden font-bold"
|
||||
>
|
||||
Xem sản phẩm
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ItemDeal;
|
||||
67
src/components/Product/BoxFilter/ActiveFilters/index.tsx
Normal file
67
src/components/Product/BoxFilter/ActiveFilters/index.tsx
Normal file
@@ -0,0 +1,67 @@
|
||||
'use client';
|
||||
import React from 'react';
|
||||
import Link from 'next/link';
|
||||
import { usePathname, useSearchParams } from 'next/navigation';
|
||||
import { PriceFilter, AttributeFilterList, BrandFilter } from '@/types';
|
||||
import { FaXmark } from 'react-icons/fa6';
|
||||
|
||||
interface Filters {
|
||||
price_filter_list?: PriceFilter[];
|
||||
attribute_filter_list?: AttributeFilterList[];
|
||||
brand_filter_list?: BrandFilter[];
|
||||
current_category?: { url: string };
|
||||
}
|
||||
|
||||
const ActiveFilters: React.FC<{ filters: Filters }> = ({ filters }) => {
|
||||
const pathname = usePathname();
|
||||
const searchParams = useSearchParams();
|
||||
const fullUrl = `${pathname}?${searchParams.toString()}`;
|
||||
|
||||
const selectedPrice = filters.price_filter_list?.filter((f) => fullUrl.includes(f.url)) ?? [];
|
||||
const selectedBrand = filters.brand_filter_list?.filter((f) => fullUrl.includes(f.url)) ?? [];
|
||||
const selectedAttr =
|
||||
filters.attribute_filter_list?.flatMap((attr) =>
|
||||
attr.value_list.filter((v) => pathname.includes(v.url)),
|
||||
) ?? [];
|
||||
|
||||
const allSelected = [...selectedPrice, ...selectedBrand, ...selectedAttr];
|
||||
const isFiltered = allSelected.length;
|
||||
|
||||
console.log(isFiltered);
|
||||
|
||||
if (isFiltered === 0) return null;
|
||||
|
||||
return (
|
||||
<div className="info-filter-category flex gap-3">
|
||||
<p className="title">Lọc theo:</p>
|
||||
<div className="list-filter-category list-filter-active list-filter-last flex flex-wrap items-center gap-3">
|
||||
{selectedPrice.map((item) => (
|
||||
<Link key={item.url} href={item.url} className="item flex items-center gap-2 bg-white">
|
||||
{item.name} <FaXmark />
|
||||
</Link>
|
||||
))}
|
||||
{selectedBrand.map((item) => (
|
||||
<Link key={item.url} href={item.url} className="item flex items-center gap-2 bg-white">
|
||||
{item.name} <FaXmark />
|
||||
</Link>
|
||||
))}
|
||||
{selectedAttr.map((val) => (
|
||||
<Link key={val.url} href={val.url} className="item flex items-center gap-2 bg-white">
|
||||
{val.name} <FaXmark />
|
||||
</Link>
|
||||
))}
|
||||
|
||||
{isFiltered >= 1 && (
|
||||
<Link
|
||||
href={filters.current_category?.url ?? '#'}
|
||||
className="item delete-filter-all flex items-center gap-2 bg-white"
|
||||
>
|
||||
Xóa tất cả <FaXmark />
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ActiveFilters;
|
||||
106
src/components/Product/BoxFilter/index.tsx
Normal file
106
src/components/Product/BoxFilter/index.tsx
Normal file
@@ -0,0 +1,106 @@
|
||||
'use client';
|
||||
import React from 'react';
|
||||
import Link from 'next/link';
|
||||
import { PriceFilter, AttributeFilterList, BrandFilter } from '@/types';
|
||||
import { FaSortDown } from 'react-icons/fa6';
|
||||
import ActiveFilters from './ActiveFilters';
|
||||
|
||||
interface Filters {
|
||||
price_filter_list?: PriceFilter[];
|
||||
attribute_filter_list?: AttributeFilterList[];
|
||||
brand_filter_list?: BrandFilter[];
|
||||
}
|
||||
|
||||
interface BoxFilterProps {
|
||||
filters: Filters;
|
||||
}
|
||||
|
||||
const BoxFilter: React.FC<BoxFilterProps> = ({ filters }) => {
|
||||
const { price_filter_list, attribute_filter_list, brand_filter_list } = filters;
|
||||
|
||||
return (
|
||||
<div className="box-filter-category boder-radius-10">
|
||||
{/* khoảng giá */}
|
||||
{price_filter_list && (
|
||||
<div className="info-filter-category flex gap-10">
|
||||
<p className="title">Khoảng giá:</p>
|
||||
<div className="list-filter-category flex flex-1 flex-wrap items-center gap-2">
|
||||
{price_filter_list.map((ItemPrice, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className={`item item-cetner flex gap-4 ${ItemPrice.is_selected == '1' ? 'current' : ''}`}
|
||||
>
|
||||
<Link href={ItemPrice.url}>{ItemPrice.name}</Link>
|
||||
<a href={ItemPrice.url}>
|
||||
({ItemPrice.is_selected == '1' ? 'Xóa' : ItemPrice.count})
|
||||
</a>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* chọn thiêu tiêu trí */}
|
||||
{attribute_filter_list && (
|
||||
<div className="info-filter-category flex gap-10">
|
||||
<p className="title">Chọn theo tiêu chí:</p>
|
||||
<div className="list-filter-category flex flex-1 flex-wrap items-center gap-3">
|
||||
{/* thương hiệu */}
|
||||
{brand_filter_list && brand_filter_list.length > 0 && (
|
||||
<div className={`item ${brand_filter_list[0].is_selected === '1' ? 'current' : ''}`}>
|
||||
<div className="flex items-center">
|
||||
{brand_filter_list[0].is_selected === '1' ? (
|
||||
<span>{brand_filter_list[0].name}</span>
|
||||
) : (
|
||||
<span>Thương hiệu</span>
|
||||
)}
|
||||
<FaSortDown size={16} style={{ marginBottom: 8 }} />
|
||||
</div>
|
||||
<ul>
|
||||
{brand_filter_list.map((item, idx) => (
|
||||
<li key={idx} className="flex items-center gap-3">
|
||||
<Link href={item.url}>{item.name}</Link>
|
||||
<Link href={item.url}>({item.is_selected === '1' ? 'Xóa' : item.count})</Link>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Attribute filter */}
|
||||
{attribute_filter_list && attribute_filter_list.length > 0 && (
|
||||
<>
|
||||
{attribute_filter_list.map((attr, idx) => (
|
||||
<div
|
||||
key={idx}
|
||||
className={`item ${attr.value_list[0]?.is_selected === '1' ? 'current' : ''}`}
|
||||
>
|
||||
<a href="javascript:void(0)" className="flex items-center">
|
||||
{attr.value_list[0]?.is_selected === '1' ? (
|
||||
<span>{attr.value_list[0].name}</span>
|
||||
) : (
|
||||
<span>{attr.name}</span>
|
||||
)}
|
||||
<FaSortDown size={16} style={{ marginBottom: 8 }} />
|
||||
</a>
|
||||
<ul>
|
||||
{attr.value_list.map((val) => (
|
||||
<li key={val.id} className="flex items-center gap-3">
|
||||
<Link href={val.url}>{val.name}</Link>
|
||||
<Link href={val.url}>{val.is_selected === '1' ? 'Xóa' : val.count}</Link>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<ActiveFilters filters={filters} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
export default BoxFilter;
|
||||
93
src/components/Product/BoxSort/index.tsx
Normal file
93
src/components/Product/BoxSort/index.tsx
Normal file
@@ -0,0 +1,93 @@
|
||||
'use client';
|
||||
import React from 'react';
|
||||
import Link from 'next/link';
|
||||
import { usePathname } from 'next/navigation';
|
||||
import { FaGrip, FaList } from 'react-icons/fa6';
|
||||
|
||||
interface SortItem {
|
||||
key: string;
|
||||
url: string;
|
||||
}
|
||||
|
||||
interface SortProps {
|
||||
sort_by_collection: SortItem[];
|
||||
product_display_type?: 'grid' | 'list';
|
||||
}
|
||||
|
||||
const BoxSort: React.FC<SortProps> = ({ sort_by_collection, product_display_type }) => {
|
||||
const pathname = usePathname();
|
||||
|
||||
return (
|
||||
<div className="box-sort-category flex items-center justify-between">
|
||||
<div className="sort-option flex items-center gap-3">
|
||||
{sort_by_collection
|
||||
.filter((item) =>
|
||||
['price-desc', 'price-asc', 'comment', 'rating', 'name'].includes(item.key),
|
||||
)
|
||||
.map((item) => {
|
||||
let label: string | null = null;
|
||||
let iconClass: string | null = null;
|
||||
|
||||
switch (item.key) {
|
||||
case 'price-desc':
|
||||
label = 'Giá giảm dần';
|
||||
iconClass = 'sprite-more sprite-gia-giam-category';
|
||||
break;
|
||||
case 'price-asc':
|
||||
label = 'Giá tăng dần';
|
||||
iconClass = 'sprite-more sprite-gia-tang-cateogry';
|
||||
break;
|
||||
case 'comment':
|
||||
label = 'Trao đổi';
|
||||
iconClass = 'sprite-more sprite-trao-doi-category';
|
||||
break;
|
||||
case 'rating':
|
||||
label = 'Đánh giá';
|
||||
iconClass = 'sprite-more sprite-danh-gia-category';
|
||||
break;
|
||||
case 'name':
|
||||
label = 'Tên A->Z';
|
||||
break;
|
||||
}
|
||||
return (
|
||||
<Link
|
||||
key={item.key}
|
||||
href={item.url}
|
||||
className={`item flex items-center ${
|
||||
pathname.includes(item.key) ? 'selected' : ''
|
||||
}`}
|
||||
>
|
||||
{iconClass && <i className={iconClass}></i>}
|
||||
<span>{label}</span>
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<div className="sort-bar-select-category flex items-center gap-3">
|
||||
<a
|
||||
href="javascript:;"
|
||||
className={`item-sort-bar d-flex align-items-center ${
|
||||
product_display_type === 'grid' ? 'active' : ''
|
||||
}`}
|
||||
onClick={() => {
|
||||
window.location.reload();
|
||||
}}
|
||||
>
|
||||
<FaGrip />
|
||||
</a>
|
||||
<a
|
||||
href="javascript:;"
|
||||
className={`item-sort-bar ${product_display_type === 'list' ? 'active' : ''}`}
|
||||
onClick={() => {
|
||||
console.log('Set display to list');
|
||||
window.location.reload();
|
||||
}}
|
||||
>
|
||||
<FaList />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default BoxSort;
|
||||
@@ -1,8 +1,9 @@
|
||||
'use client';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { parse } from 'date-fns';
|
||||
|
||||
interface CountDownProps {
|
||||
deadline: Date | string;
|
||||
deadline: number | string;
|
||||
}
|
||||
|
||||
const CounDown: React.FC<CountDownProps> = ({ deadline }) => {
|
||||
@@ -13,8 +14,10 @@ const CounDown: React.FC<CountDownProps> = ({ deadline }) => {
|
||||
|
||||
const getTime = () => {
|
||||
let time: number;
|
||||
if (deadline instanceof Date) {
|
||||
time = deadline.getTime() - Date.now();
|
||||
|
||||
if (typeof deadline == 'string') {
|
||||
const parsed = parse(deadline as string, 'dd-MM-yyyy, h:mm a', new Date());
|
||||
time = parsed.getTime() - Date.now();
|
||||
} else {
|
||||
time = Number(deadline) * 1000 - Date.now();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user