makestx-frontend 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/next.config.js +9 -0
- package/package.json +30 -0
- package/postcss.config.js +6 -0
- package/src/components/NFTCard.tsx +64 -0
- package/src/components/Navbar.tsx +137 -0
- package/src/hooks/useContract.ts +74 -0
- package/src/hooks/useWallet.tsx +75 -0
- package/src/pages/_app.tsx +11 -0
- package/src/pages/_document.tsx +18 -0
- package/src/pages/auto-mint.tsx +286 -0
- package/src/pages/explore.tsx +107 -0
- package/src/pages/index.tsx +241 -0
- package/src/pages/mint.tsx +182 -0
- package/src/pages/multi-mint.tsx +358 -0
- package/src/pages/my-nfts.tsx +55 -0
- package/src/styles/globals.css +264 -0
- package/tailwind.config.js +33 -0
- package/tsconfig.json +21 -0
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
import Head from 'next/head';
|
|
2
|
+
import { useState, useRef } from 'react';
|
|
3
|
+
import {
|
|
4
|
+
makeContractCall,
|
|
5
|
+
broadcastTransaction,
|
|
6
|
+
AnchorMode,
|
|
7
|
+
PostConditionMode,
|
|
8
|
+
principalCV,
|
|
9
|
+
} from '@stacks/transactions';
|
|
10
|
+
import { generateWallet, getStxAddress } from '@stacks/wallet-sdk';
|
|
11
|
+
import { TransactionVersion } from '@stacks/transactions';
|
|
12
|
+
import { network, CONTRACT_ADDRESS, CONTRACT_NAME } from '@/hooks/useWallet';
|
|
13
|
+
import Navbar from '@/components/Navbar';
|
|
14
|
+
import { Terminal, Play, Square, AlertTriangle, CheckCircle2, XCircle, Clock } from 'lucide-react';
|
|
15
|
+
|
|
16
|
+
interface LogEntry {
|
|
17
|
+
time: string;
|
|
18
|
+
msg: string;
|
|
19
|
+
type: 'ok' | 'err' | 'info';
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export default function AutoMint() {
|
|
23
|
+
const [mnemonic, setMnemonic] = useState('');
|
|
24
|
+
const [totalTx, setTotalTx] = useState(100);
|
|
25
|
+
const [delay, setDelay] = useState(200);
|
|
26
|
+
const [recipient, setRecipient] = useState('');
|
|
27
|
+
const [running, setRunning] = useState(false);
|
|
28
|
+
const [logs, setLogs] = useState<LogEntry[]>([]);
|
|
29
|
+
const [sent, setSent] = useState(0);
|
|
30
|
+
const [ok, setOk] = useState(0);
|
|
31
|
+
const [err, setErr] = useState(0);
|
|
32
|
+
const [eta, setEta] = useState('—');
|
|
33
|
+
const [progress, setProgress] = useState(0);
|
|
34
|
+
const stopFlag = useRef(false);
|
|
35
|
+
const logRef = useRef<HTMLDivElement>(null);
|
|
36
|
+
|
|
37
|
+
const addLog = (msg: string, type: LogEntry['type'] = 'info') => {
|
|
38
|
+
const time = new Date().toLocaleTimeString('tr-TR');
|
|
39
|
+
setLogs(prev => [...prev, { time, msg, type }].slice(-300));
|
|
40
|
+
setTimeout(() => {
|
|
41
|
+
if (logRef.current) logRef.current.scrollTop = logRef.current.scrollHeight;
|
|
42
|
+
}, 10);
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const fmtETA = (ms: number) => {
|
|
46
|
+
if (!ms || ms < 0) return '—';
|
|
47
|
+
const s = Math.round(ms / 1000);
|
|
48
|
+
if (s < 60) return `${s}s`;
|
|
49
|
+
const m = Math.floor(s / 60);
|
|
50
|
+
return m < 60 ? `${m}m ${s % 60}s` : `${Math.floor(m / 60)}h ${m % 60}m`;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const sleep = (ms: number) => new Promise(r => setTimeout(r, ms));
|
|
54
|
+
|
|
55
|
+
const getNonce = async (address: string) => {
|
|
56
|
+
const res = await fetch(`https://api.mainnet.hiro.so/v2/accounts/${address}?unanchored=true`);
|
|
57
|
+
if (!res.ok) throw new Error('HTTP ' + res.status);
|
|
58
|
+
return parseInt((await res.json()).nonce);
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const deriveAccount = async (mn: string) => {
|
|
62
|
+
const words = mn.trim().split(/\s+/);
|
|
63
|
+
if (words.length !== 12 && words.length !== 24) throw new Error('12 or 24 words required');
|
|
64
|
+
const wallet = await generateWallet({ secretKey: mn.trim(), password: '' });
|
|
65
|
+
const account = wallet.accounts[0];
|
|
66
|
+
return {
|
|
67
|
+
privateKey: account.stxPrivateKey,
|
|
68
|
+
address: getStxAddress({ account, transactionVersion: TransactionVersion.Mainnet }),
|
|
69
|
+
};
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
const mintOne = async (privateKey: string, to: string, nonce: number) => {
|
|
73
|
+
const tx = await makeContractCall({
|
|
74
|
+
contractAddress: CONTRACT_ADDRESS,
|
|
75
|
+
contractName: CONTRACT_NAME,
|
|
76
|
+
functionName: 'mint',
|
|
77
|
+
functionArgs: [principalCV(to)],
|
|
78
|
+
senderKey: privateKey,
|
|
79
|
+
network,
|
|
80
|
+
nonce: BigInt(nonce),
|
|
81
|
+
anchorMode: AnchorMode.Any,
|
|
82
|
+
postConditionMode: PostConditionMode.Allow,
|
|
83
|
+
fee: BigInt(2000),
|
|
84
|
+
});
|
|
85
|
+
const result = await broadcastTransaction(tx, network);
|
|
86
|
+
if (result.error) throw new Error(result.error + (result.reason ? ` (${result.reason})` : ''));
|
|
87
|
+
return result.txid;
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
const startMint = async () => {
|
|
91
|
+
if (running) return;
|
|
92
|
+
if (!mnemonic.trim()) { addLog('ERROR: Seed phrase is empty', 'err'); return; }
|
|
93
|
+
|
|
94
|
+
stopFlag.current = false;
|
|
95
|
+
setRunning(true);
|
|
96
|
+
setLogs([]);
|
|
97
|
+
setSent(0); setOk(0); setErr(0); setProgress(0); setEta('—');
|
|
98
|
+
|
|
99
|
+
let privateKey: string, address: string;
|
|
100
|
+
try {
|
|
101
|
+
({ privateKey, address } = await deriveAccount(mnemonic));
|
|
102
|
+
addLog(`WALLET: ${address}`, 'ok');
|
|
103
|
+
} catch (e: any) {
|
|
104
|
+
addLog('ERR: ' + e.message, 'err');
|
|
105
|
+
setRunning(false); return;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const to = recipient.trim() || address;
|
|
109
|
+
addLog(`RECIPIENT: ${to}`, 'info');
|
|
110
|
+
|
|
111
|
+
let nonce: number;
|
|
112
|
+
try {
|
|
113
|
+
nonce = await getNonce(address);
|
|
114
|
+
addLog(`NONCE: ${nonce} | TOTAL TX: ${totalTx}`, 'info');
|
|
115
|
+
} catch (e: any) {
|
|
116
|
+
addLog('NONCE_ERR: ' + e.message, 'err');
|
|
117
|
+
setRunning(false); return;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
let okCount = 0, errCount = 0;
|
|
121
|
+
const startTime = Date.now();
|
|
122
|
+
|
|
123
|
+
for (let i = 0; i < totalTx; i++) {
|
|
124
|
+
if (stopFlag.current) { addLog('// PROCESS TERMINATED BY USER', 'info'); break; }
|
|
125
|
+
|
|
126
|
+
try {
|
|
127
|
+
const txid = await mintOne(privateKey, to, nonce + i);
|
|
128
|
+
okCount++;
|
|
129
|
+
addLog(`TX_${String(i + 1).padStart(4,'0')} SUCCESS · ${txid.slice(0, 20)}...`, 'ok');
|
|
130
|
+
} catch (e: any) {
|
|
131
|
+
errCount++;
|
|
132
|
+
addLog(`TX_${String(i + 1).padStart(4,'0')} FAILED · ${e.message}`, 'err');
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const s = i + 1;
|
|
136
|
+
const elapsed = Date.now() - startTime;
|
|
137
|
+
const etaMs = (elapsed / s) * (totalTx - s);
|
|
138
|
+
setSent(s); setOk(okCount); setErr(errCount);
|
|
139
|
+
setProgress(Math.round((s / totalTx) * 100));
|
|
140
|
+
setEta(fmtETA(etaMs));
|
|
141
|
+
|
|
142
|
+
if (i < totalTx - 1 && !stopFlag.current) await sleep(delay);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const total = Date.now() - startTime;
|
|
146
|
+
addLog(`// COMPLETE · ${okCount} OK · ${errCount} FAILED · ${fmtETA(total)}`, okCount > 0 ? 'ok' : 'err');
|
|
147
|
+
setRunning(false);
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
return (
|
|
151
|
+
<>
|
|
152
|
+
<Head>
|
|
153
|
+
<title>Auto Mint — MakeSTX</title>
|
|
154
|
+
</Head>
|
|
155
|
+
<div className="min-h-screen bg-ms-bg grid-bg">
|
|
156
|
+
<Navbar />
|
|
157
|
+
|
|
158
|
+
<main className="pt-28 pb-16 px-4 max-w-xl mx-auto">
|
|
159
|
+
|
|
160
|
+
{/* Header */}
|
|
161
|
+
<div className="mb-8">
|
|
162
|
+
<p className="font-mono text-xs text-ms-muted tracking-widest mb-1">// auto_mint_v1.js</p>
|
|
163
|
+
<h1 className="font-display font-black text-3xl text-ms-text tracking-tight">
|
|
164
|
+
AUTO <span className="text-ms-cyan">MINT</span>
|
|
165
|
+
</h1>
|
|
166
|
+
<p className="font-mono text-xs text-ms-muted mt-1">
|
|
167
|
+
{CONTRACT_ADDRESS.slice(0, 10)}...{CONTRACT_ADDRESS.slice(-6)}.{CONTRACT_NAME}
|
|
168
|
+
</p>
|
|
169
|
+
</div>
|
|
170
|
+
|
|
171
|
+
{/* Warning */}
|
|
172
|
+
<div className="flex gap-2 p-3 mb-5 bg-yellow-500/5 border border-yellow-500/30">
|
|
173
|
+
<AlertTriangle size={14} className="text-yellow-400 shrink-0 mt-0.5" />
|
|
174
|
+
<p className="text-xs font-mono text-yellow-400/80">
|
|
175
|
+
SEED PHRASE NEVER LEAVES YOUR BROWSER. USE A DEDICATED MINT WALLET.
|
|
176
|
+
</p>
|
|
177
|
+
</div>
|
|
178
|
+
|
|
179
|
+
{/* Seed phrase */}
|
|
180
|
+
<div className="card-dark p-4 mb-3">
|
|
181
|
+
<label className="block text-xs font-mono text-ms-muted tracking-widest mb-2">
|
|
182
|
+
// SEED_PHRASE (12 OR 24 WORDS)
|
|
183
|
+
</label>
|
|
184
|
+
<input
|
|
185
|
+
type="password"
|
|
186
|
+
value={mnemonic}
|
|
187
|
+
onChange={e => setMnemonic(e.target.value)}
|
|
188
|
+
placeholder="word1 word2 word3 ..."
|
|
189
|
+
disabled={running}
|
|
190
|
+
className="w-full bg-ms-bg border border-ms-border px-3 py-2 text-xs font-mono text-ms-text placeholder-ms-muted focus:outline-none focus:border-ms-cyan disabled:opacity-40 tracking-wider"
|
|
191
|
+
/>
|
|
192
|
+
</div>
|
|
193
|
+
|
|
194
|
+
{/* Config */}
|
|
195
|
+
<div className="card-dark p-4 mb-4">
|
|
196
|
+
<div className="grid grid-cols-2 gap-3 mb-3">
|
|
197
|
+
<div>
|
|
198
|
+
<label className="block text-xs font-mono text-ms-muted tracking-widest mb-2">TOTAL_TX</label>
|
|
199
|
+
<input type="number" value={totalTx} onChange={e => setTotalTx(parseInt(e.target.value))}
|
|
200
|
+
min={1} max={10000} disabled={running}
|
|
201
|
+
className="w-full bg-ms-bg border border-ms-border px-3 py-2 text-xs font-mono text-ms-text focus:outline-none focus:border-ms-cyan disabled:opacity-40" />
|
|
202
|
+
</div>
|
|
203
|
+
<div>
|
|
204
|
+
<label className="block text-xs font-mono text-ms-muted tracking-widest mb-2">DELAY_MS</label>
|
|
205
|
+
<input type="number" value={delay} onChange={e => setDelay(parseInt(e.target.value))}
|
|
206
|
+
min={50} disabled={running}
|
|
207
|
+
className="w-full bg-ms-bg border border-ms-border px-3 py-2 text-xs font-mono text-ms-text focus:outline-none focus:border-ms-cyan disabled:opacity-40" />
|
|
208
|
+
</div>
|
|
209
|
+
</div>
|
|
210
|
+
<div>
|
|
211
|
+
<label className="block text-xs font-mono text-ms-muted tracking-widest mb-2">RECIPIENT (OPTIONAL)</label>
|
|
212
|
+
<input type="text" value={recipient} onChange={e => setRecipient(e.target.value)}
|
|
213
|
+
placeholder="SP... (default: sender)" disabled={running}
|
|
214
|
+
className="w-full bg-ms-bg border border-ms-border px-3 py-2 text-xs font-mono text-ms-text placeholder-ms-muted focus:outline-none focus:border-ms-cyan disabled:opacity-40" />
|
|
215
|
+
</div>
|
|
216
|
+
</div>
|
|
217
|
+
|
|
218
|
+
{/* Stats */}
|
|
219
|
+
<div className="grid grid-cols-4 gap-2 mb-3">
|
|
220
|
+
{[
|
|
221
|
+
{ label: 'SENT', value: sent, icon: <Clock size={11} />, color: 'text-ms-cyan' },
|
|
222
|
+
{ label: 'OK', value: ok, icon: <CheckCircle2 size={11} />, color: 'text-green-400' },
|
|
223
|
+
{ label: 'FAIL', value: err, icon: <XCircle size={11} />, color: 'text-red-400' },
|
|
224
|
+
{ label: 'ETA', value: eta, icon: <Terminal size={11} />, color: 'text-ms-muted' },
|
|
225
|
+
].map(({ label, value, icon, color }) => (
|
|
226
|
+
<div key={label} className="card-dark p-3">
|
|
227
|
+
<div className={`flex items-center gap-1 ${color} mb-1`}>{icon}<span className="font-mono text-xs text-ms-muted">{label}</span></div>
|
|
228
|
+
<div className={`text-lg font-display font-bold ${color}`}>{value}</div>
|
|
229
|
+
</div>
|
|
230
|
+
))}
|
|
231
|
+
</div>
|
|
232
|
+
|
|
233
|
+
{/* Progress */}
|
|
234
|
+
<div className="mb-4">
|
|
235
|
+
<div className="h-px bg-ms-surface overflow-hidden mb-1">
|
|
236
|
+
<div
|
|
237
|
+
className="h-full bg-ms-cyan transition-all duration-200"
|
|
238
|
+
style={{ width: `${progress}%`, boxShadow: '0 0 8px #00f5ff' }}
|
|
239
|
+
/>
|
|
240
|
+
</div>
|
|
241
|
+
<div className="flex items-center justify-between font-mono text-xs text-ms-muted">
|
|
242
|
+
<span>{sent} / {totalTx} TX</span>
|
|
243
|
+
<span>{progress}%</span>
|
|
244
|
+
</div>
|
|
245
|
+
</div>
|
|
246
|
+
|
|
247
|
+
{/* Log terminal */}
|
|
248
|
+
<div
|
|
249
|
+
ref={logRef}
|
|
250
|
+
className="h-52 overflow-y-auto bg-ms-bg border border-ms-border p-3 font-mono text-xs mb-4"
|
|
251
|
+
style={{ fontFamily: "'Share Tech Mono', monospace" }}
|
|
252
|
+
>
|
|
253
|
+
<div className="text-ms-muted mb-2">MakeSTX Auto-Mint Terminal v1.0</div>
|
|
254
|
+
<div className="text-ms-muted mb-2 border-b border-ms-border pb-2">{'>'} Awaiting input...</div>
|
|
255
|
+
{logs.map((l, i) => (
|
|
256
|
+
<div key={i} className={
|
|
257
|
+
l.type === 'ok' ? 'text-green-400' :
|
|
258
|
+
l.type === 'err' ? 'text-red-400' :
|
|
259
|
+
'text-ms-muted'
|
|
260
|
+
}>
|
|
261
|
+
<span className="text-ms-border">[{l.time}]</span> {l.msg}
|
|
262
|
+
</div>
|
|
263
|
+
))}
|
|
264
|
+
{running && <div className="text-ms-cyan animate-pulse mt-1">▋</div>}
|
|
265
|
+
</div>
|
|
266
|
+
|
|
267
|
+
{/* Button */}
|
|
268
|
+
{!running ? (
|
|
269
|
+
<button onClick={startMint} className="btn-primary w-full py-4 text-sm flex items-center justify-center gap-2">
|
|
270
|
+
<Play size={14} />
|
|
271
|
+
<span>START AUTO MINT</span>
|
|
272
|
+
</button>
|
|
273
|
+
) : (
|
|
274
|
+
<button
|
|
275
|
+
onClick={() => { stopFlag.current = true; }}
|
|
276
|
+
className="w-full py-4 border border-red-500 text-red-400 font-mono text-xs tracking-widest hover:bg-red-500/10 transition-all flex items-center justify-center gap-2"
|
|
277
|
+
>
|
|
278
|
+
<Square size={12} fill="currentColor" />
|
|
279
|
+
TERMINATE PROCESS
|
|
280
|
+
</button>
|
|
281
|
+
)}
|
|
282
|
+
</main>
|
|
283
|
+
</div>
|
|
284
|
+
</>
|
|
285
|
+
);
|
|
286
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import Head from 'next/head';
|
|
2
|
+
import { useState } from 'react';
|
|
3
|
+
import Navbar from '@/components/Navbar';
|
|
4
|
+
import NFTCard from '@/components/NFTCard';
|
|
5
|
+
import { Search, SlidersHorizontal } from 'lucide-react';
|
|
6
|
+
|
|
7
|
+
const MOCK_NFTS = Array.from({ length: 24 }, (_, i) => ({
|
|
8
|
+
tokenId: i + 1,
|
|
9
|
+
name: `MakeSTX #${String(i + 1).padStart(4, '0')}`,
|
|
10
|
+
image: `https://api.dicebear.com/8.x/pixel-art/svg?seed=mkstx${i + 1}&backgroundColor=030308&colorful=true`,
|
|
11
|
+
owner: `SP${Math.random().toString(36).slice(2, 10).toUpperCase()}ABCDEF`,
|
|
12
|
+
price: Math.random() > 0.4 ? Math.floor((Math.random() * 100 + 1) * 1_000_000) : undefined,
|
|
13
|
+
isListed: Math.random() > 0.4,
|
|
14
|
+
}));
|
|
15
|
+
|
|
16
|
+
const SORT_OPTIONS = ['RECENTLY LISTED', 'PRICE: LOW → HIGH', 'PRICE: HIGH → LOW', 'TOKEN ID'];
|
|
17
|
+
|
|
18
|
+
export default function Explore() {
|
|
19
|
+
const [search, setSearch] = useState('');
|
|
20
|
+
const [sort, setSort] = useState('RECENTLY LISTED');
|
|
21
|
+
const [filterListed, setFilterListed] = useState(false);
|
|
22
|
+
|
|
23
|
+
const filtered = MOCK_NFTS
|
|
24
|
+
.filter(n => {
|
|
25
|
+
if (filterListed && !n.isListed) return false;
|
|
26
|
+
if (search && !n.name.toLowerCase().includes(search.toLowerCase())) return false;
|
|
27
|
+
return true;
|
|
28
|
+
})
|
|
29
|
+
.sort((a, b) => {
|
|
30
|
+
if (sort === 'PRICE: LOW → HIGH') return (a.price || 0) - (b.price || 0);
|
|
31
|
+
if (sort === 'PRICE: HIGH → LOW') return (b.price || 0) - (a.price || 0);
|
|
32
|
+
if (sort === 'TOKEN ID') return a.tokenId - b.tokenId;
|
|
33
|
+
return b.tokenId - a.tokenId;
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
return (
|
|
37
|
+
<>
|
|
38
|
+
<Head>
|
|
39
|
+
<title>Explore — MakeSTX</title>
|
|
40
|
+
<meta name="description" content="Browse MakeSTX Cyberpunk NFTs." />
|
|
41
|
+
</Head>
|
|
42
|
+
|
|
43
|
+
<div className="min-h-screen bg-ms-bg">
|
|
44
|
+
<Navbar />
|
|
45
|
+
|
|
46
|
+
<main className="pt-24 pb-16 px-4 max-w-7xl mx-auto">
|
|
47
|
+
<div className="mb-8">
|
|
48
|
+
<p className="font-mono text-xs text-ms-muted tracking-widest mb-1">// explore_collection.tsx</p>
|
|
49
|
+
<h1 className="font-display font-black text-4xl text-ms-text tracking-tight">
|
|
50
|
+
EXPLORE <span className="text-ms-cyan">COLLECTION</span>
|
|
51
|
+
</h1>
|
|
52
|
+
<p className="font-mono text-xs text-ms-muted mt-2">
|
|
53
|
+
{filtered.length} NFTs · {MOCK_NFTS.filter(n => n.isListed).length} LISTED
|
|
54
|
+
</p>
|
|
55
|
+
</div>
|
|
56
|
+
|
|
57
|
+
{/* Filters */}
|
|
58
|
+
<div className="flex flex-col sm:flex-row gap-3 mb-8">
|
|
59
|
+
<div className="relative flex-1">
|
|
60
|
+
<Search size={14} className="absolute left-3 top-1/2 -translate-y-1/2 text-ms-muted" />
|
|
61
|
+
<input
|
|
62
|
+
type="text"
|
|
63
|
+
placeholder="SEARCH BY NAME OR ID..."
|
|
64
|
+
value={search}
|
|
65
|
+
onChange={e => setSearch(e.target.value)}
|
|
66
|
+
className="w-full bg-ms-surface border border-ms-border pl-9 pr-4 py-2.5 text-xs font-mono text-ms-text placeholder-ms-muted focus:outline-none focus:border-ms-cyan transition-colors tracking-widest"
|
|
67
|
+
/>
|
|
68
|
+
</div>
|
|
69
|
+
|
|
70
|
+
<select
|
|
71
|
+
value={sort}
|
|
72
|
+
onChange={e => setSort(e.target.value)}
|
|
73
|
+
className="bg-ms-surface border border-ms-border px-4 py-2.5 text-xs font-mono text-ms-text focus:outline-none focus:border-ms-cyan tracking-widest"
|
|
74
|
+
>
|
|
75
|
+
{SORT_OPTIONS.map(o => <option key={o}>{o}</option>)}
|
|
76
|
+
</select>
|
|
77
|
+
|
|
78
|
+
<button
|
|
79
|
+
onClick={() => setFilterListed(!filterListed)}
|
|
80
|
+
className={`flex items-center gap-2 px-4 py-2.5 text-xs font-mono tracking-widest border transition-all ${
|
|
81
|
+
filterListed
|
|
82
|
+
? 'border-ms-cyan text-ms-cyan bg-ms-cyan/10'
|
|
83
|
+
: 'border-ms-border text-ms-muted hover:border-ms-cyan hover:text-ms-cyan'
|
|
84
|
+
}`}
|
|
85
|
+
>
|
|
86
|
+
<SlidersHorizontal size={13} />
|
|
87
|
+
LISTED ONLY
|
|
88
|
+
</button>
|
|
89
|
+
</div>
|
|
90
|
+
|
|
91
|
+
{/* Grid */}
|
|
92
|
+
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-6 gap-4">
|
|
93
|
+
{filtered.map(nft => (
|
|
94
|
+
<NFTCard key={nft.tokenId} {...nft} />
|
|
95
|
+
))}
|
|
96
|
+
</div>
|
|
97
|
+
|
|
98
|
+
{filtered.length === 0 && (
|
|
99
|
+
<div className="text-center py-24">
|
|
100
|
+
<p className="font-mono text-ms-muted text-sm tracking-widest">// NO_RESULTS_FOUND</p>
|
|
101
|
+
</div>
|
|
102
|
+
)}
|
|
103
|
+
</main>
|
|
104
|
+
</div>
|
|
105
|
+
</>
|
|
106
|
+
);
|
|
107
|
+
}
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
import Head from 'next/head';
|
|
2
|
+
import Link from 'next/link';
|
|
3
|
+
import Navbar from '@/components/Navbar';
|
|
4
|
+
import NFTCard from '@/components/NFTCard';
|
|
5
|
+
import { useWallet } from '@/hooks/useWallet';
|
|
6
|
+
import { ArrowRight, Zap, Shield, TrendingUp, Terminal, Activity } from 'lucide-react';
|
|
7
|
+
|
|
8
|
+
const STATS = [
|
|
9
|
+
{ label: 'TOTAL MINTED', value: '0', unit: '/ 10,000' },
|
|
10
|
+
{ label: 'TOTAL VOLUME', value: '0', unit: 'STX' },
|
|
11
|
+
{ label: 'HOLDERS', value: '0', unit: 'wallets' },
|
|
12
|
+
{ label: 'TRANSACTIONS', value: '0', unit: 'tx' },
|
|
13
|
+
];
|
|
14
|
+
|
|
15
|
+
const FEATURES = [
|
|
16
|
+
{
|
|
17
|
+
icon: Zap,
|
|
18
|
+
title: 'FREE MINT',
|
|
19
|
+
desc: 'Gas fee only. No whitelist, no presale. Just connect and mint.',
|
|
20
|
+
color: 'text-ms-cyan',
|
|
21
|
+
border: 'border-ms-cyan/30',
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
icon: Shield,
|
|
25
|
+
title: 'BITCOIN SECURED',
|
|
26
|
+
desc: 'Every transaction settled on Bitcoin through the Stacks layer.',
|
|
27
|
+
color: 'text-ms-magenta',
|
|
28
|
+
border: 'border-ms-magenta/30',
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
icon: TrendingUp,
|
|
32
|
+
title: '5% ROYALTIES',
|
|
33
|
+
desc: 'Creator royalties enforced on-chain via smart contract.',
|
|
34
|
+
color: 'text-purple-400',
|
|
35
|
+
border: 'border-purple-400/30',
|
|
36
|
+
},
|
|
37
|
+
];
|
|
38
|
+
|
|
39
|
+
const FEATURED = Array.from({ length: 6 }, (_, i) => ({
|
|
40
|
+
tokenId: i + 1,
|
|
41
|
+
name: `MakeSTX #${String(i + 1).padStart(4, '0')}`,
|
|
42
|
+
image: `https://api.dicebear.com/8.x/pixel-art/svg?seed=makestx${i + 100}&backgroundColor=030308&colorful=true`,
|
|
43
|
+
price: Math.floor((Math.random() * 80 + 5) * 1_000_000),
|
|
44
|
+
isListed: Math.random() > 0.4,
|
|
45
|
+
}));
|
|
46
|
+
|
|
47
|
+
export default function Home() {
|
|
48
|
+
const { isConnected, connect } = useWallet();
|
|
49
|
+
|
|
50
|
+
return (
|
|
51
|
+
<>
|
|
52
|
+
<Head>
|
|
53
|
+
<title>MakeSTX — Cyberpunk NFT Collection on Stacks</title>
|
|
54
|
+
<meta name="description" content="10,000 cyberpunk NFTs. Free mint. Bitcoin-secured via Stacks blockchain." />
|
|
55
|
+
<link rel="icon" href="/favicon.ico" />
|
|
56
|
+
</Head>
|
|
57
|
+
|
|
58
|
+
<div className="min-h-screen bg-ms-bg grid-bg">
|
|
59
|
+
<Navbar />
|
|
60
|
+
|
|
61
|
+
{/* ── HERO ──────────────────────────────────────────────── */}
|
|
62
|
+
<section className="relative pt-36 pb-28 px-4 overflow-hidden">
|
|
63
|
+
{/* Glowing orbs */}
|
|
64
|
+
<div className="absolute top-20 left-1/4 w-96 h-96 rounded-full bg-ms-cyan/5 blur-3xl pointer-events-none" />
|
|
65
|
+
<div className="absolute top-40 right-1/4 w-80 h-80 rounded-full bg-ms-magenta/5 blur-3xl pointer-events-none" />
|
|
66
|
+
<div className="absolute bottom-0 left-1/2 -translate-x-1/2 w-full h-px bg-gradient-to-r from-transparent via-ms-cyan/30 to-transparent" />
|
|
67
|
+
|
|
68
|
+
<div className="relative max-w-5xl mx-auto text-center">
|
|
69
|
+
{/* Badge */}
|
|
70
|
+
<div className="inline-flex items-center gap-2 tag tag-cyan mb-8">
|
|
71
|
+
<Terminal size={10} />
|
|
72
|
+
STACKS MAINNET · CYBERPUNK COLLECTION · 10,000 SUPPLY
|
|
73
|
+
</div>
|
|
74
|
+
|
|
75
|
+
{/* Headline */}
|
|
76
|
+
<h1 className="font-display font-black leading-none mb-6">
|
|
77
|
+
<span className="block text-6xl md:text-8xl text-ms-text tracking-tight">
|
|
78
|
+
MAKE
|
|
79
|
+
</span>
|
|
80
|
+
<span className="block text-6xl md:text-8xl gradient-text tracking-tight flicker">
|
|
81
|
+
STX
|
|
82
|
+
</span>
|
|
83
|
+
<span className="block text-xl md:text-2xl text-ms-muted tracking-[0.3em] mt-2 font-normal">
|
|
84
|
+
CYBERPUNK NFT COLLECTION
|
|
85
|
+
</span>
|
|
86
|
+
</h1>
|
|
87
|
+
|
|
88
|
+
<p className="text-ms-muted text-base md:text-lg max-w-xl mx-auto mb-12 leading-relaxed font-light tracking-wide">
|
|
89
|
+
10,000 unique cyberpunk NFTs living on the Stacks blockchain.
|
|
90
|
+
Free mint. No gas wars. Secured by Bitcoin.
|
|
91
|
+
</p>
|
|
92
|
+
|
|
93
|
+
<div className="flex flex-col sm:flex-row gap-4 justify-center">
|
|
94
|
+
<Link
|
|
95
|
+
href="/mint"
|
|
96
|
+
className="btn-primary inline-flex items-center justify-center gap-2 px-10 py-4 text-sm neon-pulse"
|
|
97
|
+
>
|
|
98
|
+
<span>MINT FREE NFT</span>
|
|
99
|
+
<ArrowRight size={16} />
|
|
100
|
+
</Link>
|
|
101
|
+
<Link
|
|
102
|
+
href="/explore"
|
|
103
|
+
className="btn-outline inline-flex items-center justify-center gap-2 px-10 py-4 text-sm font-mono tracking-widest text-ms-muted"
|
|
104
|
+
>
|
|
105
|
+
EXPLORE COLLECTION
|
|
106
|
+
</Link>
|
|
107
|
+
</div>
|
|
108
|
+
|
|
109
|
+
{/* Supply bar */}
|
|
110
|
+
<div className="mt-16 max-w-md mx-auto">
|
|
111
|
+
<div className="flex items-center justify-between text-xs font-mono text-ms-muted mb-2">
|
|
112
|
+
<span>MINTED</span>
|
|
113
|
+
<span>0 / 10,000</span>
|
|
114
|
+
</div>
|
|
115
|
+
<div className="h-1 bg-ms-surface border border-ms-border overflow-hidden">
|
|
116
|
+
<div className="h-full bg-ms-cyan w-0 transition-all duration-1000" style={{ boxShadow: '0 0 8px #00f5ff' }} />
|
|
117
|
+
</div>
|
|
118
|
+
<p className="text-xs font-mono text-ms-muted mt-2 text-center">10,000 REMAINING</p>
|
|
119
|
+
</div>
|
|
120
|
+
</div>
|
|
121
|
+
</section>
|
|
122
|
+
|
|
123
|
+
{/* ── STATS ─────────────────────────────────────────────── */}
|
|
124
|
+
<section className="py-12 border-y border-ms-border bg-ms-surface/30">
|
|
125
|
+
<div className="max-w-5xl mx-auto px-4">
|
|
126
|
+
<div className="grid grid-cols-2 md:grid-cols-4 gap-8">
|
|
127
|
+
{STATS.map((s) => (
|
|
128
|
+
<div key={s.label} className="text-center">
|
|
129
|
+
<p className="text-3xl md:text-4xl font-display font-black gradient-text-cyan">{s.value}</p>
|
|
130
|
+
<p className="text-xs font-mono text-ms-muted mt-1 tracking-widest">{s.label}</p>
|
|
131
|
+
<p className="text-xs font-mono text-ms-muted/50">{s.unit}</p>
|
|
132
|
+
</div>
|
|
133
|
+
))}
|
|
134
|
+
</div>
|
|
135
|
+
</div>
|
|
136
|
+
</section>
|
|
137
|
+
|
|
138
|
+
{/* ── FEATURED NFTs ─────────────────────────────────────── */}
|
|
139
|
+
<section className="py-24 px-4">
|
|
140
|
+
<div className="max-w-7xl mx-auto">
|
|
141
|
+
<div className="flex items-center justify-between mb-10">
|
|
142
|
+
<div>
|
|
143
|
+
<h2 className="text-2xl font-display font-bold text-ms-text tracking-widest">
|
|
144
|
+
COLLECTION <span className="text-ms-cyan">PREVIEW</span>
|
|
145
|
+
</h2>
|
|
146
|
+
<p className="text-ms-muted text-sm font-mono mt-1">// cyberpunk_series_001</p>
|
|
147
|
+
</div>
|
|
148
|
+
<Link href="/explore" className="btn-outline px-4 py-2 text-xs font-mono flex items-center gap-2">
|
|
149
|
+
VIEW ALL <ArrowRight size={12} />
|
|
150
|
+
</Link>
|
|
151
|
+
</div>
|
|
152
|
+
|
|
153
|
+
<div className="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-6 gap-4">
|
|
154
|
+
{FEATURED.map((nft) => (
|
|
155
|
+
<NFTCard key={nft.tokenId} {...nft} />
|
|
156
|
+
))}
|
|
157
|
+
</div>
|
|
158
|
+
</div>
|
|
159
|
+
</section>
|
|
160
|
+
|
|
161
|
+
{/* ── FEATURES ──────────────────────────────────────────── */}
|
|
162
|
+
<section className="py-20 px-4 border-t border-ms-border">
|
|
163
|
+
<div className="max-w-5xl mx-auto">
|
|
164
|
+
<div className="text-center mb-14">
|
|
165
|
+
<h2 className="text-2xl font-display font-bold text-ms-text tracking-widest">
|
|
166
|
+
WHY <span className="gradient-text">MAKESTX</span>?
|
|
167
|
+
</h2>
|
|
168
|
+
<p className="text-ms-muted font-mono text-sm mt-2">// built_different.clar</p>
|
|
169
|
+
</div>
|
|
170
|
+
<div className="grid md:grid-cols-3 gap-6">
|
|
171
|
+
{FEATURES.map((f) => (
|
|
172
|
+
<div key={f.title} className={`card-cyber p-6 rounded-none hover:${f.border} transition-colors`}>
|
|
173
|
+
<div className={`w-10 h-10 border ${f.border} flex items-center justify-center mb-4 ${f.color}`}>
|
|
174
|
+
<f.icon size={20} />
|
|
175
|
+
</div>
|
|
176
|
+
<h3 className="font-display font-bold text-ms-text text-sm tracking-widest mb-2">{f.title}</h3>
|
|
177
|
+
<p className="text-ms-muted text-sm leading-relaxed">{f.desc}</p>
|
|
178
|
+
</div>
|
|
179
|
+
))}
|
|
180
|
+
</div>
|
|
181
|
+
</div>
|
|
182
|
+
</section>
|
|
183
|
+
|
|
184
|
+
{/* ── CTA ───────────────────────────────────────────────── */}
|
|
185
|
+
<section className="py-24 px-4">
|
|
186
|
+
<div className="max-w-2xl mx-auto text-center">
|
|
187
|
+
<div className="card-cyber p-12 relative overflow-hidden">
|
|
188
|
+
<div className="absolute inset-0 bg-gradient-to-br from-ms-cyan/5 to-ms-magenta/5 pointer-events-none" />
|
|
189
|
+
<Activity size={32} className="text-ms-cyan mx-auto mb-4 relative" />
|
|
190
|
+
<h2 className="text-2xl font-display font-bold text-ms-text mb-3 relative tracking-widest">
|
|
191
|
+
READY TO MINT?
|
|
192
|
+
</h2>
|
|
193
|
+
<p className="text-ms-muted mb-8 relative font-mono text-sm">
|
|
194
|
+
Connect your Stacks wallet. Pay only gas. Own a piece of the cyberpunk universe.
|
|
195
|
+
</p>
|
|
196
|
+
<div className="flex flex-col sm:flex-row gap-3 justify-center relative">
|
|
197
|
+
{!isConnected ? (
|
|
198
|
+
<button onClick={connect} className="btn-primary px-10 py-4 text-sm neon-pulse">
|
|
199
|
+
<span>CONNECT & MINT</span>
|
|
200
|
+
</button>
|
|
201
|
+
) : (
|
|
202
|
+
<Link href="/mint" className="btn-primary px-10 py-4 text-sm">
|
|
203
|
+
<span>MINT NOW — FREE</span>
|
|
204
|
+
</Link>
|
|
205
|
+
)}
|
|
206
|
+
<Link href="/multi-mint" className="btn-magenta px-10 py-4 text-xs">
|
|
207
|
+
⚡ MULTI MINT
|
|
208
|
+
</Link>
|
|
209
|
+
</div>
|
|
210
|
+
</div>
|
|
211
|
+
</div>
|
|
212
|
+
</section>
|
|
213
|
+
|
|
214
|
+
{/* ── FOOTER ────────────────────────────────────────────── */}
|
|
215
|
+
<footer className="border-t border-ms-border py-8 px-4 bg-ms-surface/30">
|
|
216
|
+
<div className="max-w-7xl mx-auto flex flex-col md:flex-row items-center justify-between gap-4">
|
|
217
|
+
<div className="flex items-center gap-2">
|
|
218
|
+
<span className="font-display font-black text-sm tracking-widest">
|
|
219
|
+
MAKE<span className="text-ms-cyan">STX</span>
|
|
220
|
+
</span>
|
|
221
|
+
<span className="text-ms-muted font-mono text-xs">© 2025</span>
|
|
222
|
+
</div>
|
|
223
|
+
<div className="flex items-center gap-6 text-xs font-mono text-ms-muted">
|
|
224
|
+
{[
|
|
225
|
+
{ href: '/explore', label: 'EXPLORE' },
|
|
226
|
+
{ href: '/mint', label: 'MINT' },
|
|
227
|
+
{ href: '/multi-mint',label: 'MULTI MINT' },
|
|
228
|
+
{ href: 'https://stacks.co', label: 'STACKS', ext: true },
|
|
229
|
+
].map(({ href, label, ext }) => (
|
|
230
|
+
<a key={label} href={href} target={ext ? '_blank' : undefined} rel={ext ? 'noopener noreferrer' : undefined}
|
|
231
|
+
className="hover:text-ms-cyan transition-colors tracking-widest">
|
|
232
|
+
{label}
|
|
233
|
+
</a>
|
|
234
|
+
))}
|
|
235
|
+
</div>
|
|
236
|
+
</div>
|
|
237
|
+
</footer>
|
|
238
|
+
</div>
|
|
239
|
+
</>
|
|
240
|
+
);
|
|
241
|
+
}
|