diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 00000000..2384a0ed --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/src/commands/contract/new.ts b/src/commands/contract/new.ts index 0936663f..2c10c3d6 100644 --- a/src/commands/contract/new.ts +++ b/src/commands/contract/new.ts @@ -5,9 +5,9 @@ import { checkCliDependencies, copyContractTemplateFiles, processTemplates, - getTemplates, + getTemplates, copyFrontendTemplateFiles, } from "../../lib/index.js"; -import { email, name, pickTemplate } from "../../lib/prompts.js"; +import { choice, email, name, pickTemplate } from "../../lib/prompts.js"; import { paramCase, pascalCase, snakeCase } from "change-case"; import { execaCommandSync } from "execa"; import inquirer from "inquirer"; @@ -59,6 +59,7 @@ export class NewContract extends SwankyCommand { "What is your name?" ), email(), + choice("useFrontendTemplate", "Do you want to use a frontend template?"), ]; const answers = await inquirer.prompt(questions); @@ -78,6 +79,14 @@ export class NewContract extends SwankyCommand { "Copying contract template files" ); + if(answers.useFrontendTemplate) { + await copyFrontendTemplateFiles( + templates.frontendTemplatesPath, + args.contractName, + projectPath, + ) + } + await this.spinner.runCommand( () => processTemplates(projectPath, { diff --git a/src/commands/init/index.ts b/src/commands/init/index.ts index 45177372..05b6befa 100644 --- a/src/commands/init/index.ts +++ b/src/commands/init/index.ts @@ -17,6 +17,7 @@ import { processTemplates, swankyNode, getTemplates, + copyFrontendTemplateFiles } from "../../lib/index.js"; import { DEFAULT_ASTAR_NETWORK_URL, @@ -268,6 +269,21 @@ export class Init extends SwankyCommand { ], runningMessage: "Copying contract template files", }); + const {useFrontendTemplate} = await inquirer.prompt([ + choice("useFrontendTemplate", "Do you want to use a frontend template?"), + ]); + if(useFrontendTemplate) + { + this.taskQueue.push({ + task: copyFrontendTemplateFiles, + args: [ + templates.frontendTemplatesPath, + contractName, + this.projectPath, + ], + runningMessage: "Copying frontend template files", + }); + } this.taskQueue.push({ task: processTemplates, @@ -402,19 +418,19 @@ export class Init extends SwankyCommand { runningMessage: "Copying workspace files", }); - const existingPJsonPath = path.resolve(pathToExistingProject, "package.json"); + const existingPJsonPath = path.resolve(pathToExistingProject, "package.json.hbs"); if (await pathExists(existingPJsonPath)) { this.taskQueue.push({ task: async (pJsonPath, projectPath) => { const existingPJson = await readJSON(pJsonPath); - const templatePJsonPath = path.resolve(projectPath, "package.json"); + const templatePJsonPath = path.resolve(projectPath, "package.json.hbs"); const templatePJson = await readJSON(templatePJsonPath); const mergedJson = merge(templatePJson, existingPJson); await remove(templatePJsonPath); await writeJSON(templatePJsonPath, mergedJson, { spaces: 2 }); }, args: [existingPJsonPath, this.projectPath], - runningMessage: "Merging package.json", + runningMessage: "Merging package.json.hbs", }); } } diff --git a/src/lib/tasks.ts b/src/lib/tasks.ts index 20c0efdc..51e68fab 100644 --- a/src/lib/tasks.ts +++ b/src/lib/tasks.ts @@ -11,6 +11,7 @@ import decompress from "decompress"; import { Spinner } from "./spinner.js"; import { SupportedPlatforms, SupportedArch } from "../types/index.js"; import { ConfigError, NetworkError } from "./errors.js"; +import { existsSync } from "node:fs"; export async function checkCliDependencies(spinner: Spinner) { const dependencyList = [ @@ -59,6 +60,31 @@ export async function copyContractTemplateFiles( ); } +export async function copyFrontendTemplateFiles( + frontendTemplatePath: string, + contractName: string, + projectPath: string, +) { + if(!existsSync(path.resolve(projectPath, "frontends"))) { + await copy( + path.resolve(frontendTemplatePath, "ui"), + path.resolve(projectPath, "frontends", "ui") + ) + await copy( + path.resolve(frontendTemplatePath, "package.json.hbs"), + path.resolve(projectPath, "frontends", "package.json.hbs") + ) + await copy( + path.resolve(frontendTemplatePath, "pnpm-workspace.yaml.hbs"), + path.resolve(projectPath, "frontends", "pnpm-workspace.yaml.hbs") + ) + } + await copy( + path.resolve(frontendTemplatePath, contractName), + path.resolve(projectPath, "frontends", contractName) + ) +} + export async function processTemplates(projectPath: string, templateData: Record) { const templateFiles = await globby(projectPath, { expandDirectories: { extensions: ["hbs"] }, diff --git a/src/lib/templates.ts b/src/lib/templates.ts index ef814298..b40499d5 100644 --- a/src/lib/templates.ts +++ b/src/lib/templates.ts @@ -8,6 +8,7 @@ const __dirname = path.dirname(__filename); export function getTemplates() { const templatesPath = path.resolve(__dirname, "..", "templates"); const contractTemplatesPath = path.resolve(templatesPath, "contracts"); + const frontendTemplatesPath = path.resolve(templatesPath, "frontends"); const fileList = readdirSync(contractTemplatesPath, { withFileTypes: true, }); @@ -19,5 +20,6 @@ export function getTemplates() { templatesPath, contractTemplatesPath, contractTemplatesList, + frontendTemplatesPath, }; } diff --git a/src/templates/frontends/blank/README.md.hbs b/src/templates/frontends/blank/README.md.hbs new file mode 100644 index 00000000..ad88c572 --- /dev/null +++ b/src/templates/frontends/blank/README.md.hbs @@ -0,0 +1,4 @@ +# Have Questions? + +For any questions about building front end applications with [useink](https://use.ink/frontend/overview/), join the [Element chat](https://matrix.to/#/%23useink:parity.io). + diff --git a/src/templates/frontends/blank/hbs.sh.hbs b/src/templates/frontends/blank/hbs.sh.hbs new file mode 100644 index 00000000..72ce75a5 --- /dev/null +++ b/src/templates/frontends/blank/hbs.sh.hbs @@ -0,0 +1,5 @@ +files=$(find . -type f | tr " " "\n") +for f in $files; do + mv $f "$f.hbs" +done +echo "done" diff --git a/src/templates/frontends/blank/index.html.hbs b/src/templates/frontends/blank/index.html.hbs new file mode 100644 index 00000000..e4d2ac59 --- /dev/null +++ b/src/templates/frontends/blank/index.html.hbs @@ -0,0 +1,13 @@ + + + + + + + ink! Examples + + +
+ + + diff --git a/src/templates/frontends/blank/package.json.hbs b/src/templates/frontends/blank/package.json.hbs new file mode 100644 index 00000000..1f0b8b77 --- /dev/null +++ b/src/templates/frontends/blank/package.json.hbs @@ -0,0 +1,26 @@ +{ + "name": "blank", + "private": true, + "version": "0.1.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc && vite build", + "lint": "eslint src --ext ts,tsx --report-unused-disable-directives --max-warnings 0", + "preview": "vite preview" + }, + "dependencies": { + "ui": "workspace:ui@*" + }, + "devDependencies": { + "@types/react": "^18.0.37", + "@types/react-dom": "^18.0.11", + "@vitejs/plugin-react": "^4.0.0", + "autoprefixer": "^10.4.14", + "eslint": "^8.38.0", + "postcss": "^8.4.24", + "tailwindcss": "^3.3.2", + "typescript": "^5.0.2", + "vite": "^4.3.9" + } +} diff --git a/src/templates/frontends/blank/postcss.config.js.hbs b/src/templates/frontends/blank/postcss.config.js.hbs new file mode 100644 index 00000000..2aa7205d --- /dev/null +++ b/src/templates/frontends/blank/postcss.config.js.hbs @@ -0,0 +1,6 @@ +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +}; diff --git a/src/templates/frontends/blank/public/logo.svg.hbs b/src/templates/frontends/blank/public/logo.svg.hbs new file mode 100644 index 00000000..c31ded82 --- /dev/null +++ b/src/templates/frontends/blank/public/logo.svg.hbs @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/templates/frontends/blank/src/App.tsx.hbs b/src/templates/frontends/blank/src/App.tsx.hbs new file mode 100644 index 00000000..6d5cd37a --- /dev/null +++ b/src/templates/frontends/blank/src/App.tsx.hbs @@ -0,0 +1,12 @@ +import { InkLayout } from 'ui'; +function App() { + return ( + + + ); +} + +export default App; diff --git a/src/templates/frontends/blank/src/Global.css.hbs b/src/templates/frontends/blank/src/Global.css.hbs new file mode 100644 index 00000000..c2f5c12e --- /dev/null +++ b/src/templates/frontends/blank/src/Global.css.hbs @@ -0,0 +1,7 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +body { + background-color: #1A1452; +} \ No newline at end of file diff --git a/src/templates/frontends/blank/src/main.tsx.hbs b/src/templates/frontends/blank/src/main.tsx.hbs new file mode 100644 index 00000000..39efb84c --- /dev/null +++ b/src/templates/frontends/blank/src/main.tsx.hbs @@ -0,0 +1,26 @@ +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import 'ui/style.css'; +import { UseInkProvider } from 'useink'; +import { RococoContractsTestnet } from 'useink/chains'; +import { NotificationsProvider } from 'useink/notifications'; +import App from './App.tsx'; +import './Global.css'; + +ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render( + + + + + + + , +); diff --git a/src/templates/frontends/blank/src/vite-env.d.ts.hbs b/src/templates/frontends/blank/src/vite-env.d.ts.hbs new file mode 100644 index 00000000..11f02fe2 --- /dev/null +++ b/src/templates/frontends/blank/src/vite-env.d.ts.hbs @@ -0,0 +1 @@ +/// diff --git a/src/templates/frontends/blank/tailwind.config.js.hbs b/src/templates/frontends/blank/tailwind.config.js.hbs new file mode 100644 index 00000000..ceec5b29 --- /dev/null +++ b/src/templates/frontends/blank/tailwind.config.js.hbs @@ -0,0 +1,2 @@ +import config from '../ui/tailwind.config'; +export default config; diff --git a/src/templates/frontends/blank/tsconfig.json.hbs b/src/templates/frontends/blank/tsconfig.json.hbs new file mode 100644 index 00000000..1922c64a --- /dev/null +++ b/src/templates/frontends/blank/tsconfig.json.hbs @@ -0,0 +1,25 @@ +{ + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx", + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["src", "../../ui/src/contexts/DeployerContextTsx"], + "references": [{ "path": "./tsconfig.node.json" }] +} diff --git a/src/templates/frontends/blank/tsconfig.node.json.hbs b/src/templates/frontends/blank/tsconfig.node.json.hbs new file mode 100644 index 00000000..42872c59 --- /dev/null +++ b/src/templates/frontends/blank/tsconfig.node.json.hbs @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "composite": true, + "skipLibCheck": true, + "module": "ESNext", + "moduleResolution": "bundler", + "allowSyntheticDefaultImports": true + }, + "include": ["vite.config.ts"] +} diff --git a/src/templates/frontends/blank/vite.config.ts.hbs b/src/templates/frontends/blank/vite.config.ts.hbs new file mode 100644 index 00000000..4e7004eb --- /dev/null +++ b/src/templates/frontends/blank/vite.config.ts.hbs @@ -0,0 +1,7 @@ +import react from '@vitejs/plugin-react'; +import { defineConfig } from 'vite'; + +// https://vitejs.dev/config/ +export default defineConfig({ + plugins: [react()], +}); diff --git a/src/templates/frontends/flipper/.gitignore.hbs b/src/templates/frontends/flipper/.gitignore.hbs new file mode 100644 index 00000000..c13f37b6 --- /dev/null +++ b/src/templates/frontends/flipper/.gitignore.hbs @@ -0,0 +1,21 @@ +# Logs +logs +*.log +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/src/templates/frontends/flipper/README.md.hbs b/src/templates/frontends/flipper/README.md.hbs new file mode 100644 index 00000000..ad88c572 --- /dev/null +++ b/src/templates/frontends/flipper/README.md.hbs @@ -0,0 +1,4 @@ +# Have Questions? + +For any questions about building front end applications with [useink](https://use.ink/frontend/overview/), join the [Element chat](https://matrix.to/#/%23useink:parity.io). + diff --git a/src/templates/frontends/flipper/hbs.sh.hbs b/src/templates/frontends/flipper/hbs.sh.hbs new file mode 100644 index 00000000..72ce75a5 --- /dev/null +++ b/src/templates/frontends/flipper/hbs.sh.hbs @@ -0,0 +1,5 @@ +files=$(find . -type f | tr " " "\n") +for f in $files; do + mv $f "$f.hbs" +done +echo "done" diff --git a/src/templates/frontends/flipper/index.html.hbs b/src/templates/frontends/flipper/index.html.hbs new file mode 100644 index 00000000..e4d2ac59 --- /dev/null +++ b/src/templates/frontends/flipper/index.html.hbs @@ -0,0 +1,13 @@ + + + + + + + ink! Examples + + +
+ + + diff --git a/src/templates/frontends/flipper/package.json.hbs b/src/templates/frontends/flipper/package.json.hbs new file mode 100644 index 00000000..c459a6c3 --- /dev/null +++ b/src/templates/frontends/flipper/package.json.hbs @@ -0,0 +1,31 @@ +{ + "name": "flipper", + "private": true, + "version": "0.1.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc && vite build", + "lint": "eslint src --ext ts,tsx --report-unused-disable-directives --max-warnings 0", + "preview": "vite preview" + }, + "dependencies": { + "ui": "workspace:ui@*", + "useink": "^1.14.1" + }, + "devDependencies": { + "@types/react": "^18.0.37", + "@types/react-dom": "^18.0.11", + "@typescript-eslint/eslint-plugin": "^5.59.0", + "@typescript-eslint/parser": "^5.59.0", + "@vitejs/plugin-react": "^4.0.0", + "autoprefixer": "^10.4.14", + "eslint": "^8.38.0", + "eslint-plugin-react-hooks": "^4.6.0", + "eslint-plugin-react-refresh": "^0.3.4", + "postcss": "^8.4.24", + "tailwindcss": "^3.3.2", + "typescript": "^5.0.2", + "vite": "^4.3.9" + } +} diff --git a/src/templates/frontends/flipper/postcss.config.js.hbs b/src/templates/frontends/flipper/postcss.config.js.hbs new file mode 100644 index 00000000..2aa7205d --- /dev/null +++ b/src/templates/frontends/flipper/postcss.config.js.hbs @@ -0,0 +1,6 @@ +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +}; diff --git a/src/templates/frontends/flipper/public/logo.svg.hbs b/src/templates/frontends/flipper/public/logo.svg.hbs new file mode 100644 index 00000000..c31ded82 --- /dev/null +++ b/src/templates/frontends/flipper/public/logo.svg.hbs @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/templates/frontends/flipper/src/App.tsx.hbs b/src/templates/frontends/flipper/src/App.tsx.hbs new file mode 100644 index 00000000..c7ff7db5 --- /dev/null +++ b/src/templates/frontends/flipper/src/App.tsx.hbs @@ -0,0 +1,26 @@ +import {InkLayout, DeployerProvider} from 'ui'; +import metadata from './assets/flipper.json'; +import {Flipper} from './components' + +function App() { + return ( + + + + + + ); +} + +export default App; diff --git a/src/templates/frontends/flipper/src/Global.css.hbs b/src/templates/frontends/flipper/src/Global.css.hbs new file mode 100644 index 00000000..bd6213e1 --- /dev/null +++ b/src/templates/frontends/flipper/src/Global.css.hbs @@ -0,0 +1,3 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; \ No newline at end of file diff --git a/src/templates/frontends/flipper/src/assets/flipper.json.hbs b/src/templates/frontends/flipper/src/assets/flipper.json.hbs new file mode 100644 index 00000000..f59c673a --- /dev/null +++ b/src/templates/frontends/flipper/src/assets/flipper.json.hbs @@ -0,0 +1,396 @@ +{ + "source": { + "hash": "0x0f466d0c332258ce41c0b4aaaba4f1708671da275c8fbe395dc81b1fdfedc662", + "language": "ink! 4.2.0", + "compiler": "rustc 1.69.0", + "build_info": { + "build_mode": "Release", + "cargo_contract_version": "2.2.1", + "rust_toolchain": "stable-x86_64-apple-darwin", + "wasm_opt_settings": { + "keep_debug_symbols": false, + "optimization_passes": "Z" + } + } + }, + "contract": { + "name": "flipper", + "version": "4.2.0", + "authors": [ + "Parity Technologies " + ] + }, + "spec": { + "constructors": [ + { + "args": [ + { + "label": "init_value", + "type": { + "displayName": [ + "bool" + ], + "type": 0 + } + } + ], + "default": false, + "docs": [ + "Creates a new flipper smart contract initialized with the given value." + ], + "label": "new", + "payable": false, + "returnType": { + "displayName": [ + "ink_primitives", + "ConstructorResult" + ], + "type": 1 + }, + "selector": "0x9bae9d5e" + }, + { + "args": [], + "default": false, + "docs": [ + "Creates a new flipper smart contract initialized to `false`." + ], + "label": "new_default", + "payable": false, + "returnType": { + "displayName": [ + "ink_primitives", + "ConstructorResult" + ], + "type": 1 + }, + "selector": "0x61ef7e3e" + } + ], + "docs": [], + "environment": { + "accountId": { + "displayName": [ + "AccountId" + ], + "type": 5 + }, + "balance": { + "displayName": [ + "Balance" + ], + "type": 8 + }, + "blockNumber": { + "displayName": [ + "BlockNumber" + ], + "type": 11 + }, + "chainExtension": { + "displayName": [ + "ChainExtension" + ], + "type": 12 + }, + "hash": { + "displayName": [ + "Hash" + ], + "type": 9 + }, + "maxEventTopics": 4, + "timestamp": { + "displayName": [ + "Timestamp" + ], + "type": 10 + } + }, + "events": [], + "lang_error": { + "displayName": [ + "ink", + "LangError" + ], + "type": 3 + }, + "messages": [ + { + "args": [], + "default": false, + "docs": [ + " Flips the current value of the FlipperTsx's boolean." + ], + "label": "flip", + "mutates": true, + "payable": false, + "returnType": { + "displayName": [ + "ink", + "MessageResult" + ], + "type": 1 + }, + "selector": "0x633aa551" + }, + { + "args": [], + "default": false, + "docs": [ + " Returns the current value of the FlipperTsx's boolean." + ], + "label": "get", + "mutates": false, + "payable": false, + "returnType": { + "displayName": [ + "ink", + "MessageResult" + ], + "type": 4 + }, + "selector": "0x2f865bd9" + } + ] + }, + "storage": { + "root": { + "layout": { + "struct": { + "fields": [ + { + "layout": { + "leaf": { + "key": "0x00000000", + "ty": 0 + } + }, + "name": "value" + } + ], + "name": "FlipperTsx" + } + }, + "root_key": "0x00000000" + } + }, + "types": [ + { + "id": 0, + "type": { + "def": { + "primitive": "bool" + } + } + }, + { + "id": 1, + "type": { + "def": { + "variant": { + "variants": [ + { + "fields": [ + { + "type": 2 + } + ], + "index": 0, + "name": "Ok" + }, + { + "fields": [ + { + "type": 3 + } + ], + "index": 1, + "name": "Err" + } + ] + } + }, + "params": [ + { + "name": "T", + "type": 2 + }, + { + "name": "E", + "type": 3 + } + ], + "path": [ + "Result" + ] + } + }, + { + "id": 2, + "type": { + "def": { + "tuple": [] + } + } + }, + { + "id": 3, + "type": { + "def": { + "variant": { + "variants": [ + { + "index": 1, + "name": "CouldNotReadInput" + } + ] + } + }, + "path": [ + "ink_primitives", + "LangError" + ] + } + }, + { + "id": 4, + "type": { + "def": { + "variant": { + "variants": [ + { + "fields": [ + { + "type": 0 + } + ], + "index": 0, + "name": "Ok" + }, + { + "fields": [ + { + "type": 3 + } + ], + "index": 1, + "name": "Err" + } + ] + } + }, + "params": [ + { + "name": "T", + "type": 0 + }, + { + "name": "E", + "type": 3 + } + ], + "path": [ + "Result" + ] + } + }, + { + "id": 5, + "type": { + "def": { + "composite": { + "fields": [ + { + "type": 6, + "typeName": "[u8; 32]" + } + ] + } + }, + "path": [ + "ink_primitives", + "types", + "AccountId" + ] + } + }, + { + "id": 6, + "type": { + "def": { + "array": { + "len": 32, + "type": 7 + } + } + } + }, + { + "id": 7, + "type": { + "def": { + "primitive": "u8" + } + } + }, + { + "id": 8, + "type": { + "def": { + "primitive": "u128" + } + } + }, + { + "id": 9, + "type": { + "def": { + "composite": { + "fields": [ + { + "type": 6, + "typeName": "[u8; 32]" + } + ] + } + }, + "path": [ + "ink_primitives", + "types", + "Hash" + ] + } + }, + { + "id": 10, + "type": { + "def": { + "primitive": "u64" + } + } + }, + { + "id": 11, + "type": { + "def": { + "primitive": "u32" + } + } + }, + { + "id": 12, + "type": { + "def": { + "variant": {} + }, + "path": [ + "ink_env", + "types", + "NoChainExtension" + ] + } + } + ], + "version": "4" +} \ No newline at end of file diff --git a/src/templates/frontends/flipper/src/components/Flipper/Flipper.tsx.hbs b/src/templates/frontends/flipper/src/components/Flipper/Flipper.tsx.hbs new file mode 100644 index 00000000..d6ac4229 --- /dev/null +++ b/src/templates/frontends/flipper/src/components/Flipper/Flipper.tsx.hbs @@ -0,0 +1,46 @@ +import {Card, formatContractName, Button, ConnectButton, useDeployerState} from 'ui'; +import { useCallSubscription, useContract, useTx, useWallet } from 'useink'; +import { useTxNotifications } from 'useink/notifications'; +import {pickDecoded, shouldDisable} from 'useink/utils'; +import metadata from "../../assets/flipper.json"; + +export const Flipper: React.FC = () => { + const { contractAddress } = useDeployerState(); + const contract = useContract(contractAddress || '', metadata); + + const flip = useTx(contract, 'flip'); + useTxNotifications(flip); + + const { account } = useWallet(); + + const getSub = useCallSubscription(contract, 'get', [], { + defaultCaller: true, + }); + + return ( +
+ +

+ {formatContractName(metadata.contract.name)} +

+ +

+ Flipped:{' '} + {pickDecoded(getSub.result)?.toString()} +

+ + {account ? ( + + ) : ( + + )} +
+
+ ); +}; \ No newline at end of file diff --git a/src/templates/frontends/flipper/src/components/Flipper/index.ts.hbs b/src/templates/frontends/flipper/src/components/Flipper/index.ts.hbs new file mode 100644 index 00000000..215e0954 --- /dev/null +++ b/src/templates/frontends/flipper/src/components/Flipper/index.ts.hbs @@ -0,0 +1 @@ +export * from './Flipper.tsx'; \ No newline at end of file diff --git a/src/templates/frontends/flipper/src/components/index.ts.hbs b/src/templates/frontends/flipper/src/components/index.ts.hbs new file mode 100644 index 00000000..ed5fa815 --- /dev/null +++ b/src/templates/frontends/flipper/src/components/index.ts.hbs @@ -0,0 +1 @@ +export * from './Flipper/index.ts'; \ No newline at end of file diff --git a/src/templates/frontends/flipper/src/constants.ts.hbs b/src/templates/frontends/flipper/src/constants.ts.hbs new file mode 100644 index 00000000..549db8ba --- /dev/null +++ b/src/templates/frontends/flipper/src/constants.ts.hbs @@ -0,0 +1,2 @@ +export const CONTRACT_ROCOCO_ADDRESS = + '5Fsk6oqWHJzMAQmkBTVzxxqZPPngLbHG48Tro3i53LC3quao'; diff --git a/src/templates/frontends/flipper/src/main.tsx.hbs b/src/templates/frontends/flipper/src/main.tsx.hbs new file mode 100644 index 00000000..d8e1e0bc --- /dev/null +++ b/src/templates/frontends/flipper/src/main.tsx.hbs @@ -0,0 +1,27 @@ +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import 'ui/style.css'; +import { UseInkProvider } from 'useink'; +import { RococoContractsTestnet } from 'useink/chains'; +import { NotificationsProvider } from 'useink/notifications'; +import App from './App.tsx'; +import './Global.css'; +import metadata from './assets/flipper.json'; + +ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render( + + + + + + + , +); diff --git a/src/templates/frontends/flipper/src/vite-env.d.ts.hbs b/src/templates/frontends/flipper/src/vite-env.d.ts.hbs new file mode 100644 index 00000000..11f02fe2 --- /dev/null +++ b/src/templates/frontends/flipper/src/vite-env.d.ts.hbs @@ -0,0 +1 @@ +/// diff --git a/src/templates/frontends/flipper/tailwind.config.js.hbs b/src/templates/frontends/flipper/tailwind.config.js.hbs new file mode 100644 index 00000000..ceec5b29 --- /dev/null +++ b/src/templates/frontends/flipper/tailwind.config.js.hbs @@ -0,0 +1,2 @@ +import config from '../ui/tailwind.config'; +export default config; diff --git a/src/templates/frontends/flipper/tsconfig.json.hbs b/src/templates/frontends/flipper/tsconfig.json.hbs new file mode 100644 index 00000000..a7fc6fbf --- /dev/null +++ b/src/templates/frontends/flipper/tsconfig.json.hbs @@ -0,0 +1,25 @@ +{ + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx", + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["src"], + "references": [{ "path": "./tsconfig.node.json" }] +} diff --git a/src/templates/frontends/flipper/tsconfig.node.json.hbs b/src/templates/frontends/flipper/tsconfig.node.json.hbs new file mode 100644 index 00000000..42872c59 --- /dev/null +++ b/src/templates/frontends/flipper/tsconfig.node.json.hbs @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "composite": true, + "skipLibCheck": true, + "module": "ESNext", + "moduleResolution": "bundler", + "allowSyntheticDefaultImports": true + }, + "include": ["vite.config.ts"] +} diff --git a/src/templates/frontends/flipper/vite.config.ts.hbs b/src/templates/frontends/flipper/vite.config.ts.hbs new file mode 100644 index 00000000..4e7004eb --- /dev/null +++ b/src/templates/frontends/flipper/vite.config.ts.hbs @@ -0,0 +1,7 @@ +import react from '@vitejs/plugin-react'; +import { defineConfig } from 'vite'; + +// https://vitejs.dev/config/ +export default defineConfig({ + plugins: [react()], +}); diff --git a/src/templates/frontends/package.json.hbs b/src/templates/frontends/package.json.hbs new file mode 100644 index 00000000..21927c7f --- /dev/null +++ b/src/templates/frontends/package.json.hbs @@ -0,0 +1,37 @@ +{ + "name": "{{project_name}}", + "version": "1.0.0", + "description": "Example dApps for ink! contracts.", + "main": "index.js", + "keywords": [], + {{#if author_email}} + "author": "{{author_name}} <{{author_email}}>", + {{else}} + "author": "{{author_name}}", + {{/if}} + "license": "MIT", + "scripts": { + "format": "biome format . --write", + "lint": "biome check ./*", + "lint:fix": "pnpm lint --apply-unsafe", + "flipper": "pnpm --filter flipper dev", + "psp22": "pnpm --filter psp22 dev" + }, + "packages": [ + "ui", + "*" + ], + "dependencies": { + "react": "^18.2.0", + "react-dom": "^18.2.0", + "useink": "^1.13.0" + }, + "devDependencies": { + "@biomejs/biome": "^1.3.3", + "autoprefixer": "^10.4.14", + "classnames": "^2.3.2", + "postcss": "^8.4.24", + "tailwindcss": "^3.3.2" + }, + "packageManager": "pnpm@8.4.0" +} diff --git a/src/templates/frontends/pnpm-workspace.yaml.hbs b/src/templates/frontends/pnpm-workspace.yaml.hbs new file mode 100644 index 00000000..34a1d005 --- /dev/null +++ b/src/templates/frontends/pnpm-workspace.yaml.hbs @@ -0,0 +1,3 @@ +packages: + - "ui" + - "*" \ No newline at end of file diff --git a/src/templates/frontends/psp22/README.md.hbs b/src/templates/frontends/psp22/README.md.hbs new file mode 100644 index 00000000..ad88c572 --- /dev/null +++ b/src/templates/frontends/psp22/README.md.hbs @@ -0,0 +1,4 @@ +# Have Questions? + +For any questions about building front end applications with [useink](https://use.ink/frontend/overview/), join the [Element chat](https://matrix.to/#/%23useink:parity.io). + diff --git a/src/templates/frontends/psp22/assets/psp22.json.hbs b/src/templates/frontends/psp22/assets/psp22.json.hbs new file mode 100644 index 00000000..6e0edb48 --- /dev/null +++ b/src/templates/frontends/psp22/assets/psp22.json.hbs @@ -0,0 +1,972 @@ +{ + "source": { + "hash": "0x6bf4efb5bc41b9dba3c3bdbd602e513a253ef3482e91168fb30b7b3d6eb3b6fa", + "language": "ink! 4.3.0", + "compiler": "rustc 1.70.0", + "build_info": { + "build_mode": "Debug", + "cargo_contract_version": "3.2.0", + "rust_toolchain": "stable-x86_64-unknown-linux-gnu", + "wasm_opt_settings": { + "keep_debug_symbols": false, + "optimization_passes": "Z" + } + } + }, + "contract": { + "name": "psp22", + "version": "0.1.0", + "authors": [ + "prxgr4mm3r" + ] + }, + "spec": { + "constructors": [ + { + "args": [ + { + "label": "total_supply", + "type": { + "displayName": [ + "Balance" + ], + "type": 0 + } + } + ], + "default": false, + "docs": [], + "label": "new", + "payable": false, + "returnType": { + "displayName": [ + "ink_primitives", + "ConstructorResult" + ], + "type": 1 + }, + "selector": "0x9bae9d5e" + } + ], + "docs": [], + "environment": { + "accountId": { + "displayName": [ + "AccountId" + ], + "type": 5 + }, + "balance": { + "displayName": [ + "Balance" + ], + "type": 0 + }, + "blockNumber": { + "displayName": [ + "BlockNumber" + ], + "type": 17 + }, + "chainExtension": { + "displayName": [ + "ChainExtension" + ], + "type": 18 + }, + "hash": { + "displayName": [ + "Hash" + ], + "type": 16 + }, + "maxEventTopics": 4, + "timestamp": { + "displayName": [ + "Timestamp" + ], + "type": 13 + } + }, + "events": [ + { + "args": [ + { + "docs": [], + "indexed": true, + "label": "from", + "type": { + "displayName": [ + "Option" + ], + "type": 15 + } + }, + { + "docs": [], + "indexed": true, + "label": "to", + "type": { + "displayName": [ + "Option" + ], + "type": 15 + } + }, + { + "docs": [], + "indexed": false, + "label": "value", + "type": { + "displayName": [ + "Balance" + ], + "type": 0 + } + } + ], + "docs": [], + "label": "TransferEvent" + }, + { + "args": [ + { + "docs": [], + "indexed": true, + "label": "owner", + "type": { + "displayName": [ + "AccountId" + ], + "type": 5 + } + }, + { + "docs": [], + "indexed": true, + "label": "spender", + "type": { + "displayName": [ + "AccountId" + ], + "type": 5 + } + }, + { + "docs": [], + "indexed": false, + "label": "value", + "type": { + "displayName": [ + "Balance" + ], + "type": 0 + } + } + ], + "docs": [], + "label": "ApprovalEvent" + } + ], + "lang_error": { + "displayName": [ + "ink", + "LangError" + ], + "type": 3 + }, + "messages": [ + { + "args": [], + "default": false, + "docs": [], + "label": "get_total_supply", + "mutates": false, + "payable": false, + "returnType": { + "displayName": [ + "ink", + "MessageResult" + ], + "type": 4 + }, + "selector": "0xb079adab" + }, + { + "args": [ + { + "label": "owner", + "type": { + "displayName": [ + "psp22_external", + "AllowanceInput1" + ], + "type": 5 + } + }, + { + "label": "spender", + "type": { + "displayName": [ + "psp22_external", + "AllowanceInput2" + ], + "type": 5 + } + } + ], + "default": false, + "docs": [], + "label": "PSP22::allowance", + "mutates": false, + "payable": false, + "returnType": { + "displayName": [ + "ink", + "MessageResult" + ], + "type": 4 + }, + "selector": "0x4d47d921" + }, + { + "args": [ + { + "label": "spender", + "type": { + "displayName": [ + "psp22_external", + "IncreaseAllowanceInput1" + ], + "type": 5 + } + }, + { + "label": "delta_value", + "type": { + "displayName": [ + "psp22_external", + "IncreaseAllowanceInput2" + ], + "type": 0 + } + } + ], + "default": false, + "docs": [], + "label": "PSP22::increase_allowance", + "mutates": true, + "payable": false, + "returnType": { + "displayName": [ + "ink", + "MessageResult" + ], + "type": 8 + }, + "selector": "0x96d6b57a" + }, + { + "args": [], + "default": false, + "docs": [], + "label": "PSP22::total_supply", + "mutates": false, + "payable": false, + "returnType": { + "displayName": [ + "ink", + "MessageResult" + ], + "type": 4 + }, + "selector": "0x162df8c2" + }, + { + "args": [ + { + "label": "spender", + "type": { + "displayName": [ + "psp22_external", + "DecreaseAllowanceInput1" + ], + "type": 5 + } + }, + { + "label": "delta_value", + "type": { + "displayName": [ + "psp22_external", + "DecreaseAllowanceInput2" + ], + "type": 0 + } + } + ], + "default": false, + "docs": [], + "label": "PSP22::decrease_allowance", + "mutates": true, + "payable": false, + "returnType": { + "displayName": [ + "ink", + "MessageResult" + ], + "type": 8 + }, + "selector": "0xfecb57d5" + }, + { + "args": [ + { + "label": "owner", + "type": { + "displayName": [ + "psp22_external", + "BalanceOfInput1" + ], + "type": 5 + } + } + ], + "default": false, + "docs": [], + "label": "PSP22::balance_of", + "mutates": false, + "payable": false, + "returnType": { + "displayName": [ + "ink", + "MessageResult" + ], + "type": 4 + }, + "selector": "0x6568382f" + }, + { + "args": [ + { + "label": "to", + "type": { + "displayName": [ + "psp22_external", + "TransferInput1" + ], + "type": 5 + } + }, + { + "label": "value", + "type": { + "displayName": [ + "psp22_external", + "TransferInput2" + ], + "type": 0 + } + }, + { + "label": "data", + "type": { + "displayName": [ + "psp22_external", + "TransferInput3" + ], + "type": 14 + } + } + ], + "default": false, + "docs": [], + "label": "PSP22::transfer", + "mutates": true, + "payable": false, + "returnType": { + "displayName": [ + "ink", + "MessageResult" + ], + "type": 8 + }, + "selector": "0xdb20f9f5" + }, + { + "args": [ + { + "label": "from", + "type": { + "displayName": [ + "psp22_external", + "TransferFromInput1" + ], + "type": 5 + } + }, + { + "label": "to", + "type": { + "displayName": [ + "psp22_external", + "TransferFromInput2" + ], + "type": 5 + } + }, + { + "label": "value", + "type": { + "displayName": [ + "psp22_external", + "TransferFromInput3" + ], + "type": 0 + } + }, + { + "label": "data", + "type": { + "displayName": [ + "psp22_external", + "TransferFromInput4" + ], + "type": 14 + } + } + ], + "default": false, + "docs": [], + "label": "PSP22::transfer_from", + "mutates": true, + "payable": false, + "returnType": { + "displayName": [ + "ink", + "MessageResult" + ], + "type": 8 + }, + "selector": "0x54b3c76e" + } + ] + }, + "storage": { + "root": { + "layout": { + "struct": { + "fields": [ + { + "layout": { + "struct": { + "fields": [ + { + "layout": { + "root": { + "layout": { + "leaf": { + "key": "0x270a8fc3", + "ty": 0 + } + }, + "root_key": "0x270a8fc3" + } + }, + "name": "supply" + }, + { + "layout": { + "root": { + "layout": { + "leaf": { + "key": "0xc2664826", + "ty": 0 + } + }, + "root_key": "0xc2664826" + } + }, + "name": "balances" + }, + { + "layout": { + "root": { + "layout": { + "leaf": { + "key": "0xf8d71e22", + "ty": 0 + } + }, + "root_key": "0xf8d71e22" + } + }, + "name": "allowances" + } + ], + "name": "Data" + } + }, + "name": "psp22" + } + ], + "name": "Psp22" + } + }, + "root_key": "0x00000000" + } + }, + "types": [ + { + "id": 0, + "type": { + "def": { + "primitive": "u128" + } + } + }, + { + "id": 1, + "type": { + "def": { + "variant": { + "variants": [ + { + "fields": [ + { + "type": 2 + } + ], + "index": 0, + "name": "Ok" + }, + { + "fields": [ + { + "type": 3 + } + ], + "index": 1, + "name": "Err" + } + ] + } + }, + "params": [ + { + "name": "T", + "type": 2 + }, + { + "name": "E", + "type": 3 + } + ], + "path": [ + "Result" + ] + } + }, + { + "id": 2, + "type": { + "def": { + "tuple": [] + } + } + }, + { + "id": 3, + "type": { + "def": { + "variant": { + "variants": [ + { + "index": 1, + "name": "CouldNotReadInput" + } + ] + } + }, + "path": [ + "ink_primitives", + "LangError" + ] + } + }, + { + "id": 4, + "type": { + "def": { + "variant": { + "variants": [ + { + "fields": [ + { + "type": 0 + } + ], + "index": 0, + "name": "Ok" + }, + { + "fields": [ + { + "type": 3 + } + ], + "index": 1, + "name": "Err" + } + ] + } + }, + "params": [ + { + "name": "T", + "type": 0 + }, + { + "name": "E", + "type": 3 + } + ], + "path": [ + "Result" + ] + } + }, + { + "id": 5, + "type": { + "def": { + "composite": { + "fields": [ + { + "type": 6, + "typeName": "[u8; 32]" + } + ] + } + }, + "path": [ + "ink_primitives", + "types", + "AccountId" + ] + } + }, + { + "id": 6, + "type": { + "def": { + "array": { + "len": 32, + "type": 7 + } + } + } + }, + { + "id": 7, + "type": { + "def": { + "primitive": "u8" + } + } + }, + { + "id": 8, + "type": { + "def": { + "variant": { + "variants": [ + { + "fields": [ + { + "type": 9 + } + ], + "index": 0, + "name": "Ok" + }, + { + "fields": [ + { + "type": 3 + } + ], + "index": 1, + "name": "Err" + } + ] + } + }, + "params": [ + { + "name": "T", + "type": 9 + }, + { + "name": "E", + "type": 3 + } + ], + "path": [ + "Result" + ] + } + }, + { + "id": 9, + "type": { + "def": { + "variant": { + "variants": [ + { + "fields": [ + { + "type": 2 + } + ], + "index": 0, + "name": "Ok" + }, + { + "fields": [ + { + "type": 10 + } + ], + "index": 1, + "name": "Err" + } + ] + } + }, + "params": [ + { + "name": "T", + "type": 2 + }, + { + "name": "E", + "type": 10 + } + ], + "path": [ + "Result" + ] + } + }, + { + "id": 10, + "type": { + "def": { + "variant": { + "variants": [ + { + "fields": [ + { + "type": 11, + "typeName": "String" + } + ], + "index": 0, + "name": "Custom" + }, + { + "index": 1, + "name": "InsufficientBalance" + }, + { + "index": 2, + "name": "InsufficientAllowance" + }, + { + "index": 3, + "name": "RecipientIsNotSet" + }, + { + "index": 4, + "name": "SenderIsNotSet" + }, + { + "fields": [ + { + "type": 11, + "typeName": "String" + } + ], + "index": 5, + "name": "SafeTransferCheckFailed" + }, + { + "index": 6, + "name": "PermitInvalidSignature" + }, + { + "index": 7, + "name": "PermitExpired" + }, + { + "fields": [ + { + "type": 12, + "typeName": "NoncesError" + } + ], + "index": 8, + "name": "NoncesError" + } + ] + } + }, + "path": [ + "openbrush_contracts", + "traits", + "errors", + "psp22", + "PSP22Error" + ] + } + }, + { + "id": 11, + "type": { + "def": { + "primitive": "str" + } + } + }, + { + "id": 12, + "type": { + "def": { + "variant": { + "variants": [ + { + "fields": [ + { + "type": 5, + "typeName": "AccountId" + }, + { + "type": 13, + "typeName": "u64" + } + ], + "index": 0, + "name": "InvalidAccountNonce" + }, + { + "index": 1, + "name": "NonceOverflow" + } + ] + } + }, + "path": [ + "openbrush_contracts", + "traits", + "errors", + "nonces", + "NoncesError" + ] + } + }, + { + "id": 13, + "type": { + "def": { + "primitive": "u64" + } + } + }, + { + "id": 14, + "type": { + "def": { + "sequence": { + "type": 7 + } + } + } + }, + { + "id": 15, + "type": { + "def": { + "variant": { + "variants": [ + { + "index": 0, + "name": "None" + }, + { + "fields": [ + { + "type": 5 + } + ], + "index": 1, + "name": "Some" + } + ] + } + }, + "params": [ + { + "name": "T", + "type": 5 + } + ], + "path": [ + "Option" + ] + } + }, + { + "id": 16, + "type": { + "def": { + "composite": { + "fields": [ + { + "type": 6, + "typeName": "[u8; 32]" + } + ] + } + }, + "path": [ + "ink_primitives", + "types", + "Hash" + ] + } + }, + { + "id": 17, + "type": { + "def": { + "primitive": "u32" + } + } + }, + { + "id": 18, + "type": { + "def": { + "variant": {} + }, + "path": [ + "ink_env", + "types", + "NoChainExtension" + ] + } + } + ], + "version": "4" +} \ No newline at end of file diff --git a/src/templates/frontends/psp22/hbs.sh.hbs b/src/templates/frontends/psp22/hbs.sh.hbs new file mode 100644 index 00000000..71db5bbb --- /dev/null +++ b/src/templates/frontends/psp22/hbs.sh.hbs @@ -0,0 +1,4 @@ +files=$(find . -type f | tr " " "\n") +for f in $files; do + mv -- $f "$f.hbs" +done \ No newline at end of file diff --git a/src/templates/frontends/psp22/index.html.hbs b/src/templates/frontends/psp22/index.html.hbs new file mode 100644 index 00000000..e4d2ac59 --- /dev/null +++ b/src/templates/frontends/psp22/index.html.hbs @@ -0,0 +1,13 @@ + + + + + + + ink! Examples + + +
+ + + diff --git a/src/templates/frontends/psp22/package.json.hbs b/src/templates/frontends/psp22/package.json.hbs new file mode 100644 index 00000000..e5018dd8 --- /dev/null +++ b/src/templates/frontends/psp22/package.json.hbs @@ -0,0 +1,26 @@ +{ + "name": "psp22", + "private": true, + "version": "0.1.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc && vite build", + "lint": "eslint src --ext ts,tsx --report-unused-disable-directives --max-warnings 0", + "preview": "vite preview" + }, + "dependencies": { + "ui": "workspace:ui@*" + }, + "devDependencies": { + "@types/react": "^18.0.37", + "@types/react-dom": "^18.0.11", + "@vitejs/plugin-react": "^4.0.0", + "autoprefixer": "^10.4.14", + "eslint": "^8.38.0", + "postcss": "^8.4.24", + "tailwindcss": "^3.3.2", + "typescript": "^5.0.2", + "vite": "^4.3.9" + } +} diff --git a/src/templates/frontends/psp22/postcss.config.js.hbs b/src/templates/frontends/psp22/postcss.config.js.hbs new file mode 100644 index 00000000..2aa7205d --- /dev/null +++ b/src/templates/frontends/psp22/postcss.config.js.hbs @@ -0,0 +1,6 @@ +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +}; diff --git a/src/templates/frontends/psp22/public/logo.svg.hbs b/src/templates/frontends/psp22/public/logo.svg.hbs new file mode 100644 index 00000000..c31ded82 --- /dev/null +++ b/src/templates/frontends/psp22/public/logo.svg.hbs @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/templates/frontends/psp22/src/App.tsx.hbs b/src/templates/frontends/psp22/src/App.tsx.hbs new file mode 100644 index 00000000..41404af1 --- /dev/null +++ b/src/templates/frontends/psp22/src/App.tsx.hbs @@ -0,0 +1,28 @@ +import { DeployerProvider, InkLayout } from 'ui'; +import metadata from '../assets/psp22.json'; +import { Psp22 } from './components'; + +const ONE_BILLION_TOKENS = '1000000000000000000000'; + +function App() { + return ( + + + + + + ); +} + +export default App; diff --git a/src/templates/frontends/psp22/src/Global.css.hbs b/src/templates/frontends/psp22/src/Global.css.hbs new file mode 100644 index 00000000..c2f5c12e --- /dev/null +++ b/src/templates/frontends/psp22/src/Global.css.hbs @@ -0,0 +1,7 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +body { + background-color: #1A1452; +} \ No newline at end of file diff --git a/src/templates/frontends/psp22/src/Other.tsx.hbs b/src/templates/frontends/psp22/src/Other.tsx.hbs new file mode 100644 index 00000000..d8da8abb --- /dev/null +++ b/src/templates/frontends/psp22/src/Other.tsx.hbs @@ -0,0 +1,30 @@ +import { Card } from 'ui'; +import { useCallSubscription, useContract } from 'useink'; +import { pickDecoded } from 'useink/utils'; +import otherContractMetadata from './assets/other_contract.json'; + +interface Props { + address: string; +} + +export const Other: React.FC = ({ address }) => { + const otherContract = useContract(address, otherContractMetadata); + const getOtherSub = useCallSubscription(otherContract, 'get', [], { + defaultCaller: true, + }); + + return ( + +

+ {otherContractMetadata.contract.name.toUpperCase()} +

+ +

+ Flipped:{' '} + + {pickDecoded(getOtherSub.result)?.toString()} + +

+
+ ); +}; diff --git a/src/templates/frontends/psp22/src/components/Psp22/Psp22.tsx.hbs b/src/templates/frontends/psp22/src/components/Psp22/Psp22.tsx.hbs new file mode 100644 index 00000000..7ccd4a80 --- /dev/null +++ b/src/templates/frontends/psp22/src/components/Psp22/Psp22.tsx.hbs @@ -0,0 +1,78 @@ +import { useMemo, useState } from 'react'; +import { Card, Tab, Tabs, formatContractName, useDeployerState } from 'ui'; +import { useCallSubscription, useContract, useTx, useWallet } from 'useink'; +import { useTxNotifications } from 'useink/notifications'; +import { + pickDecoded, + planckToDecimalFormatted, + stringNumberToBN, +} from 'useink/utils'; +import metadata from '../../../assets/psp22.json'; +import { ReadView } from '../ReadView'; +import { WriteView } from '../WriteView'; + +export const Psp22: React.FC = () => { + const { contractAddress } = useDeployerState(); + const psp22 = useContract(contractAddress || '', metadata); + const [view, setView] = useState<'read' | 'write'>('read'); + + const approve = useTx(psp22, 'psp22::approve'); + useTxNotifications(approve); + + const { account } = useWallet(); + const balanceOf = useCallSubscription(psp22, 'psp22::balanceOf', [ + account?.address || '', + ]); + const yourBalance = useMemo(() => { + const stringWithCommas = pickDecoded(balanceOf.result) || '0'; + return stringNumberToBN(stringWithCommas); + }, [balanceOf.result]); + + return ( +
+ +
+

+ {formatContractName(metadata.contract.name)} +

+ + {psp22 && account && ( +
+

Your Balance

+

+ {yourBalance + ? planckToDecimalFormatted(yourBalance, { + api: psp22?.contract.api, + symbol: 'CLAMS', + }) + : '--'} +

+
+ )} +
+ + {psp22 && ( + <> + + setView('read')} isSelected={view === 'read'}> + Read + + setView('write')} + isSelected={view === 'write'} + > + Write + + + + {'read' === view ? ( + + ) : ( + + )} + + )} +
+
+ ); +}; diff --git a/src/templates/frontends/psp22/src/components/Psp22/index.ts.hbs b/src/templates/frontends/psp22/src/components/Psp22/index.ts.hbs new file mode 100644 index 00000000..3c98601e --- /dev/null +++ b/src/templates/frontends/psp22/src/components/Psp22/index.ts.hbs @@ -0,0 +1 @@ +export * from './Psp22.tsx'; diff --git a/src/templates/frontends/psp22/src/components/ReadView/ReadView.tsx.hbs b/src/templates/frontends/psp22/src/components/ReadView/ReadView.tsx.hbs new file mode 100644 index 00000000..b218f04b --- /dev/null +++ b/src/templates/frontends/psp22/src/components/ReadView/ReadView.tsx.hbs @@ -0,0 +1,122 @@ +import { useMemo, useState } from 'react'; +import { Button, InputField } from 'ui'; +import { ChainContract, useCall, useCallSubscription } from 'useink'; +import { + pickDecoded, + planckToDecimalFormatted, + stringNumberToBN, +} from 'useink/utils'; + +const symbol = 'CLAMS'; + +interface Props { + psp22: ChainContract; +} + +export const ReadView: React.FC = ({ psp22 }) => { + const balanceOf = useCall(psp22, 'psp22::balanceOf'); + const [balanceOfOwner, setBalOfOwner] = useState(''); + const balanceResult = useMemo(() => { + // Convert string to BN. e.g. `1,000,000,000` -> BN + const stringWithCommas = pickDecoded(balanceOf.result); + if (!stringWithCommas) return; + return stringNumberToBN(stringWithCommas); + }, [balanceOf.result]); + + const allowance = useCall(psp22, 'psp22::allowance'); + const [allowanceOwner, setAllowanceOwner] = useState(''); + const [allowanceSpender, setAllowanceSpender] = useState(''); + const allowanceResult = useMemo(() => { + const stringWithCommas = pickDecoded(allowance.result); + if (!stringWithCommas) return; + return stringNumberToBN(stringWithCommas); + }, [allowance.result]); + + const totalSupply = useCallSubscription(psp22, 'psp22::totalSupply'); + const totalSupplyResult = useMemo(() => { + if (!totalSupply || !totalSupply?.result) return; + const stringWithCommas = pickDecoded(totalSupply.result) || '0'; + return stringNumberToBN(stringWithCommas); + }, [totalSupply.result]); + + return ( +
+

+ Total Supply:{' '} + {totalSupplyResult + ? planckToDecimalFormatted(totalSupplyResult, { + api: psp22?.contract.api, + symbol, + }) + : '--'} +

+ +
+ + setBalOfOwner(e.target.value)} + placeholder='Enter an Address...' + disabled={balanceOf.isSubmitting} + /> + + + {balanceResult && ( +

+ {planckToDecimalFormatted(balanceResult, { + api: psp22?.contract.api, + symbol: 'CLAMS', + })} +

+ )} +
+ +
+ + setAllowanceOwner(e.target.value)} + disabled={allowance.isSubmitting} + placeholder='Enter an Address...' + /> + + setAllowanceSpender(e.target.value)} + disabled={allowance.isSubmitting} + placeholder='Enter an Address...' + /> + + + {allowanceResult && ( +

+ {planckToDecimalFormatted(allowanceResult, { + api: psp22?.contract.api, + symbol: 'CLAMS', + })} +

+ )} +
+
+ ); +}; diff --git a/src/templates/frontends/psp22/src/components/ReadView/index.ts.hbs b/src/templates/frontends/psp22/src/components/ReadView/index.ts.hbs new file mode 100644 index 00000000..c4729ab7 --- /dev/null +++ b/src/templates/frontends/psp22/src/components/ReadView/index.ts.hbs @@ -0,0 +1 @@ +export * from './ReadView'; diff --git a/src/templates/frontends/psp22/src/components/WriteView/WriteView.tsx.hbs b/src/templates/frontends/psp22/src/components/WriteView/WriteView.tsx.hbs new file mode 100644 index 00000000..3f3c4257 --- /dev/null +++ b/src/templates/frontends/psp22/src/components/WriteView/WriteView.tsx.hbs @@ -0,0 +1,216 @@ +import { useState } from 'react'; +import { BigIntInputField, Button, ConnectButton, InputField, Label } from 'ui'; +import { ChainContract, useTx, useWallet } from 'useink'; +import { useTxNotifications } from 'useink/notifications'; +import { + isPendingSignature, + planckToDecimalFormatted, + shouldDisable, +} from 'useink/utils'; + +interface Props { + psp22: ChainContract; +} + +export const WriteView: React.FC = ({ psp22 }) => { + const { account } = useWallet(); + + const [transferAmount, setTransferAmount] = useState(0n); + const [transferToAddress, setTransferToAddress] = useState(''); + const transfer = useTx(psp22, 'psp22::transfer'); + useTxNotifications(transfer); + + const [approveAmount, setApproveAmount] = useState(0n); + const [approveSpender, setApproveSpender] = useState(''); + const increaseAllowance = useTx(psp22, 'psp22::increaseAllowance'); + useTxNotifications(increaseAllowance); + const decreaseAllowance = useTx(psp22, 'psp22::decreaseAllowance'); + useTxNotifications(decreaseAllowance); + + const [transferFromAmount, setTransferFromAmount] = useState(0n); + const [transferFromAddress, setTransferFromAddress] = useState(''); + const [transferFromToAddress, setTransferFromToAddress] = useState(''); + const transferFrom = useTx(psp22, 'psp22::transferFrom'); + useTxNotifications(transferFrom); + + if (!account) return ; + + return ( +
+
+ + setTransferToAddress(e.target.value)} + placeholder='To address...' + disabled={shouldDisable(transfer)} + /> + + + {transferAmount !== undefined && ( +

+ {planckToDecimalFormatted(transferAmount, { + api: psp22.contract.api, + symbol: 'CLAMS', + })} +

+ )} +
+ + +
+ +
+ + setApproveSpender(e.target.value)} + placeholder='Spender address...' + disabled={shouldDisable(increaseAllowance)} + /> + + + {transferAmount !== undefined && ( +

+ {planckToDecimalFormatted(approveAmount, { + api: psp22.contract.api, + symbol: 'CLAMS', + })} +

+ )} +
+ + +
+ +
+ + setApproveSpender(e.target.value)} + placeholder='Spender address...' + disabled={shouldDisable(decreaseAllowance)} + /> + + + {transferAmount !== undefined && ( +

+ {planckToDecimalFormatted(approveAmount, { + api: psp22.contract.api, + symbol: 'CLAMS', + })} +

+ )} +
+ + +
+ +
+ + setTransferFromAddress(e.target.value)} + placeholder='From address...' + disabled={shouldDisable(transferFrom)} + /> + + setTransferFromToAddress(e.target.value)} + placeholder='To address...' + disabled={shouldDisable(transferFrom)} + /> + + + {transferAmount !== undefined && ( +

+ {planckToDecimalFormatted(transferFromAmount, { + api: psp22.contract.api, + symbol: 'CLAMS', + })} +

+ )} +
+ + +
+
+ ); +}; diff --git a/src/templates/frontends/psp22/src/components/WriteView/index.ts.hbs b/src/templates/frontends/psp22/src/components/WriteView/index.ts.hbs new file mode 100644 index 00000000..98738aa5 --- /dev/null +++ b/src/templates/frontends/psp22/src/components/WriteView/index.ts.hbs @@ -0,0 +1 @@ +export * from './WriteView'; diff --git a/src/templates/frontends/psp22/src/components/index.ts.hbs b/src/templates/frontends/psp22/src/components/index.ts.hbs new file mode 100644 index 00000000..2c112e17 --- /dev/null +++ b/src/templates/frontends/psp22/src/components/index.ts.hbs @@ -0,0 +1 @@ +export * from './Psp22'; diff --git a/src/templates/frontends/psp22/src/main.tsx.hbs b/src/templates/frontends/psp22/src/main.tsx.hbs new file mode 100644 index 00000000..df24d5af --- /dev/null +++ b/src/templates/frontends/psp22/src/main.tsx.hbs @@ -0,0 +1,27 @@ +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import 'ui/style.css'; +import { UseInkProvider } from 'useink'; +import { RococoContractsTestnet } from 'useink/chains'; +import { NotificationsProvider } from 'useink/notifications'; +import metadata from '../assets/psp22.json'; +import App from './App.tsx'; +import './Global.css'; + +ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render( + + + + + + + , +); diff --git a/src/templates/frontends/psp22/src/vite-env.d.ts.hbs b/src/templates/frontends/psp22/src/vite-env.d.ts.hbs new file mode 100644 index 00000000..11f02fe2 --- /dev/null +++ b/src/templates/frontends/psp22/src/vite-env.d.ts.hbs @@ -0,0 +1 @@ +/// diff --git a/src/templates/frontends/psp22/tailwind.config.js.hbs b/src/templates/frontends/psp22/tailwind.config.js.hbs new file mode 100644 index 00000000..ceec5b29 --- /dev/null +++ b/src/templates/frontends/psp22/tailwind.config.js.hbs @@ -0,0 +1,2 @@ +import config from '../ui/tailwind.config'; +export default config; diff --git a/src/templates/frontends/psp22/tsconfig.json.hbs b/src/templates/frontends/psp22/tsconfig.json.hbs new file mode 100644 index 00000000..1922c64a --- /dev/null +++ b/src/templates/frontends/psp22/tsconfig.json.hbs @@ -0,0 +1,25 @@ +{ + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx", + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["src", "../../ui/src/contexts/DeployerContextTsx"], + "references": [{ "path": "./tsconfig.node.json" }] +} diff --git a/src/templates/frontends/psp22/tsconfig.node.json.hbs b/src/templates/frontends/psp22/tsconfig.node.json.hbs new file mode 100644 index 00000000..42872c59 --- /dev/null +++ b/src/templates/frontends/psp22/tsconfig.node.json.hbs @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "composite": true, + "skipLibCheck": true, + "module": "ESNext", + "moduleResolution": "bundler", + "allowSyntheticDefaultImports": true + }, + "include": ["vite.config.ts"] +} diff --git a/src/templates/frontends/psp22/vite.config.ts.hbs b/src/templates/frontends/psp22/vite.config.ts.hbs new file mode 100644 index 00000000..4e7004eb --- /dev/null +++ b/src/templates/frontends/psp22/vite.config.ts.hbs @@ -0,0 +1,7 @@ +import react from '@vitejs/plugin-react'; +import { defineConfig } from 'vite'; + +// https://vitejs.dev/config/ +export default defineConfig({ + plugins: [react()], +}); diff --git a/src/templates/frontends/ui/README.md.hbs b/src/templates/frontends/ui/README.md.hbs new file mode 100644 index 00000000..977e9cd9 --- /dev/null +++ b/src/templates/frontends/ui/README.md.hbs @@ -0,0 +1,4 @@ +# useink UI + +This is a UI library for demonstrating [useink](https://use.ink/frontend/overview/) integrations, and is the UI library for +[ink-examples](github.com/paritytech/ink-examples/). \ No newline at end of file diff --git a/src/templates/frontends/ui/package.json.hbs b/src/templates/frontends/ui/package.json.hbs new file mode 100644 index 00000000..19fc4d1c --- /dev/null +++ b/src/templates/frontends/ui/package.json.hbs @@ -0,0 +1,35 @@ +{ + "name": "ui", + "version": "0.1.0", + "type": "module", + "license": "Apache 2.0", + "scripts": { + "build": "tsc && vite build && pnpm run build-tailwind", + "dev": "vite build --watch && pnpm run build-tailwind", + "build-tailwind": "NODE_ENV=production npx tailwindcss -o ./dist/style.css -m", + "lint": "rome check ./*", + "lint:fix": "pnpm lint --apply-unsafe" + }, + "sideEffects": false, + "dependencies": { + "@headlessui/react": "^1.7.14", + "@heroicons/react": "^2.0.18", + "@lottiefiles/react-lottie-player": "^3.5.3", + "howler": "^2.2.3" + }, + "devDependencies": { + "@types/howler": "^2.2.7", + "@types/react": "^18.0.37", + "@types/react-dom": "^18.0.11", + "@typescript-eslint/eslint-plugin": "^5.59.8", + "@typescript-eslint/parser": "^5.59.8", + "@vitejs/plugin-react": "^4.0.0", + "typescript": "^5.0.2", + "vite": "^4.3.9", + "vite-plugin-dts": "^2.3.0" + }, + "exports": { + ".": "./src/index.ts", + "./style.css": "./src/index.css" + } +} diff --git a/src/templates/frontends/ui/postcss.config.hbs b/src/templates/frontends/ui/postcss.config.hbs new file mode 100644 index 00000000..e2dc4780 --- /dev/null +++ b/src/templates/frontends/ui/postcss.config.hbs @@ -0,0 +1,6 @@ +module.exports = { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + } +} \ No newline at end of file diff --git a/src/templates/frontends/ui/src/Accounts/Accounts.tsx.hbs b/src/templates/frontends/ui/src/Accounts/Accounts.tsx.hbs new file mode 100644 index 00000000..967b3502 --- /dev/null +++ b/src/templates/frontends/ui/src/Accounts/Accounts.tsx.hbs @@ -0,0 +1,94 @@ +import { Listbox, Transition } from '@headlessui/react'; +import { CheckIcon, ChevronUpDownIcon } from '@heroicons/react/24/solid'; +import classNames from 'classnames'; +import React, { Fragment } from 'react'; +import { useWallet } from 'useink'; +import { ClassNameable } from '../types'; + +export const Accounts: React.FC = ({ className }) => { + const { setAccount, account, accounts } = useWallet(); + + if (!accounts?.length) return null; + + return ( +
+

