Rebuild market with Tailwind + shadcn/ui design system

Complete UI overhaul to match lux.exchange look and feel:

- Replace all inline styles with Tailwind CSS utility classes
- Add shadcn/ui components: Button, Card, Dialog, DropdownMenu, Input
- Replace RainbowKit with plain Wagmi (useAccount/useConnect/useDisconnect)
- Dark theme with HSL CSS variables matching exchange
- Inter + JetBrains Mono fonts via next/font/google
- Migrate to Tailwind v4 (@import "tailwindcss" + @theme inline)
- Rebuild all 8 components and 7 pages
- Add shared utils (cn, shortenAddress, formatNumber, formatCurrency)
This commit is contained in:
2026-03-05 08:50:47 -08:00
parent f37135d835
commit ac3daa0251
30 changed files with 1883 additions and 1109 deletions
+7 -2
View File
@@ -17,7 +17,12 @@ jobs:
packages: write packages: write
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- uses: docker/setup-buildx-action@v3
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to GitHub Container Registry - name: Log in to GitHub Container Registry
uses: docker/login-action@v3 uses: docker/login-action@v3
@@ -42,6 +47,6 @@ jobs:
push: true push: true
tags: ${{ steps.meta.outputs.tags }} tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }} labels: ${{ steps.meta.outputs.labels }}
platforms: linux/amd64 platforms: linux/amd64,linux/arm64
cache-from: type=gha cache-from: type=gha
cache-to: type=gha,mode=max cache-to: type=gha,mode=max
+17
View File
@@ -0,0 +1,17 @@
# Packaging-only Dockerfile - copies pre-built standalone output
FROM node:22-alpine
WORKDIR /app
ENV NODE_ENV=production
ENV NEXT_TELEMETRY_DISABLED=1
RUN addgroup --system --gid 1001 nodejs && adduser --system --uid 1001 nextjs
COPY public ./public
COPY .next/standalone ./
COPY .next/static ./.next/static
USER nextjs
EXPOSE 3000
ENV PORT=3000
ENV HOSTNAME="0.0.0.0"
CMD ["node", "server.js"]
+8 -1
View File
@@ -12,11 +12,18 @@
"lint": "next lint" "lint": "next lint"
}, },
"dependencies": { "dependencies": {
"@rainbow-me/rainbowkit": "2.1.2", "@radix-ui/react-dialog": "^1.1.0",
"@radix-ui/react-dropdown-menu": "^2.1.0",
"@radix-ui/react-slot": "^1.1.0",
"@tanstack/react-query": "5.77.2", "@tanstack/react-query": "5.77.2",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.0",
"lucide-react": "^0.511.0",
"next": "15.5.0", "next": "15.5.0",
"next-themes": "^0.4.0",
"react": "19.1.0", "react": "19.1.0",
"react-dom": "19.1.0", "react-dom": "19.1.0",
"tailwind-merge": "^2.3.0",
"viem": "2.43.3", "viem": "2.43.3",
"wagmi": "2.15.5" "wagmi": "2.15.5"
}, },
+684 -140
View File
File diff suppressed because it is too large Load Diff
+12 -24
View File
@@ -28,38 +28,26 @@ export default function ActivityPage() {
return ( return (
<div> <div>
<Header /> <Header />
<main style={{ maxWidth: 1200, margin: '0 auto', padding: '32px 24px' }}> <main className="max-w-[1200px] mx-auto px-6 py-8">
<h1 style={{ fontSize: 32, fontWeight: 700, marginBottom: 8 }}>Activity</h1> <h1 className="text-3xl font-bold mb-2">Activity</h1>
<p style={{ color: 'var(--muted)', marginBottom: 32, fontSize: 14 }}> <p className="text-muted-foreground mb-8 text-sm">
Recent NFT transfers, mints, and sales across all collections. Recent NFT transfers, mints, and sales across all collections.
</p> </p>
{/* Column headers */} {/* Column headers */}
<div <div className="flex items-center gap-3 py-2 border-b border-border text-[11px] text-muted-foreground uppercase tracking-widest">
style={{ <div className="w-[72px] text-center">Event</div>
display: 'flex', <div className="flex-1">Item</div>
alignItems: 'center', <div className="w-[200px] text-center">From / To</div>
gap: 12, <div className="w-10 text-right">Chain</div>
padding: '8px 0', <div className="w-20 text-right">Date</div>
borderBottom: '1px solid var(--border)', <div className="w-5" />
fontSize: 11,
color: 'var(--muted)',
textTransform: 'uppercase',
letterSpacing: 1,
}}
>
<div style={{ width: 72, textAlign: 'center' }}>Event</div>
<div style={{ flex: 1 }}>Item</div>
<div style={{ width: 200, textAlign: 'center' }}>From / To</div>
<div style={{ width: 40, textAlign: 'right' }}>Chain</div>
<div style={{ width: 80, textAlign: 'right' }}>Date</div>
<div style={{ width: 20 }} />
</div> </div>
{isLoading ? ( {isLoading ? (
<div style={{ textAlign: 'center', padding: 48, color: 'var(--muted)' }}>Loading activity...</div> <div className="text-center py-12 text-muted-foreground">Loading activity...</div>
) : !data?.items?.length ? ( ) : !data?.items?.length ? (
<div style={{ textAlign: 'center', padding: 48, color: 'var(--muted)' }}> <div className="text-center py-12 text-muted-foreground">
No NFT activity found on this chain yet. No NFT activity found on this chain yet.
</div> </div>
) : ( ) : (
+34 -51
View File
@@ -7,6 +7,8 @@ import { TraitFilter } from '@/components/TraitFilter'
import { ActivityRow } from '@/components/ActivityRow' import { ActivityRow } from '@/components/ActivityRow'
import { useCollection, useTokenInstances, useCollectionTransfers } from '@/hooks/useNFTData' import { useCollection, useTokenInstances, useCollectionTransfers } from '@/hooks/useNFTData'
import { CHAIN_INFO, EXPLORER_API } from '@/lib/chains' import { CHAIN_INFO, EXPLORER_API } from '@/lib/chains'
import { cn } from '@/lib/utils'
import { ExternalLink } from 'lucide-react'
type Tab = 'items' | 'activity' type Tab = 'items' | 'activity'
@@ -23,7 +25,7 @@ export default function CollectionDetailPage({
const [tab, setTab] = useState<Tab>('items') const [tab, setTab] = useState<Tab>('items')
const [selectedTraits, setSelectedTraits] = useState<Record<string, Set<string>>>({}) const [selectedTraits, setSelectedTraits] = useState<Record<string, Set<string>>>({})
const { data: collection, isLoading: loadingCollection } = useCollection(chainId, address) const { data: collection } = useCollection(chainId, address)
const { data: instances, isLoading: loadingInstances } = useTokenInstances(chainId, address) const { data: instances, isLoading: loadingInstances } = useTokenInstances(chainId, address)
const { data: transfers } = useCollectionTransfers(chainId, address) const { data: transfers } = useCollectionTransfers(chainId, address)
@@ -59,82 +61,69 @@ export default function CollectionDetailPage({
return ( return (
<div> <div>
<Header /> <Header />
<main style={{ maxWidth: 1400, margin: '0 auto', padding: '32px 24px' }}> <main className="max-w-[1400px] mx-auto px-6 py-8">
{/* Collection header */} {/* Collection header */}
<div style={{ marginBottom: 32 }}> <div className="mb-8">
<div style={{ display: 'flex', alignItems: 'center', gap: 16, marginBottom: 16 }}> <div className="flex items-center gap-4 mb-4">
<div <div
style={{ className="w-[72px] h-[72px] rounded-[14px] shrink-0 flex items-center justify-center text-2xl font-bold text-muted-foreground/30 bg-cover bg-center"
width: 72, style={
height: 72, collection?.icon_url
borderRadius: 14, ? { backgroundImage: `url(${collection.icon_url})` }
background: collection?.icon_url : { background: 'linear-gradient(135deg, hsl(var(--secondary)), hsl(var(--muted)))' }
? `url(${collection.icon_url}) center/cover` }
: 'linear-gradient(135deg, #1a1a2e, #0f3460)',
flexShrink: 0,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
fontSize: 24,
fontWeight: 700,
color: '#333',
}}
> >
{!collection?.icon_url && (collection?.symbol?.charAt(0) ?? '?')} {!collection?.icon_url && (collection?.symbol?.charAt(0) ?? '?')}
</div> </div>
<div> <div>
<h1 style={{ fontSize: 28, fontWeight: 700 }}>{collection?.name ?? 'Loading...'}</h1> <h1 className="text-[28px] font-bold">{collection?.name ?? 'Loading...'}</h1>
<div style={{ display: 'flex', gap: 12, marginTop: 4, fontSize: 13, color: 'var(--muted)' }}> <div className="flex gap-3 mt-1 text-[13px] text-muted-foreground">
{chainInfo && <span style={{ color: chainInfo.color }}>{chainInfo.name}</span>} {chainInfo && <span style={{ color: chainInfo.color }}>{chainInfo.name}</span>}
<span>{collection?.type}</span> <span>{collection?.type}</span>
<a <a
href={`${explorerBase}/token/${address}`} href={`${explorerBase}/token/${address}`}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
style={{ color: 'var(--muted)', textDecoration: 'none' }} className="text-muted-foreground no-underline hover:text-foreground flex items-center gap-1"
> >
{address.slice(0, 6)}...{address.slice(-4)} &nearr; <span className="font-mono">{address.slice(0, 6)}...{address.slice(-4)}</span>
<ExternalLink className="h-3 w-3" />
</a> </a>
</div> </div>
</div> </div>
</div> </div>
{/* Stats row */} {/* Stats row */}
<div style={{ display: 'flex', gap: 24, marginBottom: 16 }}> <div className="flex gap-6 mb-4">
{[ {[
{ label: 'Items', value: collection?.total_supply ? Number(collection.total_supply).toLocaleString() : '---' }, { label: 'Items', value: collection?.total_supply ? Number(collection.total_supply).toLocaleString() : '---' },
{ label: 'Holders', value: collection?.holders ? Number(collection.holders).toLocaleString() : '---' }, { label: 'Holders', value: collection?.holders ? Number(collection.holders).toLocaleString() : '---' },
].map((stat) => ( ].map((stat) => (
<div key={stat.label}> <div key={stat.label}>
<div style={{ fontSize: 11, color: 'var(--muted)', textTransform: 'uppercase', letterSpacing: 1 }}> <div className="text-[11px] text-muted-foreground uppercase tracking-widest">
{stat.label} {stat.label}
</div> </div>
<div style={{ fontSize: 20, fontWeight: 700, marginTop: 2 }}>{stat.value}</div> <div className="text-xl font-bold mt-0.5 font-mono">{stat.value}</div>
</div> </div>
))} ))}
</div> </div>
{/* Tabs */} {/* Tabs */}
<div style={{ display: 'flex', gap: 4, borderBottom: '1px solid var(--border)', paddingBottom: 0 }}> <div className="flex gap-1 border-b border-border">
{(['items', 'activity'] as Tab[]).map((t) => ( {(['items', 'activity'] as Tab[]).map((t) => (
<button <button
key={t} key={t}
onClick={() => setTab(t)} onClick={() => setTab(t)}
style={{ className={cn(
padding: '10px 20px', 'px-5 py-2.5 bg-transparent border-none text-sm font-semibold cursor-pointer capitalize transition-colors border-b-2',
background: 'none', tab === t
border: 'none', ? 'text-foreground border-foreground'
borderBottom: tab === t ? '2px solid var(--foreground)' : '2px solid transparent', : 'text-muted-foreground border-transparent hover:text-foreground'
color: tab === t ? 'var(--foreground)' : 'var(--muted)', )}
fontSize: 14,
fontWeight: 600,
cursor: 'pointer',
textTransform: 'capitalize',
}}
> >
{t} {t}
{t === 'items' && instances?.items && ( {t === 'items' && instances?.items && (
<span style={{ marginLeft: 6, fontSize: 12, color: 'var(--muted)' }}> <span className="ml-1.5 text-xs text-muted-foreground">
({filteredInstances.length}) ({filteredInstances.length})
</span> </span>
)} )}
@@ -145,7 +134,7 @@ export default function CollectionDetailPage({
{/* Tab content */} {/* Tab content */}
{tab === 'items' && ( {tab === 'items' && (
<div style={{ display: 'flex', gap: 24 }}> <div className="flex gap-6">
{/* Trait sidebar */} {/* Trait sidebar */}
<TraitFilter <TraitFilter
instances={instances?.items ?? []} instances={instances?.items ?? []}
@@ -155,19 +144,13 @@ export default function CollectionDetailPage({
/> />
{/* NFT grid */} {/* NFT grid */}
<div style={{ flex: 1 }}> <div className="flex-1">
{loadingInstances ? ( {loadingInstances ? (
<div style={{ textAlign: 'center', padding: 48, color: 'var(--muted)' }}>Loading NFTs...</div> <div className="text-center py-12 text-muted-foreground">Loading NFTs...</div>
) : filteredInstances.length === 0 ? ( ) : filteredInstances.length === 0 ? (
<div style={{ textAlign: 'center', padding: 48, color: 'var(--muted)' }}>No NFTs found</div> <div className="text-center py-12 text-muted-foreground">No NFTs found</div>
) : ( ) : (
<div <div className="grid grid-cols-[repeat(auto-fill,minmax(200px,1fr))] gap-3">
style={{
display: 'grid',
gridTemplateColumns: 'repeat(auto-fill, minmax(200px, 1fr))',
gap: 12,
}}
>
{filteredInstances.map((inst) => ( {filteredInstances.map((inst) => (
<NFTCard <NFTCard
key={`${inst.token.address}-${inst.id}`} key={`${inst.token.address}-${inst.id}`}
@@ -186,7 +169,7 @@ export default function CollectionDetailPage({
{tab === 'activity' && ( {tab === 'activity' && (
<div> <div>
{!transfers?.items?.length ? ( {!transfers?.items?.length ? (
<div style={{ textAlign: 'center', padding: 48, color: 'var(--muted)' }}>No activity yet</div> <div className="text-center py-12 text-muted-foreground">No activity yet</div>
) : ( ) : (
transfers.items.map((t, i) => <ActivityRow key={`${t.tx_hash}-${i}`} transfer={t} chainId={chainId} />) transfers.items.map((t, i) => <ActivityRow key={`${t.tx_hash}-${i}`} transfer={t} chainId={chainId} />)
)} )}
+16 -32
View File
@@ -3,10 +3,12 @@
import { useState } from 'react' import { useState } from 'react'
import { Header } from '@/components/Header' import { Header } from '@/components/Header'
import { CollectionCard } from '@/components/CollectionCard' import { CollectionCard } from '@/components/CollectionCard'
import { SearchBar } from '@/components/SearchBar'
import { useChainContext } from '@/hooks/useChain' import { useChainContext } from '@/hooks/useChain'
import { useCollections } from '@/hooks/useNFTData' import { useCollections } from '@/hooks/useNFTData'
import { CHAIN_INFO } from '@/lib/chains' import { CHAIN_INFO } from '@/lib/chains'
import { Input } from '@/components/ui/input'
import { Card } from '@/components/ui/card'
import { Search } from 'lucide-react'
export default function CollectionsPage() { export default function CollectionsPage() {
const { chainId } = useChainContext() const { chainId } = useChainContext()
@@ -17,57 +19,39 @@ export default function CollectionsPage() {
return ( return (
<div> <div>
<Header /> <Header />
<main style={{ maxWidth: 1200, margin: '0 auto', padding: '32px 24px' }}> <main className="max-w-[1200px] mx-auto px-6 py-8">
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 8 }}> <div className="flex justify-between items-center mb-2">
<h1 style={{ fontSize: 32, fontWeight: 700 }}>Collections</h1> <h1 className="text-3xl font-bold">Collections</h1>
</div> </div>
<p style={{ color: 'var(--muted)', marginBottom: 24, fontSize: 14 }}> <p className="text-muted-foreground mb-6 text-sm">
Browse NFT collections on{' '} Browse NFT collections on{' '}
<span style={{ color: chainInfo?.color, fontWeight: 600 }}>{chainInfo?.name ?? 'Lux'}</span>. <span className="font-semibold" style={{ color: chainInfo?.color }}>{chainInfo?.name ?? 'Lux'}</span>.
Collections with LSSVM pools have instant buy/sell liquidity. Collections with LSSVM pools have instant buy/sell liquidity.
</p> </p>
{/* Search */} {/* Search */}
<div style={{ marginBottom: 24 }}> <div className="mb-6 relative max-w-[400px]">
<input <Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
<Input
type="text" type="text"
value={search} value={search}
onChange={(e) => setSearch(e.target.value)} onChange={(e) => setSearch(e.target.value)}
placeholder="Search collections by name..." placeholder="Search collections by name..."
style={{ className="pl-9 h-10 rounded-[10px] bg-card"
width: '100%',
maxWidth: 400,
padding: '10px 16px',
background: 'var(--card)',
border: '1px solid var(--border)',
borderRadius: 10,
color: 'var(--foreground)',
fontSize: 14,
outline: 'none',
}}
/> />
</div> </div>
{/* Collection list */} {/* Collection list */}
{isLoading ? ( {isLoading ? (
<div style={{ textAlign: 'center', padding: 48, color: 'var(--muted)' }}>Loading collections...</div> <div className="text-center py-12 text-muted-foreground">Loading collections...</div>
) : !collections?.items?.length ? ( ) : !collections?.items?.length ? (
<div <Card className="text-center py-12 text-muted-foreground">
style={{
textAlign: 'center',
padding: 48,
background: 'var(--card)',
borderRadius: 12,
border: '1px solid var(--border)',
color: 'var(--muted)',
}}
>
{search {search
? `No collections matching "${search}" on ${chainInfo?.name ?? 'this chain'}.` ? `No collections matching "${search}" on ${chainInfo?.name ?? 'this chain'}.`
: `No NFT collections found on ${chainInfo?.name ?? 'this chain'} yet.`} : `No NFT collections found on ${chainInfo?.name ?? 'this chain'} yet.`}
</div> </Card>
) : ( ) : (
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}> <div className="flex flex-col gap-2">
{collections.items.map((token, i) => ( {collections.items.map((token, i) => (
<CollectionCard key={token.address} token={token} chainId={chainId} rank={i + 1} /> <CollectionCard key={token.address} token={token} chainId={chainId} rank={i + 1} />
))} ))}
+31 -74
View File
@@ -1,4 +1,8 @@
'use client'
import { Header } from '@/components/Header' import { Header } from '@/components/Header'
import { Card } from '@/components/ui/card'
import { ArrowRight } from 'lucide-react'
const GENESIS_TIERS = [ const GENESIS_TIERS = [
{ {
@@ -8,7 +12,6 @@ const GENESIS_TIERS = [
'The pinnacle of the Lux Genesis Collection. Each Genesis NFT permanently locks 1 billion LUX, with all staking rewards flowing directly to the holder.', 'The pinnacle of the Lux Genesis Collection. Each Genesis NFT permanently locks 1 billion LUX, with all staking rewards flowing directly to the holder.',
maxSupply: 10, maxSupply: 10,
rewardMultiplier: '10x', rewardMultiplier: '10x',
gradient: 'linear-gradient(135deg, #1a1a2e 0%, #16213e 50%, #0f3460 100%)',
}, },
{ {
name: 'Validator', name: 'Validator',
@@ -17,7 +20,6 @@ const GENESIS_TIERS = [
'Validator-tier Genesis NFTs lock 100M LUX permanently. Holders receive validator staking rewards through the ValidatorVault and LiquidLUX (xLUX) system.', 'Validator-tier Genesis NFTs lock 100M LUX permanently. Holders receive validator staking rewards through the ValidatorVault and LiquidLUX (xLUX) system.',
maxSupply: 100, maxSupply: 100,
rewardMultiplier: '5x', rewardMultiplier: '5x',
gradient: 'linear-gradient(135deg, #0f0f23 0%, #1a1a3e 50%, #2d1b69 100%)',
}, },
{ {
name: 'Mini', name: 'Mini',
@@ -26,7 +28,6 @@ const GENESIS_TIERS = [
'Mini Genesis NFTs lock 10M LUX permanently. A more accessible entry point to Genesis rewards with proportional staking returns.', 'Mini Genesis NFTs lock 10M LUX permanently. A more accessible entry point to Genesis rewards with proportional staking returns.',
maxSupply: 1000, maxSupply: 1000,
rewardMultiplier: '2x', rewardMultiplier: '2x',
gradient: 'linear-gradient(135deg, #1a0f2e 0%, #2d1654 50%, #1a1a3e 100%)',
}, },
{ {
name: 'Nano', name: 'Nano',
@@ -35,7 +36,6 @@ const GENESIS_TIERS = [
'Nano Genesis NFTs lock 1M LUX permanently. The most accessible Genesis tier, providing base-level staking rewards to holders.', 'Nano Genesis NFTs lock 1M LUX permanently. The most accessible Genesis tier, providing base-level staking rewards to holders.',
maxSupply: 10000, maxSupply: 10000,
rewardMultiplier: '1x', rewardMultiplier: '1x',
gradient: 'linear-gradient(135deg, #0f1a2e 0%, #1a2d4e 50%, #0f2040 100%)',
}, },
] ]
@@ -43,95 +43,52 @@ export default function GenesisPage() {
return ( return (
<div> <div>
<Header /> <Header />
<main style={{ maxWidth: 1200, margin: '0 auto', padding: '32px 24px' }}> <main className="max-w-[1200px] mx-auto px-6 py-8">
<section style={{ textAlign: 'center', marginBottom: 48 }}> <section className="text-center mb-12">
<h1 style={{ fontSize: 40, fontWeight: 700, marginBottom: 12 }}>Genesis NFTs</h1> <h1 className="text-4xl font-bold mb-3">Genesis NFTs</h1>
<p style={{ fontSize: 16, color: 'var(--muted)', maxWidth: 600, margin: '0 auto' }}> <p className="text-base text-muted-foreground max-w-[600px] mx-auto">
Permanently locked LUX tokens with staking rewards flowing to NFT holders. Genesis NFTs control validator Permanently locked LUX tokens with staking rewards flowing to NFT holders. Genesis NFTs control validator
staking returns through the ValidatorVault and LiquidLUX (xLUX) system. staking returns through the ValidatorVault and LiquidLUX (xLUX) system.
</p> </p>
</section> </section>
{/* Reward flow diagram */} {/* Reward flow diagram */}
<section <Card className="p-6 mb-12 text-center">
style={{ <h3 className="text-base font-semibold mb-4">Reward Flow</h3>
background: 'var(--card)', <div className="flex items-center justify-center gap-4 flex-wrap text-sm">
borderRadius: 12, <span>Validator Staking</span>
border: '1px solid var(--border)', <ArrowRight className="h-4 w-4 text-muted-foreground" />
padding: 24, <span>ValidatorVault.depositRewards()</span>
marginBottom: 48, <ArrowRight className="h-4 w-4 text-muted-foreground" />
textAlign: 'center', <span>LiquidLUX.depositValidatorRewards()</span>
}} <ArrowRight className="h-4 w-4 text-muted-foreground" />
> <span className="font-semibold">xLUX holders</span>
<h3 style={{ fontSize: 16, fontWeight: 600, marginBottom: 16 }}>Reward Flow</h3>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 16, flexWrap: 'wrap' }}>
<span style={{ fontSize: 14 }}>Validator Staking</span>
<span style={{ color: 'var(--muted)' }}>&rarr;</span>
<span style={{ fontSize: 14 }}>ValidatorVault.depositRewards()</span>
<span style={{ color: 'var(--muted)' }}>&rarr;</span>
<span style={{ fontSize: 14 }}>LiquidLUX.depositValidatorRewards()</span>
<span style={{ color: 'var(--muted)' }}>&rarr;</span>
<span style={{ fontSize: 14, fontWeight: 600 }}>xLUX holders</span>
</div> </div>
</section> </Card>
{/* Tier grid */} {/* Tier grid */}
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(280px, 1fr))', gap: 24 }}> <div className="grid grid-cols-[repeat(auto-fill,minmax(280px,1fr))] gap-6">
{GENESIS_TIERS.map((tier) => ( {GENESIS_TIERS.map((tier) => (
<div <Card key={tier.name} className="overflow-hidden">
key={tier.name} <div className="aspect-[4/3] bg-gradient-to-br from-secondary to-muted flex flex-col items-center justify-center gap-2">
style={{ <span className="text-xs text-muted-foreground uppercase tracking-[2px]">
background: 'var(--card)',
borderRadius: 16,
overflow: 'hidden',
border: '1px solid var(--border)',
}}
>
<div
style={{
aspectRatio: '4/3',
background: tier.gradient,
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
gap: 8,
}}
>
<span
style={{
fontSize: 12,
color: 'var(--muted)',
textTransform: 'uppercase',
letterSpacing: 2,
}}
>
Genesis Genesis
</span> </span>
<span style={{ fontSize: 36, fontWeight: 700 }}>{tier.name}</span> <span className="text-4xl font-bold">{tier.name}</span>
<span style={{ fontSize: 14, color: 'var(--accent)' }}>{tier.rewardMultiplier} rewards</span> <span className="text-sm text-primary">{tier.rewardMultiplier} rewards</span>
</div> </div>
<div style={{ padding: '20px' }}> <div className="p-5">
<div style={{ fontSize: 13, color: 'var(--muted)', marginBottom: 4 }}>Permanently Locked</div> <div className="text-[13px] text-muted-foreground mb-1">Permanently Locked</div>
<div style={{ fontSize: 24, fontWeight: 700, marginBottom: 12 }}>{tier.lockedLux} LUX</div> <div className="text-2xl font-bold mb-3 font-mono">{tier.lockedLux} LUX</div>
<p style={{ fontSize: 13, color: 'var(--muted)', lineHeight: 1.5, marginBottom: 16 }}> <p className="text-[13px] text-muted-foreground leading-relaxed mb-4">
{tier.description} {tier.description}
</p> </p>
<div <div className="flex justify-between text-[13px] text-muted-foreground border-t border-border pt-3">
style={{
display: 'flex',
justifyContent: 'space-between',
fontSize: 13,
color: 'var(--muted)',
borderTop: '1px solid var(--border)',
paddingTop: 12,
}}
>
<span>Max Supply: {tier.maxSupply.toLocaleString()}</span> <span>Max Supply: {tier.maxSupply.toLocaleString()}</span>
<span>Rewards: {tier.rewardMultiplier}</span> <span>Rewards: {tier.rewardMultiplier}</span>
</div> </div>
</div> </div>
</div> </Card>
))} ))}
</div> </div>
</main> </main>
+156 -56
View File
@@ -1,78 +1,178 @@
@import 'tailwindcss'; @import "tailwindcss";
:root { /* Default to dark mode */
--background: #0a0a0a; html {
--foreground: #ededed; color-scheme: dark;
--accent: #e8e8e8;
--muted: #666;
--border: #1a1a1a;
--card: #0f0f0f;
--card-hover: #151515;
--success: #00cc66;
--error: #ff4444;
--warning: #ffaa00;
} }
body { /* CSS Variables - Light theme */
color: var(--foreground); :root {
background: var(--background); --radius: 0.75rem;
font-family: --background: 0 0% 100%;
'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; --foreground: 240 10% 3.9%;
-webkit-font-smoothing: antialiased; --card: 0 0% 100%;
-moz-osx-font-smoothing: grayscale; --card-foreground: 240 10% 3.9%;
--popover: 0 0% 100%;
--popover-foreground: 240 10% 3.9%;
--primary: 240 5.9% 10%;
--primary-foreground: 0 0% 98%;
--secondary: 240 4.8% 95.9%;
--secondary-foreground: 240 5.9% 10%;
--muted: 240 4.8% 95.9%;
--muted-foreground: 240 3.8% 46.1%;
--accent: 240 4.8% 95.9%;
--accent-foreground: 240 5.9% 10%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 0 0% 98%;
--border: 240 5.9% 90%;
--input: 240 5.9% 90%;
--ring: 240 5.9% 10%;
--chart-1: 142.1 76.2% 36.3%;
--chart-2: 217.2 91.2% 59.8%;
--chart-3: 316.6 73.3% 69.4%;
--chart-4: 38.3 95.8% 56.7%;
--chart-5: 0 84.2% 60.2%;
--success: 142.1 76.2% 36.3%;
--success-foreground: 0 0% 98%;
--warning: 38.3 95.8% 56.7%;
--warning-foreground: 0 0% 0%;
--info: 217.2 91.2% 59.8%;
--info-foreground: 0 0% 98%;
}
/* Dark theme - Primary for Lux apps */
.dark {
--background: 240 10% 3.9%;
--foreground: 0 0% 98%;
--card: 240 10% 3.9%;
--card-foreground: 0 0% 98%;
--popover: 240 10% 3.9%;
--popover-foreground: 0 0% 98%;
--primary: 0 0% 98%;
--primary-foreground: 240 5.9% 10%;
--secondary: 240 3.7% 15.9%;
--secondary-foreground: 0 0% 98%;
--muted: 240 3.7% 15.9%;
--muted-foreground: 240 5% 64.9%;
--accent: 240 3.7% 15.9%;
--accent-foreground: 0 0% 98%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 0 0% 98%;
--border: 240 3.7% 15.9%;
--input: 240 3.7% 15.9%;
--ring: 240 4.9% 83.9%;
--chart-1: 142.1 70.6% 45.3%;
--chart-2: 217.2 91.2% 59.8%;
--chart-3: 316.6 73.3% 52%;
--chart-4: 38.3 95.8% 56.7%;
--chart-5: 0 84.2% 60.2%;
--success: 142.1 70.6% 45.3%;
--success-foreground: 0 0% 98%;
--warning: 38.3 95.8% 56.7%;
--warning-foreground: 0 0% 0%;
--info: 217.2 91.2% 59.8%;
--info-foreground: 0 0% 98%;
}
@theme inline {
--radius: 0.75rem;
--color-background: hsl(var(--background));
--color-foreground: hsl(var(--foreground));
--color-card: hsl(var(--card));
--color-card-foreground: hsl(var(--card-foreground));
--color-popover: hsl(var(--popover));
--color-popover-foreground: hsl(var(--popover-foreground));
--color-primary: hsl(var(--primary));
--color-primary-foreground: hsl(var(--primary-foreground));
--color-secondary: hsl(var(--secondary));
--color-secondary-foreground: hsl(var(--secondary-foreground));
--color-muted: hsl(var(--muted));
--color-muted-foreground: hsl(var(--muted-foreground));
--color-accent: hsl(var(--accent));
--color-accent-foreground: hsl(var(--accent-foreground));
--color-destructive: hsl(var(--destructive));
--color-destructive-foreground: hsl(var(--destructive-foreground));
--color-border: hsl(var(--border));
--color-input: hsl(var(--input));
--color-ring: hsl(var(--ring));
--color-success: hsl(var(--success));
--color-success-foreground: hsl(var(--success-foreground));
--color-warning: hsl(var(--warning));
--color-warning-foreground: hsl(var(--warning-foreground));
--color-info: hsl(var(--info));
--color-info-foreground: hsl(var(--info-foreground));
--color-chart-1: hsl(var(--chart-1));
--color-chart-2: hsl(var(--chart-2));
--color-chart-3: hsl(var(--chart-3));
--color-chart-4: hsl(var(--chart-4));
--color-chart-5: hsl(var(--chart-5));
--font-sans: var(--font-sans, ui-sans-serif, system-ui, sans-serif);
--font-mono: var(--font-mono, ui-monospace, monospace);
--animate-in: enter 0.2s ease-out;
--animate-out: exit 0.2s ease-in;
@keyframes enter {
from { opacity: 0; transform: scale(0.95); }
to { opacity: 1; transform: scale(1); }
}
@keyframes exit {
from { opacity: 1; transform: scale(1); }
to { opacity: 0; transform: scale(0.95); }
}
} }
* { * {
box-sizing: border-box; border-color: hsl(var(--border));
margin: 0;
padding: 0;
} }
a { html {
color: inherit; scroll-behavior: smooth;
text-decoration: none; -webkit-font-smoothing: antialiased;
} }
/* Custom scrollbar */ body {
::-webkit-scrollbar { background-color: hsl(var(--background));
width: 6px; color: hsl(var(--foreground));
font-feature-settings: "rlig" 1, "calt" 1;
} }
::-webkit-scrollbar-track { .glass {
background: transparent; background-color: hsl(var(--background) / 0.8);
backdrop-filter: blur(24px);
} }
::-webkit-scrollbar-thumb { .glow {
background: #333; box-shadow: 0 0 20px rgba(255, 255, 255, 0.1);
border-radius: 3px;
} }
::-webkit-scrollbar-thumb:hover { .no-scrollbar {
background: #444; -ms-overflow-style: none;
scrollbar-width: none;
}
.no-scrollbar::-webkit-scrollbar {
display: none;
} }
/* Selection */ .shimmer {
::selection { position: relative;
background: rgba(232, 232, 232, 0.15); overflow: hidden;
}
.shimmer::after {
position: absolute;
inset: 0;
transform: translateX(-100%);
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.1), transparent);
animation: shimmer 2s infinite;
content: "";
} }
/* Focus visible */ @keyframes shimmer {
:focus-visible { 100% { transform: translateX(100%); }
outline: 1px solid var(--accent);
outline-offset: 2px;
} }
/* Input/select reset */ ::-webkit-scrollbar { width: 8px; height: 8px; }
input, select, textarea { ::-webkit-scrollbar-track { background: transparent; }
font-family: inherit; ::-webkit-scrollbar-thumb { background: hsl(var(--muted)); border-radius: 9999px; }
} ::-webkit-scrollbar-thumb:hover { background: hsl(var(--muted-foreground) / 0.5); }
input::-webkit-outer-spin-button,
input::-webkit-inner-spin-button {
-webkit-appearance: none;
margin: 0;
}
input[type=number] {
-moz-appearance: textfield;
}
+14 -4
View File
@@ -1,14 +1,24 @@
import type { Metadata, Viewport } from 'next' import type { Metadata, Viewport } from 'next'
import { Inter, JetBrains_Mono } from 'next/font/google'
import { ClientProviders } from './client-providers' import { ClientProviders } from './client-providers'
import './globals.css' import './globals.css'
const inter = Inter({
subsets: ['latin'],
variable: '--font-sans',
})
const jetbrainsMono = JetBrains_Mono({
subsets: ['latin'],
variable: '--font-mono',
})
export const metadata: Metadata = { export const metadata: Metadata = {
title: { title: {
default: 'Lux Market - NFT Marketplace', default: 'Lux Market - NFT Marketplace',
template: '%s | Lux Market', template: '%s | Lux Market',
}, },
description: description: 'Trade NFTs across all Lux chains. Seaport-powered P2P trading with LSSVM AMM liquidity pools.',
'Trade NFTs across all Lux chains. Seaport-powered P2P trading with LSSVM AMM liquidity pools. Genesis NFTs with permanent LUX staking rewards.',
openGraph: { openGraph: {
title: 'Lux Market', title: 'Lux Market',
description: 'The NFT marketplace for the Lux ecosystem', description: 'The NFT marketplace for the Lux ecosystem',
@@ -32,8 +42,8 @@ export const viewport: Viewport = {
export default function RootLayout({ children }: { children: React.ReactNode }) { export default function RootLayout({ children }: { children: React.ReactNode }) {
return ( return (
<html lang="en"> <html lang="en" className={`dark ${inter.variable} ${jetbrainsMono.variable}`}>
<body> <body className="min-h-screen bg-background font-sans antialiased">
<ClientProviders>{children}</ClientProviders> <ClientProviders>{children}</ClientProviders>
</body> </body>
</html> </html>
@@ -3,6 +3,7 @@
import { use } from 'react' import { use } from 'react'
import { useAccount } from 'wagmi' import { useAccount } from 'wagmi'
import Image from 'next/image' import Image from 'next/image'
import Link from 'next/link'
import { Header } from '@/components/Header' import { Header } from '@/components/Header'
import { ListingForm } from '@/components/ListingForm' import { ListingForm } from '@/components/ListingForm'
import { OfferForm } from '@/components/OfferForm' import { OfferForm } from '@/components/OfferForm'
@@ -10,12 +11,11 @@ import { ActivityRow } from '@/components/ActivityRow'
import { useTokenInstance, useTokenInstanceTransfers } from '@/hooks/useNFTData' import { useTokenInstance, useTokenInstanceTransfers } from '@/hooks/useNFTData'
import { getNftImageUrl, resolveMediaUrl } from '@/lib/explorer' import { getNftImageUrl, resolveMediaUrl } from '@/lib/explorer'
import { CHAIN_INFO, EXPLORER_API } from '@/lib/chains' import { CHAIN_INFO, EXPLORER_API } from '@/lib/chains'
import { shortenAddress } from '@/lib/utils'
import { Card } from '@/components/ui/card'
import { ExternalLink } from 'lucide-react'
import type { Address } from 'viem' import type { Address } from 'viem'
function shortenAddress(addr: string): string {
return `${addr.slice(0, 6)}...${addr.slice(-4)}`
}
export default function NFTDetailPage({ export default function NFTDetailPage({
params, params,
}: { }: {
@@ -39,24 +39,14 @@ export default function NFTDetailPage({
return ( return (
<div> <div>
<Header /> <Header />
<main style={{ maxWidth: 1200, margin: '0 auto', padding: '32px 24px' }}> <main className="max-w-[1200px] mx-auto px-6 py-8">
{isLoading ? ( {isLoading ? (
<div style={{ textAlign: 'center', padding: 64, color: 'var(--muted)' }}>Loading NFT...</div> <div className="text-center py-16 text-muted-foreground">Loading NFT...</div>
) : ( ) : (
<div style={{ display: 'grid', gridTemplateColumns: '1fr 400px', gap: 32, alignItems: 'start' }}> <div className="grid grid-cols-1 lg:grid-cols-[1fr_400px] gap-8 items-start">
{/* Left: Media */} {/* Left: Media */}
<div> <div>
<div <div className="aspect-square rounded-2xl border border-border overflow-hidden max-w-[600px] relative bg-secondary">
style={{
aspectRatio: '1',
borderRadius: 16,
border: '1px solid var(--border)',
overflow: 'hidden',
maxWidth: 600,
position: 'relative',
background: '#111',
}}
>
{animationUrl ? ( {animationUrl ? (
<video <video
src={animationUrl} src={animationUrl}
@@ -64,31 +54,19 @@ export default function NFTDetailPage({
loop loop
muted muted
playsInline playsInline
style={{ width: '100%', height: '100%', objectFit: 'cover' }} className="w-full h-full object-cover"
/> />
) : imageUrl ? ( ) : imageUrl ? (
<Image <Image
src={imageUrl} src={imageUrl}
alt={metadata?.name ?? `#${tokenId}`} alt={metadata?.name ?? `#${tokenId}`}
fill fill
style={{ objectFit: 'cover' }} className="object-cover"
unoptimized unoptimized
sizes="600px" sizes="600px"
/> />
) : ( ) : (
<div <div className="w-full h-full flex items-center justify-center bg-gradient-to-br from-secondary to-muted text-5xl font-bold text-muted-foreground/30">
style={{
width: '100%',
height: '100%',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
background: 'linear-gradient(135deg, #1a1a2e 0%, #16213e 100%)',
fontSize: 48,
fontWeight: 700,
color: '#333',
}}
>
#{tokenId} #{tokenId}
</div> </div>
)} )}
@@ -96,143 +74,91 @@ export default function NFTDetailPage({
{/* Description */} {/* Description */}
{metadata?.description && ( {metadata?.description && (
<div <Card className="mt-4 p-4">
style={{ <div className="text-xs text-muted-foreground mb-2 font-semibold uppercase tracking-widest">
marginTop: 16,
background: 'var(--card)',
borderRadius: 12,
border: '1px solid var(--border)',
padding: 16,
}}
>
<div style={{ fontSize: 12, color: 'var(--muted)', marginBottom: 8, fontWeight: 600, textTransform: 'uppercase', letterSpacing: 1 }}>
Description Description
</div> </div>
<div style={{ fontSize: 14, color: 'var(--foreground)', lineHeight: 1.6 }}> <div className="text-sm text-foreground leading-relaxed">
{metadata.description} {metadata.description}
</div> </div>
</div> </Card>
)} )}
{/* Traits */} {/* Traits */}
{metadata?.attributes && metadata.attributes.length > 0 && ( {metadata?.attributes && metadata.attributes.length > 0 && (
<div <Card className="mt-4 p-4">
style={{ <div className="text-xs text-muted-foreground mb-3 font-semibold uppercase tracking-widest">
marginTop: 16,
background: 'var(--card)',
borderRadius: 12,
border: '1px solid var(--border)',
padding: 16,
}}
>
<div style={{ fontSize: 12, color: 'var(--muted)', marginBottom: 12, fontWeight: 600, textTransform: 'uppercase', letterSpacing: 1 }}>
Traits Traits
</div> </div>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(140px, 1fr))', gap: 8 }}> <div className="grid grid-cols-[repeat(auto-fill,minmax(140px,1fr))] gap-2">
{metadata.attributes.map((trait, i) => ( {metadata.attributes.map((trait, i) => (
<div <div
key={`${trait.trait_type}-${i}`} key={`${trait.trait_type}-${i}`}
style={{ className="bg-primary/5 rounded-lg px-3 py-2 border border-border"
background: 'rgba(232,232,232,0.05)',
borderRadius: 8,
padding: '8px 12px',
border: '1px solid var(--border)',
}}
> >
<div style={{ fontSize: 10, color: 'var(--muted)', textTransform: 'uppercase', letterSpacing: 1 }}> <div className="text-[10px] text-muted-foreground uppercase tracking-widest">
{trait.trait_type} {trait.trait_type}
</div> </div>
<div style={{ fontSize: 13, fontWeight: 600, marginTop: 2 }}> <div className="text-[13px] font-semibold mt-0.5">
{String(trait.value)} {String(trait.value)}
</div> </div>
</div> </div>
))} ))}
</div> </div>
</div> </Card>
)} )}
{/* Transfer History */} {/* Transfer History */}
{transfers?.items && transfers.items.length > 0 && ( {transfers?.items && transfers.items.length > 0 && (
<div <Card className="mt-4 p-4">
style={{ <div className="text-xs text-muted-foreground mb-3 font-semibold uppercase tracking-widest">
marginTop: 16,
background: 'var(--card)',
borderRadius: 12,
border: '1px solid var(--border)',
padding: 16,
}}
>
<div style={{ fontSize: 12, color: 'var(--muted)', marginBottom: 12, fontWeight: 600, textTransform: 'uppercase', letterSpacing: 1 }}>
Transfer History Transfer History
</div> </div>
{transfers.items.slice(0, 20).map((t, i) => ( {transfers.items.slice(0, 20).map((t, i) => (
<ActivityRow key={`${t.tx_hash}-${i}`} transfer={t} chainId={chainId} /> <ActivityRow key={`${t.tx_hash}-${i}`} transfer={t} chainId={chainId} />
))} ))}
</div> </Card>
)} )}
</div> </div>
{/* Right: Details + Actions */} {/* Right: Details + Actions */}
<div style={{ display: 'flex', flexDirection: 'column', gap: 16, position: 'sticky', top: 80 }}> <div className="flex flex-col gap-4 lg:sticky lg:top-20">
{/* Collection & Name */} {/* Collection & Name */}
<div> <div>
<a <Link
href={`/collection/${chainId}/${contractAddress}`} href={`/collection/${chainId}/${contractAddress}`}
style={{ fontSize: 13, color: 'var(--accent)', textDecoration: 'none' }} className="text-[13px] text-primary no-underline hover:underline"
> >
{instance?.token?.name ?? 'Collection'} {instance?.token?.name ?? 'Collection'}
</a> </Link>
<h1 style={{ fontSize: 28, fontWeight: 700, marginTop: 4 }}> <h1 className="text-[28px] font-bold mt-1">
{metadata?.name ?? `#${tokenId}`} {metadata?.name ?? `#${tokenId}`}
</h1> </h1>
</div> </div>
{/* Owner */} {/* Owner */}
<div <Card className="p-3.5">
style={{ <div className="text-[11px] text-muted-foreground mb-1 uppercase tracking-widest">
background: 'var(--card)',
borderRadius: 12,
padding: 14,
border: '1px solid var(--border)',
}}
>
<div style={{ fontSize: 11, color: 'var(--muted)', marginBottom: 4, textTransform: 'uppercase', letterSpacing: 1 }}>
Owner Owner
</div> </div>
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}> <div className="flex items-center gap-2">
<span style={{ fontSize: 14, fontFamily: 'monospace' }}> <span className="text-sm font-mono">
{ownerAddress ? shortenAddress(ownerAddress) : '---'} {ownerAddress ? shortenAddress(ownerAddress) : '---'}
</span> </span>
{isOwner && ( {isOwner && (
<span <span className="text-[10px] px-1.5 py-0.5 rounded bg-success/10 text-success font-semibold">
style={{
fontSize: 10,
padding: '2px 6px',
borderRadius: 4,
background: 'rgba(0,200,100,0.1)',
color: '#0c6',
fontWeight: 600,
}}
>
YOU YOU
</span> </span>
)} )}
</div> </div>
</div> </Card>
{/* Details */} {/* Details */}
<div <Card className="p-3.5">
style={{ <div className="text-[11px] text-muted-foreground mb-2 uppercase tracking-widest">
background: 'var(--card)',
borderRadius: 12,
padding: 14,
border: '1px solid var(--border)',
}}
>
<div style={{ fontSize: 11, color: 'var(--muted)', marginBottom: 8, textTransform: 'uppercase', letterSpacing: 1 }}>
Details Details
</div> </div>
<div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}> <div className="flex flex-col gap-1.5">
{[ {[
{ label: 'Chain', value: chainInfo?.name ?? String(chainId), color: chainInfo?.color }, { label: 'Chain', value: chainInfo?.name ?? String(chainId), color: chainInfo?.color },
{ {
@@ -243,26 +169,30 @@ export default function NFTDetailPage({
{ label: 'Token ID', value: tokenId }, { label: 'Token ID', value: tokenId },
{ label: 'Token Standard', value: instance?.token?.type ?? 'ERC-721' }, { label: 'Token Standard', value: instance?.token?.type ?? 'ERC-721' },
].map((row) => ( ].map((row) => (
<div key={row.label} style={{ display: 'flex', justifyContent: 'space-between', fontSize: 13 }}> <div key={row.label} className="flex justify-between text-[13px]">
<span style={{ color: 'var(--muted)' }}>{row.label}</span> <span className="text-muted-foreground">{row.label}</span>
{row.href ? ( {row.href ? (
<a <a
href={row.href} href={row.href}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
style={{ fontFamily: 'monospace', color: 'var(--foreground)', textDecoration: 'none' }} className="font-mono text-foreground no-underline flex items-center gap-1 hover:underline"
> >
{row.value} &nearr; {row.value}
<ExternalLink className="h-3 w-3" />
</a> </a>
) : ( ) : (
<span style={{ color: row.color, fontFamily: row.label === 'Token ID' ? 'monospace' : undefined }}> <span
className={row.label === 'Token ID' ? 'font-mono' : ''}
style={row.color ? { color: row.color } : undefined}
>
{row.value} {row.value}
</span> </span>
)} )}
</div> </div>
))} ))}
</div> </div>
</div> </Card>
{/* External links */} {/* External links */}
{metadata?.external_url && ( {metadata?.external_url && (
@@ -270,37 +200,20 @@ export default function NFTDetailPage({
href={metadata.external_url} href={metadata.external_url}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
style={{ className="block text-center py-2.5 bg-card border border-border rounded-[10px] text-primary no-underline text-[13px] hover:bg-secondary transition-colors"
display: 'block',
textAlign: 'center',
padding: '10px',
background: 'var(--card)',
border: '1px solid var(--border)',
borderRadius: 10,
color: 'var(--accent)',
textDecoration: 'none',
fontSize: 13,
}}
> >
View External &nearr; View External <ExternalLink className="inline h-3 w-3 ml-1" />
</a> </a>
)} )}
{/* Trading Actions */} {/* Trading Actions */}
<div <Card className="p-4">
style={{
background: 'var(--card)',
borderRadius: 12,
padding: 16,
border: '1px solid var(--border)',
}}
>
{isOwner ? ( {isOwner ? (
<ListingForm contractAddress={contractAddress as Address} tokenId={tokenId} /> <ListingForm contractAddress={contractAddress as Address} tokenId={tokenId} />
) : ( ) : (
<OfferForm contractAddress={contractAddress as Address} tokenId={tokenId} /> <OfferForm contractAddress={contractAddress as Address} tokenId={tokenId} />
)} )}
</div> </Card>
</div> </div>
</div> </div>
)} )}
+38 -80
View File
@@ -7,6 +7,8 @@ import { useChainContext } from '@/hooks/useChain'
import { useCollections } from '@/hooks/useNFTData' import { useCollections } from '@/hooks/useNFTData'
import Link from 'next/link' import Link from 'next/link'
import { CHAIN_INFO } from '@/lib/chains' import { CHAIN_INFO } from '@/lib/chains'
import { Card } from '@/components/ui/card'
import { ArrowRight } from 'lucide-react'
const GENESIS_TIERS = [ const GENESIS_TIERS = [
{ name: 'Genesis', locked: '1B LUX', tier: 'Tier 1', multiplier: '10x' }, { name: 'Genesis', locked: '1B LUX', tier: 'Tier 1', multiplier: '10x' },
@@ -23,106 +25,70 @@ export default function Home() {
return ( return (
<div> <div>
<Header /> <Header />
<main style={{ maxWidth: 1200, margin: '0 auto', padding: '32px 24px' }}> <main className="max-w-[1200px] mx-auto px-6 py-8">
{/* Hero */} {/* Hero */}
<section style={{ textAlign: 'center', marginBottom: 48 }}> <section className="text-center mb-12">
<h1 style={{ fontSize: 48, fontWeight: 700, marginBottom: 12, letterSpacing: '-0.03em' }}>Lux Market</h1> <h1 className="text-5xl font-bold mb-3 tracking-tight">Lux Market</h1>
<p style={{ fontSize: 16, color: 'var(--muted)', maxWidth: 560, margin: '0 auto 24px' }}> <p className="text-base text-muted-foreground max-w-[560px] mx-auto mb-6">
Trade NFTs across all Lux chains. Seaport-powered P2P trading with LSSVM AMM liquidity. Trade NFTs across all Lux chains. Seaport-powered P2P trading with LSSVM AMM liquidity.
</p> </p>
<div style={{ maxWidth: 480, margin: '0 auto' }}> <div className="max-w-[480px] mx-auto">
<SearchBar /> <SearchBar />
</div> </div>
</section> </section>
{/* Genesis Featured */} {/* Genesis Featured */}
<section style={{ marginBottom: 48 }}> <section className="mb-12">
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 16 }}> <div className="flex justify-between items-center mb-4">
<h2 style={{ fontSize: 20, fontWeight: 600 }}>Genesis Collection</h2> <h2 className="text-xl font-semibold">Genesis Collection</h2>
<Link href="/genesis" style={{ color: 'var(--accent)', textDecoration: 'none', fontSize: 13, fontWeight: 500 }}> <Link href="/genesis" className="text-primary no-underline text-[13px] font-medium flex items-center gap-1 hover:underline">
View All &rarr; View All <ArrowRight className="h-3.5 w-3.5" />
</Link> </Link>
</div> </div>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(200px, 1fr))', gap: 12 }}> <div className="grid grid-cols-[repeat(auto-fill,minmax(200px,1fr))] gap-3">
{GENESIS_TIERS.map((nft) => ( {GENESIS_TIERS.map((nft) => (
<Link key={nft.name} href="/genesis" style={{ textDecoration: 'none', color: 'inherit' }}> <Link key={nft.name} href="/genesis" className="no-underline text-inherit group">
<div <Card className="overflow-hidden transition-all duration-200 hover:border-muted-foreground/30 hover:-translate-y-0.5">
style={{ <div className="aspect-square bg-gradient-to-br from-secondary to-muted flex flex-col items-center justify-center gap-1.5">
background: 'var(--card)', <span className="text-[11px] text-muted-foreground uppercase tracking-widest">
borderRadius: 12,
overflow: 'hidden',
border: '1px solid var(--border)',
transition: 'all 200ms ease',
cursor: 'pointer',
}}
onMouseEnter={(e) => {
e.currentTarget.style.borderColor = '#333'
e.currentTarget.style.transform = 'translateY(-2px)'
}}
onMouseLeave={(e) => {
e.currentTarget.style.borderColor = 'var(--border)'
e.currentTarget.style.transform = 'translateY(0)'
}}
>
<div
style={{
aspectRatio: '1',
background: 'linear-gradient(135deg, #1a1a2e 0%, #16213e 50%, #0f3460 100%)',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
gap: 6,
}}
>
<span style={{ fontSize: 11, color: '#555', textTransform: 'uppercase', letterSpacing: 1 }}>
{nft.tier} {nft.tier}
</span> </span>
<span style={{ fontSize: 24, fontWeight: 700 }}>{nft.name}</span> <span className="text-2xl font-bold">{nft.name}</span>
<span style={{ fontSize: 12, color: 'var(--accent)' }}>{nft.multiplier} rewards</span> <span className="text-xs text-primary">{nft.multiplier} rewards</span>
</div> </div>
<div style={{ padding: '10px 12px' }}> <div className="px-3 py-2.5">
<div style={{ fontSize: 11, color: 'var(--muted)' }}>Permanently Locked</div> <div className="text-[11px] text-muted-foreground">Permanently Locked</div>
<div style={{ fontSize: 14, fontWeight: 600, marginTop: 2 }}>{nft.locked}</div> <div className="text-sm font-semibold mt-0.5">{nft.locked}</div>
</div> </div>
</div> </Card>
</Link> </Link>
))} ))}
</div> </div>
</section> </section>
{/* Collections on selected chain */} {/* Collections on selected chain */}
<section style={{ marginBottom: 48 }}> <section className="mb-12">
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 16 }}> <div className="flex justify-between items-center mb-4">
<h2 style={{ fontSize: 20, fontWeight: 600 }}> <h2 className="text-xl font-semibold">
Collections on{' '} Collections on{' '}
<span style={{ color: chainInfo?.color }}>{chainInfo?.name ?? 'Lux'}</span> <span style={{ color: chainInfo?.color }}>{chainInfo?.name ?? 'Lux'}</span>
</h2> </h2>
<Link <Link
href="/collections" href="/collections"
style={{ color: 'var(--accent)', textDecoration: 'none', fontSize: 13, fontWeight: 500 }} className="text-primary no-underline text-[13px] font-medium flex items-center gap-1 hover:underline"
> >
View All &rarr; View All <ArrowRight className="h-3.5 w-3.5" />
</Link> </Link>
</div> </div>
{isLoading ? ( {isLoading ? (
<div style={{ textAlign: 'center', padding: 32, color: 'var(--muted)' }}>Loading collections...</div> <div className="text-center py-8 text-muted-foreground">Loading collections...</div>
) : !collections?.items?.length ? ( ) : !collections?.items?.length ? (
<div <Card className="text-center py-8 text-muted-foreground">
style={{
textAlign: 'center',
padding: 32,
background: 'var(--card)',
borderRadius: 12,
border: '1px solid var(--border)',
color: 'var(--muted)',
}}
>
No NFT collections found on {chainInfo?.name ?? 'this chain'} yet. Deploy an ERC-721 or ERC-1155 to get started. No NFT collections found on {chainInfo?.name ?? 'this chain'} yet. Deploy an ERC-721 or ERC-1155 to get started.
</div> </Card>
) : ( ) : (
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}> <div className="flex flex-col gap-2">
{collections.items.slice(0, 10).map((token, i) => ( {collections.items.slice(0, 10).map((token, i) => (
<CollectionCard key={token.address} token={token} chainId={chainId} rank={i + 1} /> <CollectionCard key={token.address} token={token} chainId={chainId} rank={i + 1} />
))} ))}
@@ -131,28 +97,20 @@ export default function Home() {
</section> </section>
{/* Stats */} {/* Stats */}
<section style={{ marginBottom: 48 }}> <section className="mb-12">
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(180px, 1fr))', gap: 12 }}> <div className="grid grid-cols-[repeat(auto-fit,minmax(180px,1fr))] gap-3">
{[ {[
{ label: 'Supported Chains', value: '5' }, { label: 'Supported Chains', value: '5' },
{ label: 'Collections', value: collections?.items?.length?.toString() ?? '---' }, { label: 'Collections', value: collections?.items?.length?.toString() ?? '---' },
{ label: 'Trading Protocol', value: 'Seaport' }, { label: 'Trading Protocol', value: 'Seaport' },
{ label: 'AMM Protocol', value: 'LSSVM' }, { label: 'AMM Protocol', value: 'LSSVM' },
].map((stat) => ( ].map((stat) => (
<div <Card key={stat.label} className="px-5 py-4">
key={stat.label} <div className="text-[11px] text-muted-foreground mb-1 uppercase tracking-widest">
style={{
background: 'var(--card)',
borderRadius: 12,
padding: '16px 20px',
border: '1px solid var(--border)',
}}
>
<div style={{ fontSize: 11, color: 'var(--muted)', marginBottom: 4, textTransform: 'uppercase', letterSpacing: 1 }}>
{stat.label} {stat.label}
</div> </div>
<div style={{ fontSize: 20, fontWeight: 700 }}>{stat.value}</div> <div className="text-xl font-bold">{stat.value}</div>
</div> </Card>
))} ))}
</div> </div>
</section> </section>
+29 -42
View File
@@ -6,6 +6,11 @@ import { NFTCard } from '@/components/NFTCard'
import { useChainContext } from '@/hooks/useChain' import { useChainContext } from '@/hooks/useChain'
import { usePortfolioNFTs } from '@/hooks/useNFTData' import { usePortfolioNFTs } from '@/hooks/useNFTData'
import { CHAIN_INFO } from '@/lib/chains' import { CHAIN_INFO } from '@/lib/chains'
import { Card } from '@/components/ui/card'
import { Button } from '@/components/ui/button'
import { Wallet } from 'lucide-react'
import { useConnect } from 'wagmi'
import { injected } from 'wagmi/connectors'
const ALL_CHAIN_IDS = [96369, 200200, 36963, 36911, 494949] const ALL_CHAIN_IDS = [96369, 200200, 36963, 36911, 494949]
@@ -13,21 +18,15 @@ function ChainPortfolio({ chainId, walletAddress }: { chainId: number; walletAdd
const { data, isLoading } = usePortfolioNFTs(chainId, walletAddress) const { data, isLoading } = usePortfolioNFTs(chainId, walletAddress)
const chainInfo = CHAIN_INFO[chainId] const chainInfo = CHAIN_INFO[chainId]
if (isLoading) return <div style={{ color: 'var(--muted)', padding: '12px 0' }}>Loading {chainInfo?.name}...</div> if (isLoading) return <div className="text-muted-foreground py-3">Loading {chainInfo?.name}...</div>
if (!data?.items?.length) return null if (!data?.items?.length) return null
return ( return (
<section style={{ marginBottom: 32 }}> <section className="mb-8">
<h3 style={{ fontSize: 16, fontWeight: 600, marginBottom: 12, color: chainInfo?.color }}> <h3 className="text-base font-semibold mb-3" style={{ color: chainInfo?.color }}>
{chainInfo?.name} ({data.items.length}) {chainInfo?.name} ({data.items.length})
</h3> </h3>
<div <div className="grid grid-cols-[repeat(auto-fill,minmax(180px,1fr))] gap-3">
style={{
display: 'grid',
gridTemplateColumns: 'repeat(auto-fill, minmax(180px, 1fr))',
gap: 12,
}}
>
{data.items.map((item) => ( {data.items.map((item) => (
<NFTCard <NFTCard
key={`${item.token.address}-${item.token_id}`} key={`${item.token.address}-${item.token_id}`}
@@ -45,48 +44,36 @@ function ChainPortfolio({ chainId, walletAddress }: { chainId: number; walletAdd
export default function PortfolioPage() { export default function PortfolioPage() {
const { address, isConnected } = useAccount() const { address, isConnected } = useAccount()
const { chainId: selectedChain } = useChainContext() const { connect } = useConnect()
return ( return (
<div> <div>
<Header /> <Header />
<main style={{ maxWidth: 1200, margin: '0 auto', padding: '32px 24px' }}> <main className="max-w-[1200px] mx-auto px-6 py-8">
<h1 style={{ fontSize: 32, fontWeight: 700, marginBottom: 24 }}>My NFTs</h1> <h1 className="text-3xl font-bold mb-6">My NFTs</h1>
{!isConnected ? ( {!isConnected ? (
<div <Card className="text-center py-16 px-6">
style={{ <Wallet className="h-12 w-12 mx-auto mb-4 text-muted-foreground" />
textAlign: 'center', <h2 className="text-xl font-semibold mb-2">Connect your wallet</h2>
padding: '64px 24px', <p className="text-muted-foreground text-sm mb-6">
background: 'var(--card)',
borderRadius: 16,
border: '1px solid var(--border)',
}}
>
<div style={{ fontSize: 48, marginBottom: 16 }}>&#x1F4BC;</div>
<h2 style={{ fontSize: 20, fontWeight: 600, marginBottom: 8 }}>Connect your wallet</h2>
<p style={{ color: 'var(--muted)', fontSize: 14 }}>
Connect a wallet to view your NFTs across all Lux chains. Connect a wallet to view your NFTs across all Lux chains.
</p> </p>
</div> <Button
variant="connect"
size="lg"
onClick={() => connect({ connector: injected() })}
>
<Wallet className="h-4 w-4" />
Connect Wallet
</Button>
</Card>
) : ( ) : (
<div> <div>
<div <Card className="flex items-center gap-2 mb-6 px-4 py-3 text-[13px]">
style={{ <span className="text-muted-foreground">Wallet:</span>
display: 'flex', <span className="font-mono">{address}</span>
alignItems: 'center', </Card>
gap: 8,
marginBottom: 24,
padding: '12px 16px',
background: 'var(--card)',
borderRadius: 10,
border: '1px solid var(--border)',
fontSize: 13,
}}
>
<span style={{ color: 'var(--muted)' }}>Wallet:</span>
<span style={{ fontFamily: 'monospace' }}>{address}</span>
</div>
{/* NFTs grouped by chain */} {/* NFTs grouped by chain */}
{ALL_CHAIN_IDS.map((cId) => ( {ALL_CHAIN_IDS.map((cId) => (
+19 -24
View File
@@ -1,39 +1,34 @@
'use client' 'use client'
import { RainbowKitProvider, darkTheme } from '@rainbow-me/rainbowkit'
import '@rainbow-me/rainbowkit/styles.css'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query' import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { useState, type ReactNode } from 'react' import { useState, type ReactNode } from 'react'
import { WagmiProvider } from 'wagmi' import { WagmiProvider, createConfig, http } from 'wagmi'
import { config } from '@/lib/wagmi' import { injected } from 'wagmi/connectors'
import { ChainContext } from '@/hooks/useChain' import { ChainContext } from '@/hooks/useChain'
import { supportedChains } from '@/lib/chains'
const queryClient = new QueryClient({ function createWagmiConfig() {
defaultOptions: { return createConfig({
queries: { chains: supportedChains,
refetchOnWindowFocus: false, connectors: [injected()],
retry: 2, transports: Object.fromEntries(supportedChains.map((c) => [c.id, http()])) as any,
}, ssr: true,
}, })
}) }
export function Providers({ children }: { children: ReactNode }) { export function Providers({ children }: { children: ReactNode }) {
const [chainId, setChainId] = useState(96369) // Default to Lux mainnet const [chainId, setChainId] = useState(96369)
const [config] = useState(() => createWagmiConfig())
const [queryClient] = useState(() => new QueryClient({
defaultOptions: { queries: { refetchOnWindowFocus: false, retry: 2, staleTime: 60_000 } },
}))
return ( return (
<WagmiProvider config={config}> <WagmiProvider config={config}>
<QueryClientProvider client={queryClient}> <QueryClientProvider client={queryClient}>
<RainbowKitProvider <ChainContext.Provider value={{ chainId, setChainId }}>
theme={darkTheme({ {children}
accentColor: '#E8E8E8', </ChainContext.Provider>
accentColorForeground: '#000',
borderRadius: 'medium',
})}
>
<ChainContext.Provider value={{ chainId, setChainId }}>
{children}
</ChainContext.Provider>
</RainbowKitProvider>
</QueryClientProvider> </QueryClientProvider>
</WagmiProvider> </WagmiProvider>
) )
+26 -49
View File
@@ -2,17 +2,15 @@
import Link from 'next/link' import Link from 'next/link'
import { CHAIN_INFO, EXPLORER_API } from '@/lib/chains' import { CHAIN_INFO, EXPLORER_API } from '@/lib/chains'
import { ExternalLink } from 'lucide-react'
import type { ExplorerTransfer } from '@/lib/explorer' import type { ExplorerTransfer } from '@/lib/explorer'
import { shortenAddress } from '@/lib/utils'
function shortenAddress(addr: string): string { function getEventType(method: string): { label: string } {
return `${addr.slice(0, 6)}...${addr.slice(-4)}`
}
function getEventType(method: string): { label: string; color: string } {
const m = method.toLowerCase() const m = method.toLowerCase()
if (m.includes('mint') || m === 'transfer' && true) return { label: 'Transfer', color: '#888' } if (m.includes('mint') || m === 'transfer') return { label: 'Transfer' }
if (m.includes('safe')) return { label: 'Transfer', color: '#888' } if (m.includes('safe')) return { label: 'Transfer' }
return { label: method || 'Transfer', color: '#888' } return { label: method || 'Transfer' }
} }
interface ActivityRowProps { interface ActivityRowProps {
@@ -29,63 +27,44 @@ export function ActivityRow({ transfer, chainId }: ActivityRowProps) {
const tokenId = transfer.total?.token_id ?? '?' const tokenId = transfer.total?.token_id ?? '?'
return ( return (
<div <div className="flex items-center gap-3 py-3 border-b border-border text-[13px]">
style={{
display: 'flex',
alignItems: 'center',
gap: 12,
padding: '12px 0',
borderBottom: '1px solid var(--border)',
fontSize: 13,
}}
>
{/* Event type */} {/* Event type */}
<div <div
style={{ className={`px-2.5 py-1 rounded-md text-xs font-semibold w-[72px] text-center shrink-0 ${
padding: '4px 10px', isMint
borderRadius: 6, ? 'bg-success/10 text-success'
background: isMint ? 'rgba(0,200,100,0.1)' : 'rgba(136,136,136,0.1)', : 'bg-muted-foreground/10 text-muted-foreground'
color: isMint ? '#0c6' : event.color, }`}
fontSize: 12,
fontWeight: 600,
width: 72,
textAlign: 'center',
flexShrink: 0,
}}
> >
{isMint ? 'Mint' : event.label} {isMint ? 'Mint' : event.label}
</div> </div>
{/* Item */} {/* Item */}
<div style={{ flex: 1, minWidth: 0 }}> <div className="flex-1 min-w-0">
<Link <Link
href={`/nft/${chainId}/${transfer.token.address}/${tokenId}`} href={`/nft/${chainId}/${transfer.token.address}/${tokenId}`}
style={{ className="text-foreground no-underline font-medium hover:underline"
color: 'var(--foreground)',
textDecoration: 'none',
fontWeight: 500,
}}
> >
{transfer.token.name} #{tokenId} {transfer.token.name} #{tokenId}
</Link> </Link>
</div> </div>
{/* From → To */} {/* From -> To */}
<div style={{ display: 'flex', alignItems: 'center', gap: 6, color: 'var(--muted)', flexShrink: 0 }}> <div className="flex items-center gap-1.5 text-muted-foreground shrink-0">
<a <a
href={`${explorerBase}/address/${transfer.from.hash}`} href={`${explorerBase}/address/${transfer.from.hash}`}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
style={{ color: 'var(--muted)', textDecoration: 'none', fontFamily: 'monospace', fontSize: 12 }} className="text-muted-foreground no-underline font-mono text-xs hover:text-foreground"
> >
{isMint ? 'NullAddress' : shortenAddress(transfer.from.hash)} {isMint ? 'NullAddress' : shortenAddress(transfer.from.hash)}
</a> </a>
<span style={{ fontSize: 10 }}>&rarr;</span> <span className="text-[10px]">&rarr;</span>
<a <a
href={`${explorerBase}/address/${transfer.to.hash}`} href={`${explorerBase}/address/${transfer.to.hash}`}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
style={{ color: 'var(--muted)', textDecoration: 'none', fontFamily: 'monospace', fontSize: 12 }} className="text-muted-foreground no-underline font-mono text-xs hover:text-foreground"
> >
{shortenAddress(transfer.to.hash)} {shortenAddress(transfer.to.hash)}
</a> </a>
@@ -93,13 +72,16 @@ export function ActivityRow({ transfer, chainId }: ActivityRowProps) {
{/* Chain */} {/* Chain */}
{chainInfo && ( {chainInfo && (
<div style={{ fontSize: 11, color: chainInfo.color, fontWeight: 600, width: 40, textAlign: 'right', flexShrink: 0 }}> <div
className="text-[11px] font-semibold w-10 text-right shrink-0"
style={{ color: chainInfo.color }}
>
{chainInfo.name} {chainInfo.name}
</div> </div>
)} )}
{/* Time */} {/* Time */}
<div style={{ color: 'var(--muted)', fontSize: 12, width: 80, textAlign: 'right', flexShrink: 0 }}> <div className="text-muted-foreground text-xs w-20 text-right shrink-0">
{transfer.timestamp ? new Date(transfer.timestamp).toLocaleDateString() : '---'} {transfer.timestamp ? new Date(transfer.timestamp).toLocaleDateString() : '---'}
</div> </div>
@@ -108,15 +90,10 @@ export function ActivityRow({ transfer, chainId }: ActivityRowProps) {
href={`${explorerBase}/tx/${transfer.tx_hash}`} href={`${explorerBase}/tx/${transfer.tx_hash}`}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
style={{ className="text-muted-foreground shrink-0 hover:text-foreground transition-colors"
color: 'var(--muted)',
textDecoration: 'none',
fontSize: 11,
flexShrink: 0,
}}
title="View on explorer" title="View on explorer"
> >
&nearr; <ExternalLink className="h-3.5 w-3.5" />
</a> </a>
</div> </div>
) )
+15 -52
View File
@@ -16,78 +16,41 @@ export function CollectionCard({ token, chainId, rank }: CollectionCardProps) {
return ( return (
<Link <Link
href={`/collection/${chainId}/${token.address}`} href={`/collection/${chainId}/${token.address}`}
style={{ textDecoration: 'none', color: 'inherit' }} className="no-underline text-inherit"
> >
<div <div className="bg-card rounded-xl p-4 border border-border flex items-center gap-4 transition-colors cursor-pointer hover:border-muted-foreground/30 hover:bg-secondary/50">
style={{
background: 'var(--card)',
borderRadius: 12,
padding: 16,
border: '1px solid var(--border)',
display: 'flex',
alignItems: 'center',
gap: 16,
transition: 'all 150ms ease',
cursor: 'pointer',
}}
onMouseEnter={(e) => {
e.currentTarget.style.borderColor = '#333'
e.currentTarget.style.background = 'var(--card-hover)'
}}
onMouseLeave={(e) => {
e.currentTarget.style.borderColor = 'var(--border)'
e.currentTarget.style.background = 'var(--card)'
}}
>
{rank !== undefined && ( {rank !== undefined && (
<div style={{ fontSize: 14, fontWeight: 700, color: 'var(--muted)', width: 24, textAlign: 'center' }}> <div className="text-sm font-bold text-muted-foreground w-6 text-center font-mono">
{rank} {rank}
</div> </div>
)} )}
{/* Collection icon */} {/* Collection icon */}
<div <div
style={{ className="w-14 h-14 rounded-[10px] shrink-0 flex items-center justify-center text-xl font-bold text-muted-foreground/30 bg-cover bg-center"
width: 56, style={
height: 56, token.icon_url
borderRadius: 10, ? { backgroundImage: `url(${token.icon_url})` }
background: token.icon_url : { background: 'linear-gradient(135deg, hsl(var(--secondary)), hsl(var(--muted)))' }
? `url(${token.icon_url}) center/cover` }
: 'linear-gradient(135deg, #1a1a2e, #0f3460)',
flexShrink: 0,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
fontSize: 20,
fontWeight: 700,
color: '#333',
}}
> >
{!token.icon_url && (token.symbol?.charAt(0) ?? '?')} {!token.icon_url && (token.symbol?.charAt(0) ?? '?')}
</div> </div>
<div style={{ flex: 1, minWidth: 0 }}> <div className="flex-1 min-w-0">
<div <div className="text-[15px] font-semibold truncate">
style={{
fontSize: 15,
fontWeight: 600,
whiteSpace: 'nowrap',
overflow: 'hidden',
textOverflow: 'ellipsis',
}}
>
{token.name} {token.name}
</div> </div>
<div style={{ display: 'flex', gap: 16, marginTop: 4, fontSize: 12, color: 'var(--muted)' }}> <div className="flex gap-4 mt-1 text-xs text-muted-foreground">
<span>{token.type}</span> <span>{token.type}</span>
{chainInfo && ( {chainInfo && (
<span style={{ color: chainInfo.color }}>{chainInfo.name}</span> <span style={{ color: chainInfo.color }}>{chainInfo.name}</span>
)} )}
</div> </div>
</div> </div>
<div style={{ textAlign: 'right', flexShrink: 0 }}> <div className="text-right shrink-0">
<div style={{ fontSize: 13 }}> <div className="text-[13px]">
{token.holders ? Number(token.holders).toLocaleString() : '---'} holders {token.holders ? Number(token.holders).toLocaleString() : '---'} holders
</div> </div>
<div style={{ fontSize: 11, color: 'var(--muted)', marginTop: 2 }}> <div className="text-[11px] text-muted-foreground mt-0.5">
{token.total_supply ? Number(token.total_supply).toLocaleString() : '---'} items {token.total_supply ? Number(token.total_supply).toLocaleString() : '---'} items
</div> </div>
</div> </div>
+53 -55
View File
@@ -1,10 +1,15 @@
'use client' 'use client'
import { ConnectButton } from '@rainbow-me/rainbowkit'
import Link from 'next/link' import Link from 'next/link'
import { usePathname } from 'next/navigation' import { usePathname } from 'next/navigation'
import { useAccount, useConnect, useDisconnect } from 'wagmi'
import { injected } from 'wagmi/connectors'
import { useChainContext } from '@/hooks/useChain' import { useChainContext } from '@/hooks/useChain'
import { CHAIN_INFO } from '@/lib/chains' import { CHAIN_INFO } from '@/lib/chains'
import { Button } from '@/components/ui/button'
import { Wallet, LogOut } from 'lucide-react'
import { cn } from '@/lib/utils'
import { shortenAddress } from '@/lib/utils'
const NAV_LINKS = [ const NAV_LINKS = [
{ href: '/', label: 'Explore' }, { href: '/', label: 'Explore' },
@@ -19,51 +24,32 @@ const CHAIN_IDS = [96369, 200200, 36963, 36911, 494949]
export function Header() { export function Header() {
const pathname = usePathname() const pathname = usePathname()
const { chainId, setChainId } = useChainContext() const { chainId, setChainId } = useChainContext()
const { address, isConnected } = useAccount()
const { connect } = useConnect()
const { disconnect } = useDisconnect()
return ( return (
<header <header className="sticky top-0 z-50 flex items-center justify-between px-6 py-3 border-b border-border bg-background/80 backdrop-blur-xl">
style={{ <div className="flex items-center gap-8">
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
padding: '12px 24px',
borderBottom: '1px solid var(--border)',
position: 'sticky',
top: 0,
background: 'rgba(10, 10, 10, 0.85)',
backdropFilter: 'blur(12px)',
zIndex: 100,
}}
>
<div style={{ display: 'flex', alignItems: 'center', gap: 32 }}>
<Link <Link
href="/" href="/"
style={{ className="text-lg font-bold tracking-tight text-foreground no-underline"
textDecoration: 'none',
color: 'var(--foreground)',
fontSize: 18,
fontWeight: 700,
letterSpacing: '-0.02em',
}}
> >
Lux Market Lux Market
</Link> </Link>
<nav style={{ display: 'flex', gap: 4 }}> <nav className="flex gap-1">
{NAV_LINKS.map((link) => { {NAV_LINKS.map((link) => {
const isActive = link.href === '/' ? pathname === '/' : pathname.startsWith(link.href) const isActive = link.href === '/' ? pathname === '/' : pathname.startsWith(link.href)
return ( return (
<Link <Link
key={link.href} key={link.href}
href={link.href} href={link.href}
style={{ className={cn(
textDecoration: 'none', 'px-3 py-1.5 rounded-lg text-sm no-underline transition-colors',
color: isActive ? 'var(--foreground)' : 'var(--muted)', isActive
fontSize: 14, ? 'text-foreground bg-card'
padding: '6px 12px', : 'text-muted-foreground hover:text-foreground'
borderRadius: 8, )}
background: isActive ? 'var(--card)' : 'transparent',
transition: 'all 150ms ease',
}}
> >
{link.label} {link.label}
</Link> </Link>
@@ -72,9 +58,9 @@ export function Header() {
</nav> </nav>
</div> </div>
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}> <div className="flex items-center gap-3">
{/* Chain selector */} {/* Chain selector */}
<div style={{ display: 'flex', gap: 2, background: 'var(--card)', borderRadius: 8, padding: 2 }}> <div className="flex gap-0.5 bg-card rounded-lg p-0.5">
{CHAIN_IDS.map((id) => { {CHAIN_IDS.map((id) => {
const info = CHAIN_INFO[id] const info = CHAIN_INFO[id]
const isActive = chainId === id const isActive = chainId === id
@@ -83,17 +69,13 @@ export function Header() {
key={id} key={id}
onClick={() => setChainId(id)} onClick={() => setChainId(id)}
title={info.name} title={info.name}
style={{ className={cn(
padding: '6px 10px', 'px-2.5 py-1.5 text-xs font-semibold rounded-md cursor-pointer transition-colors border-none',
fontSize: 12, isActive
fontWeight: 600, ? 'text-black'
border: 'none', : 'text-muted-foreground bg-transparent hover:text-foreground'
borderRadius: 6, )}
cursor: 'pointer', style={isActive ? { background: info.color } : undefined}
background: isActive ? info.color : 'transparent',
color: isActive ? '#000' : 'var(--muted)',
transition: 'all 150ms ease',
}}
> >
{info.name} {info.name}
</button> </button>
@@ -105,19 +87,35 @@ export function Header() {
href="https://exchange.lux.network" href="https://exchange.lux.network"
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
style={{ className="px-3 py-1.5 text-[13px] text-muted-foreground no-underline border border-border rounded-lg hover:text-foreground transition-colors"
textDecoration: 'none',
color: 'var(--muted)',
fontSize: 13,
padding: '6px 12px',
border: '1px solid var(--border)',
borderRadius: 8,
}}
> >
Exchange Exchange
</a> </a>
<ConnectButton showBalance={false} chainStatus="icon" accountStatus="avatar" /> {isConnected ? (
<div className="flex items-center gap-2">
<span className="text-sm font-mono text-muted-foreground">
{address ? shortenAddress(address) : ''}
</span>
<Button
variant="ghost"
size="icon"
onClick={() => disconnect()}
title="Disconnect"
>
<LogOut className="h-4 w-4" />
</Button>
</div>
) : (
<Button
variant="connect"
size="sm"
onClick={() => connect({ connector: injected() })}
>
<Wallet className="h-4 w-4" />
Connect
</Button>
)}
</div> </div>
</header> </header>
) )
+17 -50
View File
@@ -4,6 +4,8 @@ import { useState } from 'react'
import { parseEther, type Address } from 'viem' import { parseEther, type Address } from 'viem'
import { useWriteContract, useAccount, useChainId } from 'wagmi' import { useWriteContract, useAccount, useChainId } from 'wagmi'
import { CONTRACTS, MARKET_ABI, ERC721_ABI } from '@/lib/contracts' import { CONTRACTS, MARKET_ABI, ERC721_ABI } from '@/lib/contracts'
import { Input } from '@/components/ui/input'
import { Button } from '@/components/ui/button'
interface ListingFormProps { interface ListingFormProps {
contractAddress: Address contractAddress: Address
@@ -51,43 +53,26 @@ export function ListingForm({ contractAddress, tokenId }: ListingFormProps) {
} }
return ( return (
<div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}> <div className="flex flex-col gap-3">
<h3 style={{ fontSize: 16, fontWeight: 600 }}>List for Sale</h3> <h3 className="text-base font-semibold">List for Sale</h3>
<div> <div>
<label style={{ fontSize: 12, color: 'var(--muted)', display: 'block', marginBottom: 4 }}>Price (LUX)</label> <label className="text-xs text-muted-foreground block mb-1">Price (LUX)</label>
<input <Input
type="number" type="number"
value={price} value={price}
onChange={(e) => setPrice(e.target.value)} onChange={(e) => setPrice(e.target.value)}
placeholder="0.0" placeholder="0.0"
style={{ className="h-10 text-base bg-card"
width: '100%',
padding: '10px 12px',
background: 'var(--card)',
border: '1px solid var(--border)',
borderRadius: 8,
color: 'var(--foreground)',
fontSize: 16,
outline: 'none',
}}
/> />
</div> </div>
<div> <div>
<label style={{ fontSize: 12, color: 'var(--muted)', display: 'block', marginBottom: 4 }}> <label className="text-xs text-muted-foreground block mb-1">
Duration (days) Duration (days)
</label> </label>
<select <select
value={duration} value={duration}
onChange={(e) => setDuration(e.target.value)} onChange={(e) => setDuration(e.target.value)}
style={{ className="w-full h-10 px-3 bg-card border border-input rounded-md text-foreground text-sm focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
width: '100%',
padding: '10px 12px',
background: 'var(--card)',
border: '1px solid var(--border)',
borderRadius: 8,
color: 'var(--foreground)',
fontSize: 14,
}}
> >
<option value="1">1 day</option> <option value="1">1 day</option>
<option value="3">3 days</option> <option value="3">3 days</option>
@@ -96,40 +81,22 @@ export function ListingForm({ contractAddress, tokenId }: ListingFormProps) {
<option value="30">30 days</option> <option value="30">30 days</option>
</select> </select>
</div> </div>
<div style={{ display: 'flex', gap: 8 }}> <div className="flex gap-2">
<button <Button
variant="outline"
className="flex-1"
onClick={handleApproveAndList} onClick={handleApproveAndList}
disabled={isPending || !price} disabled={isPending || !price}
style={{
flex: 1,
padding: '12px',
background: 'transparent',
border: '1px solid var(--border)',
borderRadius: 8,
color: 'var(--foreground)',
cursor: isPending ? 'wait' : 'pointer',
fontSize: 14,
}}
> >
Approve Approve
</button> </Button>
<button <Button
className="flex-1"
onClick={handleList} onClick={handleList}
disabled={isPending || !price} disabled={isPending || !price}
style={{
flex: 1,
padding: '12px',
background: 'var(--accent)',
border: 'none',
borderRadius: 8,
color: '#000',
fontWeight: 600,
cursor: isPending ? 'wait' : 'pointer',
fontSize: 14,
}}
> >
{isPending ? 'Listing...' : 'List NFT'} {isPending ? 'Listing...' : 'List NFT'}
</button> </Button>
</div> </div>
</div> </div>
) )
+13 -72
View File
@@ -5,6 +5,7 @@ import Link from 'next/link'
import { formatEther } from 'viem' import { formatEther } from 'viem'
import { getNftImageUrl, type ExplorerTokenInstance } from '@/lib/explorer' import { getNftImageUrl, type ExplorerTokenInstance } from '@/lib/explorer'
import { CHAIN_INFO } from '@/lib/chains' import { CHAIN_INFO } from '@/lib/chains'
import { Card } from '@/components/ui/card'
export interface NFTCardProps { export interface NFTCardProps {
contractAddress: string contractAddress: string
@@ -26,110 +27,50 @@ export function NFTCard({ contractAddress, tokenId, name, imageUrl, collectionNa
return ( return (
<Link <Link
href={`/nft/${chainId}/${contractAddress}/${tokenId}`} href={`/nft/${chainId}/${contractAddress}/${tokenId}`}
style={{ textDecoration: 'none', color: 'inherit' }} className="no-underline text-inherit group"
> >
<div <Card className="overflow-hidden transition-all duration-200 hover:border-muted-foreground/30 hover:-translate-y-0.5 hover:shadow-lg hover:shadow-black/30">
style={{ <div className="aspect-square relative bg-secondary">
background: 'var(--card)',
borderRadius: 12,
overflow: 'hidden',
border: '1px solid var(--border)',
transition: 'all 200ms ease',
cursor: 'pointer',
}}
onMouseEnter={(e) => {
e.currentTarget.style.borderColor = '#333'
e.currentTarget.style.transform = 'translateY(-2px)'
e.currentTarget.style.boxShadow = '0 8px 24px rgba(0,0,0,0.3)'
}}
onMouseLeave={(e) => {
e.currentTarget.style.borderColor = 'var(--border)'
e.currentTarget.style.transform = 'translateY(0)'
e.currentTarget.style.boxShadow = 'none'
}}
>
<div style={{ aspectRatio: '1', position: 'relative', background: '#111' }}>
{resolvedImage ? ( {resolvedImage ? (
<Image <Image
src={resolvedImage} src={resolvedImage}
alt={resolvedName ?? `#${tokenId}`} alt={resolvedName ?? `#${tokenId}`}
fill fill
style={{ objectFit: 'cover' }} className="object-cover"
unoptimized unoptimized
sizes="(max-width: 768px) 50vw, 25vw" sizes="(max-width: 768px) 50vw, 25vw"
/> />
) : ( ) : (
<div <div className="w-full h-full flex items-center justify-center bg-gradient-to-br from-secondary to-muted text-4xl font-bold text-muted-foreground/30">
style={{
width: '100%',
height: '100%',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
background: 'linear-gradient(135deg, #1a1a2e 0%, #16213e 100%)',
fontSize: 36,
fontWeight: 700,
color: '#333',
}}
>
#{tokenId} #{tokenId}
</div> </div>
)} )}
{/* Chain badge */} {/* Chain badge */}
{chainInfo && ( {chainInfo && (
<div <div
style={{ className="absolute top-2 right-2 px-2 py-0.5 rounded-md bg-black/60 backdrop-blur-sm text-[10px] font-semibold"
position: 'absolute', style={{ color: chainInfo.color }}
top: 8,
right: 8,
padding: '2px 8px',
borderRadius: 6,
background: 'rgba(0,0,0,0.6)',
backdropFilter: 'blur(4px)',
fontSize: 10,
fontWeight: 600,
color: chainInfo.color,
}}
> >
{chainInfo.name} {chainInfo.name}
</div> </div>
)} )}
</div> </div>
<div style={{ padding: '10px 12px' }}> <div className="px-3 py-2.5">
{resolvedCollection && ( {resolvedCollection && (
<div <div className="text-[11px] text-muted-foreground uppercase tracking-wider truncate">
style={{
fontSize: 11,
color: 'var(--muted)',
textTransform: 'uppercase',
letterSpacing: '0.05em',
whiteSpace: 'nowrap',
overflow: 'hidden',
textOverflow: 'ellipsis',
}}
>
{resolvedCollection} {resolvedCollection}
</div> </div>
)} )}
<div <div className="text-sm font-semibold mt-0.5 truncate">
style={{
fontSize: 14,
fontWeight: 600,
marginTop: 2,
whiteSpace: 'nowrap',
overflow: 'hidden',
textOverflow: 'ellipsis',
}}
>
{resolvedName ?? `#${tokenId}`} {resolvedName ?? `#${tokenId}`}
</div> </div>
{price !== undefined && price > 0n && ( {price !== undefined && price > 0n && (
<div style={{ fontSize: 13, color: 'var(--accent)', marginTop: 6, fontWeight: 500 }}> <div className="text-[13px] text-primary mt-1.5 font-medium font-mono">
{formatEther(price)} {chainInfo?.symbol ?? 'LUX'} {formatEther(price)} {chainInfo?.symbol ?? 'LUX'}
</div> </div>
)} )}
</div> </div>
</div> </Card>
</Link> </Link>
) )
} }
+12 -37
View File
@@ -4,6 +4,8 @@ import { useState } from 'react'
import { parseEther, type Address } from 'viem' import { parseEther, type Address } from 'viem'
import { useWriteContract, useChainId } from 'wagmi' import { useWriteContract, useChainId } from 'wagmi'
import { CONTRACTS, MARKET_ABI } from '@/lib/contracts' import { CONTRACTS, MARKET_ABI } from '@/lib/contracts'
import { Input } from '@/components/ui/input'
import { Button } from '@/components/ui/button'
interface OfferFormProps { interface OfferFormProps {
contractAddress: Address contractAddress: Address
@@ -39,43 +41,26 @@ export function OfferForm({ contractAddress, tokenId }: OfferFormProps) {
} }
return ( return (
<div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}> <div className="flex flex-col gap-3">
<h3 style={{ fontSize: 16, fontWeight: 600 }}>Make an Offer</h3> <h3 className="text-base font-semibold">Make an Offer</h3>
<div> <div>
<label style={{ fontSize: 12, color: 'var(--muted)', display: 'block', marginBottom: 4 }}> <label className="text-xs text-muted-foreground block mb-1">
Offer Amount (LUX) Offer Amount (LUX)
</label> </label>
<input <Input
type="number" type="number"
value={amount} value={amount}
onChange={(e) => setAmount(e.target.value)} onChange={(e) => setAmount(e.target.value)}
placeholder="0.0" placeholder="0.0"
style={{ className="h-10 text-base bg-card"
width: '100%',
padding: '10px 12px',
background: 'var(--card)',
border: '1px solid var(--border)',
borderRadius: 8,
color: 'var(--foreground)',
fontSize: 16,
outline: 'none',
}}
/> />
</div> </div>
<div> <div>
<label style={{ fontSize: 12, color: 'var(--muted)', display: 'block', marginBottom: 4 }}>Expires in</label> <label className="text-xs text-muted-foreground block mb-1">Expires in</label>
<select <select
value={duration} value={duration}
onChange={(e) => setDuration(e.target.value)} onChange={(e) => setDuration(e.target.value)}
style={{ className="w-full h-10 px-3 bg-card border border-input rounded-md text-foreground text-sm focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
width: '100%',
padding: '10px 12px',
background: 'var(--card)',
border: '1px solid var(--border)',
borderRadius: 8,
color: 'var(--foreground)',
fontSize: 14,
}}
> >
<option value="1">1 day</option> <option value="1">1 day</option>
<option value="3">3 days</option> <option value="3">3 days</option>
@@ -83,23 +68,13 @@ export function OfferForm({ contractAddress, tokenId }: OfferFormProps) {
<option value="14">14 days</option> <option value="14">14 days</option>
</select> </select>
</div> </div>
<button <Button
className="w-full"
onClick={handleOffer} onClick={handleOffer}
disabled={isPending || !amount} disabled={isPending || !amount}
style={{
width: '100%',
padding: '12px',
background: 'var(--accent)',
border: 'none',
borderRadius: 8,
color: '#000',
fontWeight: 600,
cursor: isPending ? 'wait' : 'pointer',
fontSize: 14,
}}
> >
{isPending ? 'Submitting...' : 'Make Offer'} {isPending ? 'Submitting...' : 'Make Offer'}
</button> </Button>
</div> </div>
) )
} }
+27 -67
View File
@@ -4,6 +4,8 @@ import { useState, useRef, useEffect } from 'react'
import Link from 'next/link' import Link from 'next/link'
import { useSearchCollections } from '@/hooks/useNFTData' import { useSearchCollections } from '@/hooks/useNFTData'
import { useChainContext } from '@/hooks/useChain' import { useChainContext } from '@/hooks/useChain'
import { Input } from '@/components/ui/input'
import { Search } from 'lucide-react'
export function SearchBar() { export function SearchBar() {
const [query, setQuery] = useState('') const [query, setQuery] = useState('')
@@ -27,45 +29,23 @@ export function SearchBar() {
) ?? [] ) ?? []
return ( return (
<div ref={ref} style={{ position: 'relative', width: '100%', maxWidth: 480 }}> <div ref={ref} className="relative w-full max-w-[480px]">
<input <div className="relative">
type="text" <Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
value={query} <Input
onChange={(e) => { type="text"
setQuery(e.target.value) value={query}
setIsOpen(true) onChange={(e) => {
}} setQuery(e.target.value)
onFocus={() => setIsOpen(true)} setIsOpen(true)
placeholder="Search collections, NFTs..."
style={{
width: '100%',
padding: '10px 16px',
background: 'var(--card)',
border: '1px solid var(--border)',
borderRadius: 10,
color: 'var(--foreground)',
fontSize: 14,
outline: 'none',
}}
/>
{isOpen && query.length >= 2 && nftResults.length > 0 && (
<div
style={{
position: 'absolute',
top: '100%',
left: 0,
right: 0,
marginTop: 4,
background: 'var(--card)',
border: '1px solid var(--border)',
borderRadius: 12,
padding: 8,
maxHeight: 320,
overflowY: 'auto',
zIndex: 200,
boxShadow: '0 8px 32px rgba(0,0,0,0.4)',
}} }}
> onFocus={() => setIsOpen(true)}
placeholder="Search collections, NFTs..."
className="pl-9 h-10 rounded-[10px] bg-card"
/>
</div>
{isOpen && query.length >= 2 && nftResults.length > 0 && (
<div className="absolute top-full left-0 right-0 mt-1 bg-card border border-border rounded-xl p-2 max-h-80 overflow-y-auto z-[200] shadow-xl shadow-black/40">
{nftResults.map((item: { address: string; name: string; symbol: string; type: string; icon_url: string | null }) => ( {nftResults.map((item: { address: string; name: string; symbol: string; type: string; icon_url: string | null }) => (
<Link <Link
key={item.address} key={item.address}
@@ -74,41 +54,21 @@ export function SearchBar() {
setIsOpen(false) setIsOpen(false)
setQuery('') setQuery('')
}} }}
style={{ className="flex items-center gap-3 px-3 py-2 rounded-lg no-underline text-foreground transition-colors hover:bg-secondary"
display: 'flex',
alignItems: 'center',
gap: 12,
padding: '8px 12px',
borderRadius: 8,
textDecoration: 'none',
color: 'var(--foreground)',
transition: 'background 100ms ease',
}}
onMouseEnter={(e) => (e.currentTarget.style.background = 'var(--card-hover)')}
onMouseLeave={(e) => (e.currentTarget.style.background = 'transparent')}
> >
<div <div
style={{ className="w-9 h-9 rounded-lg shrink-0 flex items-center justify-center text-sm font-bold text-muted-foreground/50 bg-cover bg-center"
width: 36, style={
height: 36, item.icon_url
borderRadius: 8, ? { backgroundImage: `url(${item.icon_url})` }
background: item.icon_url : { background: 'linear-gradient(135deg, hsl(var(--secondary)), hsl(var(--muted)))' }
? `url(${item.icon_url}) center/cover` }
: 'linear-gradient(135deg, #1a1a2e, #0f3460)',
flexShrink: 0,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
fontSize: 14,
fontWeight: 700,
color: '#444',
}}
> >
{!item.icon_url && (item.symbol?.charAt(0) ?? '?')} {!item.icon_url && (item.symbol?.charAt(0) ?? '?')}
</div> </div>
<div> <div>
<div style={{ fontSize: 14, fontWeight: 500 }}>{item.name}</div> <div className="text-sm font-medium">{item.name}</div>
<div style={{ fontSize: 11, color: 'var(--muted)' }}>{item.type}</div> <div className="text-[11px] text-muted-foreground">{item.type}</div>
</div> </div>
</Link> </Link>
))} ))}
+23 -51
View File
@@ -1,6 +1,8 @@
'use client' 'use client'
import { useState } from 'react' import { useState } from 'react'
import { ChevronUp, ChevronDown } from 'lucide-react'
import { cn } from '@/lib/utils'
import type { NFTTrait } from '@/lib/explorer' import type { NFTTrait } from '@/lib/explorer'
interface TraitGroup { interface TraitGroup {
@@ -50,28 +52,13 @@ export function TraitFilter({ instances, selectedTraits, onTraitToggle, onClear
if (traitGroups.length === 0) return null if (traitGroups.length === 0) return null
return ( return (
<div <div className="bg-card rounded-xl border border-border p-4 w-60 shrink-0">
style={{ <div className="flex justify-between items-center mb-3">
background: 'var(--card)', <div className="text-sm font-semibold">Traits</div>
borderRadius: 12,
border: '1px solid var(--border)',
padding: 16,
width: 240,
flexShrink: 0,
}}
>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 12 }}>
<div style={{ fontSize: 14, fontWeight: 600 }}>Traits</div>
{hasSelected && ( {hasSelected && (
<button <button
onClick={onClear} onClick={onClear}
style={{ className="bg-transparent border-none text-primary text-xs cursor-pointer hover:underline"
background: 'none',
border: 'none',
color: 'var(--accent)',
fontSize: 12,
cursor: 'pointer',
}}
> >
Clear all Clear all
</button> </button>
@@ -84,7 +71,7 @@ export function TraitFilter({ instances, selectedTraits, onTraitToggle, onClear
const activeCount = selected?.size ?? 0 const activeCount = selected?.size ?? 0
return ( return (
<div key={group.trait_type} style={{ marginBottom: 4 }}> <div key={group.trait_type} className="mb-1">
<button <button
onClick={() => { onClick={() => {
const next = new Set(expandedGroups) const next = new Set(expandedGroups)
@@ -92,54 +79,39 @@ export function TraitFilter({ instances, selectedTraits, onTraitToggle, onClear
else next.add(group.trait_type) else next.add(group.trait_type)
setExpandedGroups(next) setExpandedGroups(next)
}} }}
style={{ className="w-full flex justify-between items-center py-2 bg-transparent border-none text-foreground cursor-pointer text-[13px] font-medium"
width: '100%',
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
padding: '8px 0',
background: 'none',
border: 'none',
color: 'var(--foreground)',
cursor: 'pointer',
fontSize: 13,
fontWeight: 500,
}}
> >
<span> <span>
{group.trait_type} {group.trait_type}
{activeCount > 0 && ( {activeCount > 0 && (
<span style={{ marginLeft: 6, color: 'var(--accent)', fontSize: 11 }}>({activeCount})</span> <span className="ml-1.5 text-primary text-[11px]">({activeCount})</span>
)} )}
</span> </span>
<span style={{ fontSize: 10, color: 'var(--muted)' }}>{isExpanded ? '\u25B2' : '\u25BC'}</span> {isExpanded ? (
<ChevronUp className="h-3 w-3 text-muted-foreground" />
) : (
<ChevronDown className="h-3 w-3 text-muted-foreground" />
)}
</button> </button>
{isExpanded && ( {isExpanded && (
<div style={{ paddingLeft: 4, paddingBottom: 8, display: 'flex', flexDirection: 'column', gap: 2 }}> <div className="pl-1 pb-2 flex flex-col gap-0.5">
{group.values.slice(0, 20).map((v) => { {group.values.slice(0, 20).map((v) => {
const isActive = selected?.has(v.value) ?? false const isActive = selected?.has(v.value) ?? false
return ( return (
<button <button
key={v.value} key={v.value}
onClick={() => onTraitToggle(group.trait_type, v.value)} onClick={() => onTraitToggle(group.trait_type, v.value)}
style={{ className={cn(
display: 'flex', 'flex justify-between items-center px-2 py-1 rounded-md cursor-pointer text-xs text-left border transition-colors',
justifyContent: 'space-between', isActive
alignItems: 'center', ? 'bg-primary/10 border-primary/50 text-foreground'
padding: '4px 8px', : 'bg-transparent border-transparent text-muted-foreground hover:text-foreground hover:bg-secondary'
borderRadius: 6, )}
background: isActive ? 'rgba(232,232,232,0.1)' : 'transparent',
border: isActive ? '1px solid var(--accent)' : '1px solid transparent',
color: isActive ? 'var(--foreground)' : 'var(--muted)',
cursor: 'pointer',
fontSize: 12,
textAlign: 'left',
}}
> >
<span style={{ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}> <span className="overflow-hidden text-ellipsis whitespace-nowrap">
{v.value} {v.value}
</span> </span>
<span style={{ fontSize: 11, flexShrink: 0, marginLeft: 8 }}>{v.count}</span> <span className="text-[11px] shrink-0 ml-2 font-mono">{v.count}</span>
</button> </button>
) )
})} })}
+61
View File
@@ -0,0 +1,61 @@
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const buttonVariants = cva(
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
{
variants: {
variant: {
default:
"bg-primary text-primary-foreground shadow hover:bg-primary/90",
destructive:
"bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90",
outline:
"border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground",
secondary:
"bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground",
link: "text-primary underline-offset-4 hover:underline",
// DEX-specific variants
swap: "bg-gradient-to-r from-primary to-accent text-primary-foreground shadow-lg hover:opacity-90",
connect:
"bg-accent text-accent-foreground shadow-sm hover:bg-accent/80 border border-border",
},
size: {
default: "h-9 px-4 py-2",
sm: "h-8 rounded-md px-3 text-xs",
lg: "h-10 rounded-md px-8",
xl: "h-12 rounded-xl px-8 text-base",
icon: "h-9 w-9",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
)
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
asChild?: boolean
}
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button"
return (
<Comp
className={cn(buttonVariants({ variant, size, className }))}
ref={ref}
{...props}
/>
)
}
)
Button.displayName = "Button"
export { Button, buttonVariants }
+75
View File
@@ -0,0 +1,75 @@
import * as React from "react"
import { cn } from "@/lib/utils"
const Card = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn(
"rounded-xl border bg-card text-card-foreground shadow",
className
)}
{...props}
/>
))
Card.displayName = "Card"
const CardHeader = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("flex flex-col space-y-1.5 p-6", className)}
{...props}
/>
))
CardHeader.displayName = "CardHeader"
const CardTitle = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("font-semibold leading-none tracking-tight", className)}
{...props}
/>
))
CardTitle.displayName = "CardTitle"
const CardDescription = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
))
CardDescription.displayName = "CardDescription"
const CardContent = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
))
CardContent.displayName = "CardContent"
const CardFooter = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("flex items-center p-6 pt-0", className)}
{...props}
/>
))
CardFooter.displayName = "CardFooter"
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
+121
View File
@@ -0,0 +1,121 @@
"use client"
import * as React from "react"
import * as DialogPrimitive from "@radix-ui/react-dialog"
import { X } from "lucide-react"
import { cn } from "@/lib/utils"
const Dialog = DialogPrimitive.Root
const DialogTrigger = DialogPrimitive.Trigger
const DialogPortal = DialogPrimitive.Portal
const DialogClose = DialogPrimitive.Close
const DialogOverlay = React.forwardRef<
React.ComponentRef<typeof DialogPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Overlay
ref={ref}
className={cn(
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
className
)}
{...props}
/>
))
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
const DialogContent = React.forwardRef<
React.ComponentRef<typeof DialogPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
>(({ className, children, ...props }, ref) => (
<DialogPortal>
<DialogOverlay />
<DialogPrimitive.Content
ref={ref}
className={cn(
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
className
)}
{...props}
>
{children}
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
<X className="h-4 w-4" />
<span className="sr-only">Close</span>
</DialogPrimitive.Close>
</DialogPrimitive.Content>
</DialogPortal>
))
DialogContent.displayName = DialogPrimitive.Content.displayName
const DialogHeader = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col space-y-1.5 text-center sm:text-left",
className
)}
{...props}
/>
)
DialogHeader.displayName = "DialogHeader"
const DialogFooter = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
className
)}
{...props}
/>
)
DialogFooter.displayName = "DialogFooter"
const DialogTitle = React.forwardRef<
React.ComponentRef<typeof DialogPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Title
ref={ref}
className={cn(
"text-lg font-semibold leading-none tracking-tight",
className
)}
{...props}
/>
))
DialogTitle.displayName = DialogPrimitive.Title.displayName
const DialogDescription = React.forwardRef<
React.ComponentRef<typeof DialogPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Description
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
))
DialogDescription.displayName = DialogPrimitive.Description.displayName
export {
Dialog,
DialogPortal,
DialogOverlay,
DialogClose,
DialogTrigger,
DialogContent,
DialogHeader,
DialogFooter,
DialogTitle,
DialogDescription,
}
+199
View File
@@ -0,0 +1,199 @@
"use client"
import * as React from "react"
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
import { Check, ChevronRight, Circle } from "lucide-react"
import { cn } from "@/lib/utils"
const DropdownMenu = DropdownMenuPrimitive.Root
const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger
const DropdownMenuGroup = DropdownMenuPrimitive.Group
const DropdownMenuPortal = DropdownMenuPrimitive.Portal
const DropdownMenuSub = DropdownMenuPrimitive.Sub
const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup
const DropdownMenuSubTrigger = React.forwardRef<
React.ComponentRef<typeof DropdownMenuPrimitive.SubTrigger>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
inset?: boolean
}
>(({ className, inset, children, ...props }, ref) => (
<DropdownMenuPrimitive.SubTrigger
ref={ref}
className={cn(
"flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent",
inset && "pl-8",
className
)}
{...props}
>
{children}
<ChevronRight className="ml-auto h-4 w-4" />
</DropdownMenuPrimitive.SubTrigger>
))
DropdownMenuSubTrigger.displayName =
DropdownMenuPrimitive.SubTrigger.displayName
const DropdownMenuSubContent = React.forwardRef<
React.ComponentRef<typeof DropdownMenuPrimitive.SubContent>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>
>(({ className, ...props }, ref) => (
<DropdownMenuPrimitive.SubContent
ref={ref}
className={cn(
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className
)}
{...props}
/>
))
DropdownMenuSubContent.displayName =
DropdownMenuPrimitive.SubContent.displayName
const DropdownMenuContent = React.forwardRef<
React.ComponentRef<typeof DropdownMenuPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
>(({ className, sideOffset = 4, ...props }, ref) => (
<DropdownMenuPrimitive.Portal>
<DropdownMenuPrimitive.Content
ref={ref}
sideOffset={sideOffset}
className={cn(
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className
)}
{...props}
/>
</DropdownMenuPrimitive.Portal>
))
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName
const DropdownMenuItem = React.forwardRef<
React.ComponentRef<typeof DropdownMenuPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
inset?: boolean
}
>(({ className, inset, ...props }, ref) => (
<DropdownMenuPrimitive.Item
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
inset && "pl-8",
className
)}
{...props}
/>
))
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName
const DropdownMenuCheckboxItem = React.forwardRef<
React.ComponentRef<typeof DropdownMenuPrimitive.CheckboxItem>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem>
>(({ className, children, checked, ...props }, ref) => (
<DropdownMenuPrimitive.CheckboxItem
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className
)}
checked={checked}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<Check className="h-4 w-4" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.CheckboxItem>
))
DropdownMenuCheckboxItem.displayName =
DropdownMenuPrimitive.CheckboxItem.displayName
const DropdownMenuRadioItem = React.forwardRef<
React.ComponentRef<typeof DropdownMenuPrimitive.RadioItem>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem>
>(({ className, children, ...props }, ref) => (
<DropdownMenuPrimitive.RadioItem
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className
)}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<Circle className="h-2 w-2 fill-current" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.RadioItem>
))
DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName
const DropdownMenuLabel = React.forwardRef<
React.ComponentRef<typeof DropdownMenuPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
inset?: boolean
}
>(({ className, inset, ...props }, ref) => (
<DropdownMenuPrimitive.Label
ref={ref}
className={cn(
"px-2 py-1.5 text-sm font-semibold",
inset && "pl-8",
className
)}
{...props}
/>
))
DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName
const DropdownMenuSeparator = React.forwardRef<
React.ComponentRef<typeof DropdownMenuPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>
>(({ className, ...props }, ref) => (
<DropdownMenuPrimitive.Separator
ref={ref}
className={cn("-mx-1 my-1 h-px bg-muted", className)}
{...props}
/>
))
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName
const DropdownMenuShortcut = ({
className,
...props
}: React.HTMLAttributes<HTMLSpanElement>) => {
return (
<span
className={cn("ml-auto text-xs tracking-widest opacity-60", className)}
{...props}
/>
)
}
DropdownMenuShortcut.displayName = "DropdownMenuShortcut"
export {
DropdownMenu,
DropdownMenuTrigger,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuCheckboxItem,
DropdownMenuRadioItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuGroup,
DropdownMenuPortal,
DropdownMenuSub,
DropdownMenuSubContent,
DropdownMenuSubTrigger,
DropdownMenuRadioGroup,
}
+24
View File
@@ -0,0 +1,24 @@
import * as React from "react"
import { cn } from "@/lib/utils"
export interface InputProps
extends React.InputHTMLAttributes<HTMLInputElement> {}
const Input = React.forwardRef<HTMLInputElement, InputProps>(
({ className, type, ...props }, ref) => {
return (
<input
type={type}
className={cn(
"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
className
)}
ref={ref}
{...props}
/>
)
}
)
Input.displayName = "Input"
export { Input }
+91
View File
@@ -0,0 +1,91 @@
import { clsx, type ClassValue } from "clsx"
import { twMerge } from "tailwind-merge"
/**
* Merge Tailwind CSS classes with clsx
*/
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}
/**
* Format a date to a readable string
*/
export function formatDate(input: string | number | Date): string {
const date = new Date(input)
return date.toLocaleDateString("en-US", {
month: "long",
day: "numeric",
year: "numeric",
})
}
/**
* Format a number with commas
*/
export function formatNumber(num: number, decimals = 2): string {
return num.toLocaleString("en-US", {
minimumFractionDigits: decimals,
maximumFractionDigits: decimals,
})
}
/**
* Shorten an Ethereum address
*/
export function shortenAddress(address: string, chars = 4): string {
if (!address) return ""
return `${address.slice(0, chars + 2)}...${address.slice(-chars)}`
}
/**
* Format a currency amount
*/
export function formatCurrency(
amount: number,
currency = "USD",
decimals = 2
): string {
return new Intl.NumberFormat("en-US", {
style: "currency",
currency,
minimumFractionDigits: decimals,
maximumFractionDigits: decimals,
}).format(amount)
}
/**
* Format a percentage
*/
export function formatPercent(value: number, decimals = 2): string {
return `${value >= 0 ? "+" : ""}${value.toFixed(decimals)}%`
}
/**
* Check if a value is a valid Ethereum address
*/
export function isAddress(value: string): boolean {
return /^0x[a-fA-F0-9]{40}$/.test(value)
}
/**
* Debounce a function
*/
export function debounce<T extends (...args: unknown[]) => unknown>(
fn: T,
delay: number
): (...args: Parameters<T>) => void {
let timeoutId: ReturnType<typeof setTimeout>
return (...args: Parameters<T>) => {
clearTimeout(timeoutId)
timeoutId = setTimeout(() => fn(...args), delay)
}
}
/**
* Get the absolute URL for a path
*/
export function absoluteUrl(path: string): string {
const baseUrl = process.env.NEXT_PUBLIC_APP_URL || "https://lux.market"
return `${baseUrl}${path}`
}
+7 -6
View File
@@ -1,9 +1,10 @@
import { getDefaultConfig } from '@rainbow-me/rainbowkit' import { createConfig, http } from 'wagmi'
import { luxMainnet, zooMainnet, hanzoMainnet, spcMainnet, parsMainnet } from './chains' import { injected } from 'wagmi/connectors'
import { supportedChains } from './chains'
export const config = getDefaultConfig({ export const config = createConfig({
appName: 'Lux Market', chains: supportedChains,
projectId: 'a739fdd0c00cbb93bc06df1e27a83d63', connectors: [injected()],
chains: [luxMainnet, zooMainnet, hanzoMainnet, spcMainnet, parsMainnet], transports: Object.fromEntries(supportedChains.map((c) => [c.id, http()])) as any,
ssr: true, ssr: true,
}) })
File diff suppressed because one or more lines are too long