🎨 How I Built the MyZubster NFT Dashboard – A React + TypeScript Journey
After building the backend Rust template for Tari NFTs and dockerizing it, the next challenge was to create a user-friendly interface. I wanted a dashboard that anyone could use, even without blockchain experience.
The result is MyZubster NFT Dashboard – a complete React + TypeScript frontend for managing NFT collections on Tari.
In this article, I'll walk you through the architecture, design decisions, and key components that make the dashboard work.
🧠 Why React + TypeScript?
I chose React 19 with TypeScript for several reasons:
- Type Safety: TypeScript catches errors at compile time, reducing runtime bugs
- Component Reusability: React's component model makes it easy to build and maintain complex UIs
- Rich Ecosystem: Vite, ESLint, and a huge library of community packages
- Performance: Vite's fast HMR makes development a joy
📁 Architecture Overview
The dashboard follows a clean component-based architecture:
src/
├── components/
│ ├── ConnectWallet.tsx # Wallet connection UI
│ ├── CreateCollection.tsx # Create new NFT collection
│ ├── MintNFT.tsx # Mint NFTs
│ └── NFTGallery.tsx # Display NFT gallery
├── hooks/
│ └── useTariWallet.ts # Wallet connection logic
├── App.tsx # Main dashboard
└── App.css # Global styles
text
🔐 ConnectWallet – The Gateway
The wallet connection component is the first thing users see:
tsx
const ConnectWallet: React.FC = () => {
const { isConnected, isLoading, connectWallet, disconnectWallet } = useTariWallet();
return (
<div className="card">
<h3>🔐 Connessione Wallet</h3>
{!isConnected ? (
<button onClick={connectWallet} disabled={isLoading}>
{isLoading ? '⏳ Connessione in corso...' : '🔗 Connetti Wallet Tari'}
</button>
) : (
<div>
<p className="status-connected">✅ Wallet connesso con successo!</p>
<button className="danger" onClick={disconnectWallet}>
🔌 Disconnetti
</button>
</div>
)}
</div>
);
};
Key Features:
Loading state to prevent double-clicks
Disabled state when wallet is not connected
Clear visual feedback for connection status
🏗️ CreateCollection – Building NFT Collections
This component calls the new() function on the Tari template:
tsx
const handleCreateCollection = async () => {
if (!isConnected) {
alert('⚠️ Devi connettere il wallet prima di creare una collezione!');
return;
}
// TODO: Replace with real wallet integration
await new Promise(resolve => setTimeout(resolve, 2000));
setResult({
success: true,
message: `✅ Collezione "${name}" (${symbol}) creata con successo!`,
});
};
UX Flow:
User enters collection name and symbol
Validates that the wallet is connected
Simulates the transaction (ready for real integration)
Shows success/error feedback
🎨 MintNFT – Creating Digital Assets
Minting NFTs is the core feature. Users need:
Collection address (where to mint)
NFT ID (unique identifier)
Immutable data (e.g., image URL)
Mutable data (e.g., status)
tsx
const handleMintNFT = async () => {
if (!componentAddress || !nftId || !immutableData) {
alert('⚠️ Compila tutti i campi obbligatori!');
return;
}
// Simulate minting
await new Promise(resolve => setTimeout(resolve, 2000));
setResult({ success: true, message: `✅ NFT "${nftId}" mintato con successo!` });
};
🖼️ NFTGallery – Showcasing NFTs
The gallery fetches and displays NFTs from a collection:
tsx
const NFTGallery: React.FC = () => {
const [nfts, setNfts] = useState<NFT[]>([]);
const [componentAddress, setComponentAddress] = useState('');
const fetchNFTs = async () => {
// TODO: Replace with real blockchain data
const mockNFTs: NFT[] = [
{ id: 'nft-001', immutableData: 'https://example.com/nft1.png', mutableData: 'Disponibile' },
{ id: 'nft-002', immutableData: 'https://example.com/nft2.png', mutableData: 'In vendita' },
];
setNfts(mockNFTs);
};
return (
<div className="card">
<h3>🖼️ Galleria NFT</h3>
<input
placeholder="Indirizzo della collezione"
value={componentAddress}
onChange={(e) => setComponentAddress(e.target.value)}
/>
<button onClick={fetchNFTs}>🔍 Cerca NFT</button>
<div className="grid">
{nfts.map((nft) => (
<NFTCard key={nft.id} nft={nft} />
))}
</div>
</div>
);
};
🎨 Design System – Dark Mode GitHub Style
I opted for a dark theme inspired by GitHub:
Background: #0d1117
Cards: #161b22 with #30363d borders
Accent: #58a6ff (blue) and #3fb950 (green)
Typography: System font stack
css
body {
background: #0d1117;
color: #e6edf3;
}
.card {
background: #161b22;
border: 1px solid #30363d;
border-radius: 12px;
padding: 1.5rem;
}
button {
background: #238636;
color: white;
border: none;
padding: 0.6rem 1.2rem;
border-radius: 8px;
}
🔗 The Missing Piece: Real Wallet Integration
Currently, the dashboard uses simulated data. The next step is integrating tari.js for real wallet connection:
typescript
import { WalletDaemonTariSigner } from '@tari-project/tarijs';
const connectWallet = async () => {
const walletSigner = await WalletDaemonTariSigner.buildFetchSigner({
serverUrl: 'http://localhost:18103',
});
// ... handle connection
};Running the Dashboard
bash
git clone https://github.com/DanielIoni-creator/MyZubster.git
cd MyZubster/myzubster-frontend
npm install
npm run dev
The dashboard will be available at http://localhost:5173.
🚀 What's Next?
Real Wallet Integration – Connect to Tari wallet via tari.js
Deploy on VPS – Make the dashboard publicly accessible
Advanced Features – Royalty, whitelist, limited minting
Mobile App – React Native for iOS and Android
🔗 Resources
GitHub: https://github.com/DanielIoni-creator/MyZubster
Docker Hub: https://hub.docker.com/repository/docker/myzubster/tari-nft-template
Backend Template: https://dev.to/danielioni/i-built-an-nft-template-for-tari-monero-sidechain-heres-how-33k
VPS Deployment Guide: https://dev.to/danielioni/how-i-deployed-myzubster-on-a-vps-a-complete-guide-51ok
📬 Connect with Me
📖 Dev.to: https://dev.to/danielioni
🐦 Twitter: https://x.com/myzubster
💼 LinkedIn: https://linkedin.com/in/daniel-ioni-62b2b9423/
🐙 GitHub: https://github.com/DanielIoni-creator
Built with ❤️ for the Monero and Tari community
If you're a React or blockchain developer, I'd love to hear your feedback!
Top comments (0)