Accounts

+ + { + setAccount(a); + }} + > +
+ + + {account?.name || account?.address} + + + + + + + {accounts?.map((acc) => ( + + classNames( + 'relative cursor-default select-none py-2 pl-10 pr-4 hover:cursor-pointer', + active ? 'bg-violet-800 text-gray-300' : 'text-gray-300', + ) + } + value={acc} + > + {() => { + const selected = + account && account.address === acc?.address; + return ( + <> + + {acc.name || acc.address} + + + {selected && ( + + + )} + + ); + }} + + ))} + + +
+
+
+ ); +}; diff --git a/src/templates/frontends/ui/src/Accounts/index.ts.hbs b/src/templates/frontends/ui/src/Accounts/index.ts.hbs new file mode 100644 index 00000000..99ab81bf --- /dev/null +++ b/src/templates/frontends/ui/src/Accounts/index.ts.hbs @@ -0,0 +1 @@ +export * from './Accounts'; diff --git a/src/templates/frontends/ui/src/BigIntInputField/BigIntInputField.tsx.hbs b/src/templates/frontends/ui/src/BigIntInputField/BigIntInputField.tsx.hbs new file mode 100644 index 00000000..b52260b4 --- /dev/null +++ b/src/templates/frontends/ui/src/BigIntInputField/BigIntInputField.tsx.hbs @@ -0,0 +1,29 @@ +import React, { DetailedHTMLProps, InputHTMLAttributes } from 'react'; +import { InputField } from '..'; + +interface Props + extends DetailedHTMLProps< + InputHTMLAttributes, + HTMLInputElement + > { + value: string; + onDigitChange: (digits: bigint) => void; +} + +export const BigIntInputField: React.FC = ({ + className, + onDigitChange, + ...props +}) => { + const handleChange = (val: string) => { + if (/^\d*$/.test(val)) onDigitChange(BigInt(val)); + }; + + return ( + handleChange(e.target.value)} + /> + ); +}; diff --git a/src/templates/frontends/ui/src/BigIntInputField/index.ts.hbs b/src/templates/frontends/ui/src/BigIntInputField/index.ts.hbs new file mode 100644 index 00000000..9f60a5ea --- /dev/null +++ b/src/templates/frontends/ui/src/BigIntInputField/index.ts.hbs @@ -0,0 +1 @@ +export * from './BigIntInputField'; diff --git a/src/templates/frontends/ui/src/Button/Button.tsx.hbs b/src/templates/frontends/ui/src/Button/Button.tsx.hbs new file mode 100644 index 00000000..482b776e --- /dev/null +++ b/src/templates/frontends/ui/src/Button/Button.tsx.hbs @@ -0,0 +1,33 @@ +import classNames from 'classnames'; +import React, { + ButtonHTMLAttributes, + DetailedHTMLProps, + PropsWithChildren, +} from 'react'; + +type ButtomHTMLProps = DetailedHTMLProps< + ButtonHTMLAttributes, + HTMLButtonElement +>; + +export type ButtonProps = PropsWithChildren; + +export const Button: React.FC = ({ + children, + className, + onClick, + ...rest +}) => { + const classes = classNames( + 'bg-warning-500/90 hover:bg-warning-600/90 transition ease-in-out px-6 py-2 border-none block', + 'text-base tracking-wide font-semibold rounded-full disabled:bg-warning-500/60', + 'focus:ring-4 border-warning-600 ring-warning-600 disabled:cursor-not-allowed focus:outline-none focus:ring-offset-0 text-white', + className, + ); + + return ( + + ); +}; diff --git a/src/templates/frontends/ui/src/Button/index.ts.hbs b/src/templates/frontends/ui/src/Button/index.ts.hbs new file mode 100644 index 00000000..8b166a86 --- /dev/null +++ b/src/templates/frontends/ui/src/Button/index.ts.hbs @@ -0,0 +1 @@ +export * from './Button'; diff --git a/src/templates/frontends/ui/src/Card/Card.tsx.hbs b/src/templates/frontends/ui/src/Card/Card.tsx.hbs new file mode 100644 index 00000000..bda80d2a --- /dev/null +++ b/src/templates/frontends/ui/src/Card/Card.tsx.hbs @@ -0,0 +1,14 @@ +import classNames from 'classnames'; +import React from 'react'; +import { InkComponent } from '../types'; + +export const Card: React.FC = ({ children, className }) => { + const classes = classNames( + 'text-white/90 lg:max-w-[50%] md:max-w-[65%] max-w-[75%] rounded-2xl bg-white/10 p-3', + className, + ); + + return ( +
{children}
+ ); +}; diff --git a/src/templates/frontends/ui/src/Card/index.ts.hbs b/src/templates/frontends/ui/src/Card/index.ts.hbs new file mode 100644 index 00000000..ca0b0604 --- /dev/null +++ b/src/templates/frontends/ui/src/Card/index.ts.hbs @@ -0,0 +1 @@ +export * from './Card'; diff --git a/src/templates/frontends/ui/src/ConnectButton/ConnectButton.tsx.hbs b/src/templates/frontends/ui/src/ConnectButton/ConnectButton.tsx.hbs new file mode 100644 index 00000000..49eac232 --- /dev/null +++ b/src/templates/frontends/ui/src/ConnectButton/ConnectButton.tsx.hbs @@ -0,0 +1,16 @@ +import React from 'react'; +import { Button, ButtonProps, useUI } from '..'; + +export const ConnectButton: React.FC> = ({ + className, + children = 'Connect Wallet', + ...rest +}) => { + const { setView } = useUI(); + + return ( + + ); +}; diff --git a/src/templates/frontends/ui/src/ConnectButton/index.ts.hbs b/src/templates/frontends/ui/src/ConnectButton/index.ts.hbs new file mode 100644 index 00000000..916bf3f7 --- /dev/null +++ b/src/templates/frontends/ui/src/ConnectButton/index.ts.hbs @@ -0,0 +1 @@ +export * from './ConnectButton'; diff --git a/src/templates/frontends/ui/src/ConnectWallet/ConnectWallet.tsx.hbs b/src/templates/frontends/ui/src/ConnectWallet/ConnectWallet.tsx.hbs new file mode 100644 index 00000000..56525760 --- /dev/null +++ b/src/templates/frontends/ui/src/ConnectWallet/ConnectWallet.tsx.hbs @@ -0,0 +1,62 @@ +import React from 'react'; +import { useInstalledWallets, useUninstalledWallets, useWallet } from 'useink'; +import { Button } from '../Button'; + +export const ConnectWallet: React.FC = () => { + const { account, connect } = useWallet(); + const installed = useInstalledWallets(); + const uninstalled = useUninstalledWallets(); + + if (account) return null; + + return ( +
+

Connect Wallet

+ + {!account && installed.length > 0 && ( +
    + {installed.map((w) => ( +
  • + +
  • + ))} +
+ )} + + {!account && uninstalled.length && installed.length === 0 && ( + <> +

+ Please install one of these supported wallets. +

+ +
    + {uninstalled.map((w) => ( +
  • + +
  • + ))} +
+ + )} +
+ ); +}; diff --git a/src/templates/frontends/ui/src/ConnectWallet/index.ts.hbs b/src/templates/frontends/ui/src/ConnectWallet/index.ts.hbs new file mode 100644 index 00000000..59af7d91 --- /dev/null +++ b/src/templates/frontends/ui/src/ConnectWallet/index.ts.hbs @@ -0,0 +1 @@ +export * from './ConnectWallet'; diff --git a/src/templates/frontends/ui/src/Events/Events.tsx.hbs b/src/templates/frontends/ui/src/Events/Events.tsx.hbs new file mode 100644 index 00000000..9e3a6f46 --- /dev/null +++ b/src/templates/frontends/ui/src/Events/Events.tsx.hbs @@ -0,0 +1,47 @@ +import classNames from 'classnames'; +import React from 'react'; +import { EventRecord } from 'useink/core'; +import { + asContractInstantiatedEvent, + formatEventName, + isContractInstantiatedEvent, + isExtrinsicFailedEvent, +} from 'useink/utils'; +import { ClassNameable } from '..'; + +export interface Props extends ClassNameable { + events?: EventRecord[]; +} + +export const Events: React.FC = ({ events, className }) => { + if (!events || events.length === 0) return null; + + return ( +
    + {events.map((event) => ( +
  • + {isContractInstantiatedEvent(event) ? ( +
    +

    {formatEventName(event)}

    +

    Deployer: {asContractInstantiatedEvent(event)?.deployer}

    + +

    + Contract Address:{' '} + {asContractInstantiatedEvent(event)?.contractAddress} +

    +
    + ) : ( + formatEventName(event) + )} +
  • + ))} +
+ ); +}; diff --git a/src/templates/frontends/ui/src/Events/index.ts.hbs b/src/templates/frontends/ui/src/Events/index.ts.hbs new file mode 100644 index 00000000..e162ea91 --- /dev/null +++ b/src/templates/frontends/ui/src/Events/index.ts.hbs @@ -0,0 +1 @@ +export * from './Events'; diff --git a/src/templates/frontends/ui/src/InkLayout/InkLayout.tsx.hbs b/src/templates/frontends/ui/src/InkLayout/InkLayout.tsx.hbs new file mode 100644 index 00000000..7087f0be --- /dev/null +++ b/src/templates/frontends/ui/src/InkLayout/InkLayout.tsx.hbs @@ -0,0 +1,42 @@ +import classNames from 'classnames'; +import React from 'react'; +import { InkComponent, Notifications } from '..'; +import { LottieEntity } from '../LottieEntity'; +import { UIProvider } from '../contexts'; +import { Screen } from './Screen'; +import { ScreenDarkener } from './ScreenDarkener'; +import { Submarine } from './Submarine'; + +type Props = InkComponent & { + animationSrc?: string; +}; + +const InkLayoutInner: React.FC = ({ children, animationSrc }) => { + return ( +
+ {animationSrc && ( + + )} + + + + {children} + +
+ ); +}; + +export const InkLayout: React.FC = (props) => { + return ( + + + + ); +}; diff --git a/src/templates/frontends/ui/src/InkLayout/ManageWallet/ManageWallet.tsx.hbs b/src/templates/frontends/ui/src/InkLayout/ManageWallet/ManageWallet.tsx.hbs new file mode 100644 index 00000000..c5d3e9af --- /dev/null +++ b/src/templates/frontends/ui/src/InkLayout/ManageWallet/ManageWallet.tsx.hbs @@ -0,0 +1,30 @@ +import React from 'react'; +import { useWallet } from 'useink'; +import { Accounts, Button, ConnectWallet, useUI } from '../..'; + +export const ManageWallet: React.FC = () => { + const { account, disconnect } = useWallet(); + const { setView } = useUI(); + + return ( + <> + + + + {account && ( + <> + + + + + )} + + ); +}; diff --git a/src/templates/frontends/ui/src/InkLayout/ManageWallet/index.ts.hbs b/src/templates/frontends/ui/src/InkLayout/ManageWallet/index.ts.hbs new file mode 100644 index 00000000..5c713bd0 --- /dev/null +++ b/src/templates/frontends/ui/src/InkLayout/ManageWallet/index.ts.hbs @@ -0,0 +1 @@ +export * from './ManageWallet'; diff --git a/src/templates/frontends/ui/src/InkLayout/Screen/Screen.tsx.hbs b/src/templates/frontends/ui/src/InkLayout/Screen/Screen.tsx.hbs new file mode 100644 index 00000000..924aea61 --- /dev/null +++ b/src/templates/frontends/ui/src/InkLayout/Screen/Screen.tsx.hbs @@ -0,0 +1,35 @@ +import classNames from 'classnames'; +import * as React from 'react'; +import { Card, useUI } from '../..'; +import screenMask from '../../assets/screen-mask.svg'; +import { ManageWallet } from '../ManageWallet'; + +export const Screen: React.FC = ({ children }) => { + const { view, showScreen, screenPosition } = useUI(); + + return ( +
+
+ {view === 'contract' && children} + {view === 'wallet' && ( +
+ + + +
+ )} +
+
+ ); +}; diff --git a/src/templates/frontends/ui/src/InkLayout/Screen/index.ts.hbs b/src/templates/frontends/ui/src/InkLayout/Screen/index.ts.hbs new file mode 100644 index 00000000..16c4a6f4 --- /dev/null +++ b/src/templates/frontends/ui/src/InkLayout/Screen/index.ts.hbs @@ -0,0 +1 @@ +export * from './Screen'; diff --git a/src/templates/frontends/ui/src/InkLayout/ScreenDarkener/ScreenDarkener.tsx.hbs b/src/templates/frontends/ui/src/InkLayout/ScreenDarkener/ScreenDarkener.tsx.hbs new file mode 100644 index 00000000..a366dbe7 --- /dev/null +++ b/src/templates/frontends/ui/src/InkLayout/ScreenDarkener/ScreenDarkener.tsx.hbs @@ -0,0 +1,15 @@ +import classNames from 'classnames'; +import * as React from 'react'; +import { useUI } from '../..'; + +export const ScreenDarkener: React.FC = () => { + const { showScreen } = useUI(); + return ( +
+ ); +}; diff --git a/src/templates/frontends/ui/src/InkLayout/ScreenDarkener/index.ts.hbs b/src/templates/frontends/ui/src/InkLayout/ScreenDarkener/index.ts.hbs new file mode 100644 index 00000000..9e6b2591 --- /dev/null +++ b/src/templates/frontends/ui/src/InkLayout/ScreenDarkener/index.ts.hbs @@ -0,0 +1 @@ +export * from './ScreenDarkener'; diff --git a/src/templates/frontends/ui/src/InkLayout/Submarine/Submarine.tsx.hbs b/src/templates/frontends/ui/src/InkLayout/Submarine/Submarine.tsx.hbs new file mode 100644 index 00000000..435bda08 --- /dev/null +++ b/src/templates/frontends/ui/src/InkLayout/Submarine/Submarine.tsx.hbs @@ -0,0 +1,2831 @@ +import classNames from 'classnames'; +import { useCallback, useEffect, useRef } from 'react'; +import * as React from 'react'; +import { useWallet } from 'useink'; +import { useUI } from '../..'; + +export const Submarine: React.FC = () => { + const { + setView, + view, + setScreenPosition, + setPlayAudio, + playAudio, + playLedSwitch, + } = useUI(); + const { account } = useWallet(); + + const buttonClassNames = + 'fill-brand-1000 hover:fill-brand-1000/90 cursor-pointer transition duration-75'; + const activeButtonClasses = 'fill-brand-950 hover:fill-brand-950 cursor-auto'; + + const handleToggleAudio = React.useCallback(() => { + playLedSwitch(); + setPlayAudio(!playAudio); + }, [playLedSwitch, playAudio]); + + const handleScreenOnOff = React.useCallback(() => { + playLedSwitch(); + + if (view === 'off') { + setView('contract'); + return; + } + + setView('off'); + }, [playLedSwitch, view]); + + const handleContractView = React.useCallback(() => { + setView('contract'); + playLedSwitch(); + }, [playLedSwitch]); + + const handleWalletView = React.useCallback(() => { + playLedSwitch(); + setView('wallet'); + }, [playLedSwitch]); + + const handleFaucetClicked = React.useCallback(() => { + const baseURL = 'https://use.ink/faucet'; + const url = account ? `${baseURL}?acc=${account.address}` : baseURL; + window.open(url, '_blank'); + }, [account]); + + const screenRef = useRef(null); + + const setPosition = useCallback(() => { + if (screenRef?.current) { + setScreenPosition({ + top: screenRef.current.getBoundingClientRect().top, + left: screenRef.current.getBoundingClientRect().left, + right: screenRef.current.getBoundingClientRect().right, + bottom: screenRef.current.getBoundingClientRect().bottom, + width: screenRef.current.getBoundingClientRect().width, + height: screenRef.current.getBoundingClientRect().height, + }); + } + }, []); + + useEffect(() => { + setPosition(); + window.addEventListener('resize', setPosition); + return () => window.removeEventListener('resize', setPosition); + }, [setPosition]); + + return ( + /* biome-ignore lint/a11y/noSvgWithoutTitle: We don't want a tooltip to appear */ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ); +}; diff --git a/src/templates/frontends/ui/src/InkLayout/Submarine/index.ts.hbs b/src/templates/frontends/ui/src/InkLayout/Submarine/index.ts.hbs new file mode 100644 index 00000000..fb109b99 --- /dev/null +++ b/src/templates/frontends/ui/src/InkLayout/Submarine/index.ts.hbs @@ -0,0 +1 @@ +export * from './Submarine'; diff --git a/src/templates/frontends/ui/src/InkLayout/index.ts.hbs b/src/templates/frontends/ui/src/InkLayout/index.ts.hbs new file mode 100644 index 00000000..02bd33ff --- /dev/null +++ b/src/templates/frontends/ui/src/InkLayout/index.ts.hbs @@ -0,0 +1 @@ +export * from './InkLayout'; diff --git a/src/templates/frontends/ui/src/InputField/InputField.tsx.hbs b/src/templates/frontends/ui/src/InputField/InputField.tsx.hbs new file mode 100644 index 00000000..f8a81526 --- /dev/null +++ b/src/templates/frontends/ui/src/InputField/InputField.tsx.hbs @@ -0,0 +1,32 @@ +import classnames from 'classnames'; +import React, { + ChangeEventHandler, + DetailedHTMLProps, + InputHTMLAttributes, +} from 'react'; + +interface Props + extends DetailedHTMLProps< + InputHTMLAttributes, + HTMLInputElement + > { + onChange: ChangeEventHandler; + value: string; + placeholder?: string; + disabled?: boolean; +} + +export const InputField: React.FC = ({ className, ...props }) => { + return ( + + ); +}; diff --git a/src/templates/frontends/ui/src/InputField/index.ts.hbs b/src/templates/frontends/ui/src/InputField/index.ts.hbs new file mode 100644 index 00000000..9cd70224 --- /dev/null +++ b/src/templates/frontends/ui/src/InputField/index.ts.hbs @@ -0,0 +1 @@ +export * from './InputField'; diff --git a/src/templates/frontends/ui/src/Label/Label.tsx.hbs b/src/templates/frontends/ui/src/Label/Label.tsx.hbs new file mode 100644 index 00000000..ee376216 --- /dev/null +++ b/src/templates/frontends/ui/src/Label/Label.tsx.hbs @@ -0,0 +1,15 @@ +import classNames from 'classnames'; +import React, { PropsWithChildren } from 'react'; + +interface Props { + className?: string; +} + +export const Label: React.FC> = ({ + children, + className, +}) => ( + +); diff --git a/src/templates/frontends/ui/src/Label/index.ts.hbs b/src/templates/frontends/ui/src/Label/index.ts.hbs new file mode 100644 index 00000000..ca58c61a --- /dev/null +++ b/src/templates/frontends/ui/src/Label/index.ts.hbs @@ -0,0 +1 @@ +export * from './Label'; diff --git a/src/templates/frontends/ui/src/Link/Link.tsx.hbs b/src/templates/frontends/ui/src/Link/Link.tsx.hbs new file mode 100644 index 00000000..347d5cb9 --- /dev/null +++ b/src/templates/frontends/ui/src/Link/Link.tsx.hbs @@ -0,0 +1,26 @@ +import classNames from 'classnames'; +import React, { + AnchorHTMLAttributes, + DetailedHTMLProps, + PropsWithChildren, +} from 'react'; + +type LinkHTMLProps = DetailedHTMLProps< + AnchorHTMLAttributes, + HTMLAnchorElement +>; + +type LinkProps = PropsWithChildren; + +export const Link: React.FC = ({ children, className, ...rest }) => { + const classes = classNames( + 'underline text-sm text-brand-300 font-semibold transition duration-25 hover:text-brand-450', + className, + ); + + return ( + + {children} + + ); +}; diff --git a/src/templates/frontends/ui/src/Link/index.ts.hbs b/src/templates/frontends/ui/src/Link/index.ts.hbs new file mode 100644 index 00000000..3db78f51 --- /dev/null +++ b/src/templates/frontends/ui/src/Link/index.ts.hbs @@ -0,0 +1 @@ +export * from './Link'; diff --git a/src/templates/frontends/ui/src/Logo/Logo.tsx.hbs b/src/templates/frontends/ui/src/Logo/Logo.tsx.hbs new file mode 100644 index 00000000..c870254e --- /dev/null +++ b/src/templates/frontends/ui/src/Logo/Logo.tsx.hbs @@ -0,0 +1,77 @@ +import React from 'react'; +import { ClassNameable } from '../types'; + +export const Logo: React.FC = ({ className }) => ( + + Squink + + + + + + + + + + + + + + + + + + + + + + + +); \ No newline at end of file diff --git a/src/templates/frontends/ui/src/Logo/index.ts.hbs b/src/templates/frontends/ui/src/Logo/index.ts.hbs new file mode 100644 index 00000000..d97c6951 --- /dev/null +++ b/src/templates/frontends/ui/src/Logo/index.ts.hbs @@ -0,0 +1 @@ +export * from './Logo'; diff --git a/src/templates/frontends/ui/src/LottieEntity/LottieEntity.tsx.hbs b/src/templates/frontends/ui/src/LottieEntity/LottieEntity.tsx.hbs new file mode 100644 index 00000000..02fbcde0 --- /dev/null +++ b/src/templates/frontends/ui/src/LottieEntity/LottieEntity.tsx.hbs @@ -0,0 +1,15 @@ +import { Player } from '@lottiefiles/react-lottie-player'; +import React from 'react'; +import { ClassNameable } from '..'; + +type Props = { + src: string; +} & ClassNameable; + +export const LottieEntity: React.FC = ({ src, className }) => { + return ( +
+ +
+ ); +}; diff --git a/src/templates/frontends/ui/src/LottieEntity/index.ts.hbs b/src/templates/frontends/ui/src/LottieEntity/index.ts.hbs new file mode 100644 index 00000000..023cb2d4 --- /dev/null +++ b/src/templates/frontends/ui/src/LottieEntity/index.ts.hbs @@ -0,0 +1 @@ +export * from './LottieEntity'; diff --git a/src/templates/frontends/ui/src/Modal/Modal.tsx.hbs b/src/templates/frontends/ui/src/Modal/Modal.tsx.hbs new file mode 100644 index 00000000..56769aa8 --- /dev/null +++ b/src/templates/frontends/ui/src/Modal/Modal.tsx.hbs @@ -0,0 +1,32 @@ +import classNames from 'classnames'; +import React, { PropsWithChildren } from 'react'; +import { Card } from '../Card'; + +type Props = { + open: boolean; + handleClose?: () => void; + className?: string; +}; + +export const Modal: React.FC> = ({ + open, + children, + className, +}) => { + if (!open) return null; + + return ( +
+
+ + {children} + +
+
+ ); +}; diff --git a/src/templates/frontends/ui/src/Modal/index.ts.hbs b/src/templates/frontends/ui/src/Modal/index.ts.hbs new file mode 100644 index 00000000..cb89ee17 --- /dev/null +++ b/src/templates/frontends/ui/src/Modal/index.ts.hbs @@ -0,0 +1 @@ +export * from './Modal'; diff --git a/src/templates/frontends/ui/src/Notifications/Notifications.tsx.hbs b/src/templates/frontends/ui/src/Notifications/Notifications.tsx.hbs new file mode 100644 index 00000000..7e59f3ed --- /dev/null +++ b/src/templates/frontends/ui/src/Notifications/Notifications.tsx.hbs @@ -0,0 +1,24 @@ +import React from 'react'; +// eslint-disable-next-line import/no-unresolved +import { toNotificationLevel, useNotifications } from 'useink/notifications'; +import { Snackbar } from '../Snackbar'; + +export const Notifications: React.FC = () => { + const { notifications } = useNotifications(); + + if (!notifications.length) return null; + + return ( +
    + {notifications.map((n) => ( +
  • + +
  • + ))} +
