商品グリッドリストブロック
ListProductsGridコンポーネントは、ReactとTypeScriptで構築された商品表示用の完全にカスタマイズ可能でアクセシブルなグリッドレイアウトです。モダンなデザインパターン、柔軟なカスタマイズオプション、タイトル、アイテム、カード用の構成可能なサブコンポーネントを備えた完全な商品リスティングインターフェースを提供します。
🚀 インストール
npm install @nodeblocks/frontend-list-products-grid-block@0.2.0
📖 使用法
import {ListProductsGrid} from '@nodeblocks/frontend-list-products-grid-block';
- 基本的な使用法
- 高度な使用法
ライブエディター
function SimpleListProductsGrid() { const products = [ { id: '1', title: 'ワイヤレスヘッドホン', description: 'プレミアムノイズキャンセリングヘッドホン', price: '¥19,999', imageUrl: 'https://placehold.co/600x400', badges: ['新着'], }, { id: '2', title: 'スマートウォッチ', description: 'フィットネストラッキングと通知機能', price: '¥29,999', originalPrice: '¥34,999', imageUrl: 'https://placehold.co/600x400', badges: ['セール'], }, { id: '3', title: 'ラップトップスタンド', description: 'エルゴノミックアルミニウムスタンド', price: '¥7,999', imageUrl: 'https://placehold.co/600x400', }, { id: '4', title: 'メカニカルキーボード', description: 'RGBバックライト付きゲーミングキーボード', price: '¥14,999', imageUrl: 'https://placehold.co/600x400', badges: ['人気'], }, ]; const handleNavigate = (href) => console.log('ナビゲート:', href); return ( <ListProductsGrid listProductsGridTitle="注目商品" subtitle="おすすめ商品をご覧ください"> <ListProductsGrid.Title /> <ListProductsGrid.Items> {products.map(product => ( <ListProductsGrid.Items.GridCard key={product.id} title={product.title} imageUrl={product.imageUrl} summary={product.description} chips={product.badges?.map(b => ({label: b}))} linkProps={{href: `/products/${product.id}`, onNavigate: handleNavigate}} button={{children: product.price}} /> ))} </ListProductsGrid.Items> </ListProductsGrid> ); }
結果
Loading...
ライブエディター
function AdvancedListProductsGrid() { const [currentPage, setCurrentPage] = useState(1); const [sortBy, setSortBy] = useState('featured'); const [viewMode, setViewMode] = useState('grid'); const products = [ { id: '1', title: 'プロワイヤレスイヤホン', description: 'アクティブノイズキャンセリング、24時間バッテリー寿命', price: '¥24,999', originalPrice: '¥29,999', imageUrl: 'https://placehold.co/600x400', badges: ['ベストセラー', '-17%'], rating: 4.8, reviewCount: 1234, isFeatured: true, }, { id: '2', title: 'Ultra HDモニター 32"', description: '4K解像度、HDRサポート', price: '¥59,999', imageUrl: 'https://placehold.co/600x400', badges: ['新着'], rating: 4.6, reviewCount: 567, isNew: true, }, { id: '3', title: 'エルゴノミックオフィスチェア', description: '腰部サポートと調整可能なアームレスト', price: '¥44,999', originalPrice: '¥54,999', imageUrl: 'https://placehold.co/600x400', badges: ['セール'], rating: 4.7, reviewCount: 890, }, { id: '4', title: 'ポータブルパワーバンク', description: '20000mAh急速充電', price: '¥6,999', imageUrl: 'https://placehold.co/600x400', rating: 4.5, reviewCount: 2345, }, { id: '5', title: 'ワイヤレスゲーミングマウス', description: '超低遅延、RGBライティング', price: '¥12,999', imageUrl: 'https://placehold.co/600x400', badges: ['人気'], rating: 4.9, reviewCount: 3456, }, { id: '6', title: 'USB-Cハブ 10-in-1', description: 'ユニバーサル接続ソリューション', price: '¥8,999', originalPrice: '¥10,999', imageUrl: 'https://placehold.co/600x400', rating: 4.4, reviewCount: 789, }, ]; const handleAddToCart = (productId) => { console.log('カートに追加:', productId); }; const handleAddToWishlist = (productId) => { console.log('ウィッシュリストに追加:', productId); }; return ( <ListProductsGrid listProductsGridTitle="商品" subtitle={`${products.length}件の商品を表示中(全120件)`} sx={{ maxWidth: 1400, mx: 'auto', px: 3, }} > {({defaultBlocks}) => { const customTitle = ( <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '24px', padding: '16px 0', borderBottom: '1px solid #e2e8f0', }} > <div> <h1 style={{ fontSize: '28px', fontWeight: '700', color: '#1e293b', margin: '0 0 4px 0', }} > 商品 </h1> <p style={{fontSize: '14px', color: '#64748b', margin: 0}}>{products.length}件の商品を表示中(全120件)</p> </div> <div style={{display: 'flex', gap: '12px', alignItems: 'center'}}> <select value={sortBy} onChange={e => setSortBy(e.target.value)} style={{ padding: '10px 16px', borderRadius: '8px', border: '1px solid #e2e8f0', fontSize: '14px', color: '#475569', cursor: 'pointer', }} > <option value="featured">注目</option> <option value="newest">新着</option> <option value="price-low">価格: 安い順</option> <option value="price-high">価格: 高い順</option> <option value="rating">評価順</option> </select> <div style={{ display: 'flex', borderRadius: '8px', overflow: 'hidden', border: '1px solid #e2e8f0', }} > <button onClick={() => setViewMode('grid')} style={{ padding: '10px 14px', background: viewMode === 'grid' ? '#6366f1' : '#ffffff', color: viewMode === 'grid' ? '#ffffff' : '#64748b', border: 'none', cursor: 'pointer', fontSize: '16px', }} > ⊞ </button> <button onClick={() => setViewMode('list')} style={{ padding: '10px 14px', background: viewMode === 'list' ? '#6366f1' : '#ffffff', color: viewMode === 'list' ? '#ffffff' : '#64748b', border: 'none', borderLeft: '1px solid #e2e8f0', cursor: 'pointer', fontSize: '16px', }} > ☰ </button> </div> </div> </div> ); const customItems = ( <div style={{ display: 'grid', gridTemplateColumns: viewMode === 'grid' ? 'repeat(auto-fill, minmax(280px, 1fr))' : '1fr', gap: '24px', }} > {products.map(product => ( <div key={product.id} style={{ display: viewMode === 'list' ? 'flex' : 'block', gap: viewMode === 'list' ? '24px' : '0', background: '#ffffff', borderRadius: '16px', overflow: 'hidden', border: '1px solid #e2e8f0', transition: 'all 0.2s ease', cursor: 'pointer', }} > <div style={{ position: 'relative', width: viewMode === 'list' ? '200px' : '100%', aspectRatio: '1', background: '#f8fafc', flexShrink: 0, }} > <img src={product.imageUrl} alt={product.title} style={{ width: '100%', height: '100%', objectFit: 'cover', }} /> {product.badges && product.badges.length > 0 && ( <div style={{ position: 'absolute', top: '12px', left: '12px', display: 'flex', flexWrap: 'wrap', gap: '6px', }} > {product.badges.map((badge, i) => ( <span key={i} style={{ padding: '4px 10px', borderRadius: '6px', fontSize: '11px', fontWeight: '600', background: badge.includes('%') ? '#ef4444' : badge === '新着' ? '#10b981' : '#6366f1', color: '#ffffff', }} > {badge} </span> ))} </div> )} <button onClick={e => { e.stopPropagation(); handleAddToWishlist(product.id); }} style={{ position: 'absolute', top: '12px', right: '12px', width: '36px', height: '36px', borderRadius: '50%', background: '#ffffff', border: 'none', cursor: 'pointer', display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: '16px', boxShadow: '0 2px 8px rgba(0,0,0,0.1)', }} > ♡ </button> </div> <div style={{padding: '16px', flex: 1}}> <h3 style={{ fontSize: '16px', fontWeight: '600', color: '#1e293b', margin: '0 0 8px 0', }} > {product.title} </h3> {product.description && ( <p style={{ fontSize: '14px', color: '#64748b', margin: '0 0 12px 0', lineHeight: '1.5', }} > {product.description} </p> )} {product.rating && ( <div style={{ display: 'flex', alignItems: 'center', gap: '6px', marginBottom: '12px', }} > <span style={{color: '#f59e0b', fontSize: '14px'}}>{'★'.repeat(Math.floor(product.rating))}</span> <span style={{ fontSize: '13px', color: '#64748b', }} > {product.rating} ({product.reviewCount}) </span> </div> )} <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', }} > <div style={{display: 'flex', alignItems: 'baseline', gap: '8px'}}> <span style={{ fontSize: '20px', fontWeight: '700', color: '#1e293b', }} > {product.price} </span> {product.originalPrice && ( <span style={{ fontSize: '14px', color: '#94a3b8', textDecoration: 'line-through', }} > {product.originalPrice} </span> )} </div> <button onClick={e => { e.stopPropagation(); handleAddToCart(product.id); }} style={{ padding: '10px 20px', borderRadius: '10px', background: 'linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%)', color: '#ffffff', border: 'none', fontSize: '14px', fontWeight: '600', cursor: 'pointer', }} > カートに追加 </button> </div> </div> </div> ))} </div> ); const customPagination = ( <div style={{ display: 'flex', justifyContent: 'center', gap: '8px', marginTop: '32px', paddingTop: '24px', borderTop: '1px solid #e2e8f0', }} > {[1, 2, 3, 4, 5].map(page => ( <button key={page} onClick={() => setCurrentPage(page)} style={{ width: '40px', height: '40px', borderRadius: '10px', border: 'none', background: page === currentPage ? '#6366f1' : '#f1f5f9', color: page === currentPage ? '#ffffff' : '#475569', fontSize: '14px', fontWeight: '600', cursor: 'pointer', }} > {page} </button> ))} </div> ); return { blocks: { ...defaultBlocks, title: customTitle, items: customItems, pagination: customPagination, }, blockOrder: ['title', 'items', 'pagination'], }; }} </ListProductsGrid> ); }
結果
Loading...
🔧 プロパティリファレンス
メインコンポーネントのプロパティ
| プロパティ | 型 | デフォルト | 説明 |
|---|---|---|---|
listProductsGridTitle | ReactNode | undefined | 商品リストのメインタイトル |
subtitle | string | undefined | メインタイトルの上に表示されるセカンダリタイトル |
spacing | number | 5 | 子要素間のスペーシング |
className | string | undefined | グリッドコンテナスタイリング用の追加CSSクラス名 |
children | BlocksOverride | undefined | デフォルトレンダリングをオーバーライドするカスタムブロックコンポーネント |
注意: メインコンポーネントはMUI StackProps を拡張します。デフォルトスタイリングには px: 2 と py: 5 のパディングが含まれます。
サブコンポーネント
ListProductsGridコンポーネントは複数のサブコンポーネントを提供します。すべてのサブコンポーネントは、メインコンポーネントのコンテキストからデフォルト値を受け取り、プロパティを通じてこれらの値をオーバーライドできます。
ListProductsGrid.Title
サブタイトルとメインタイトルを持つグリッドタイトルセクションのコンテナ。
| プロパティ | 型 | デフォルト | 説明 |
|---|---|---|---|
listProductsGridTitle | ReactNode | コンテキストから | メインタイトルとして表示するコンテンツ |
subtitle | string | コンテキストから | サブタイトルとして表示するコンテンツ(タイトルの上に表示) |
spacing | number | 0.5 | サブタイトルとタイトル間のスペーシング |
children | ReactNode | デフォルトタイトルレイアウト | デフォルトタイトルレイアウトをオーバーライドするカスタムコンテンツ |
className | string | undefined | スタイリング用の追加CSSクラス名 |
注意: このコンポーネントはMUI StackProps を拡張します。デフォルトスタイリングには alignItems: 'center' が含まれます。
ListProductsGrid.Items
商品グリッドカードのコンテナ。
| プロパティ | 型 | デフォルト | 説明 |
|---|---|---|---|
spacing | number | 2 | グリッドアイテム間のスペーシング |
children | ReactNode | undefined | レンダリングするグリッドカードコンポーネント |
className | string | undefined | スタイリング用の追加CSSクラス名 |
注意: このコンポーネントはMUI StackProps を拡張します。
ListProductsGrid.Items.GridCard
リッチコンテンツオプションを持つ個別の商品カードコンポーネント。
| プロパティ | 型 | デフォルト | 説明 |
|---|---|---|---|
title | string | 必須 | カードに表示されるメインタイトル |
subtitle | string | undefined | タイトル下に表示されるセカンダリテキスト |
subtitleImageUrl | string | undefined | サブタイトル横に表示される小さな画像のURL |
summary | string | undefined | 簡潔な説明や要約テキスト |
imageUrl | string | undefined | カードに表示されるメイン画像のURL |
tertiaryText | string | undefined | タイトルの上に表示される追加情報 |
content | string | undefined | カード用の制限なしテキストコンテンツ |
chips | ChipProps[] | undefined | オプションの leftIcon、rightIcon を持つMUI Chipプロパティの配列 |
tags | Array<{icon?: ReactElement, label: string | string[]}> | undefined | アイコンとラベルを持つタグオブジェクトの配列 |
titleIcon | ReactElement | undefined | タイトルの前に表示されるアイコン |
titleAction | IconButtonProps | undefined | タイトルの後に表示されるアクションボタン |
button | ButtonProps | undefined | カードのメインコールトゥアクションボタン |
status | StatusObject | undefined | ラベルとオプションのアクションを持つステータスコールアウト |
linkProps | LinkProps | undefined | カードをクリック可能にするナビゲーションプロパティ |
hasDivider | boolean | false | カードコンテンツセクション間にディバイダーを追加 |
disabled | boolean | false | カードスタイルを無効状態に設定 |
className | string | undefined | スタイリング用の追加CSSクラス名 |
注意: このコンポーネントはMUI CardProps を拡張します。
🎨 設定例
カスタムグリッドレイアウト:
<ListProductsGrid
listProductsGridTitle="注目商品"
subtitle="ベストセラー"
spacing={3}
sx={{ bgcolor: '#f5f5f5', borderRadius: 2 }}
>
<ListProductsGrid.Title sx={{ textAlign: 'left', alignItems: 'flex-start' }} />
<ListProductsGrid.Items
sx={{
display: 'grid',
gridTemplateColumns: 'repeat(auto-fill, minmax(280px, 1fr))',
gap: 3
}}
>
{products.map((product, i) => (
<ListProductsGrid.Items.GridCard key={i} {...product} />
))}
</ListProductsGrid.Items>
</ListProductsGrid>
ステータスとアクション付きカード:
<ListProductsGrid.Items.GridCard
title="プレミアムヘッドホン"
subtitle="ソニーエレクトロニクス"
imageUrl="/images/headphones.jpg"
chips={[{label: '新着', color: 'primary'}]}
status={{
label: '在庫あり',
color: 'primary',
action: {
children: '詳細を見る',
href: '/products/123',
onNavigate: href => router.push(href),
},
}}
button={{
children: 'カートに追加',
variant: 'contained',
color: 'primary',
}}
linkProps={{
href: '/products/123',
onNavigate: href => router.push(href),
}}
/>
ブロックオーバーライドパターン:
<ListProductsGrid
listProductsGridTitle="ショップコレクション"
subtitle="厳選アイテム"
>
{({defaultBlocks}) => ({
blocks: {
...defaultBlocks,
title: customTitle,
items: customItems,
pagination: customPagination,
},
blockOrder: ['title', 'items', 'pagination'],
})}
</ListProductsGrid>
🔧 TypeScriptサポート
包括的な型定義による完全なTypeScriptサポート:
import {ListProductsGrid} from '@nodeblocks/frontend-list-products-grid-block';
import {ReactElement} from 'react';
import {ChipProps, ButtonProps, IconButtonProps} from '@mui/material';
// Product grid item interface
interface ProductGridItem {
id: string;
title: string;
description?: string;
price: string;
originalPrice?: string;
imageUrl: string;
badges?: string[];
rating?: number;
reviewCount?: number;
isNew?: boolean;
isFeatured?: boolean;
}
// Link props for card navigation
interface LinkProps {
href?: string;
onClick?: (e: React.MouseEvent) => void;
onNavigate: ((href: string) => void) | 'standard-html-link';
openInNewTab?: boolean;
}
// Status object type
interface StatusObject {
label: string;
color?: 'default' | 'primary';
action?: Partial<ButtonProps> & {
href?: string;
onNavigate?: ((href: string) => void) | 'standard-html-link';
openInNewTab?: boolean;
};
}
// Grid card props type
interface GridCardProps {
title: string;
subtitle?: string;
subtitleImageUrl?: string;
summary?: string;
imageUrl?: string;
tertiaryText?: string;
content?: string;
chips?: (ChipProps & {
leftIcon?: ReactElement;
rightIcon?: ReactElement;
label: string;
})[];
tags?: {icon?: ReactElement; label: string | string[]}[];
titleIcon?: ReactElement;
titleAction?: Partial<IconButtonProps>;
button?: Partial<ButtonProps>;
status?: StatusObject;
linkProps?: LinkProps;
hasDivider?: boolean;
disabled?: boolean;
}
function TypedListProductsGrid() {
const productData: ProductGridItem[] = [
{
id: 'prod-1',
title: 'プレミアムラップトップ',
description: '15インチディスプレイ、16GB RAM、512GB SSD',
price: '¥179,999',
imageUrl: 'https://placehold.co/600x400',
badges: ['新着'],
rating: 4.8,
reviewCount: 256,
},
{
id: 'prod-2',
title: 'ワイヤレス充電器',
description: '急速充電、すべてのQiデバイス対応',
price: '¥5,999',
imageUrl: 'https://placehold.co/600x400',
rating: 4.5,
reviewCount: 1024,
},
];
const handleNavigate = (href: string): void => {
console.log('ナビゲート先:', href);
};
return (
<ListProductsGrid
listProductsGridTitle="商品一覧"
subtitle="厳選された電子機器"
sx={{
maxWidth: 1200,
mx: 'auto',
p: 3,
}}
>
<ListProductsGrid.Title />
<ListProductsGrid.Items>
{productData.map(product => (
<ListProductsGrid.Items.GridCard
key={product.id}
title={product.title}
imageUrl={product.imageUrl}
summary={product.description}
chips={product.badges?.map(b => ({label: b}))}
linkProps={{href: `/products/${product.id}`, onNavigate: handleNavigate}}
button={{children: product.price}}
/>
))}
</ListProductsGrid.Items>
</ListProductsGrid>
);
}
📝 注意事項
- メインコンポーネントはデフォルト
spacing={5}とパディングpx: 2, py: 5を持つMUIStackPropsを拡張します ListProductsGrid.TitleはTypographyコンポーネントを使用してメインタイトルの上にサブタイトルを表示しますListProductsGrid.Items.GridCardはtitleプロパティが必要です。その他のプロパティはすべてオプションですlinkPropsが提供されると、CardActionAreaを介してカード全体がクリック可能になりますonNavigateプロパティはネイティブブラウザナビゲーションのために'standard-html-link'に設定できますlinkPropsが存在する場合、status.actionはボタンとしてレンダリングされます(ネストされた<a>タグを避けるため)chipsはMUIChipPropsに加えて、オプションのleftIconとrightIcon要素をサポートします- 配列ラベルを持つ
tagsは " / " セパレータで結合されます(例:['A', 'B']→ "A / B") - カード画像は
object-fit: coverで固定高さ120pxを持ちます - 無効化されたカードは不透明度が低下(0.6)し、ポインターイベントが無効化されます
React、TypeScript、MUIを使用して❤️で構築されました。