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
package/next.config.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "makestx-frontend",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"private": false,
|
|
5
|
+
"scripts": {
|
|
6
|
+
"dev": "next dev",
|
|
7
|
+
"build": "next build",
|
|
8
|
+
"start": "next start",
|
|
9
|
+
"lint": "next lint"
|
|
10
|
+
},
|
|
11
|
+
"dependencies": {
|
|
12
|
+
"@stacks/connect": "^7.10.0",
|
|
13
|
+
"@stacks/network": "^6.13.0",
|
|
14
|
+
"@stacks/transactions": "^6.17.0",
|
|
15
|
+
"@stacks/wallet-sdk": "^6.17.0",
|
|
16
|
+
"lucide-react": "^0.400.0",
|
|
17
|
+
"next": "14.2.3",
|
|
18
|
+
"react": "^18.3.1",
|
|
19
|
+
"react-dom": "^18.3.1"
|
|
20
|
+
},
|
|
21
|
+
"devDependencies": {
|
|
22
|
+
"@types/node": "^20",
|
|
23
|
+
"@types/react": "^18",
|
|
24
|
+
"@types/react-dom": "^18",
|
|
25
|
+
"autoprefixer": "^10.4.19",
|
|
26
|
+
"postcss": "^8.4.38",
|
|
27
|
+
"tailwindcss": "^3.4.4",
|
|
28
|
+
"typescript": "^5"
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { ExternalLink, Tag } from 'lucide-react';
|
|
2
|
+
|
|
3
|
+
interface Props {
|
|
4
|
+
tokenId: number;
|
|
5
|
+
name: string;
|
|
6
|
+
image: string;
|
|
7
|
+
owner?: string;
|
|
8
|
+
price?: number;
|
|
9
|
+
isListed?: boolean;
|
|
10
|
+
onBuy?: () => void;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export default function NFTCard({ tokenId, name, image, owner, price, isListed, onBuy }: Props) {
|
|
14
|
+
return (
|
|
15
|
+
<div className="nft-card rounded-none overflow-hidden group cursor-pointer" style={{ clipPath: 'polygon(0 0, calc(100% - 12px) 0, 100% 12px, 100% 100%, 12px 100%, 0 calc(100% - 12px))' }}>
|
|
16
|
+
{/* Image */}
|
|
17
|
+
<div className="relative aspect-square overflow-hidden bg-gradient-to-br from-ms-cyan/10 to-ms-magenta/10">
|
|
18
|
+
<img
|
|
19
|
+
src={image}
|
|
20
|
+
alt={name}
|
|
21
|
+
className="w-full h-full object-cover transition-transform duration-500 group-hover:scale-110"
|
|
22
|
+
/>
|
|
23
|
+
<div className="absolute inset-0 bg-gradient-to-t from-ms-bg/80 to-transparent opacity-0 group-hover:opacity-100 transition-opacity" />
|
|
24
|
+
{isListed && (
|
|
25
|
+
<div className="absolute top-2 right-2">
|
|
26
|
+
<span className="tag tag-magenta flex items-center gap-1">
|
|
27
|
+
<Tag size={8} /> LISTED
|
|
28
|
+
</span>
|
|
29
|
+
</div>
|
|
30
|
+
)}
|
|
31
|
+
<div className="absolute top-2 left-2">
|
|
32
|
+
<span className="tag tag-cyan">#{tokenId}</span>
|
|
33
|
+
</div>
|
|
34
|
+
</div>
|
|
35
|
+
|
|
36
|
+
{/* Info */}
|
|
37
|
+
<div className="p-3 border-t border-ms-border">
|
|
38
|
+
<p className="text-xs font-display font-bold text-ms-text truncate tracking-wider">{name}</p>
|
|
39
|
+
{owner && (
|
|
40
|
+
<p className="text-xs font-mono text-ms-muted mt-0.5 truncate">
|
|
41
|
+
{owner.slice(0, 8)}...{owner.slice(-4)}
|
|
42
|
+
</p>
|
|
43
|
+
)}
|
|
44
|
+
{isListed && price ? (
|
|
45
|
+
<div className="flex items-center justify-between mt-2">
|
|
46
|
+
<span className="text-xs font-mono text-ms-cyan">
|
|
47
|
+
{(price / 1_000_000).toFixed(2)} STX
|
|
48
|
+
</span>
|
|
49
|
+
{onBuy && (
|
|
50
|
+
<button
|
|
51
|
+
onClick={onBuy}
|
|
52
|
+
className="text-xs font-mono text-ms-bg bg-ms-cyan px-2 py-0.5 hover:bg-white transition-colors"
|
|
53
|
+
>
|
|
54
|
+
BUY
|
|
55
|
+
</button>
|
|
56
|
+
)}
|
|
57
|
+
</div>
|
|
58
|
+
) : (
|
|
59
|
+
<p className="text-xs font-mono text-ms-muted mt-1">NOT LISTED</p>
|
|
60
|
+
)}
|
|
61
|
+
</div>
|
|
62
|
+
</div>
|
|
63
|
+
);
|
|
64
|
+
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import Link from 'next/link';
|
|
3
|
+
import { useWallet } from '@/hooks/useWallet';
|
|
4
|
+
import { useState } from 'react';
|
|
5
|
+
import { Menu, X, Wallet, LogOut, Copy, Check, Zap } from 'lucide-react';
|
|
6
|
+
|
|
7
|
+
export default function Navbar() {
|
|
8
|
+
const { isConnected, address, connect, disconnect } = useWallet();
|
|
9
|
+
const [menuOpen, setMenuOpen] = useState(false);
|
|
10
|
+
const [copied, setCopied] = useState(false);
|
|
11
|
+
|
|
12
|
+
const short = address ? `${address.slice(0, 6)}...${address.slice(-4)}` : '';
|
|
13
|
+
|
|
14
|
+
const copy = () => {
|
|
15
|
+
if (address) {
|
|
16
|
+
navigator.clipboard.writeText(address);
|
|
17
|
+
setCopied(true);
|
|
18
|
+
setTimeout(() => setCopied(false), 2000);
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
return (
|
|
23
|
+
<nav className="fixed top-0 left-0 right-0 z-50 border-b border-ms-border bg-ms-bg/90 backdrop-blur-xl">
|
|
24
|
+
{/* top accent line */}
|
|
25
|
+
<div className="h-px bg-gradient-to-r from-transparent via-ms-cyan to-transparent opacity-60" />
|
|
26
|
+
|
|
27
|
+
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
|
28
|
+
<div className="flex items-center justify-between h-16">
|
|
29
|
+
|
|
30
|
+
{/* Logo */}
|
|
31
|
+
<Link href="/" className="flex items-center gap-3 group">
|
|
32
|
+
<div className="relative w-9 h-9 flex items-center justify-center">
|
|
33
|
+
<div className="absolute inset-0 border border-ms-cyan opacity-40 group-hover:opacity-100 transition-opacity"
|
|
34
|
+
style={{ clipPath: 'polygon(20% 0%, 80% 0%, 100% 20%, 100% 80%, 80% 100%, 20% 100%, 0% 80%, 0% 20%)' }} />
|
|
35
|
+
<span className="font-display font-black text-xs text-ms-cyan text-glow-cyan">MS</span>
|
|
36
|
+
</div>
|
|
37
|
+
<span className="font-display font-bold text-base tracking-widest text-ms-text group-hover:text-ms-cyan transition-colors">
|
|
38
|
+
MAKE<span className="text-ms-cyan">STX</span>
|
|
39
|
+
</span>
|
|
40
|
+
</Link>
|
|
41
|
+
|
|
42
|
+
{/* Desktop Nav */}
|
|
43
|
+
<div className="hidden md:flex items-center gap-8">
|
|
44
|
+
{[
|
|
45
|
+
{ href: '/explore', label: 'EXPLORE' },
|
|
46
|
+
{ href: '/mint', label: 'MINT' },
|
|
47
|
+
{ href: '/auto-mint', label: 'AUTO MINT' },
|
|
48
|
+
{ href: '/multi-mint', label: '⚡ MULTI MINT', hot: true },
|
|
49
|
+
].map(({ href, label, hot }) => (
|
|
50
|
+
<Link
|
|
51
|
+
key={href}
|
|
52
|
+
href={href}
|
|
53
|
+
className={
|
|
54
|
+
hot
|
|
55
|
+
? 'font-mono text-xs tracking-wider text-ms-magenta hover:text-white border border-ms-magenta/40 hover:border-ms-magenta px-3 py-1.5 transition-all hover:bg-ms-magenta/10'
|
|
56
|
+
: 'font-mono text-xs tracking-widest text-ms-muted hover:text-ms-cyan transition-colors'
|
|
57
|
+
}
|
|
58
|
+
>
|
|
59
|
+
{label}
|
|
60
|
+
</Link>
|
|
61
|
+
))}
|
|
62
|
+
{isConnected && (
|
|
63
|
+
<Link href="/my-nfts" className="font-mono text-xs tracking-widest text-ms-muted hover:text-ms-cyan transition-colors">
|
|
64
|
+
MY NFTS
|
|
65
|
+
</Link>
|
|
66
|
+
)}
|
|
67
|
+
</div>
|
|
68
|
+
|
|
69
|
+
{/* Wallet */}
|
|
70
|
+
<div className="hidden md:flex items-center gap-3">
|
|
71
|
+
{isConnected ? (
|
|
72
|
+
<div className="flex items-center gap-2">
|
|
73
|
+
<button
|
|
74
|
+
onClick={copy}
|
|
75
|
+
className="flex items-center gap-2 px-3 py-1.5 border border-ms-border hover:border-ms-cyan text-xs font-mono text-ms-muted hover:text-ms-cyan transition-all"
|
|
76
|
+
>
|
|
77
|
+
<div className="w-1.5 h-1.5 bg-green-400 animate-pulse" />
|
|
78
|
+
{short}
|
|
79
|
+
{copied ? <Check size={11} className="text-green-400" /> : <Copy size={11} />}
|
|
80
|
+
</button>
|
|
81
|
+
<button
|
|
82
|
+
onClick={disconnect}
|
|
83
|
+
className="p-2 border border-ms-border hover:border-red-500 text-ms-muted hover:text-red-400 transition-all"
|
|
84
|
+
title="Disconnect"
|
|
85
|
+
>
|
|
86
|
+
<LogOut size={15} />
|
|
87
|
+
</button>
|
|
88
|
+
</div>
|
|
89
|
+
) : (
|
|
90
|
+
<button
|
|
91
|
+
onClick={connect}
|
|
92
|
+
className="btn-primary flex items-center gap-2 px-5 py-2"
|
|
93
|
+
>
|
|
94
|
+
<Wallet size={13} />
|
|
95
|
+
<span>CONNECT</span>
|
|
96
|
+
</button>
|
|
97
|
+
)}
|
|
98
|
+
</div>
|
|
99
|
+
|
|
100
|
+
{/* Mobile toggle */}
|
|
101
|
+
<button onClick={() => setMenuOpen(!menuOpen)} className="md:hidden p-2 text-ms-muted hover:text-ms-cyan">
|
|
102
|
+
{menuOpen ? <X size={20} /> : <Menu size={20} />}
|
|
103
|
+
</button>
|
|
104
|
+
</div>
|
|
105
|
+
</div>
|
|
106
|
+
|
|
107
|
+
{/* Mobile menu */}
|
|
108
|
+
{menuOpen && (
|
|
109
|
+
<div className="md:hidden border-t border-ms-border bg-ms-surface px-4 py-4 space-y-3">
|
|
110
|
+
{[
|
|
111
|
+
{ href: '/explore', label: 'EXPLORE' },
|
|
112
|
+
{ href: '/mint', label: 'MINT' },
|
|
113
|
+
{ href: '/auto-mint', label: 'AUTO MINT' },
|
|
114
|
+
{ href: '/multi-mint', label: '⚡ MULTI MINT' },
|
|
115
|
+
].map(({ href, label }) => (
|
|
116
|
+
<Link key={href} href={href} className="block font-mono text-xs tracking-widest text-ms-muted hover:text-ms-cyan py-2">
|
|
117
|
+
{label}
|
|
118
|
+
</Link>
|
|
119
|
+
))}
|
|
120
|
+
{isConnected && (
|
|
121
|
+
<Link href="/my-nfts" className="block font-mono text-xs tracking-widest text-ms-muted hover:text-ms-cyan py-2">MY NFTS</Link>
|
|
122
|
+
)}
|
|
123
|
+
<div className="pt-2 border-t border-ms-border">
|
|
124
|
+
{isConnected ? (
|
|
125
|
+
<div className="flex items-center justify-between">
|
|
126
|
+
<span className="text-xs font-mono text-ms-muted">{short}</span>
|
|
127
|
+
<button onClick={disconnect} className="text-red-400 text-xs font-mono">DISCONNECT</button>
|
|
128
|
+
</div>
|
|
129
|
+
) : (
|
|
130
|
+
<button onClick={connect} className="btn-primary w-full py-2 text-xs">CONNECT WALLET</button>
|
|
131
|
+
)}
|
|
132
|
+
</div>
|
|
133
|
+
</div>
|
|
134
|
+
)}
|
|
135
|
+
</nav>
|
|
136
|
+
);
|
|
137
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import {
|
|
2
|
+
makeContractCall,
|
|
3
|
+
broadcastTransaction,
|
|
4
|
+
AnchorMode,
|
|
5
|
+
PostConditionMode,
|
|
6
|
+
uintCV,
|
|
7
|
+
principalCV,
|
|
8
|
+
} from '@stacks/transactions';
|
|
9
|
+
import { openContractCall } from '@stacks/connect';
|
|
10
|
+
import { network, CONTRACT_ADDRESS, CONTRACT_NAME, userSession } from './useWallet';
|
|
11
|
+
|
|
12
|
+
export function useContractActions() {
|
|
13
|
+
const mintNFT = async (recipient: string, onFinish?: () => void) => {
|
|
14
|
+
await openContractCall({
|
|
15
|
+
contractAddress: CONTRACT_ADDRESS,
|
|
16
|
+
contractName: CONTRACT_NAME,
|
|
17
|
+
functionName: 'mint',
|
|
18
|
+
functionArgs: [principalCV(recipient)],
|
|
19
|
+
network,
|
|
20
|
+
anchorMode: AnchorMode.Any,
|
|
21
|
+
postConditionMode: PostConditionMode.Allow,
|
|
22
|
+
onFinish: (data) => { onFinish?.(); },
|
|
23
|
+
onCancel: () => { onFinish?.(); },
|
|
24
|
+
userSession,
|
|
25
|
+
});
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const listNFT = async (tokenId: number, price: number, onFinish?: () => void) => {
|
|
29
|
+
await openContractCall({
|
|
30
|
+
contractAddress: CONTRACT_ADDRESS,
|
|
31
|
+
contractName: CONTRACT_NAME,
|
|
32
|
+
functionName: 'list-nft',
|
|
33
|
+
functionArgs: [uintCV(tokenId), uintCV(price)],
|
|
34
|
+
network,
|
|
35
|
+
anchorMode: AnchorMode.Any,
|
|
36
|
+
postConditionMode: PostConditionMode.Allow,
|
|
37
|
+
onFinish: () => { onFinish?.(); },
|
|
38
|
+
onCancel: () => { onFinish?.(); },
|
|
39
|
+
userSession,
|
|
40
|
+
});
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const unlistNFT = async (tokenId: number, onFinish?: () => void) => {
|
|
44
|
+
await openContractCall({
|
|
45
|
+
contractAddress: CONTRACT_ADDRESS,
|
|
46
|
+
contractName: CONTRACT_NAME,
|
|
47
|
+
functionName: 'unlist-nft',
|
|
48
|
+
functionArgs: [uintCV(tokenId)],
|
|
49
|
+
network,
|
|
50
|
+
anchorMode: AnchorMode.Any,
|
|
51
|
+
postConditionMode: PostConditionMode.Allow,
|
|
52
|
+
onFinish: () => { onFinish?.(); },
|
|
53
|
+
onCancel: () => { onFinish?.(); },
|
|
54
|
+
userSession,
|
|
55
|
+
});
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const buyNFT = async (tokenId: number, price: number, seller: string, onFinish?: () => void) => {
|
|
59
|
+
await openContractCall({
|
|
60
|
+
contractAddress: CONTRACT_ADDRESS,
|
|
61
|
+
contractName: CONTRACT_NAME,
|
|
62
|
+
functionName: 'buy-nft',
|
|
63
|
+
functionArgs: [uintCV(tokenId)],
|
|
64
|
+
network,
|
|
65
|
+
anchorMode: AnchorMode.Any,
|
|
66
|
+
postConditionMode: PostConditionMode.Allow,
|
|
67
|
+
onFinish: () => { onFinish?.(); },
|
|
68
|
+
onCancel: () => { onFinish?.(); },
|
|
69
|
+
userSession,
|
|
70
|
+
});
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
return { mintNFT, listNFT, unlistNFT, buyNFT };
|
|
74
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import React, { createContext, useContext, useState, useEffect, useCallback } from 'react';
|
|
3
|
+
import { AppConfig, UserSession, showConnect } from '@stacks/connect';
|
|
4
|
+
import { StacksMainnet, StacksTestnet } from '@stacks/network';
|
|
5
|
+
|
|
6
|
+
const appConfig = new AppConfig(['store_write', 'publish_data']);
|
|
7
|
+
export const userSession = new UserSession({ appConfig });
|
|
8
|
+
|
|
9
|
+
const isMainnet = process.env.NEXT_PUBLIC_STACKS_NETWORK === 'mainnet';
|
|
10
|
+
export const network = isMainnet ? new StacksMainnet() : new StacksTestnet();
|
|
11
|
+
|
|
12
|
+
export const CONTRACT_ADDRESS = process.env.NEXT_PUBLIC_CONTRACT_ADDRESS || 'SPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX';
|
|
13
|
+
export const CONTRACT_NAME = 'makestx-nft';
|
|
14
|
+
|
|
15
|
+
interface WalletContextType {
|
|
16
|
+
isConnected: boolean;
|
|
17
|
+
address: string | null;
|
|
18
|
+
connect: () => void;
|
|
19
|
+
disconnect: () => void;
|
|
20
|
+
userData: any;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const WalletContext = createContext<WalletContextType>({
|
|
24
|
+
isConnected: false,
|
|
25
|
+
address: null,
|
|
26
|
+
connect: () => {},
|
|
27
|
+
disconnect: () => {},
|
|
28
|
+
userData: null,
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
export function WalletProvider({ children }: { children: React.ReactNode }) {
|
|
32
|
+
const [isConnected, setIsConnected] = useState(false);
|
|
33
|
+
const [address, setAddress] = useState<string | null>(null);
|
|
34
|
+
const [userData, setUserData] = useState<any>(null);
|
|
35
|
+
|
|
36
|
+
useEffect(() => {
|
|
37
|
+
if (userSession.isUserSignedIn()) {
|
|
38
|
+
const data = userSession.loadUserData();
|
|
39
|
+
setIsConnected(true);
|
|
40
|
+
setUserData(data);
|
|
41
|
+
setAddress(isMainnet ? data.profile.stxAddress.mainnet : data.profile.stxAddress.testnet);
|
|
42
|
+
}
|
|
43
|
+
}, []);
|
|
44
|
+
|
|
45
|
+
const connect = useCallback(() => {
|
|
46
|
+
showConnect({
|
|
47
|
+
appDetails: { name: 'MakeSTX', icon: '/logo.png' },
|
|
48
|
+
redirectTo: '/',
|
|
49
|
+
onFinish: () => {
|
|
50
|
+
const data = userSession.loadUserData();
|
|
51
|
+
setIsConnected(true);
|
|
52
|
+
setUserData(data);
|
|
53
|
+
setAddress(isMainnet ? data.profile.stxAddress.mainnet : data.profile.stxAddress.testnet);
|
|
54
|
+
},
|
|
55
|
+
userSession,
|
|
56
|
+
});
|
|
57
|
+
}, []);
|
|
58
|
+
|
|
59
|
+
const disconnect = useCallback(() => {
|
|
60
|
+
userSession.signUserOut('/');
|
|
61
|
+
setIsConnected(false);
|
|
62
|
+
setAddress(null);
|
|
63
|
+
setUserData(null);
|
|
64
|
+
}, []);
|
|
65
|
+
|
|
66
|
+
return (
|
|
67
|
+
<WalletContext.Provider value={{ isConnected, address, connect, disconnect, userData }}>
|
|
68
|
+
{children}
|
|
69
|
+
</WalletContext.Provider>
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function useWallet() {
|
|
74
|
+
return useContext(WalletContext);
|
|
75
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { AppProps } from 'next/app';
|
|
2
|
+
import { WalletProvider } from '@/hooks/useWallet';
|
|
3
|
+
import '@/styles/globals.css';
|
|
4
|
+
|
|
5
|
+
export default function App({ Component, pageProps }: AppProps) {
|
|
6
|
+
return (
|
|
7
|
+
<WalletProvider>
|
|
8
|
+
<Component {...pageProps} />
|
|
9
|
+
</WalletProvider>
|
|
10
|
+
);
|
|
11
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { Html, Head, Main, NextScript } from 'next/document';
|
|
2
|
+
|
|
3
|
+
export default function Document() {
|
|
4
|
+
return (
|
|
5
|
+
<Html lang="en">
|
|
6
|
+
<Head>
|
|
7
|
+
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
|
8
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossOrigin="anonymous" />
|
|
9
|
+
<meta name="theme-color" content="#030308" />
|
|
10
|
+
<meta name="talentapp:project_verification" content="dd37cf261777fe9809a7b7cbdf7ade559547a19fa03f833549a727820fe69b8086867db239c5242994aefae9059143c43b741c1a50a3b482334844a756f345ee" />
|
|
11
|
+
</Head>
|
|
12
|
+
<body>
|
|
13
|
+
<Main />
|
|
14
|
+
<NextScript />
|
|
15
|
+
</body>
|
|
16
|
+
</Html>
|
|
17
|
+
);
|
|
18
|
+
}
|