Skip to main content

🚀 Setting Up Nodeblocks with VITE from Scratch

Build modern React applications with Nodeblocks components using VITE for lightning-fast development

This comprehensive guide will walk you through creating a complete application from scratch using VITE, React, and Nodeblocks frontend components.


📋 Prerequisites


🎯 Project Setup

Step 1: Create VITE Project

# Create new VITE project with React and TypeScript
npm create vite@latest my-nodeblocks-app -- --template react-ts

# Navigate to project directory
cd my-nodeblocks-app

# Install dependencies
npm install

Step 2: Install Nodeblocks Dependencies

Install the packages used by the app examples below:

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

🏗️ Basic Application Structure

Step 3: Create the Entry Point

Replace the contents of 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>
)

Step 4: Create the App Shell

Replace the contents of 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: 'Home', page: 'home' as const },
{ label: 'Catalog', page: 'products_list' as const },
{ label: 'Create product', page: 'create_product' as const },
],
[]
)

const handleLogin = async (email: string, _password: string) => {
const displayName = email.split('@')[0] || 'User'
setLoggedInUser({ name: displayName })
showNotice(`Welcome back, ${displayName}.`, 'success')
setCurrentPage('products_list')
}

const handleRegister = async (formData: {
email: string
password: string
agreesPrivacyPolicy: boolean
agreesUserAgreement: boolean
}) => {
const displayName = formData.email.split('@')[0] || 'User'
setLoggedInUser({ name: displayName })
showNotice(`Account created for ${displayName}.`, 'success')
setCurrentPage('products_list')
}

const handleResetPassword = async (email: string) => {
showNotice(email ? `Reset instructions sent to ${email}.` : 'Reset instructions sent.', 'success')
setCurrentPage('login')
}

const handleLogout = () => {
setLoggedInUser(null)
showNotice('Signed out successfully.', 'info')
setCurrentPage('home')
}

const handleCreateProduct = (product: CatalogProduct) => {
setCatalogProducts((current) => [product, ...current])
showNotice(`${product.title} was added to the catalog.`, '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 demo logo" />
}
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' }}>
Logout
</Button>
</>
) : (
<>
<Button
variant={currentPage === 'login' ? 'contained' : 'text'}
onClick={() => setCurrentPage('login')}
sx={{ borderRadius: 999, textTransform: 'none' }}
>
Sign in
</Button>
<Button
variant={currentPage === 'sign_up' ? 'contained' : 'outlined'}
onClick={() => setCurrentPage('sign_up')}
sx={{ borderRadius: 999, textTransform: 'none' }}
>
Create account
</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 demo. All rights reserved."
/>
</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>Not Found</div>
}
}

export default App

📱 Creating Page Components

Step 4: Home Page

Create 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: 'Modern Laptop',
subtitle: 'Electronics',
summary: 'High-performance laptop for professionals who need a clean, fast workflow.',
imageUrl: 'https://images.unsplash.com/photo-1496181133206-80ce9b88a853?w=1200',
chips: [{ label: 'New', color: 'primary' as const }],
tags: [{ label: 'Best seller' }],
},
{
title: 'Wireless Headphones',
subtitle: 'Electronics',
summary: 'Premium wireless headphones with strong noise cancellation and all-day comfort.',
imageUrl: 'https://images.unsplash.com/photo-1505740420928-5e560c06d30e?w=1200',
chips: [{ label: 'Sale', color: 'secondary' as const }],
tags: [{ label: 'Free Shipping' }],
},
{
title: 'Desk Organizer',
subtitle: 'Office',
summary: 'A minimal desk organizer that keeps the workspace clean and compact.',
imageUrl: 'https://images.unsplash.com/photo-1517705008128-361805f42e86?w=1200',
chips: [{ label: 'Featured', color: 'success' as const }],
tags: [{ label: 'Limited Stock' }],
},
]

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="Browse the catalog"
subtitle="Jump straight into the live demo with quick category chips."
chipSections={[
{
sectionTitle: 'Popular categories',
onClickChip: (_value) => onExploreCatalog(),
chips: [
{ value: 'electronics', label: 'Electronics' },
{ value: 'office', label: 'Office' },
{ value: 'home', label: 'Home' },
],
},
]}
/>

<ListProductsGrid listProductsGridTitle="Featured products" subtitle="Live preview" items={featuredProducts} />
</Stack>
)
}

