🚀 VITE で Nodeblocks をゼロからセットアップする
VITE を使って、Nodeblocks コンポーネントでモダンな React アプリケーションを超高速に開発 ⚡
この包括的なガイドでは、VITE、React、Nodeblocks フロントエンドコンポーネントを使って、ゼロから完全なアプリケーションを作成する手順を説明します。
📋 前提条件
🎯 プロジェクト設定
ステップ 1: VITE プロジェクトの作成
# React と TypeScript で新しい VITE プロジェクトを作成
npm create vite@latest my-nodeblocks-app -- --template react-ts
# プロジェクトディレクトリに移動
cd my-nodeblocks-app
# 依存関係をインストール
npm install
ステップ 2: Nodeblocks 依存関係のインストール
以下は、このあと出てくるアプリ例で使うパッケージです:
npm install @emotion/react @emotion/styled
npm install @mui/icons-material @mui/material @mui/x-date-pickers luxon
npm install @nodeblocks/frontend-create-product-block
npm install @nodeblocks/frontend-filter-search-panel-block
npm install @nodeblocks/frontend-footer-block
npm install @nodeblocks/frontend-list-products-grid-block
npm install @nodeblocks/frontend-navbar-block
npm install @nodeblocks/frontend-reset-password-block
npm install @nodeblocks/frontend-search-by-chip-block
npm install @nodeblocks/frontend-signin-block
npm install @nodeblocks/frontend-signup-block
npm install @nodeblocks/frontend-theme
🏗️ 基本的なアプリケーション構造
ステップ 3: エントリーポイントの作成
src/main.tsx の内容を次のように置き換えます:
import React from 'react'
import ReactDOM from 'react-dom/client'
import { ThemeProvider, defaultTheme } from '@nodeblocks/frontend-theme'
import App from './App'
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<ThemeProvider theme={defaultTheme}>
<App />
</ThemeProvider>
</React.StrictMode>
)
ステップ 4: App シェルの作成
src/App.tsx の内容を次のように置き換えます:
import React, { useEffect, useMemo, useState } from 'react'
import { Box, Button, Stack, Typography } from '@mui/material'
import { Footer } from '@nodeblocks/frontend-footer-block'
import { Navbar } from '@nodeblocks/frontend-navbar-block'
import { HomePage } from './pages/HomePage'
import { LoginPage } from './pages/LoginPage'
import { ProductsPage } from './pages/ProductsPage'
import { ResetPasswordPage } from './pages/ResetPasswordPage'
import { SignUpPage } from './pages/SignUpPage'
import { CreateProductPage } from './pages/CreateProductPage'
import { initialCatalogProducts, type CatalogProduct } from './catalog'
import './App.css'
type Page = 'home' | 'login' | 'sign_up' | 'reset_password' | 'products_list' | 'create_product'
type NoticeTone = 'success' | 'info' | 'warning'
type Notice = { message: string; tone: NoticeTone } | null
function App() {
const [currentPage, setCurrentPage] = useState<Page>('home')
const [loggedInUser, setLoggedInUser] = useState<{ name: string } | null>(null)
const [notice, setNotice] = useState<Notice>(null)
const [catalogProducts, setCatalogProducts] = useState<CatalogProduct[]>(initialCatalogProducts)
useEffect(() => {
if (!notice) return
const timeout = window.setTimeout(() => setNotice(null), 3500)
return () => window.clearTimeout(timeout)
}, [notice])
const showNotice = (message: string, tone: NoticeTone = 'info') => setNotice({ message, tone })
const navigation = useMemo(
() => [
{ label: 'ホーム', page: 'home' as const },
{ label: 'カタログ', page: 'products_list' as const },
{ label: '商品を作成', page: 'create_product' as const },
],
[]
)
const handleLogin = async (email: string, _password: string) => {
const displayName = email.split('@')[0] || 'ユーザー'
setLoggedInUser({ name: displayName })
showNotice(`おかえりなさい、${displayName}さん。`, 'success')
setCurrentPage('products_list')
}
const handleRegister = async (formData: {
email: string
password: string
agreesPrivacyPolicy: boolean
agreesUserAgreement: boolean
}) => {
const displayName = formData.email.split('@')[0] || 'ユーザー'
setLoggedInUser({ name: displayName })
showNotice(`${displayName}さんのアカウントを作成しました。`, 'success')
setCurrentPage('products_list')
}
const handleResetPassword = async (email: string) => {
showNotice(email ? `${email} にリセット手順を送信しました。` : 'リセット手順を送信しました。', 'success')
setCurrentPage('login')
}
const handleLogout = () => {
setLoggedInUser(null)
showNotice('正常にサインアウトしました。', 'info')
setCurrentPage('home')
}
const handleCreateProduct = (product: CatalogProduct) => {
setCatalogProducts((current) => [product, ...current])
showNotice(`${product.title} をカタログに追加しました。`, 'success')
setCurrentPage('products_list')
}
return (
<Box sx={{ minHeight: '100vh', display: 'flex', flexDirection: 'column' }}>
<Navbar
leftContent={
<Navbar.Logo sx={{ width: '48px' }} src="/vite.svg" alt="Nodeblocks デモロゴ" />
}
centerContent={
<Stack direction="row" spacing={1} useFlexGap flexWrap="wrap">
{navigation.map((item) => (
<Button
key={item.page}
variant={currentPage === item.page ? 'contained' : 'text'}
onClick={() => setCurrentPage(item.page)}
sx={{ borderRadius: 999, textTransform: 'none', minWidth: 0, px: 1.5 }}
>
{item.label}
</Button>
))}
</Stack>
}
rightContent={
<Stack direction="row" spacing={1} useFlexGap flexWrap="wrap">
{loggedInUser ? (
<>
<Button
variant="outlined"
onClick={() => setCurrentPage('products_list')}
sx={{ borderRadius: 999, textTransform: 'none' }}
>
{loggedInUser.name}
</Button>
<Button variant="contained" onClick={handleLogout} sx={{ borderRadius: 999, textTransform: 'none' }}>
ログアウト
</Button>
</>
) : (
<>
<Button
variant={currentPage === 'login' ? 'contained' : 'text'}
onClick={() => setCurrentPage('login')}
sx={{ borderRadius: 999, textTransform: 'none' }}
>
サインイン
</Button>
<Button
variant={currentPage === 'sign_up' ? 'contained' : 'outlined'}
onClick={() => setCurrentPage('sign_up')}
sx={{ borderRadius: 999, textTransform: 'none' }}
>
アカウントを作成
</Button>
</>
)}
</Stack>
}
/>
{notice && (
<Box
sx={{
width: 'min(1200px, calc(100% - 2rem))',
mx: 'auto',
mt: 2,
px: 2,
py: 1.25,
borderRadius: 999,
border: '1px solid rgba(15, 23, 42, 0.09)',
bgcolor: 'rgba(255, 255, 255, 0.86)',
textAlign: 'center',
fontWeight: 600,
color:
notice.tone === 'success'
? 'success.main'
: notice.tone === 'warning'
? 'warning.main'
: 'primary.main',
boxShadow: '0 12px 24px rgba(15, 23, 42, 0.06)',
}}
>
{notice.message}
</Box>
)}
<Box component="main" sx={{ flex: 1, width: '100%', display: 'flex', justifyContent: 'center' }}>
<Box sx={{ width: '100%', maxWidth: 1280 }}>
{renderPage(currentPage, {
catalogProducts,
onLogin: handleLogin,
onRegister: handleRegister,
onGoToHome: () => setCurrentPage('home'),
onGoToLogin: () => setCurrentPage('login'),
onGoToSignUp: () => setCurrentPage('sign_up'),
onGoToResetPassword: () => setCurrentPage('reset_password'),
onGoToCatalog: () => setCurrentPage('products_list'),
onGoToCreateProduct: () => setCurrentPage('create_product'),
onCreateProduct: handleCreateProduct,
onResetPassword: handleResetPassword,
onRejectAttachment: (message) => showNotice(message, 'warning'),
})}
</Box>
</Box>
<Footer
logoSrc="/vite.svg"
sx={{
'img': {
width: '48px',
},
}}
content={null}
copyright="© 2026 Nodeblocks デモ. 無断転載を禁じます。"
/>
</Box>
)
}
function renderPage(
currentPage: Page,
handlers: {
catalogProducts: CatalogProduct[]
onLogin: (email: string, password: string) => Promise<void>
onRegister: (formData: {
email: string
password: string
agreesPrivacyPolicy: boolean
agreesUserAgreement: boolean
}) => Promise<void>
onGoToHome: () => void
onGoToLogin: () => void
onGoToSignUp: () => void
onGoToResetPassword: () => void
onGoToCatalog: () => void
onGoToCreateProduct: () => void
onCreateProduct: (product: CatalogProduct) => void
onResetPassword: (email: string) => Promise<void>
onRejectAttachment: (message: string) => void
}
) {
switch (currentPage) {
case 'home':
return <HomePage onExploreCatalog={handlers.onGoToCatalog} />
case 'login':
return (
<LoginPage
onLogin={handlers.onLogin}
onGoToSignUp={handlers.onGoToSignUp}
onForgotPassword={handlers.onGoToResetPassword}
/>
)
case 'sign_up':
return <SignUpPage onRegister={handlers.onRegister} onGoToLogin={handlers.onGoToLogin} />
case 'reset_password':
return <ResetPasswordPage onSendRequest={handlers.onResetPassword} onGoToLogin={handlers.onGoToLogin} />
case 'products_list':
return (
<ProductsPage
products={handlers.catalogProducts}
onGoHome={handlers.onGoToHome}
onGoToCreateProduct={handlers.onGoToCreateProduct}
/>
)
case 'create_product':
return (
<CreateProductPage
onCreateProduct={handlers.onCreateProduct}
onGoToCatalog={handlers.onGoToCatalog}
onRejectAttachment={handlers.onRejectAttachment}
/>
)
default:
return <div>見つかりません</div>
}
}
export default App
📱 ページコンポーネントの作成
ステップ 4: ホームページ
src/pages/HomePage.tsx を作成します:
import { Stack } from '@mui/material'
import { ListProductsGrid } from '@nodeblocks/frontend-list-products-grid-block'
import { SearchByChip } from '@nodeblocks/frontend-search-by-chip-block'
const featuredProducts = [
{
title: 'モダンなノートパソコン',
subtitle: '電子機器',
summary: 'クリーンで高速なワークフローを必要とするプロ向けの高性能ノートパソコン。',
imageUrl: 'https://images.unsplash.com/photo-1496181133206-80ce9b88a853?w=1200',
chips: [{ label: '新着', color: 'primary' as const }],
tags: [{ label: 'ベストセラー' }],
},
{
title: 'ワイヤレスヘッドホン',
subtitle: '電子機器',
summary: '強力なノイズキャンセリングと終日快適な装着感を備えたプレミアムワイヤレスヘッドホン。',
imageUrl: 'https://images.unsplash.com/photo-1505740420928-5e560c06d30e?w=1200',
chips: [{ label: 'セール', color: 'secondary' as const }],
tags: [{ label: '送料無料' }],
},
{
title: 'デスクオーガナイザー',
subtitle: 'オフィス',
summary: 'ワークスペースを清潔でコンパクトに保つミニマルなデスクオーガナイザー。',
imageUrl: 'https://images.unsplash.com/photo-1517705008128-361805f42e86?w=1200',
chips: [{ label: '注目', color: 'success' as const }],
tags: [{ label: '在庫わずか' }],
},
]
interface HomePageProps {
onExploreCatalog: () => void
}
export function HomePage({ onExploreCatalog }: HomePageProps) {
return (
<Stack spacing={4} sx={{ width: '100%', px: { xs: 2, sm: 3 }, py: { xs: 2, sm: 8 } }}>
<SearchByChip
searchByChipTitle="カタログを閲覧"
subtitle="クイックカテゴリチップでライブデモにすぐ移動できます。"
chipSections={[
{
sectionTitle: '人気カテゴリ',
onClickChip: (_value) => onExploreCatalog(),
chips: [
{ value: 'electronics', label: '電子機器' },
{ value: 'office', label: 'オフィス' },
{ value: 'home', label: 'ホーム' },
],
},
]}
/>
<ListProductsGrid listProductsGridTitle="注目商品" subtitle="ライブプレビュー" items={featuredProducts} />
</Stack>
)
}
ステップ 5: 認証ページ
src/pages/LoginPage.tsx を作成します:
import React from 'react'
import { Box, Button, Stack } from '@mui/material'
import { SignIn } from '@nodeblocks/frontend-signin-block'
interface LoginPageProps {
onLogin: (email: string, password: string) => Promise<void>
onGoToSignUp: () => void
onForgotPassword: () => void
}
export function LoginPage({ onLogin, onGoToSignUp, onForgotPassword }: LoginPageProps) {
const [data, setData] = React.useState({ email: '', password: '' })
return (
<Box sx={{ width: '100%', px: { xs: 2, sm: 3 }, py: { xs: 2, sm: 3 } }}>
<Stack sx={{ maxWidth: 560, mx: 'auto' }}>
<SignIn
data={data}
onDataChange={setData}
onSubmit={async (event) => {
event.preventDefault()
await onLogin(data.email, data.password)
}}
>
<SignIn.SignInTitle>おかえりなさい</SignIn.SignInTitle>
<SignIn.EmailField label="メールアドレス" placeholder="メールアドレスを入力" />
<SignIn.PasswordField label="パスワード" placeholder="パスワードを入力" />
<SignIn.SignInButton>サインイン</SignIn.SignInButton>
<Stack spacing={0}>
<SignIn.GotoSignUp>
<Button variant="text" onClick={onGoToSignUp} sx={{ alignSelf: 'flex-start', px: 0 }}>
アカウントをお持ちでない方はこちら
</Button>
</SignIn.GotoSignUp>
<SignIn.ResetPassword>
<Button variant="text" onClick={onForgotPassword} sx={{ alignSelf: 'flex-start', px: 0 }}>
パスワードをお忘れですか?
</Button>
</SignIn.ResetPassword>
</Stack>
</SignIn>
</Stack>
</Box>
)
}
src/pages/SignUpPage.tsx を作成します:
import React from 'react'
import { Box, Button, Stack } from '@mui/material'
import { SignUp } from '@nodeblocks/frontend-signup-block'
interface SignUpPageProps {
onRegister: (formData: {
email: string
password: string
agreesPrivacyPolicy: boolean
agreesUserAgreement: boolean
}) => Promise<void>
onGoToLogin: () => void
}
export function SignUpPage({ onRegister, onGoToLogin }: SignUpPageProps) {
const [data, setData] = React.useState({
email: '',
password: '',
agreesUserAgreement: false,
agreesPrivacyPolicy: false,
})
return (
<Box sx={{ width: '100%', px: { xs: 2, sm: 3 }, py: { xs: 2, sm: 3 } }}>
<Stack sx={{ maxWidth: 560, mx: 'auto' }}>
<SignUp
data={data}
onDataChange={setData}
termsOfUseUrl="/terms-of-use"
privacyPolicyUrl="/privacy-policy"
loginUrl="/login"
onSubmit={async (event) => {
event.preventDefault()
await onRegister(data)
}}
>
<SignUp.SignUpTitle>アカウントを作成</SignUp.SignUpTitle>
<SignUp.EmailField label="メールアドレス" placeholder="メールアドレスを入力" />
<SignUp.PasswordField label="パスワード" placeholder="パスワードを作成" />
<Stack spacing={0}>
<SignUp.TermsOfUse />
<SignUp.PrivacyPolicy />
</Stack>
<Stack spacing={0}>
<SignUp.SignUpButton>アカウント作成</SignUp.SignUpButton>
<SignUp.GotoSignIn>
<Button variant="text" onClick={onGoToLogin} sx={{ alignSelf: 'flex-start', px: 0 }}>
すでにアカウントをお持ちの方はこちら
</Button>
</SignUp.GotoSignIn>
</Stack>
</SignUp>
</Stack>
</Box>
)
}
src/pages/ResetPasswordPage.tsx を作成します:
import React from 'react'
import { Box, Button, Stack } from '@mui/material'
import { ResetPassword } from '@nodeblocks/frontend-reset-password-block'
interface ResetPasswordPageProps {
onSendRequest: (email: string) => Promise<void>
onGoToLogin: () => void
}
export function ResetPasswordPage({ onSendRequest, onGoToLogin }: ResetPasswordPageProps) {
const [data, setData] = React.useState({ email: '' })
return (
<Box sx={{ width: '100%', px: { xs: 2, sm: 3 }, py: { xs: 2, sm: 3 } }}>
<Stack sx={{ maxWidth: 560, mx: 'auto' }} spacing={2}>
<ResetPassword
data={data}
onDataChange={setData}
view="request"
onSendRequest={async (formData) => {
await onSendRequest(formData.email ?? '')
}}
onResetPassword={async () => {}}
description="メールアドレスを入力すると、リセット手順をお送りします。"
resetPasswordTitle="パスワードをリセット"
>
<ResetPassword.Title />
<ResetPassword.Description />
<ResetPassword.Form />
</ResetPassword>
<Button variant="text" onClick={onGoToLogin} sx={{ alignSelf: 'flex-start', px: 0 }}>
サインインに戻る
</Button>
</Stack>
</Box>
)
}
ステップ 6: 商品カタログと共有データ
src/catalog.ts を作成します:
export type CatalogProductChip = {
label: string
color?: 'default' | 'primary' | 'secondary' | 'success' | 'error' | 'info' | 'warning'
}
export type CatalogProduct = {
id: string
categoryKey: string
title: string
subtitle: string
summary: string
imageUrl: string
chips: CatalogProductChip[]
tags: Array<{ label: string }>
}
export const catalogCategoryOptions = [
{ value: 'electronics', label: '電子機器' },
{ value: 'office', label: 'オフィス' },
{ value: 'home', label: 'ホーム' },
] as const
export const catalogFilterOptions = [{ value: 'all', label: 'すべての商品' }, ...catalogCategoryOptions] as const
export const getCatalogCategoryLabel = (categoryKey: string) =>
catalogCategoryOptions.find((option) => option.value === categoryKey)?.label ?? '未分類'
export const initialCatalogProducts: CatalogProduct[] = [
{
id: '1',
categoryKey: 'electronics',
title: 'モダンなノートパソコン',
subtitle: '電子機器',
summary: 'クリーンで高速なワークフローを必要とするプロ向けの高性能ノートパソコン。',
imageUrl: 'https://images.unsplash.com/photo-1496181133206-80ce9b88a853?w=1200',
chips: [{ label: '新着', color: 'primary' }],
tags: [{ label: 'ベストセラー' }],
},
{
id: '2',
categoryKey: 'electronics',
title: 'ワイヤレスヘッドホン',
subtitle: '電子機器',
summary: '強力なノイズキャンセリングと終日快適な装着感を備えたプレミアムワイヤレスヘッドホン。',
imageUrl: 'https://images.unsplash.com/photo-1505740420928-5e560c06d30e?w=1200',
chips: [{ label: 'セール', color: 'secondary' }],
tags: [{ label: '送料無料' }],
},
{
id: '3',
categoryKey: 'office',
title: 'デスクオーガナイザー',
subtitle: 'オフィス',
summary: 'ワークスペースを清潔でコンパクトに保つミニマルなデスクオーガナイザー。',
imageUrl: 'https://images.unsplash.com/photo-1517705008128-361805f42e86?w=1200',
chips: [{ label: '注目', color: 'success' }],
tags: [{ label: '在庫わずか' }],
},
]
src/pages/CreateProductPage.tsx を作成します:
import React from 'react'
import { Box, Button, Stack, Typography } from '@mui/material'
import { CreateProduct, japanPrefectureOptions } from '@nodeblocks/frontend-create-product-block'
import { catalogCategoryOptions, getCatalogCategoryLabel, type CatalogProduct } from '../catalog'
type ProductFormData = {
title: string
categoryId: string
typeId: string
optionId: string
quantity: string
description: string
details: string
tags: Array<{ id: string; typeId?: string; label: string }>
prefecture: string
city: string
onlineAvailability: boolean
inventoryNotes: string
availableFrom: string
availableUntil: string
additionalDetails: string
image: { url: string; type?: string; id?: string } | File | null
}
interface CreateProductPageProps {
onCreateProduct: (product: CatalogProduct) => void
onGoToCatalog: () => void
onRejectAttachment?: (message: string) => void
}
const createEmptyFormData = (): ProductFormData => ({
title: '',
categoryId: '',
typeId: '',
optionId: '',
quantity: '',
description: '',
details: '',
tags: [],
prefecture: '',
city: '',
onlineAvailability: false,
inventoryNotes: '',
availableFrom: '',
availableUntil: '',
additionalDetails: '',
image: null,
})
export function CreateProductPage({ onCreateProduct, onGoToCatalog, onRejectAttachment }: CreateProductPageProps) {
const [data, setData] = React.useState<ProductFormData>(() => createEmptyFormData())
return (
<Stack spacing={3} sx={{ width: '100%', px: { xs: 1, sm: 1 }, py: { xs: 2, sm: 3 } }}>
<Button sx={{ width: '48px' }} variant="outlined" onClick={onGoToCatalog}>
戻る
</Button>
<Stack spacing={1} alignItems="center">
<Typography variant="overline" sx={{ color: 'primary.main', fontWeight: 800, letterSpacing: '0.14em' }}>
カタログワークスペース
</Typography>
<Typography variant="h3" sx={{ letterSpacing: '-0.03em' }}>
新しい商品リスティングを作成します。
</Typography>
<Typography color="text.secondary">
ソースに沿った Create Product ブロックを使って、カスタムフォームコードなしで新しい商品を公開できます。
</Typography>
<Stack direction="row" justifyContent="space-between" alignItems="center" flexWrap="wrap" gap={1}>
</Stack>
<Box
sx={{
p: { xs: 2, sm: 3 },
borderRadius: 4,
bgcolor: 'transparent',
}}
>
<CreateProduct
data={data}
onDataChange={setData}
labels={{
mainInfoSectionTitle: 'リスティングの概要',
basicInfoSectionTitle: '基本情報',
additionalInfoTitle: '在庫と物流',
submitButton: 'リスティングを公開',
}}
placeholders={{
titleField: 'ワイヤレスヘッドホン',
descriptionField: 'カード用の短い要約',
detailsField: '長文の詳細',
prefectureField: '都道府県を選択',
cityField: '市区町村を入力',
inventoryNotesField: '発送に関するメモ',
additionalDetailsField: '追加の説明',
}}
selectOptions={{
categoryOptions: [...catalogCategoryOptions],
typeOptions: [
{ value: 'physical', label: '物理商品' },
{ value: 'digital', label: 'デジタル商品' },
],
optionOptions: [
{ value: 'standard', label: '標準' },
{ value: 'premium', label: 'プレミアム' },
],
prefectureOptions: japanPrefectureOptions,
}}
tagTypes={[
{ id: '100', label: '注目' },
{ id: '200', label: '発送' },
]}
tags={[
{ id: '101', typeId: '100', label: 'ベストセラー' },
{ id: '201', typeId: '200', label: '日本から発送' },
]}
onRejectAttachment={(file, error) => {
console.warn('添付ファイルが拒否されました:', file, error)
onRejectAttachment?.(`添付ファイルが拒否されました: ${file.name}`)
}}
onSubmit={(event) => {
event.preventDefault()
const nextTitle = data.title.trim() || '無題の商品'
const categoryKey = data.categoryId || 'electronics'
const imageUrl =
typeof data.image === 'object' && data.image && 'url' in data.image
? data.image.url
: 'https://via.placeholder.com/1200x800'
onCreateProduct({
id: Date.now().toString(),
categoryKey,
title: nextTitle,
subtitle: getCatalogCategoryLabel(categoryKey),
summary: data.description || 'まだ要約がありません。',
imageUrl,
chips: data.onlineAvailability ? [{ label: 'オンライン', color: 'success' }] : [],
tags: data.tags.map((tag) => ({ label: tag.label })),
})
setData(createEmptyFormData())
}}
/>
</Box>
</Stack>
</Stack>
)
}
src/pages/ProductsPage.tsx を作成します:
import React, { useMemo, useState } from 'react'
import { Box, Button, Stack, Typography } from '@mui/material'
import { FilterSearchPanel } from '@nodeblocks/frontend-filter-search-panel-block'
import { ListProductsGrid } from '@nodeblocks/frontend-list-products-grid-block'
import { SearchByChip } from '@nodeblocks/frontend-search-by-chip-block'
import { catalogFilterOptions, type CatalogProduct } from '../catalog'
interface ProductsPageProps {
products: CatalogProduct[]
onGoHome: () => void
onGoToCreateProduct: () => void
}
export function ProductsPage({ products, onGoHome, onGoToCreateProduct }: ProductsPageProps) {
const [layout, setLayout] = useState<'grid' | 'list'>('grid')
const [searchQuery, setSearchQuery] = useState('')
const [selectedCategory, setSelectedCategory] = useState('all')
const filteredProducts = useMemo(() => {
const query = searchQuery.trim().toLowerCase()
return products.filter((product) => {
const matchesCategory = selectedCategory === 'all' || product.categoryKey === selectedCategory
const matchesSearch =
!query ||
[product.title, product.subtitle, product.summary, ...product.chips.map((chip) => chip.label), ...product.tags.map((tag) => tag.label)]
.join(' ')
.toLowerCase()
.includes(query)
return matchesCategory && matchesSearch
})
}, [products, searchQuery, selectedCategory])
const selectedCategoryLabel =
catalogFilterOptions.find((option) => option.value === selectedCategory)?.label ?? 'すべての商品'
return (
<Stack spacing={3} sx={{ width: '100%', px: { xs: 2, sm: 3 }, py: { xs: 8, sm: 8 } }}>
<Stack spacing={1} sx={{ maxWidth: 780 }}>
<Typography variant="overline" sx={{ color: 'primary.main', fontWeight: 800, letterSpacing: '0.14em' }}>
カタログワークスペース
</Typography>
<Typography variant="h3" sx={{ letterSpacing: '-0.03em' }}>
ブロックライブラリで商品を閲覧・絞り込みできます。
</Typography>
<Typography color="text.secondary">
検索チップ、クイックフィルター、ライブグリッドを使って、カスタム表示コードなしでカタログを探索できます。
</Typography>
</Stack>
<Stack direction="row" justifyContent="space-between" alignItems="center" flexWrap="wrap" gap={1}>
<Button variant="outlined" onClick={onGoHome}>
ホームに戻る
</Button>
<Stack direction="row" gap={1} flexWrap="wrap">
<Button variant={layout === 'grid' ? 'contained' : 'outlined'} onClick={() => setLayout('grid')} sx={{ minWidth: 96 }}>
グリッド
</Button>
<Button variant={layout === 'list' ? 'contained' : 'outlined'} onClick={() => setLayout('list')} sx={{ minWidth: 96 }}>
リスト
</Button>
<Button variant="contained" onClick={onGoToCreateProduct}>
商品を作成
</Button>
</Stack>
</Stack>
<FilterSearchPanel
sx={{ backgroundColor: 'transparent' }}
filters={
selectedCategory === 'all'
? []
: [{ key: 'category', label: selectedCategoryLabel, groupName: 'カテゴリ' }]
}
searchPlaceholder="タイトル、要約、チップ、タグで検索"
noFilterText="すべての商品"
filterLabel="カタログを絞り込む"
onSearch={({ search }) => setSearchQuery(search)}
onClickRemoveFilter={() => setSelectedCategory('all')}
/>
<SearchByChip
searchByChipTitle="カテゴリから閲覧"
subtitle="クイックチップで商品グループをすばやく切り替えられます。"
chipSections={[
{
sectionTitle: 'カタログカテゴリ',
onClickChip: (value) => setSelectedCategory(value),
chips: catalogFilterOptions.map((option) => ({
value: option.value,
label: option.label,
})),
},
]}
/>
<Box sx={{ p: { xs: 2, sm: 3 }, borderRadius: 4, bgcolor: 'background.paper', boxShadow: '0 24px 56px rgba(15, 23, 42, 0.08)' }}>
<ListProductsGrid
listProductsGridTitle="注目商品"
subtitle={`全 ${products.length} 件中 ${filteredProducts.length} 件を表示`}
layout={layout}
items={filteredProducts.map((product) => ({
title: product.title,
subtitle: product.subtitle,
summary: product.summary,
imageUrl: product.imageUrl,
chips: product.chips,
tags: product.tags,
}))}
/>
</Box>
</Stack>
)
}
🎨 アプリケーションのスタイリング
ステップ 7: カスタムスタイルの追加
src/App.css を更新します:
html,
body,
#root {
min-height: 100%;
}
* {
box-sizing: border-box;
}
body {
margin: 0;
font-family: Inter, ui-sans-serif, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
background: #ffffff;
}
button,
input {
font: inherit;
}
🔧 環境設定
ステップ 8: オプションの環境変数
アプリ固有のメタデータを保持したい場合だけ、プロジェクトルートに .env ファイルを作成します:
# アプリケーション設定
VITE_APP_NAME=MyNodeBlocksApp
VITE_APP_VERSION=1.0.0
🚀 アプリケーションの実行
ステップ 9: 開発サーバーを起動する
# 開発サーバーを起動
npm run dev
# アプリケーションは次の URL で利用できます:
# http://localhost:5173
ステップ 10: 本番ビルド
# 本番用にビルド
npm run build
# 本番ビルドをプレビュー
npm run preview
📈 高度な機能
現在のテストアプリでは、すでに次の主要ブロックセットを使用しています:
@nodeblocks/frontend-theme@nodeblocks/frontend-navbar-block@nodeblocks/frontend-footer-block@nodeblocks/frontend-signin-block@nodeblocks/frontend-signup-block@nodeblocks/frontend-reset-password-block@nodeblocks/frontend-create-product-block@nodeblocks/frontend-filter-search-panel-block@nodeblocks/frontend-search-by-chip-block@nodeblocks/frontend-list-products-grid-block
より深いオーバーライドパターンについては、ブロックドキュメントと 高度な使用法ガイド を参照してください。
🔗 便利なリソース
Nodeblocks と VITE を使って、最大限の開発速度とパフォーマンスを実現するために ❤️ を込めて構築