+ ); +}; diff --git a/src/templates/frontends/ui/src/Notifications/index.ts.hbs b/src/templates/frontends/ui/src/Notifications/index.ts.hbs new file mode 100644 index 00000000..e7825351 --- /dev/null +++ b/src/templates/frontends/ui/src/Notifications/index.ts.hbs @@ -0,0 +1 @@ +export * from './Notifications'; diff --git a/src/templates/frontends/ui/src/NumberInput/NumberInput.tsx.hbs b/src/templates/frontends/ui/src/NumberInput/NumberInput.tsx.hbs new file mode 100644 index 00000000..0e31cc10 --- /dev/null +++ b/src/templates/frontends/ui/src/NumberInput/NumberInput.tsx.hbs @@ -0,0 +1,86 @@ +import { MinusIcon, PlusIcon } from '@heroicons/react/24/solid'; +import classNames from 'classnames'; +import React from 'react'; +import { ClassNameable } from '..'; + +type Props = ClassNameable & { + onChange: (v: number) => void; + value: number; + placeholder?: string; + disabled?: boolean; + max: number; + min?: number; +}; + +const COMMON_CLASSES = [ + 'bg-white/90 disabled:bg-white/50 text-black/80 border-none focus:outline-none focus-visible:outline-none', + 'focus:outline-none disabled:text-black/50 py-2 flex items-center justify-center disabled:cursor-not-allowed', + 'transition duration-75 text-base', +].join(' '); + +const BUTTON_CLASSES = 'hover:bg-white/80'; + +export const NumberInput: React.FC = ({ + value, + disabled, + onChange, + placeholder, + max, + min = 0, + className, +}) => { + const handleChange = (v: number) => { + const val = v || min; + if (val < min) return; + if (val > max) return; + onChange(val); + }; + + return ( + + + { + handleChange(parseInt(e.target.value) || 0); + }} + /> + + + ); +}; diff --git a/src/templates/frontends/ui/src/NumberInput/index.ts.hbs b/src/templates/frontends/ui/src/NumberInput/index.ts.hbs new file mode 100644 index 00000000..6433506a --- /dev/null +++ b/src/templates/frontends/ui/src/NumberInput/index.ts.hbs @@ -0,0 +1 @@ +export * from './NumberInput'; diff --git a/src/templates/frontends/ui/src/RunResults/RunResults.tsx.hbs b/src/templates/frontends/ui/src/RunResults/RunResults.tsx.hbs new file mode 100644 index 00000000..4fc0ccc3 --- /dev/null +++ b/src/templates/frontends/ui/src/RunResults/RunResults.tsx.hbs @@ -0,0 +1,73 @@ +import classNames from 'classnames'; +import React from 'react'; +import { IApiProvider } from 'useink'; +import { StorageDeposit, WeightV2 } from 'useink/core'; +import { planckToDecimalFormatted } from 'useink/utils'; + +export interface RunResultsProps { + title?: string; + className?: string; + contractAddress?: string; + storageDeposit?: StorageDeposit; + gasConsumed?: WeightV2; + gasRequired?: WeightV2; + chainApi: IApiProvider | undefined; +} + +export const RunResults: React.FC = ({ + title, + className, + storageDeposit, + contractAddress, + gasConsumed, + gasRequired, + chainApi, +}) => { + return ( +
+ {storageDeposit && ( +
+ {title &&

{title}

} + + {contractAddress && ( +
+

Contract Address

+

{contractAddress}

+
+ )} + + {gasConsumed && ( +
+

Gas Consumed

+
    +
  • refTime: {gasConsumed.refTime.toString()}
  • +
  • proof size: {gasConsumed.proofSize.toString()}
  • +
+
+ )} + + {gasRequired && ( +
+

Gas Required

+
    +
  • refTime: {gasRequired.refTime.toString()}
  • +
  • proof size: {gasRequired.proofSize.toString()}
  • +
+
+ )} + + {storageDeposit && ( +
+

+ Storage Deposit:{' '} + {planckToDecimalFormatted(storageDeposit.asCharge, { + api: chainApi?.api, + })} +

+
+ )} +
+ )} +
+ ); +}; diff --git a/src/templates/frontends/ui/src/RunResults/index.ts.hbs b/src/templates/frontends/ui/src/RunResults/index.ts.hbs new file mode 100644 index 00000000..492fdc79 --- /dev/null +++ b/src/templates/frontends/ui/src/RunResults/index.ts.hbs @@ -0,0 +1 @@ +export * from './RunResults'; diff --git a/src/templates/frontends/ui/src/Snackbar/Snackbar.tsx.hbs b/src/templates/frontends/ui/src/Snackbar/Snackbar.tsx.hbs new file mode 100644 index 00000000..45218c13 --- /dev/null +++ b/src/templates/frontends/ui/src/Snackbar/Snackbar.tsx.hbs @@ -0,0 +1,42 @@ +import { Transition } from '@headlessui/react'; +import classNames from 'classnames'; +import React, { Fragment } from 'react'; + +type NotificationLevel = 'error' | 'info' | 'success' | 'warning'; + +type Props = { + className?: string; + message: string; + show: boolean; + type: NotificationLevel; +}; + +const BG_COLORS: Record = { + error: 'bg-error-500', + info: 'bg-info-500', + success: 'bg-success-500', + warning: 'bg-warning-500', +}; + +export const Snackbar: React.FC = ({ show, message, type }) => ( + +
+
+ + {message} + +
+
+
+); diff --git a/src/templates/frontends/ui/src/Snackbar/index.ts.hbs b/src/templates/frontends/ui/src/Snackbar/index.ts.hbs new file mode 100644 index 00000000..834dd92a --- /dev/null +++ b/src/templates/frontends/ui/src/Snackbar/index.ts.hbs @@ -0,0 +1 @@ +export * from './Snackbar'; diff --git a/src/templates/frontends/ui/src/Tabs/Tabs.tsx.hbs b/src/templates/frontends/ui/src/Tabs/Tabs.tsx.hbs new file mode 100644 index 00000000..9eb90662 --- /dev/null +++ b/src/templates/frontends/ui/src/Tabs/Tabs.tsx.hbs @@ -0,0 +1,47 @@ +import classNames from 'classnames'; +import React, { PropsWithChildren } from 'react'; + +interface Props { + className?: string; +} + +export const Tabs: React.FC> = ({ + children, + className, +}) => ( +
+ {children} +
+); + +interface TabProps { + className?: string; + isSelected: boolean; + onClick: () => void; +} + +export const Tab: React.FC> = ({ + children, + className, + isSelected, + onClick, +}) => ( + +); diff --git a/src/templates/frontends/ui/src/Tabs/index.ts.hbs b/src/templates/frontends/ui/src/Tabs/index.ts.hbs new file mode 100644 index 00000000..856dbbb3 --- /dev/null +++ b/src/templates/frontends/ui/src/Tabs/index.ts.hbs @@ -0,0 +1 @@ +export * from './Tabs'; diff --git a/src/templates/frontends/ui/src/ToggleSwitch/ToggleSwitch.tsx.hbs b/src/templates/frontends/ui/src/ToggleSwitch/ToggleSwitch.tsx.hbs new file mode 100644 index 00000000..7fb9e3e0 --- /dev/null +++ b/src/templates/frontends/ui/src/ToggleSwitch/ToggleSwitch.tsx.hbs @@ -0,0 +1,40 @@ +import { Switch } from '@headlessui/react'; +import classNames from 'classnames'; +import React from 'react'; + +interface Props { + enabled: boolean; + handleClick: () => void; + screenReader?: string; +} + +export const ToggleSwitch: React.FC = ({ + enabled, + handleClick: handleClose, + screenReader, +}) => ( +
+ + {screenReader && {screenReader}} + +
+); diff --git a/src/templates/frontends/ui/src/ToggleSwitch/index.ts.hbs b/src/templates/frontends/ui/src/ToggleSwitch/index.ts.hbs new file mode 100644 index 00000000..abd33f3c --- /dev/null +++ b/src/templates/frontends/ui/src/ToggleSwitch/index.ts.hbs @@ -0,0 +1 @@ +export * from './ToggleSwitch'; diff --git a/src/templates/frontends/ui/src/assets/react.svg.hbs b/src/templates/frontends/ui/src/assets/react.svg.hbs new file mode 100644 index 00000000..6c87de9b --- /dev/null +++ b/src/templates/frontends/ui/src/assets/react.svg.hbs @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/templates/frontends/ui/src/assets/screen-mask.svg.hbs b/src/templates/frontends/ui/src/assets/screen-mask.svg.hbs new file mode 100644 index 00000000..9b1eb24a --- /dev/null +++ b/src/templates/frontends/ui/src/assets/screen-mask.svg.hbs @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/templates/frontends/ui/src/contexts/DeployerContext/DeployerContext.tsx.hbs b/src/templates/frontends/ui/src/contexts/DeployerContext/DeployerContext.tsx.hbs new file mode 100644 index 00000000..16dc3929 --- /dev/null +++ b/src/templates/frontends/ui/src/contexts/DeployerContext/DeployerContext.tsx.hbs @@ -0,0 +1,169 @@ +import React, { + PropsWithChildren, + createContext, + useCallback, + useContext, + useEffect, + useMemo, +} from 'react'; +import { useApi, useDeployer, useMetadata, useWallet } from 'useink'; +import { useTxNotifications } from 'useink/notifications'; +import { + isFinalized, + isPendingSignature, + shouldDisableStrict, +} from 'useink/utils'; +import { RunResults } from '../..'; +import { Button } from '../../Button'; +import { Card } from '../../Card'; +import { ConnectButton } from '../../ConnectButton'; +import { formatContractName } from '../../utils'; + +export interface DeployerContextProps { + metadata: Record; + constructorArgs: Record | undefined; + constructorName: string; + codeHash: string; +} + +export interface DeployerState { + contractAddress?: string; + clearContract: () => void; +} + +const DeployerContext = createContext({ + clearContract: () => null, +}); + +function getContractAddress(key: string): string | null { + return localStorage.getItem(key); +} + +function setContractAddress(key: string, address: string) { + localStorage.setItem(key, address); +} + +function removeContractAddress(key: string) { + if (getContractAddress(key)) localStorage.removeItem(key); +} + +export const DeployerProvider: React.FC< + PropsWithChildren +> = ({ children, metadata, constructorArgs, constructorName, codeHash }) => { + const chainApi = useApi(); + const { account } = useWallet(); + const M = useMetadata({ requireWasm: false }, metadata); + const D = useDeployer(); + useTxNotifications(D); + + const name = useMemo( + () => (metadata?.contract as { name?: string })?.name || '', + [metadata], + ); + const storageKey = name ? `${name}-address` : ''; + + let savedAddress = useMemo(() => getContractAddress(storageKey), [name]); + const clearContract = useCallback(() => { + removeContractAddress(storageKey); + savedAddress = null; + }, [name]); + + const signAndSend = useCallback(() => { + D.signAndSend(M.abi, constructorName, constructorArgs, { codeHash }); + }, [M.abi]); + + useEffect(() => { + chainApi?.api && + M.abi && + D.dryRun(M.abi, constructorName, constructorArgs, { + codeHash, + defaultCaller: true, + }); + }, [M.abi, chainApi?.api]); + + useEffect(() => { + if (D.contractAddress && D.wasDeployed && isFinalized(D)) { + setContractAddress(storageKey, D.contractAddress); + } + }, [D.status]); + + return ( + + {savedAddress || (D.wasDeployed && isFinalized(D)) ? ( + children + ) : ( +
+ +
+
+

+ {formatContractName(name)} +

+

+ {D.storageDeposit + ? "Let's first deploy the contract!" + : 'Loading...'} +

+
+ + {D.storageDeposit && ( + + )} + + {M.error && ( +

+ Metadata: + {M.error} +

+ )} + + {account && D.error && ( +

+ Deployer: + {D.error} +

+ )} + + {account ? ( + + ) : ( + + )} +
+
+
+ )} + +
+ ); +}; + +export const useDeployerState = () => useContext(DeployerContext); diff --git a/src/templates/frontends/ui/src/contexts/DeployerContext/index.ts.hbs b/src/templates/frontends/ui/src/contexts/DeployerContext/index.ts.hbs new file mode 100644 index 00000000..f7417ca4 --- /dev/null +++ b/src/templates/frontends/ui/src/contexts/DeployerContext/index.ts.hbs @@ -0,0 +1 @@ +export * from './DeployerContext'; diff --git a/src/templates/frontends/ui/src/contexts/UIContext/UIContext.tsx.hbs b/src/templates/frontends/ui/src/contexts/UIContext/UIContext.tsx.hbs new file mode 100644 index 00000000..2fe58442 --- /dev/null +++ b/src/templates/frontends/ui/src/contexts/UIContext/UIContext.tsx.hbs @@ -0,0 +1,105 @@ +import { Howl } from 'howler'; + +import React, { + PropsWithChildren, + createContext, + useCallback, + useEffect, + useMemo, + useState, +} from 'react'; + +type View = 'off' | 'contract' | 'wallet'; + +interface ScreenPosition { + top: number; + left: number; + right: number; + bottom: number; + width: number; + height: number; +} + +export interface UIState { + showScreen: boolean; + showClearContract: boolean; + setShowClearContract: (show: boolean) => void; + setView: (view: View) => void; + view: View; + playAudio: boolean; + setPlayAudio: (play: boolean) => void; + playLedSwitch: () => void; + setScreenPosition: (pos: ScreenPosition) => void; + screenPosition: ScreenPosition | undefined; +} + +export const UIContext = createContext({ + setView: () => null, + view: 'contract', + showScreen: true, + setShowClearContract: () => null, + showClearContract: true, + setPlayAudio: () => null, + playLedSwitch: () => null, + playAudio: true, + setScreenPosition: () => null, + screenPosition: undefined, +}); + +export const UIProvider: React.FC = ({ children }) => { + const [waterSounds, setWaterSounds] = useState(); + const [ledEffect, setLedEffect] = useState(); + const [showClearContract, setShowClearContract] = useState(false); + const [playAudio, setPlayAudio] = useState(false); + const [view, setView] = useState('contract'); + const showScreen = useMemo(() => view !== 'off', [view]); + const [screenPosition, setScreenPosition] = useState(); + + useEffect(() => { + setLedEffect( + new Howl({ + src: 'https://github.com/paritytech/ink-examples/raw/sr/submarine/ui/src/assets/audio/led.wav', + volume: 0.8, + html5: true, + }), + ); + + setWaterSounds( + new Howl({ + src: 'https://github.com/paritytech/ink-examples/raw/sr/submarine/ui/src/assets/audio/underwater.mp3', + loop: true, + volume: 1, + html5: true, + }).on('load', () => { + setPlayAudio(true); + }), + ); + }, []); + + const playLedSwitch = useCallback(() => { + playAudio && ledEffect?.play(); + }, [ledEffect, playAudio]); + + useEffect(() => { + playAudio ? waterSounds?.play() : waterSounds?.stop(); + }, [playAudio]); + + return ( + + {children} + + ); +}; diff --git a/src/templates/frontends/ui/src/contexts/UIContext/index.ts.hbs b/src/templates/frontends/ui/src/contexts/UIContext/index.ts.hbs new file mode 100644 index 00000000..40b50346 --- /dev/null +++ b/src/templates/frontends/ui/src/contexts/UIContext/index.ts.hbs @@ -0,0 +1 @@ +export * from './UIContext'; diff --git a/src/templates/frontends/ui/src/contexts/index.ts.hbs b/src/templates/frontends/ui/src/contexts/index.ts.hbs new file mode 100644 index 00000000..4a673612 --- /dev/null +++ b/src/templates/frontends/ui/src/contexts/index.ts.hbs @@ -0,0 +1,2 @@ +export * from './DeployerContext'; +export * from './UIContext'; diff --git a/src/templates/frontends/ui/src/hooks/index.ts.hbs b/src/templates/frontends/ui/src/hooks/index.ts.hbs new file mode 100644 index 00000000..c248da8b --- /dev/null +++ b/src/templates/frontends/ui/src/hooks/index.ts.hbs @@ -0,0 +1 @@ +export * from './useUI'; diff --git a/src/templates/frontends/ui/src/hooks/useUI.ts.hbs b/src/templates/frontends/ui/src/hooks/useUI.ts.hbs new file mode 100644 index 00000000..23c7c63e --- /dev/null +++ b/src/templates/frontends/ui/src/hooks/useUI.ts.hbs @@ -0,0 +1,4 @@ +import { useContext } from 'react'; +import { UIContext, UIState } from '..'; + +export const useUI = (): UIState => useContext(UIContext); diff --git a/src/templates/frontends/ui/src/index.css.hbs b/src/templates/frontends/ui/src/index.css.hbs new file mode 100644 index 00000000..aa29ec56 --- /dev/null +++ b/src/templates/frontends/ui/src/index.css.hbs @@ -0,0 +1,47 @@ +@import url('https://fonts.googleapis.com/css2?family=Montserrat:wght@400;600;800&family=Orbitron:wght@400;600;900&display=swap'); + +@tailwind base; +@tailwind components; +@tailwind utilities; + +body { + background-color: #1A1452; + font-family: Orbitron; +} + +.screenlines:before { + content: " "; + display: block; + position: absolute; + top: 0; + left: 0; + bottom: 0; + right: 0; + background: linear-gradient( + to bottom, + rgba(18, 16, 16, 0) 50%, + rgba(0, 0, 0, 0.2) 50% + ); + background-size: 100% 1.5px; + z-index: 2; + pointer-events: none; +} + +.water-effect { + bottom: 0px; + animation: waterEffect 5s ease-in-out infinite; + -moz-animation: waterEffect 5s ease-in-out infinite; /* Firefox */ + -webkit-animation: waterEffect 5s ease-in-out infinite; /* Safari and Chrome */ +} + +@keyframes waterEffect { + 0% { + bottom: 0px; + } + 50% { + bottom: -60px; + } + 100% { + bottom: 0px; + } +} \ No newline at end of file diff --git a/src/templates/frontends/ui/src/index.ts.hbs b/src/templates/frontends/ui/src/index.ts.hbs new file mode 100644 index 00000000..d80cd89c --- /dev/null +++ b/src/templates/frontends/ui/src/index.ts.hbs @@ -0,0 +1,24 @@ +export * from './Accounts'; +export * from './BigIntInputField'; +export * from './Button'; +export * from './Card'; +export * from './ConnectButton'; +export * from './ConnectWallet'; +export * from './Events'; +export * from './InkLayout'; +export * from './InputField'; +export * from './Label'; +export * from './Link'; +export * from './LottieEntity'; +export * from './Modal'; +export * from './Notifications'; +export * from './NumberInput'; +export * from './RunResults'; +export * from './Snackbar'; +export * from './Tabs'; +export * from './ToggleSwitch'; + +export * from './contexts'; +export * from './hooks'; +export * from './types'; +export * from './utils'; diff --git a/src/templates/frontends/ui/src/types/common.ts.hbs b/src/templates/frontends/ui/src/types/common.ts.hbs new file mode 100644 index 00000000..38aca385 --- /dev/null +++ b/src/templates/frontends/ui/src/types/common.ts.hbs @@ -0,0 +1,7 @@ +import { PropsWithChildren } from 'react'; + +export interface ClassNameable { + className?: string; +} + +export type InkComponent = PropsWithChildren; diff --git a/src/templates/frontends/ui/src/types/index.ts.hbs b/src/templates/frontends/ui/src/types/index.ts.hbs new file mode 100644 index 00000000..d0b93236 --- /dev/null +++ b/src/templates/frontends/ui/src/types/index.ts.hbs @@ -0,0 +1 @@ +export * from './common'; diff --git a/src/templates/frontends/ui/src/utils/formatContractName.ts.hbs b/src/templates/frontends/ui/src/utils/formatContractName.ts.hbs new file mode 100644 index 00000000..e0c3365a --- /dev/null +++ b/src/templates/frontends/ui/src/utils/formatContractName.ts.hbs @@ -0,0 +1,6 @@ +export const formatContractName = (name: string): string => + name + .toLowerCase() + .split('_') + .map((part) => part.charAt(0).toUpperCase() + part.slice(1)) + .join(' '); diff --git a/src/templates/frontends/ui/src/utils/index.ts.hbs b/src/templates/frontends/ui/src/utils/index.ts.hbs new file mode 100644 index 00000000..6190abdc --- /dev/null +++ b/src/templates/frontends/ui/src/utils/index.ts.hbs @@ -0,0 +1 @@ +export * from './formatContractName'; diff --git a/src/templates/frontends/ui/src/vite-env.d.ts.hbs b/src/templates/frontends/ui/src/vite-env.d.ts.hbs new file mode 100644 index 00000000..11f02fe2 --- /dev/null +++ b/src/templates/frontends/ui/src/vite-env.d.ts.hbs @@ -0,0 +1 @@ +/// diff --git a/src/templates/frontends/ui/tailwind.config.js.hbs b/src/templates/frontends/ui/tailwind.config.js.hbs new file mode 100644 index 00000000..a04d459e --- /dev/null +++ b/src/templates/frontends/ui/tailwind.config.js.hbs @@ -0,0 +1,64 @@ +/** @type {import('tailwindcss').Config} */ +export default { + content: ['./src/**/*.{js,ts,tsx}', '../ui/**/*.{js,ts,tsx}'], + theme: { + extend: { + fontFamily: { + montserrat: ['Montserrat'], + orbitron: ['Orbitron'], + }, + colors: { + brand: { + 300: '#E2DCFE', + 450: '#D9B7FF', + 500: '#BD83FD', + 550: '#AD63FF', + 600: '#7967EB', + 800: '#5a007e', + 900: '#4030a3', + 950: '#1A1452', + 1000: '#0C082B', + }, + background: { + 100: '#F2F2F3', + 300: '#D6D8DC', + 700: '#444950', + 800: '#242526', + 850: '#1b1b1d', + 900: '#090909', + }, + success: { + 500: '#00c900', + }, + warning: { + 500: '#FFBE54', + 600: '#DE493E', + 700: '#B23A32', + }, + error: { + 500: '#d6502b', + }, + info: { + 500: '#bc83fb', + }, + }, + maxWidth: { + biggest: '1440px', + }, + backgroundImage: { + 'brand-gradient': + 'linear-gradient(244.1deg, #9C45FC 12.55%, #A95DFC 32.31%, #BD83FD 86.37%)', + 'gradient-1': 'linear-gradient(180deg, #FFFFFF 0%, #EFEFEF 100%);', + 'gradient-1-dark': 'linear-gradient(180deg, #1B1B1D 0%, #242526 100%);', + }, + }, + keyframes: { + flicker: { + '0%': 'opacity: random()', + '50%': 'opacity: random()', + '100%': 'opacity: random()', + }, + }, + }, + plugins: [], +}; diff --git a/src/templates/frontends/ui/tsconfig.json.hbs b/src/templates/frontends/ui/tsconfig.json.hbs new file mode 100644 index 00000000..a7fc6fbf --- /dev/null +++ b/src/templates/frontends/ui/tsconfig.json.hbs @@ -0,0 +1,25 @@ +{ + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx", + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["src"], + "references": [{ "path": "./tsconfig.node.json" }] +} diff --git a/src/templates/frontends/ui/tsconfig.node.json.hbs b/src/templates/frontends/ui/tsconfig.node.json.hbs new file mode 100644 index 00000000..42872c59 --- /dev/null +++ b/src/templates/frontends/ui/tsconfig.node.json.hbs @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "composite": true, + "skipLibCheck": true, + "module": "ESNext", + "moduleResolution": "bundler", + "allowSyntheticDefaultImports": true + }, + "include": ["vite.config.ts"] +} diff --git a/src/templates/frontends/ui/vite.config.ts.hbs b/src/templates/frontends/ui/vite.config.ts.hbs new file mode 100644 index 00000000..3352655f --- /dev/null +++ b/src/templates/frontends/ui/vite.config.ts.hbs @@ -0,0 +1,43 @@ +import path from 'node:path'; +import react from '@vitejs/plugin-react'; +import { defineConfig } from 'vite'; +import dts from 'vite-plugin-dts'; + +// https://vitejs.dev/config/ +export default defineConfig({ + plugins: [ + react(), + dts({ + insertTypesEntry: true, + }), + ], + build: { + sourcemap: true, + lib: { + entry: path.resolve(__dirname, 'src/index.ts.hbs.hbs'), + name: 'ui', + formats: ['es', 'umd'], + fileName: (format) => `ui.${format}.js`, + }, + rollupOptions: { + external: [ + 'react', + 'react-dom', + 'useink', + 'useink/core', + 'useink/notifications', + 'useink/utils', + ], + output: { + globals: { + react: 'React', + 'react-dom': 'ReactDOM', + useink: 'useink', + 'useink/core': 'useink/core', + 'useink/notifications': 'useink/notifications', + 'useink/utils': 'useink/utils', + }, + }, + }, + }, +});