| name | jb-nft-gallery-ui |
| description | Interactive gallery for browsing and managing NFTs from Juicebox 721 hooks. Displays tier information, owned NFTs, and minting interfaces. |
Juicebox V5 NFT Gallery UI
Interactive gallery for browsing and managing NFTs from Juicebox 721 hooks. Displays tier information, owned NFTs, and minting interfaces.
Overview
This skill generates vanilla JS/HTML interfaces for:
- Browsing all NFT tiers for a project
- Viewing owned NFTs by wallet
- Minting NFTs from available tiers
- Tier metadata and supply tracking
- NFT transfer functionality
NFT Gallery UI Template
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Juicebox NFT Gallery</title>
<link rel="stylesheet" href="/shared/styles.css">
<style>
.gallery-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 20px;
}
.nft-card {
background: var(--bg-secondary);
border-radius: 12px;
overflow: hidden;
cursor: pointer;
transition: transform 0.2s, box-shadow 0.2s;
}
.nft-card:hover {
transform: translateY(-5px);
box-shadow: 0 10px 30px rgba(0,0,0,0.3);
}
.nft-image {
width: 100%;
aspect-ratio: 1;
background: var(--bg-tertiary);
display: flex;
align-items: center;
justify-content: center;
position: relative;
}
.nft-image img { width: 100%; height: ; : cover; }
{ : ; : ; }
, , {
: absolute;
: ;
: ;
: ;
: ;
}
{ : ; : ; : (,,,); : (--jb-yellow); }
{ : ; : ; : (,,,); : (--success); }
{ : (--error); }
{ : ; : ; : (--success); : ; }
{ : ; }
{ : ; : ; : (--text-primary); : ; }
{
: ; : (--text-muted); : ;
: ; : -webkit-box; -webkit--clamp: ;
-webkit-: vertical; : hidden;
}
{
: flex; : space-between; : center;
: ; : solid (--border-color);
}
{ : ; : ; : (--jb-yellow); }
{ : ; : (--text-muted); }
{
: ; : ; : (--jb-yellow); : ;
: none; : ; : ; : pointer; : ;
}
{ : (--jb-yellow-hover); }
{ : (--bg-tertiary); : (--text-muted); : not-allowed; }
{ : flex; : ; : wrap; : ; }
{
: ; : (--bg-tertiary); : ;
: ; : pointer; : (--text-muted);
}
{ : (--jb-yellow); : ; }
{
: none; : fixed; : ; : ; : ; : ;
: (,,,); : ; : ; : auto;
}
{ : flex; : center; : flex-start; }
{ : (--bg-secondary); : ; : ; : ; : ; }
{ : ; : ; : (--bg-tertiary); }
{ : ; : ; : contain; }
{ : ; }
{ : flex; : space-between; : flex-start; : ; }
{ : ; : ; : (--text-primary); }
{ : none; : none; : (--text-muted); : ; : pointer; }
{ : (--text-muted); : ; : ; }
{ : grid; : (, fr); : ; : ; }
{ : (--bg-tertiary); : ; : ; }
{ : ; : (--text-muted); : uppercase; }
{ : ; : ; : (--text-primary); : ; }
{ : flex; : ; }
{ : ; : ; : ; : ; : pointer; : none; }
{ : (--jb-yellow); : ; }
{ : (--bg-tertiary); : (--text-primary); : solid (--border-color); }
</style>
</head>
<body>
<div class="container">
<h1>NFT Gallery</h1>
<div class="card" style="margin-bottom: 20px;">
<div class="input-row">
<input type="text" id="hookAddress" placeholder="721 Hook Address (0x...)">
<select id="chainSelect">
<option value="1">Ethereum</option>
<option value="10">Optimism</option>
<option value="8453">Base</option>
<option value="42161">Arbitrum</option>
<option value="11155111">Sepolia</option>
</select>
<button class="btn" onclick="loadGallery()">Load Gallery</button>
</div>
</div>
<div class="card" id="walletSection" style="display: none; margin-bottom: 20px;">
<div style="display: flex; justify-content: space-between; align-items: center;">
<div>
<span id="walletStatus">Not connected</span>
<span class="code" id="walletAddress" style="margin-left: 10px;"></span>
</div>
<button class="btn-secondary" id="connectBtn" onclick="connectWallet()">Connect Wallet</button>
</div>
</div>
<div class="stats" id="statsBar" style="display: none; margin-bottom: 20px;">
<div class="stat-card">
<div class="stat-value" id="totalTiers">-</div>
<div class="stat-label">Total Tiers</div>
</div>
<div class="stat-card">
<div class="stat-value" id="totalMinted">-</div>
<div class="stat-label">Total Minted</div>
</div>
<div class="stat-card">
<div class="stat-value" id="totalSupply">-</div>
<div class="stat-label">Max Supply</div>
</div>
<div class="stat-card">
<div class="stat-value" id="floorPrice">-</div>
<div class="stat-label">Floor Price</div>
</div>
</div>
<div class="tabs" id="tabsContainer" style="display: none;">
<div class="tab active" data-tab="tiers" onclick="switchTab('tiers')">All Tiers</div>
<div class="tab" data-tab="owned" onclick="switchTab('owned')">My NFTs</div>
</div>
<div class="category-filter" id="categoryFilter"></div>
<div class="filter-bar" id="filterBar" style="display: none; margin-bottom: 20px;">
<button class="tab active" data-filter="all" onclick="filterTiers('all')">All</button>
<button class="tab" data-filter="available" onclick="filterTiers('available')">Available</button>
<button class="tab" data-filter="sold-out" onclick="filterTiers('sold-out')">Sold Out</button>
</div>
<div id="galleryContainer"></div>
</div>
<div class="modal" id="nftModal">
<div class="modal-content">
<div class="modal-image" id="modalImage"></div>
<div class="modal-info">
<div class="modal-header">
<div>
<div class="modal-title" id="modalTitle">-</div>
<span class="code" id="modalTokenId"></span>
</div>
<button class="close-btn" onclick="closeModal()">×</button>
</div>
<div class="modal-description" id="modalDescription"></div>
<div class="modal-attributes" id="modalAttributes"></div>
<div class="action-buttons" id="modalActions"></div>
</div>
</div>
</div>
<script type="module">
import { createPublicClient, http, getContract, formatEther, isAddress, createWalletClient, custom } from 'https://esm.sh/viem';
import { mainnet, optimism, base, arbitrum, sepolia } from 'https://esm.sh/viem/chains';
import { CHAIN_CONFIGS, truncateAddress } from '/shared/wallet-utils.js';
const CHAINS = { 1: mainnet, 10: optimism, 8453: base, 42161: arbitrum, 11155111: sepolia };
const HOOK_ABI = [
{ name: 'STORE', type: 'function', stateMutability: 'view', inputs: [], outputs: [{ type: 'address' }] },
{ name: 'PROJECT_ID', type: 'function', stateMutability: 'view', inputs: [], outputs: [{ type: 'uint256' }] },
{ name: 'name', type: 'function', stateMutability: 'view', inputs: [], outputs: [{ type: 'string' }] },
{ name: 'symbol', : , : , : [], : [{ : }] },
{ : , : , : , : [{ : , : }], : [{ : }] },
{ : , : , : , : [{ : , : }], : [{ : }] },
{ : , : , : , : [{ : , : }, { : , : }], : [{ : }] },
{ : , : , : , : [{ : , : }], : [{ : }] },
{ : , : , : , : [], : [{ : }] },
{ : , : , : , : [{ : , : }, { : , : }, { : , : }], : [] }
];
= [
{ : , : , : ,
: [{ : , : }, { : , : }, { : , : }, { : , : }, { : , : }],
: [{ : , : [
{ : , : }, { : , : }, { : , : },
{ : , : }, { : , : }, { : , : },
{ : , : }, { : , : }, { : , : },
{ : , : }, { : , : }, { : , : },
{ : , : }, { : , : }, { : , : }
]}]
},
{ : , : , : , : [{ : , : }], : [{ : }] }
];
publicClient = ;
walletClient = ;
hookContract = ;
storeContract = ;
allTiers = [];
ownedNFTs = [];
currentTab = ;
currentFilter = ;
selectedCategory = ;
hookAddress = ;
chainId = ;
connectedAddress = ;
. = () {
hookAddress = .().;
chainId = (.().);
(!hookAddress || !(hookAddress)) {
();
;
}
container = .();
container. = ;
chain = [chainId];
rpc = [chainId]?. || chain...[];
publicClient = ({ chain, : (rpc) });
{
storeAddr = publicClient.({ : hookAddress, : , : });
allTiers = publicClient.({
: storeAddr, : , : ,
: [hookAddress, [], , , ]
});
totalMinted = , totalMaxSupply = , floorPrice = ;
categories = ();
allTiers.( {
minted = (tier.) - (tier.);
totalMinted += minted;
totalMaxSupply += (tier.);
(tier.) categories.((tier.));
(tier. > && (floorPrice === || tier. < floorPrice)) {
floorPrice = tier.;
}
});
.(). = allTiers.;
.(). = totalMinted.();
.(). = totalMaxSupply.();
.(). = floorPrice > ? (floorPrice) + : ;
(categories. > ) {
categoryHtml = ;
[...categories].( a - b).( {
categoryHtml += ;
});
.(). = categoryHtml;
}
.().. = ;
.().. = ;
.().. = ;
.().. = ;
();
} (error) {
.(error);
container. = ;
}
};
. = () {
(!.) { (); ; }
{
[address] = ..({ : });
connectedAddress = address;
{
..({
: ,
: [{ : + chainId.() }]
});
} (e) { .(, e); }
walletClient = ({ : [chainId], : (.) });
.(). = ;
.(). = (address);
.(). = ;
.(). = disconnectWallet;
(address);
} (error) {
.(error);
();
}
};
. = () {
walletClient = ;
connectedAddress = ;
ownedNFTs = [];
.(). = ;
.(). = ;
.(). = ;
.(). = connectWallet;
(currentTab === ) (); ();
};
() {
{
storeAddr = publicClient.({ : hookAddress, : , : });
balance = publicClient.({ : hookAddress, : , : , : [ownerAddress] });
ownedNFTs = [];
( i = ; i < balance; i++) {
tokenId = publicClient.({ : hookAddress, : , : , : [ownerAddress, i] });
tierId = publicClient.({ : storeAddr, : , : , : [tokenId] });
tier = allTiers.( t..() === tierId.());
metadata = {};
{
uri = publicClient.({ : hookAddress, : , : , : [tokenId] });
(uri) {
metadataUrl = uri.() ? : uri;
response = (metadataUrl);
metadata = response.();
}
} (e) { .(, tokenId.()); }
ownedNFTs.({ : tokenId.(), : tierId.(), tier, metadata });
}
(currentTab === ) (); ();
} (error) { .(, error); }
}
. = () {
currentTab = tab;
.().( t..());
.()..();
(tab === ) {
.().. = ;
();
} {
.().. = ;
();
}
};
. = () {
currentFilter = filter;
.().( b..());
.()..();
();
};
. = () {
selectedCategory = category;
.().( c..());
(category === ) {
.()..();
} {
.().( {
(c. === ) c..();
});
}
();
};
() {
container = .();
filteredTiers = [...allTiers];
(selectedCategory !== ) filteredTiers = filteredTiers.( (t.) === selectedCategory);
(currentFilter === ) filteredTiers = filteredTiers.( t. > );
(currentFilter === ) filteredTiers = filteredTiers.( t. === );
(filteredTiers. === ) {
container. = ;
;
}
html = ;
filteredTiers.( {
minted = (tier.) - (tier.);
soldOut = tier. === ;
price = (tier.);
owned = ownedNFTs.( nft. === tier..());
html += ;
});
container. = html + ;
}
() {
container = .();
(!connectedAddress) {
container. = ;
;
}
(ownedNFTs. === ) {
container. = ;
;
}
html = ;
ownedNFTs.( {
tier = nft.;
name = nft.?. || ;
image = nft.?.;
html += ;
});
container. = html + ;
}
. = () {
tier = allTiers.( t..() === tierId.());
(!tier) ;
minted = (tier.) - (tier.);
soldOut = tier. === ;
price = (tier.);
.(). = ;
.(). = ;
.(). = tier. ? : ;
.(). = tier.
?
: ;
.(). = ;
.(). = ;
.()..();
};
. = () {
nft = ownedNFTs.( n. === tokenId);
(!nft) ;
name = nft.?. || ;
description = nft.?. || ;
image = nft.?.;
.(). = name;
.(). = ;
.(). = description;
.(). = image
?
: ;
attributesHtml = ;
(nft.?.) {
nft...( {
attributesHtml += ;
});
}
.(). = attributesHtml;
.(). = ;
.()..();
};
. = () {
.()..();
};
. = () {
(!connectedAddress) { (); ; }
{
projectId = publicClient.({ : hookAddress, : , : });
();
} (error) { .(error); ( + error.); }
};
. = () {
(!connectedAddress) { (); ; }
recipient = ();
(!recipient || !(recipient)) { (); ; }
{
hash = walletClient.({
: hookAddress, : , : ,
: [connectedAddress, recipient, (tokenId)], : connectedAddress
});
( + hash);
publicClient.({ hash });
();
(connectedAddress);
();
} (error) { .(error); ( + error.); }
};
. = () {
();
};
() {
(!uri) ;
(uri.()) ;
(uri.()) uri;
uri;
}
.(, { (e. === ) (); });
.().(, { (e.. === ) (); });
</script>
</body>
</html>
Key Features
Tier Browsing
- Grid layout with tier cards
- Supply tracking (minted/total)
- Price display in ETH
- Category filtering
- Availability filtering
Owned NFTs View
- Wallet connection via viem
- Display owned NFTs with metadata
- Transfer functionality
- Cash out guidance
Collection Stats
- Total tiers
- Total minted
- Max supply
- Floor price
NFT Details Modal
- Full metadata display
- Attribute listing
- Action buttons (mint/transfer/cash out)
Data Sources
The gallery uses on-chain data via:
JB721TiersHook - NFT contract
JB721TiersHookStore - Tier data storage
- Token metadata from IPFS
Customization Points
- Image handling: Modify
resolveUri() for different IPFS gateways
- Metadata display: Extend attributes rendering
- Minting flow: Integrate directly with terminal
- Styling: Customize via CSS variables
Integration Notes
- Works with any V5 721 Hook deployment
- Uses viem for all blockchain interactions
- Shared styles from
/shared/styles.css
- Chain config from
/shared/wallet-utils.js