@devvmichael/create-stacks-app 0.1.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/dist/commands/add.d.ts +8 -0
- package/dist/commands/add.d.ts.map +1 -0
- package/dist/commands/add.js +215 -0
- package/dist/commands/add.js.map +1 -0
- package/dist/commands/create.d.ts +3 -0
- package/dist/commands/create.d.ts.map +1 -0
- package/dist/commands/create.js +63 -0
- package/dist/commands/create.js.map +1 -0
- package/dist/commands/deploy.d.ts +7 -0
- package/dist/commands/deploy.d.ts.map +1 -0
- package/dist/commands/deploy.js +159 -0
- package/dist/commands/deploy.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +47 -0
- package/dist/index.js.map +1 -0
- package/dist/prompts/project.d.ts +4 -0
- package/dist/prompts/project.d.ts.map +1 -0
- package/dist/prompts/project.js +124 -0
- package/dist/prompts/project.js.map +1 -0
- package/dist/templates/installer.d.ts +4 -0
- package/dist/templates/installer.d.ts.map +1 -0
- package/dist/templates/installer.js +91 -0
- package/dist/templates/installer.js.map +1 -0
- package/dist/types/index.d.ts +40 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +2 -0
- package/dist/types/index.js.map +1 -0
- package/dist/utils/clarinet.d.ts +5 -0
- package/dist/utils/clarinet.d.ts.map +1 -0
- package/dist/utils/clarinet.js +72 -0
- package/dist/utils/clarinet.js.map +1 -0
- package/dist/utils/filesystem.d.ts +4 -0
- package/dist/utils/filesystem.d.ts.map +1 -0
- package/dist/utils/filesystem.js +158 -0
- package/dist/utils/filesystem.js.map +1 -0
- package/dist/utils/git.d.ts +2 -0
- package/dist/utils/git.d.ts.map +1 -0
- package/dist/utils/git.js +21 -0
- package/dist/utils/git.js.map +1 -0
- package/dist/utils/logger.d.ts +7 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +45 -0
- package/dist/utils/logger.js.map +1 -0
- package/dist/utils/package-manager.d.ts +5 -0
- package/dist/utils/package-manager.d.ts.map +1 -0
- package/dist/utils/package-manager.js +65 -0
- package/dist/utils/package-manager.js.map +1 -0
- package/dist/utils/validation.d.ts +5 -0
- package/dist/utils/validation.d.ts.map +1 -0
- package/dist/utils/validation.js +41 -0
- package/dist/utils/validation.js.map +1 -0
- package/package.json +52 -0
- package/templates/base/editorconfig +12 -0
- package/templates/base/gitignore +13 -0
- package/templates/base/prettierrc +7 -0
- package/templates/contracts/counter/counter.clar +43 -0
- package/templates/contracts/counter/counter.test.ts +127 -0
- package/templates/contracts/defi/sip010-trait.clar +11 -0
- package/templates/contracts/defi/staking-pool.clar +20 -0
- package/templates/contracts/marketplace/nft-marketplace.clar +44 -0
- package/templates/contracts/marketplace/nft-trait.clar +8 -0
- package/templates/contracts/marketplace/sip009-nft.clar +25 -0
- package/templates/contracts/nft/nft.clar +111 -0
- package/templates/contracts/nft/nft.test.ts +204 -0
- package/templates/contracts/token/token.clar +67 -0
- package/templates/contracts/token/token.test.ts +139 -0
- package/templates/frontends/nextjs/template/.env.example +2 -0
- package/templates/frontends/nextjs/template/app/globals.css +40 -0
- package/templates/frontends/nextjs/template/app/layout.tsx +36 -0
- package/templates/frontends/nextjs/template/app/page.tsx +42 -0
- package/templates/frontends/nextjs/template/app/providers.tsx +31 -0
- package/templates/frontends/nextjs/template/components/contracts/counter-interaction.tsx +80 -0
- package/templates/frontends/nextjs/template/components/header.tsx +24 -0
- package/templates/frontends/nextjs/template/components/wallet/connect-button.tsx +44 -0
- package/templates/frontends/nextjs/template/hooks/use-contract-call.ts +52 -0
- package/templates/frontends/nextjs/template/hooks/use-contract-read.ts +49 -0
- package/templates/frontends/nextjs/template/hooks/use-stacks.ts +26 -0
- package/templates/frontends/nextjs/template/lib/contracts.ts +29 -0
- package/templates/frontends/nextjs/template/lib/stacks.ts +18 -0
- package/templates/frontends/nextjs/template/next.config.js +6 -0
- package/templates/frontends/nextjs/template/package.json +29 -0
- package/templates/frontends/nextjs/template/postcss.config.js +6 -0
- package/templates/frontends/nextjs/template/public/logo.svg +3 -0
- package/templates/frontends/nextjs/template/tailwind.config.js +18 -0
- package/templates/frontends/nextjs/template/tsconfig.json +26 -0
- package/templates/frontends/react/template/.env.example +2 -0
- package/templates/frontends/react/template/index.html +13 -0
- package/templates/frontends/react/template/package.json +29 -0
- package/templates/frontends/react/template/postcss.config.js +6 -0
- package/templates/frontends/react/template/public/logo.svg +3 -0
- package/templates/frontends/react/template/src/App.tsx +100 -0
- package/templates/frontends/react/template/src/components/CounterInteraction.tsx +121 -0
- package/templates/frontends/react/template/src/components/Header.tsx +39 -0
- package/templates/frontends/react/template/src/index.css +33 -0
- package/templates/frontends/react/template/src/main.tsx +10 -0
- package/templates/frontends/react/template/tailwind.config.js +15 -0
- package/templates/frontends/react/template/tsconfig.json +25 -0
- package/templates/frontends/react/template/tsconfig.node.json +10 -0
- package/templates/frontends/react/template/vite.config.ts +12 -0
- package/templates/frontends/vue/template/.env.example +2 -0
- package/templates/frontends/vue/template/index.html +13 -0
- package/templates/frontends/vue/template/package.json +27 -0
- package/templates/frontends/vue/template/postcss.config.js +6 -0
- package/templates/frontends/vue/template/public/logo.svg +3 -0
- package/templates/frontends/vue/template/src/App.vue +98 -0
- package/templates/frontends/vue/template/src/components/AppHeader.vue +39 -0
- package/templates/frontends/vue/template/src/components/CounterInteraction.vue +120 -0
- package/templates/frontends/vue/template/src/env.d.ts +16 -0
- package/templates/frontends/vue/template/src/main.ts +5 -0
- package/templates/frontends/vue/template/src/style.css +33 -0
- package/templates/frontends/vue/template/tailwind.config.js +15 -0
- package/templates/frontends/vue/template/tsconfig.json +25 -0
- package/templates/frontends/vue/template/tsconfig.node.json +10 -0
- package/templates/frontends/vue/template/vite.config.ts +12 -0
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useConnect } from '@stacks/connect-react';
|
|
4
|
+
import { useStacks } from '@/hooks/use-stacks';
|
|
5
|
+
|
|
6
|
+
export function ConnectButton() {
|
|
7
|
+
const { authenticate } = useConnect();
|
|
8
|
+
const { userSession, address, isLoading } = useStacks();
|
|
9
|
+
|
|
10
|
+
const handleConnect = () => {
|
|
11
|
+
authenticate();
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const handleDisconnect = () => {
|
|
15
|
+
userSession.signUserOut('/');
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
if (isLoading) {
|
|
19
|
+
return (
|
|
20
|
+
<button className="btn-secondary opacity-50" disabled>
|
|
21
|
+
Loading...
|
|
22
|
+
</button>
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (address) {
|
|
27
|
+
return (
|
|
28
|
+
<div className="flex items-center gap-3">
|
|
29
|
+
<span className="text-sm bg-gray-100 dark:bg-gray-800 px-3 py-1 rounded-full">
|
|
30
|
+
{address.slice(0, 6)}...{address.slice(-4)}
|
|
31
|
+
</span>
|
|
32
|
+
<button onClick={handleDisconnect} className="btn-secondary text-sm">
|
|
33
|
+
Disconnect
|
|
34
|
+
</button>
|
|
35
|
+
</div>
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return (
|
|
40
|
+
<button onClick={handleConnect} className="btn-primary">
|
|
41
|
+
Connect Wallet
|
|
42
|
+
</button>
|
|
43
|
+
);
|
|
44
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState } from 'react';
|
|
4
|
+
import { openContractCall } from '@stacks/connect';
|
|
5
|
+
import { useStacks } from './use-stacks';
|
|
6
|
+
import type { ContractConfig } from '@/lib/contracts';
|
|
7
|
+
|
|
8
|
+
export function useContractCall(contract: ContractConfig, functionName: string) {
|
|
9
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
10
|
+
const [error, setError] = useState<Error | null>(null);
|
|
11
|
+
const [txId, setTxId] = useState<string | null>(null);
|
|
12
|
+
const { address } = useStacks();
|
|
13
|
+
|
|
14
|
+
const call = async (functionArgs: any[] = []) => {
|
|
15
|
+
if (!address) {
|
|
16
|
+
throw new Error('Wallet not connected');
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
setIsLoading(true);
|
|
20
|
+
setError(null);
|
|
21
|
+
|
|
22
|
+
try {
|
|
23
|
+
await openContractCall({
|
|
24
|
+
contractAddress: contract.address,
|
|
25
|
+
contractName: contract.name,
|
|
26
|
+
functionName,
|
|
27
|
+
functionArgs,
|
|
28
|
+
network: contract.network,
|
|
29
|
+
postConditions: [],
|
|
30
|
+
onFinish: (data) => {
|
|
31
|
+
setTxId(data.txId);
|
|
32
|
+
console.log('Transaction submitted:', data.txId);
|
|
33
|
+
},
|
|
34
|
+
onCancel: () => {
|
|
35
|
+
setIsLoading(false);
|
|
36
|
+
},
|
|
37
|
+
});
|
|
38
|
+
} catch (err) {
|
|
39
|
+
setError(err as Error);
|
|
40
|
+
throw err;
|
|
41
|
+
} finally {
|
|
42
|
+
setIsLoading(false);
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
return {
|
|
47
|
+
call,
|
|
48
|
+
isLoading,
|
|
49
|
+
error,
|
|
50
|
+
txId,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useEffect, useState, useCallback } from 'react';
|
|
4
|
+
import { callReadOnlyFunction, cvToValue } from '@stacks/transactions';
|
|
5
|
+
import type { ContractConfig } from '@/lib/contracts';
|
|
6
|
+
|
|
7
|
+
export function useContractRead(
|
|
8
|
+
contract: ContractConfig,
|
|
9
|
+
functionName: string,
|
|
10
|
+
functionArgs: any[] = []
|
|
11
|
+
) {
|
|
12
|
+
const [data, setData] = useState<any>(null);
|
|
13
|
+
const [isLoading, setIsLoading] = useState(true);
|
|
14
|
+
const [error, setError] = useState<Error | null>(null);
|
|
15
|
+
|
|
16
|
+
const fetchData = useCallback(async () => {
|
|
17
|
+
setIsLoading(true);
|
|
18
|
+
setError(null);
|
|
19
|
+
|
|
20
|
+
try {
|
|
21
|
+
const result = await callReadOnlyFunction({
|
|
22
|
+
contractAddress: contract.address,
|
|
23
|
+
contractName: contract.name,
|
|
24
|
+
functionName,
|
|
25
|
+
functionArgs,
|
|
26
|
+
network: contract.network,
|
|
27
|
+
senderAddress: contract.address,
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
setData(cvToValue(result));
|
|
31
|
+
} catch (err) {
|
|
32
|
+
setError(err as Error);
|
|
33
|
+
console.error('Contract read error:', err);
|
|
34
|
+
} finally {
|
|
35
|
+
setIsLoading(false);
|
|
36
|
+
}
|
|
37
|
+
}, [contract.address, contract.name, contract.network, functionName, functionArgs]);
|
|
38
|
+
|
|
39
|
+
useEffect(() => {
|
|
40
|
+
fetchData();
|
|
41
|
+
}, [fetchData]);
|
|
42
|
+
|
|
43
|
+
return {
|
|
44
|
+
data,
|
|
45
|
+
isLoading,
|
|
46
|
+
error,
|
|
47
|
+
refetch: fetchData,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useEffect, useState } from 'react';
|
|
4
|
+
import { userSession } from '@/lib/stacks';
|
|
5
|
+
|
|
6
|
+
export function useStacks() {
|
|
7
|
+
const [address, setAddress] = useState<string | null>(null);
|
|
8
|
+
const [isLoading, setIsLoading] = useState(true);
|
|
9
|
+
|
|
10
|
+
useEffect(() => {
|
|
11
|
+
if (userSession.isUserSignedIn()) {
|
|
12
|
+
const userData = userSession.loadUserData();
|
|
13
|
+
// Use testnet address by default, mainnet in production
|
|
14
|
+
const network = process.env.NEXT_PUBLIC_NETWORK === 'mainnet' ? 'mainnet' : 'testnet';
|
|
15
|
+
setAddress(userData.profile.stxAddress[network]);
|
|
16
|
+
}
|
|
17
|
+
setIsLoading(false);
|
|
18
|
+
}, []);
|
|
19
|
+
|
|
20
|
+
return {
|
|
21
|
+
userSession,
|
|
22
|
+
address,
|
|
23
|
+
isLoading,
|
|
24
|
+
isConnected: !!address,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { network } from './stacks';
|
|
2
|
+
import type { StacksNetwork } from '@stacks/network';
|
|
3
|
+
|
|
4
|
+
export interface ContractConfig {
|
|
5
|
+
address: string;
|
|
6
|
+
name: string;
|
|
7
|
+
network: StacksNetwork;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
// Contract address from environment or default devnet address
|
|
11
|
+
const contractAddress = process.env.NEXT_PUBLIC_CONTRACT_ADDRESS || 'ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM';
|
|
12
|
+
|
|
13
|
+
export const counterContract: ContractConfig = {
|
|
14
|
+
address: contractAddress,
|
|
15
|
+
name: 'counter',
|
|
16
|
+
network,
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export const tokenContract: ContractConfig = {
|
|
20
|
+
address: contractAddress,
|
|
21
|
+
name: 'token',
|
|
22
|
+
network,
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export const nftContract: ContractConfig = {
|
|
26
|
+
address: contractAddress,
|
|
27
|
+
name: 'nft',
|
|
28
|
+
network,
|
|
29
|
+
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { AppConfig, UserSession } from '@stacks/connect';
|
|
2
|
+
import { StacksTestnet, StacksMainnet } from '@stacks/network';
|
|
3
|
+
|
|
4
|
+
const appConfig = new AppConfig(['store_write', 'publish_data']);
|
|
5
|
+
export const userSession = new UserSession({ appConfig });
|
|
6
|
+
|
|
7
|
+
export const network =
|
|
8
|
+
process.env.NEXT_PUBLIC_NETWORK === 'mainnet'
|
|
9
|
+
? new StacksMainnet()
|
|
10
|
+
: new StacksTestnet();
|
|
11
|
+
|
|
12
|
+
export function getExplorerLink(txId: string): string {
|
|
13
|
+
const baseUrl =
|
|
14
|
+
process.env.NEXT_PUBLIC_NETWORK === 'mainnet'
|
|
15
|
+
? 'https://explorer.stacks.co'
|
|
16
|
+
: 'https://explorer.stacks.co/?chain=testnet';
|
|
17
|
+
return `${baseUrl}/txid/${txId}`;
|
|
18
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "stacks-app-frontend",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": true,
|
|
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.8.2",
|
|
13
|
+
"@stacks/connect-react": "^21.1.1",
|
|
14
|
+
"@stacks/network": "^6.13.0",
|
|
15
|
+
"@stacks/transactions": "^6.13.1",
|
|
16
|
+
"next": "14.1.0",
|
|
17
|
+
"react": "^18.2.0",
|
|
18
|
+
"react-dom": "^18.2.0"
|
|
19
|
+
},
|
|
20
|
+
"devDependencies": {
|
|
21
|
+
"@types/node": "^20",
|
|
22
|
+
"@types/react": "^18",
|
|
23
|
+
"@types/react-dom": "^18",
|
|
24
|
+
"autoprefixer": "^10.0.1",
|
|
25
|
+
"postcss": "^8",
|
|
26
|
+
"tailwindcss": "^3.3.0",
|
|
27
|
+
"typescript": "^5"
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/** @type {import('tailwindcss').Config} */
|
|
2
|
+
module.exports = {
|
|
3
|
+
content: [
|
|
4
|
+
'./app/**/*.{js,ts,jsx,tsx,mdx}',
|
|
5
|
+
'./components/**/*.{js,ts,jsx,tsx,mdx}',
|
|
6
|
+
],
|
|
7
|
+
theme: {
|
|
8
|
+
extend: {
|
|
9
|
+
colors: {
|
|
10
|
+
stacks: {
|
|
11
|
+
purple: '#5546FF',
|
|
12
|
+
'purple-dark': '#3D2DB8',
|
|
13
|
+
},
|
|
14
|
+
},
|
|
15
|
+
},
|
|
16
|
+
},
|
|
17
|
+
plugins: [],
|
|
18
|
+
};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"lib": ["dom", "dom.iterable", "esnext"],
|
|
4
|
+
"allowJs": true,
|
|
5
|
+
"skipLibCheck": true,
|
|
6
|
+
"strict": true,
|
|
7
|
+
"noEmit": true,
|
|
8
|
+
"esModuleInterop": true,
|
|
9
|
+
"module": "esnext",
|
|
10
|
+
"moduleResolution": "bundler",
|
|
11
|
+
"resolveJsonModule": true,
|
|
12
|
+
"isolatedModules": true,
|
|
13
|
+
"jsx": "preserve",
|
|
14
|
+
"incremental": true,
|
|
15
|
+
"plugins": [
|
|
16
|
+
{
|
|
17
|
+
"name": "next"
|
|
18
|
+
}
|
|
19
|
+
],
|
|
20
|
+
"paths": {
|
|
21
|
+
"@/*": ["./*"]
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
|
25
|
+
"exclude": ["node_modules"]
|
|
26
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<link rel="icon" type="image/svg+xml" href="/logo.svg" />
|
|
6
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
7
|
+
<title>Stacks App</title>
|
|
8
|
+
</head>
|
|
9
|
+
<body>
|
|
10
|
+
<div id="root"></div>
|
|
11
|
+
<script type="module" src="/src/main.tsx"></script>
|
|
12
|
+
</body>
|
|
13
|
+
</html>
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "stacks-app-frontend",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"dev": "vite",
|
|
8
|
+
"build": "tsc && vite build",
|
|
9
|
+
"preview": "vite preview",
|
|
10
|
+
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0"
|
|
11
|
+
},
|
|
12
|
+
"dependencies": {
|
|
13
|
+
"@stacks/connect": "^7.8.2",
|
|
14
|
+
"@stacks/network": "^6.13.0",
|
|
15
|
+
"@stacks/transactions": "^6.13.1",
|
|
16
|
+
"react": "^18.2.0",
|
|
17
|
+
"react-dom": "^18.2.0"
|
|
18
|
+
},
|
|
19
|
+
"devDependencies": {
|
|
20
|
+
"@types/react": "^18.2.43",
|
|
21
|
+
"@types/react-dom": "^18.2.17",
|
|
22
|
+
"@vitejs/plugin-react": "^4.2.1",
|
|
23
|
+
"autoprefixer": "^10.4.16",
|
|
24
|
+
"postcss": "^8.4.32",
|
|
25
|
+
"tailwindcss": "^3.3.6",
|
|
26
|
+
"typescript": "^5.3.3",
|
|
27
|
+
"vite": "^5.0.10"
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { useState, useEffect, useCallback } from 'react';
|
|
2
|
+
import { AppConfig, UserSession, showConnect } from '@stacks/connect';
|
|
3
|
+
import { StacksTestnet, StacksMainnet } from '@stacks/network';
|
|
4
|
+
import { Header } from './components/Header';
|
|
5
|
+
import { CounterInteraction } from './components/CounterInteraction';
|
|
6
|
+
|
|
7
|
+
const appConfig = new AppConfig(['store_write', 'publish_data']);
|
|
8
|
+
const userSession = new UserSession({ appConfig });
|
|
9
|
+
|
|
10
|
+
const network =
|
|
11
|
+
import.meta.env.VITE_NETWORK === 'mainnet'
|
|
12
|
+
? new StacksMainnet()
|
|
13
|
+
: new StacksTestnet();
|
|
14
|
+
|
|
15
|
+
function App() {
|
|
16
|
+
const [address, setAddress] = useState<string | null>(null);
|
|
17
|
+
|
|
18
|
+
useEffect(() => {
|
|
19
|
+
if (userSession.isUserSignedIn()) {
|
|
20
|
+
const userData = userSession.loadUserData();
|
|
21
|
+
const networkKey = import.meta.env.VITE_NETWORK === 'mainnet' ? 'mainnet' : 'testnet';
|
|
22
|
+
setAddress(userData.profile.stxAddress[networkKey]);
|
|
23
|
+
}
|
|
24
|
+
}, []);
|
|
25
|
+
|
|
26
|
+
const handleConnect = useCallback(() => {
|
|
27
|
+
showConnect({
|
|
28
|
+
appDetails: {
|
|
29
|
+
name: 'Stacks App',
|
|
30
|
+
icon: window.location.origin + '/logo.svg',
|
|
31
|
+
},
|
|
32
|
+
redirectTo: '/',
|
|
33
|
+
onFinish: () => {
|
|
34
|
+
window.location.reload();
|
|
35
|
+
},
|
|
36
|
+
userSession,
|
|
37
|
+
});
|
|
38
|
+
}, []);
|
|
39
|
+
|
|
40
|
+
const handleDisconnect = useCallback(() => {
|
|
41
|
+
userSession.signUserOut('/');
|
|
42
|
+
}, []);
|
|
43
|
+
|
|
44
|
+
return (
|
|
45
|
+
<div className="min-h-screen flex flex-col">
|
|
46
|
+
<Header
|
|
47
|
+
address={address}
|
|
48
|
+
onConnect={handleConnect}
|
|
49
|
+
onDisconnect={handleDisconnect}
|
|
50
|
+
/>
|
|
51
|
+
|
|
52
|
+
<main className="flex-1 container mx-auto px-4 py-8">
|
|
53
|
+
<div className="mb-8 text-center">
|
|
54
|
+
<h1 className="mb-4 text-4xl font-bold">Welcome to Your Stacks App</h1>
|
|
55
|
+
<p className="text-lg text-gray-400">
|
|
56
|
+
A full-stack Stacks blockchain application
|
|
57
|
+
</p>
|
|
58
|
+
</div>
|
|
59
|
+
|
|
60
|
+
<div className="grid gap-8 md:grid-cols-2 lg:grid-cols-3">
|
|
61
|
+
<CounterInteraction
|
|
62
|
+
network={network}
|
|
63
|
+
isConnected={!!address}
|
|
64
|
+
senderAddress={address}
|
|
65
|
+
/>
|
|
66
|
+
</div>
|
|
67
|
+
|
|
68
|
+
<div className="mt-12 text-center">
|
|
69
|
+
<h2 className="mb-4 text-2xl font-bold">Get Started</h2>
|
|
70
|
+
<div className="grid gap-4 md:grid-cols-3 max-w-3xl mx-auto">
|
|
71
|
+
<div className="card">
|
|
72
|
+
<h3 className="font-semibold mb-2">📝 Edit Contracts</h3>
|
|
73
|
+
<p className="text-sm text-gray-400">
|
|
74
|
+
Modify contracts in <code className="bg-gray-800 px-1 rounded">contracts/</code>
|
|
75
|
+
</p>
|
|
76
|
+
</div>
|
|
77
|
+
<div className="card">
|
|
78
|
+
<h3 className="font-semibold mb-2">🧪 Run Tests</h3>
|
|
79
|
+
<p className="text-sm text-gray-400">
|
|
80
|
+
Run <code className="bg-gray-800 px-1 rounded">npm run test</code>
|
|
81
|
+
</p>
|
|
82
|
+
</div>
|
|
83
|
+
<div className="card">
|
|
84
|
+
<h3 className="font-semibold mb-2">🚀 Deploy</h3>
|
|
85
|
+
<p className="text-sm text-gray-400">
|
|
86
|
+
Run <code className="bg-gray-800 px-1 rounded">npm run deploy:testnet</code>
|
|
87
|
+
</p>
|
|
88
|
+
</div>
|
|
89
|
+
</div>
|
|
90
|
+
</div>
|
|
91
|
+
</main>
|
|
92
|
+
|
|
93
|
+
<footer className="border-t border-gray-800 py-6 text-center text-sm text-gray-500">
|
|
94
|
+
Built with Create Stacks App
|
|
95
|
+
</footer>
|
|
96
|
+
</div>
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export default App;
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { useState, useEffect, useCallback } from 'react';
|
|
2
|
+
import { openContractCall } from '@stacks/connect';
|
|
3
|
+
import { callReadOnlyFunction, cvToValue } from '@stacks/transactions';
|
|
4
|
+
import type { StacksNetwork } from '@stacks/network';
|
|
5
|
+
|
|
6
|
+
interface CounterInteractionProps {
|
|
7
|
+
network: StacksNetwork;
|
|
8
|
+
isConnected: boolean;
|
|
9
|
+
senderAddress: string | null;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const contractAddress = import.meta.env.VITE_CONTRACT_ADDRESS || 'ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM';
|
|
13
|
+
const contractName = 'counter';
|
|
14
|
+
|
|
15
|
+
export function CounterInteraction({ network, isConnected, senderAddress }: CounterInteractionProps) {
|
|
16
|
+
const [counter, setCounter] = useState<number | null>(null);
|
|
17
|
+
const [isLoading, setIsLoading] = useState(true);
|
|
18
|
+
const [isIncrementing, setIsIncrementing] = useState(false);
|
|
19
|
+
const [isDecrementing, setIsDecrementing] = useState(false);
|
|
20
|
+
|
|
21
|
+
const fetchCounter = useCallback(async () => {
|
|
22
|
+
try {
|
|
23
|
+
const result = await callReadOnlyFunction({
|
|
24
|
+
contractAddress,
|
|
25
|
+
contractName,
|
|
26
|
+
functionName: 'get-counter',
|
|
27
|
+
functionArgs: [],
|
|
28
|
+
network,
|
|
29
|
+
senderAddress: contractAddress,
|
|
30
|
+
});
|
|
31
|
+
const value = cvToValue(result);
|
|
32
|
+
setCounter(value?.value ?? 0);
|
|
33
|
+
} catch (error) {
|
|
34
|
+
console.error('Failed to fetch counter:', error);
|
|
35
|
+
} finally {
|
|
36
|
+
setIsLoading(false);
|
|
37
|
+
}
|
|
38
|
+
}, [network]);
|
|
39
|
+
|
|
40
|
+
useEffect(() => {
|
|
41
|
+
fetchCounter();
|
|
42
|
+
}, [fetchCounter]);
|
|
43
|
+
|
|
44
|
+
const handleIncrement = async () => {
|
|
45
|
+
if (!senderAddress) return;
|
|
46
|
+
setIsIncrementing(true);
|
|
47
|
+
try {
|
|
48
|
+
await openContractCall({
|
|
49
|
+
contractAddress,
|
|
50
|
+
contractName,
|
|
51
|
+
functionName: 'increment',
|
|
52
|
+
functionArgs: [],
|
|
53
|
+
network,
|
|
54
|
+
onFinish: () => {
|
|
55
|
+
setTimeout(fetchCounter, 2000);
|
|
56
|
+
},
|
|
57
|
+
});
|
|
58
|
+
} catch (error) {
|
|
59
|
+
console.error('Increment failed:', error);
|
|
60
|
+
} finally {
|
|
61
|
+
setIsIncrementing(false);
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const handleDecrement = async () => {
|
|
66
|
+
if (!senderAddress) return;
|
|
67
|
+
setIsDecrementing(true);
|
|
68
|
+
try {
|
|
69
|
+
await openContractCall({
|
|
70
|
+
contractAddress,
|
|
71
|
+
contractName,
|
|
72
|
+
functionName: 'decrement',
|
|
73
|
+
functionArgs: [],
|
|
74
|
+
network,
|
|
75
|
+
onFinish: () => {
|
|
76
|
+
setTimeout(fetchCounter, 2000);
|
|
77
|
+
},
|
|
78
|
+
});
|
|
79
|
+
} catch (error) {
|
|
80
|
+
console.error('Decrement failed:', error);
|
|
81
|
+
} finally {
|
|
82
|
+
setIsDecrementing(false);
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
return (
|
|
87
|
+
<div className="card">
|
|
88
|
+
<h2 className="text-2xl font-bold mb-4">Counter Contract</h2>
|
|
89
|
+
|
|
90
|
+
<div className="mb-6 text-center">
|
|
91
|
+
<div className="text-6xl font-bold text-stacks-purple">
|
|
92
|
+
{isLoading ? '...' : counter}
|
|
93
|
+
</div>
|
|
94
|
+
<p className="text-sm text-gray-500 mt-2">Current count</p>
|
|
95
|
+
</div>
|
|
96
|
+
|
|
97
|
+
{isConnected ? (
|
|
98
|
+
<div className="flex gap-2">
|
|
99
|
+
<button
|
|
100
|
+
onClick={handleDecrement}
|
|
101
|
+
disabled={isDecrementing || counter === 0}
|
|
102
|
+
className="btn-secondary flex-1 disabled:opacity-50"
|
|
103
|
+
>
|
|
104
|
+
{isDecrementing ? 'Processing...' : '− Decrement'}
|
|
105
|
+
</button>
|
|
106
|
+
<button
|
|
107
|
+
onClick={handleIncrement}
|
|
108
|
+
disabled={isIncrementing}
|
|
109
|
+
className="btn-primary flex-1 disabled:opacity-50"
|
|
110
|
+
>
|
|
111
|
+
{isIncrementing ? 'Processing...' : '+ Increment'}
|
|
112
|
+
</button>
|
|
113
|
+
</div>
|
|
114
|
+
) : (
|
|
115
|
+
<p className="text-center text-gray-500">
|
|
116
|
+
Connect your wallet to interact with the contract
|
|
117
|
+
</p>
|
|
118
|
+
)}
|
|
119
|
+
</div>
|
|
120
|
+
);
|
|
121
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
interface HeaderProps {
|
|
2
|
+
address: string | null;
|
|
3
|
+
onConnect: () => void;
|
|
4
|
+
onDisconnect: () => void;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function Header({ address, onConnect, onDisconnect }: HeaderProps) {
|
|
8
|
+
return (
|
|
9
|
+
<header className="border-b border-gray-800">
|
|
10
|
+
<div className="container mx-auto px-4 py-4 flex items-center justify-between">
|
|
11
|
+
<a href="/" className="flex items-center gap-2">
|
|
12
|
+
<svg
|
|
13
|
+
className="w-8 h-8 text-stacks-purple"
|
|
14
|
+
viewBox="0 0 32 32"
|
|
15
|
+
fill="currentColor"
|
|
16
|
+
>
|
|
17
|
+
<path d="M16 0L3 8v16l13 8 13-8V8L16 0zm0 4l9 5.5v11L16 26l-9-5.5v-11L16 4z" />
|
|
18
|
+
</svg>
|
|
19
|
+
<span className="font-bold text-xl">Stacks App</span>
|
|
20
|
+
</a>
|
|
21
|
+
|
|
22
|
+
{address ? (
|
|
23
|
+
<div className="flex items-center gap-3">
|
|
24
|
+
<span className="text-sm bg-gray-800 px-3 py-1 rounded-full">
|
|
25
|
+
{address.slice(0, 6)}...{address.slice(-4)}
|
|
26
|
+
</span>
|
|
27
|
+
<button onClick={onDisconnect} className="btn-secondary text-sm">
|
|
28
|
+
Disconnect
|
|
29
|
+
</button>
|
|
30
|
+
</div>
|
|
31
|
+
) : (
|
|
32
|
+
<button onClick={onConnect} className="btn-primary">
|
|
33
|
+
Connect Wallet
|
|
34
|
+
</button>
|
|
35
|
+
)}
|
|
36
|
+
</div>
|
|
37
|
+
</header>
|
|
38
|
+
);
|
|
39
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
@tailwind base;
|
|
2
|
+
@tailwind components;
|
|
3
|
+
@tailwind utilities;
|
|
4
|
+
|
|
5
|
+
:root {
|
|
6
|
+
font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
|
|
7
|
+
line-height: 1.5;
|
|
8
|
+
font-weight: 400;
|
|
9
|
+
color-scheme: light dark;
|
|
10
|
+
color: rgba(255, 255, 255, 0.87);
|
|
11
|
+
background-color: #242424;
|
|
12
|
+
font-synthesis: none;
|
|
13
|
+
text-rendering: optimizeLegibility;
|
|
14
|
+
-webkit-font-smoothing: antialiased;
|
|
15
|
+
-moz-osx-font-smoothing: grayscale;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
body {
|
|
19
|
+
margin: 0;
|
|
20
|
+
min-height: 100vh;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
.btn-primary {
|
|
24
|
+
@apply bg-stacks-purple hover:bg-stacks-purple-dark text-white font-semibold py-2 px-4 rounded-lg transition-colors;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
.btn-secondary {
|
|
28
|
+
@apply border border-gray-600 hover:bg-gray-800 font-semibold py-2 px-4 rounded-lg transition-colors;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
.card {
|
|
32
|
+
@apply bg-gray-900 rounded-xl shadow-lg p-6 border border-gray-700;
|
|
33
|
+
}
|