Step 5: Authentication Pages

Create 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>Welcome Back</SignIn.SignInTitle>
<SignIn.EmailField label="Email Address" placeholder="Enter your email" />
<SignIn.PasswordField label="Password" placeholder="Enter your password" />
<SignIn.SignInButton>Sign In</SignIn.SignInButton>
<Stack spacing={0}>
<SignIn.GotoSignUp>
<Button variant="text" onClick={onGoToSignUp} sx={{ alignSelf: 'flex-start', px: 0 }}>
Don't have an account? Sign up
</Button>
</SignIn.GotoSignUp>
<SignIn.ResetPassword>
<Button variant="text" onClick={onForgotPassword} sx={{ alignSelf: 'flex-start', px: 0 }}>
Forgot your password?
</Button>
</SignIn.ResetPassword>
</Stack>
</SignIn>
</Stack>
</Box>
)
}

Create 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>Create Your Account</SignUp.SignUpTitle>
<SignUp.EmailField label="Email Address" placeholder="Enter your email" />
<SignUp.PasswordField label="Password" placeholder="Create password" />
<Stack spacing={0}>
<SignUp.TermsOfUse />
<SignUp.PrivacyPolicy />
</Stack>
<Stack spacing={0}>
<SignUp.SignUpButton>Create Account</SignUp.SignUpButton>
<SignUp.GotoSignIn>
<Button variant="text" onClick={onGoToLogin} sx={{ alignSelf: 'flex-start', px: 0 }}>
Already have an account? Sign in
</Button>
</SignUp.GotoSignIn>
</Stack>
</SignUp>
</Stack>
</Box>
)
}

Create 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="Enter your email and we'll send reset instructions."
resetPasswordTitle="Reset your password"
>
<ResetPassword.Title />
<ResetPassword.Description />
<ResetPassword.Form />
</ResetPassword>

<Button variant="text" onClick={onGoToLogin} sx={{ alignSelf: 'flex-start', px: 0 }}>
Back to sign in
</Button>
</Stack>
</Box>
)
}

Step 6: Catalog Pages and Shared Data

Create 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: 'Electronics' },
{ value: 'office', label: 'Office' },
{ value: 'home', label: 'Home' },
] as const

export const catalogFilterOptions = [{ value: 'all', label: 'All products' }, ...catalogCategoryOptions] as const

export const getCatalogCategoryLabel = (categoryKey: string) =>
catalogCategoryOptions.find((option) => option.value === categoryKey)?.label ?? 'Uncategorized'

export const initialCatalogProducts: CatalogProduct[] = [
{
id: '1',
categoryKey: 'electronics',
title: 'Modern Laptop',
subtitle: 'Electronics',
summary: 'High-performance laptop for professionals who need a clean, fast workflow.',
imageUrl: 'https://images.unsplash.com/photo-1496181133206-80ce9b88a853?w=1200',
chips: [{ label: 'New', color: 'primary' }],
tags: [{ label: 'Best seller' }],
},
{
id: '2',
categoryKey: 'electronics',
title: 'Wireless Headphones',
subtitle: 'Electronics',
summary: 'Premium wireless headphones with strong noise cancellation and all-day comfort.',
imageUrl: 'https://images.unsplash.com/photo-1505740420928-5e560c06d30e?w=1200',
chips: [{ label: 'Sale', color: 'secondary' }],
tags: [{ label: 'Free Shipping' }],
},
{
id: '3',
categoryKey: 'office',
title: 'Desk Organizer',
subtitle: 'Office',
summary: 'A minimal desk organizer that keeps the workspace clean and compact.',
imageUrl: 'https://images.unsplash.com/photo-1517705008128-361805f42e86?w=1200',
chips: [{ label: 'Featured', color: 'success' }],
tags: [{ label: 'Limited Stock' }],
},
]

Create 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}>
Back
</Button>
<Stack spacing={1} alignItems="center">
<Typography variant="overline" sx={{ color: 'primary.main', fontWeight: 800, letterSpacing: '0.14em' }}>
Catalog workspace
</Typography>
<Typography variant="h3" sx={{ letterSpacing: '-0.03em' }}>
Create a new product listing.
</Typography>
<Typography color="text.secondary">
Use the source-aligned create product block and publish a new item without custom form code.
</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: 'Listing overview',
basicInfoSectionTitle: 'Core details',
additionalInfoTitle: 'Inventory & logistics',
submitButton: 'Publish listing',
}}
placeholders={{
titleField: 'Wireless headphones',
descriptionField: 'Short summary for the card',
detailsField: 'Long-form details',
prefectureField: 'Select prefecture',
cityField: 'Enter city',
inventoryNotesField: 'Shipping notes',
additionalDetailsField: 'Any extra context',
}}
selectOptions={{
categoryOptions: [...catalogCategoryOptions],
typeOptions: [
{ value: 'physical', label: 'Physical' },
{ value: 'digital', label: 'Digital' },
],
optionOptions: [
{ value: 'standard', label: 'Standard' },
{ value: 'premium', label: 'Premium' },
],
prefectureOptions: japanPrefectureOptions,
}}
tagTypes={[
{ id: '100', label: 'Highlights' },
{ id: '200', label: 'Shipping' },
]}
tags={[
{ id: '101', typeId: '100', label: 'Best seller' },
{ id: '201', typeId: '200', label: 'Ships from Japan' },
]}
onRejectAttachment={(file, error) => {
console.warn('Attachment rejected:', file, error)
onRejectAttachment?.(`Attachment rejected: ${file.name}`)
}}
onSubmit={(event) => {
event.preventDefault()

const nextTitle = data.title.trim() || 'Untitled product'
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 || 'No summary provided yet.',
imageUrl,
chips: data.onlineAvailability ? [{ label: 'Online', color: 'success' }] : [],
tags: data.tags.map((tag) => ({ label: tag.label })),
})
setData(createEmptyFormData())
}}
/>
</Box>
</Stack>
</Stack>
)
}

Create 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 ?? 'All products'

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' }}>
Catalog workspace
</Typography>
<Typography variant="h3" sx={{ letterSpacing: '-0.03em' }}>
Browse and filter products with the block library.
</Typography>
<Typography color="text.secondary">
Use search chips, quick filters, and the live grid to explore the catalog without custom view code.
</Typography>
</Stack>

<Stack direction="row" justifyContent="space-between" alignItems="center" flexWrap="wrap" gap={1}>
<Button variant="outlined" onClick={onGoHome}>
Back home
</Button>
<Stack direction="row" gap={1} flexWrap="wrap">
<Button variant={layout === 'grid' ? 'contained' : 'outlined'} onClick={() => setLayout('grid')} sx={{ minWidth: 96 }}>
Grid
</Button>
<Button variant={layout === 'list' ? 'contained' : 'outlined'} onClick={() => setLayout('list')} sx={{ minWidth: 96 }}>
List
</Button>
<Button variant="contained" onClick={onGoToCreateProduct}>
Create product
</Button>
</Stack>
</Stack>

<FilterSearchPanel
sx={{ backgroundColor: 'transparent' }}
filters={
selectedCategory === 'all'
? []
: [{ key: 'category', label: selectedCategoryLabel, groupName: 'Category' }]
}
searchPlaceholder="Search by title, summary, chip, or tag"
noFilterText="All products"
filterLabel="Filter catalog"
onSearch={({ search }) => setSearchQuery(search)}
onClickRemoveFilter={() => setSelectedCategory('all')}
/>

<SearchByChip
searchByChipTitle="Browse by category"
subtitle="Quick chips make it easy to jump between product groups."
chipSections={[
{
sectionTitle: 'Catalog categories',
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="Featured products"
subtitle={`Showing ${filteredProducts.length} of ${products.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>
)
}

🎨 Styling Your Application

Step 7: Add Custom Styles

Update 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;
}


🔧 Environment Configuration

Step 8: Optional Environment Variables

Create a .env file in your project root only if you want to keep app-specific metadata:

# Application Configuration
VITE_APP_NAME=MyNodeBlocksApp
VITE_APP_VERSION=1.0.0

🚀 Running Your Application

Step 9: Start Development Server

# Start the development server
npm run dev

# Your application will be available at:
# http://localhost:5173

Step 10: Build for Production

# Build for production
npm run build

# Preview production build
npm run preview

📈 Advanced Features

The current test app already uses the main block set:

  • @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

For deeper override patterns, use the block docs and the Advanced Usage Guide.


🔗 Useful Resources


Built with ❤️ using Nodeblocks and VITE for maximum development speed and performance