From 1fee0d16a73e7f5209dcadfea1e934a94a4314d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20B=C3=A9ranger?= Date: Sat, 2 Nov 2024 23:47:43 +0100 Subject: [PATCH] Restructure (#49) * chore: add vscode workspace settings * add zhankai in gitignore * fix structure * add quickstart * fix gh workflow * fix tests * fix tests * update deps * content update * fix links * fix seo --- .github/workflows/run-tests.yml | 22 +- .gitignore | 4 +- .vscode/extensions.json | 11 +- .vscode/settings.json | 47 +- QUICKSTART.md | 230 ++ README.md | 16 +- genji_app_description.md | 1916 ----------------- jest.config.ts | 6 +- jest.setup.ts | 12 + package.json | 17 +- pnpm-lock.yaml | 45 +- .../.well-known}/walletconnect.txt | 0 src/__tests__/Header.test.tsx | 13 +- src/__tests__/index.test.tsx | 30 +- src/components/{layout => }/ErrorBoundary.tsx | 0 src/components/{layout => }/Head.tsx | 2 +- src/components/{layout => }/Header.tsx | 11 +- .../{layout => }/HeadingComponent.tsx | 0 src/components/LinkComponent.tsx | 37 + src/components/{layout => }/Seo.tsx | 2 +- src/components/{layout => }/ThemeSwitcher.tsx | 0 src/components/layout/LinkComponent.tsx | 36 - src/components/layout/index.tsx | 2 +- src/pages/_app.tsx | 39 +- src/pages/index.tsx | 45 +- src/pages/new/index.tsx | 35 + src/utils/test-utils.tsx | 18 + tsconfig.json | 14 +- 28 files changed, 531 insertions(+), 2079 deletions(-) create mode 100644 QUICKSTART.md delete mode 100644 genji_app_description.md rename {.well-known => public/.well-known}/walletconnect.txt (100%) rename src/components/{layout => }/ErrorBoundary.tsx (100%) rename src/components/{layout => }/Head.tsx (94%) rename src/components/{layout => }/Header.tsx (86%) rename src/components/{layout => }/HeadingComponent.tsx (100%) create mode 100644 src/components/LinkComponent.tsx rename src/components/{layout => }/Seo.tsx (97%) rename src/components/{layout => }/ThemeSwitcher.tsx (100%) delete mode 100644 src/components/layout/LinkComponent.tsx create mode 100644 src/utils/test-utils.tsx diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml index e4a8956..589b4c0 100644 --- a/.github/workflows/run-tests.yml +++ b/.github/workflows/run-tests.yml @@ -1,22 +1,22 @@ -name: CI +name: Run Tests on: push: - branches: [main, master] + branches: [main, develop] pull_request: - branches: [main, master] + branches: [main, develop] jobs: test: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v3 - name: Setup Node.js - uses: actions/setup-node@v4 + uses: actions/setup-node@v3 with: - node-version: '20' + node-version: '20.x' - name: Setup pnpm uses: pnpm/action-setup@v2 @@ -39,11 +39,9 @@ jobs: - name: Install dependencies run: pnpm install - - name: Run Prettier check - run: pnpm format:check - - name: Run tests run: pnpm test - - - name: Run ESLint - run: pnpm lint + env: + CI: true + NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID: ${{ secrets.NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID }} + NEXT_PUBLIC_RPC_ENDPOINT_URL: ${{ secrets.NEXT_PUBLIC_RPC_ENDPOINT_URL }} diff --git a/.gitignore b/.gitignore index 826da96..d355055 100644 --- a/.gitignore +++ b/.gitignore @@ -39,4 +39,6 @@ next-env.d.ts # Misc -NOTES.md \ No newline at end of file +NOTES.md +genji_app_description* +.swc \ No newline at end of file diff --git a/.vscode/extensions.json b/.vscode/extensions.json index d7df89c..0dbcbdd 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -1,3 +1,10 @@ + { - "recommendations": ["esbenp.prettier-vscode", "dbaeumer.vscode-eslint"] -} + "recommendations": [ + "dbaeumer.vscode-eslint", + "esbenp.prettier-vscode", + "bradlc.vscode-tailwindcss", + "csstools.postcss", + "ms-vscode.vscode-typescript-next" + ] + } diff --git a/.vscode/settings.json b/.vscode/settings.json index c5a02a6..d66a4ea 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,25 +1,24 @@ { - "editor.defaultFormatter": "esbenp.prettier-vscode", - "editor.formatOnSave": true, - "editor.codeActionsOnSave": { - "source.fixAll.eslint": true - }, - "[typescript]": { - "editor.defaultFormatter": "esbenp.prettier-vscode" - }, - "[typescriptreact]": { - "editor.defaultFormatter": "esbenp.prettier-vscode" - }, - "[javascript]": { - "editor.defaultFormatter": "esbenp.prettier-vscode" - }, - "[javascriptreact]": { - "editor.defaultFormatter": "esbenp.prettier-vscode" - }, - "[json]": { - "editor.defaultFormatter": "esbenp.prettier-vscode" - }, - "[markdown]": { - "editor.defaultFormatter": "esbenp.prettier-vscode" - } -} + "editor.defaultFormatter": "esbenp.prettier-vscode", + "editor.formatOnSave": true, + "editor.codeActionsOnSave": { + "source.fixAll.eslint": "explicit" + }, + "typescript.tsdk": "node_modules/typescript/lib", + "typescript.enablePromptUseWorkspaceTsdk": true, + "eslint.validate": [ + "javascript", + "javascriptreact", + "typescript", + "typescriptreact" + ], + "files.associations": { + "*.css": "tailwindcss" + }, + "[typescript]": { + "editor.defaultFormatter": "esbenp.prettier-vscode" + }, + "[typescriptreact]": { + "editor.defaultFormatter": "esbenp.prettier-vscode" + } + } \ No newline at end of file diff --git a/QUICKSTART.md b/QUICKSTART.md new file mode 100644 index 0000000..deab628 --- /dev/null +++ b/QUICKSTART.md @@ -0,0 +1,230 @@ +# Genji - Quick Start Guide + +A modern Web3 application template featuring Next.js, Reown, Ethers.js, and Chakra UI. + +🔥 [Live Demo](https://genji-app.netlify.app) + +## Core Technologies + +- 🚀 [Next.js](https://nextjs.org/) - React framework for production +- 🔗 [Reown](https://reown.com/appkit) - Web3 authentication & wallet connections +- ⚡ [Ethers.js](https://ethers.org/) (v6) - Ethereum library +- 💅 [Chakra UI](https://chakra-ui.com/) - Component library +- 🔧 [Example Smart Contract](https://github.com/w3hc/w3hc-hardhat-template/blob/main/contracts/Basic.sol) + +## Features + +- Multi-wallet support +- Email & social logins (Google, Farcaster, GitHub) +- Multiple network support (Sepolia, Optimism, zkSync, Base, etc.) +- Dark/Light theme +- Built-in faucet API +- TypeScript +- Testing setup with Jest +- ESLint + Prettier config + +## Prerequisites + +```bash +node -v # v20.9.0 or later +pnpm -v # v8.7.5 or later +``` + +## Installation + +1. Clone the repository: + +```bash +git clone https://github.com/your-username/genji.git +cd genji +``` + +2. Install dependencies: + +```bash +pnpm install +``` + +3. Set up environment: + +```bash +cp .env.example .env +``` + +4. Configure `.env`: + +``` +# Get yours at https://cloud.walletconnect.com +NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID='your_project_id' + +# RPC endpoint +NEXT_PUBLIC_RPC_ENDPOINT_URL='https://sepolia.gateway.tenderly.co' + +# Only needed if using the faucet API +NEXT_PUBLIC_SIGNER_PRIVATE_KEY='your_private_key' +``` + +5. Start development server: + +```bash +pnpm dev +``` + +Visit `http://localhost:3000` 🚀 + +## Project Structure + +``` +src/ +├── components/ # UI components +│ ├── Header.tsx # Main navigation +│ ├── layout/ # Layout components +│ └── ... +├── context/ +│ └── web3modal.tsx # Web3 configuration +├── hooks/ # Custom React hooks +├── pages/ # Next.js pages +│ ├── api/ # API routes +│ │ └── faucet.ts +│ └── index.tsx +└── utils/ # Helpers & constants + ├── config.ts + └── erc20.ts +``` + +## Usage Examples + +### 1. Connect Wallet + +```typescript +import { useAppKitAccount, useAppKitProvider } from '@reown/appkit/react' + +export default function YourComponent() { + const { address, isConnected } = useAppKitAccount() + const { walletProvider } = useAppKitProvider('eip155') + + if (!isConnected) { + return // Reown connect button + } + + return
Connected: {address}
+} +``` + +### 2. Contract Interaction + +```typescript +// Initialize contract +const ethersProvider = new BrowserProvider(walletProvider as Eip1193Provider) +const signer = await ethersProvider.getSigner() +const contract = new Contract(ERC20_CONTRACT_ADDRESS, ERC20_CONTRACT_ABI, signer) + +// Call contract method +const tx = await contract.mint(parseEther('1000')) +const receipt = await tx.wait() +``` + +### 3. UI Components + +```typescript +import { Button, useToast } from '@chakra-ui/react' + +export default function YourComponent() { + const toast = useToast() + + const handleClick = () => { + toast({ + title: 'Success!', + status: 'success', + duration: 9000, + isClosable: true, + }) + } + + return ( + + ) +} +``` + +## Testing + +Run tests: + +```bash +pnpm test # Run all tests +pnpm test:watch # Watch mode +``` + +## Network Support + +The template supports multiple networks. Configure in `src/context/web3modal.tsx`: + +```typescript +const networks = [ + sepolia, // Default network + optimism, + zksync, + base, + arbitrum, + gnosis, + polygon, + polygonZkEvm, + mantle, + celo, + avalanche, + degen, +] +``` + +## Browser Support + +- iOS: Safari 10+ (iOS 10+) +- Android: Chrome 51+ (Android 5.0+) +- Desktop: Modern browsers + +## Customization + +### Theme + +Modify in `src/utils/config.ts`: + +```typescript +export const THEME_COLOR_SCHEME = 'blue' +export const THEME_INITIAL_COLOR = 'system' +``` + +### Contract Setup + +1. Update contract details in `src/utils/erc20.ts` +2. Implement your interaction logic + +## Development Commands + +```bash +pnpm dev # Start development server +pnpm build # Production build +pnpm start # Start production server +pnpm lint # Run ESLint +pnpm format # Format code with Prettier +``` + +## Support & Resources + +- 📘 [Next.js Documentation](https://nextjs.org/docs) +- 🔧 [Reown AppKit Guide](https://reown.com/appkit) +- ⚡ [Ethers.js Documentation](https://docs.ethers.org/v6/) +- 💅 [Chakra UI Components](https://chakra-ui.com/docs/components) + +## Contact + +Need help? Reach out: + +- [Element](https://matrix.to/#/@julienbrg:matrix.org) +- [Farcaster](https://warpcast.com/julien-) +- [Telegram](https://t.me/julienbrg) +- [Twitter](https://twitter.com/julienbrg) +- [Discord](https://discordapp.com/users/julienbrg) +- [LinkedIn](https://www.linkedin.com/in/julienberanger/) diff --git a/README.md b/README.md index 70865ed..0a07b24 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,8 @@ A Next.js Web3 app template. +You can read **[our quickstart](https://github.com/w3hc/genji/blob/main/README.md)** to get started! + ## Features - [Next.js](https://nextjs.org/) @@ -27,24 +29,12 @@ Create a `.env` file: cp .env.example .env ``` -Add your own keys in the `.env` file (you can get it in your [Wallet Connect dashboard](https://cloud.walletconnect.com)), then: +Add your own keys in the `.env` file (you can get it in your [Reown dashboard](https://cloud.reown.com/)), then: ```bash pnpm dev ``` -## Requirements - -Here are the known minimal mobile hardware requirements: - -- iOS: Safari 10+ (iOS 10+) -- Android: Chrome 51+ (Android 5.0+) - -## Versions - -- pnpm `v8.7.5` -- node `v20.9.0` - ## Support You can contact me via [Element](https://matrix.to/#/@julienbrg:matrix.org), [Farcaster](https://warpcast.com/julien-), [Telegram](https://t.me/julienbrg), [Twitter](https://twitter.com/julienbrg), [Discord](https://discordapp.com/users/julienbrg), or [LinkedIn](https://www.linkedin.com/in/julienberanger/). diff --git a/genji_app_description.md b/genji_app_description.md deleted file mode 100644 index 113059e..0000000 --- a/genji_app_description.md +++ /dev/null @@ -1,1916 +0,0 @@ -# genji - - -### .env.example - -``` -NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID='88888' # Get yours at https://cloud.walletconnect.com -NEXT_PUBLIC_RPC_ENDPOINT_URL='https://sepolia.gateway.tenderly.co' -NEXT_PUBLIC_SIGNER_PRIVATE_KEY='88888' -``` - -### .eslintignore - -``` -# Ignore everything -# * -``` - -### .eslintrc.json - -```json -{ - "extends": ["next/core-web-vitals", "prettier"], - "plugins": ["prettier"], - "rules": { - "prettier/prettier": "error", - "arrow-body-style": "off", - "prefer-arrow-callback": "off" - } -} - -``` - -### .gitignore - -``` -# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. - -# dependencies -/node_modules -/.pnp -.pnp.js - -# testing -/coverage - -# next.js -/.next/ -/out/ - -# production -/build - -# misc -.DS_Store -*.pem - -# debug -npm-debug.log* -yarn-debug.log* -yarn-error.log* - -# local env files -.env*.local -.env - -# vercel -.vercel - -# typescript - -*.tsbuildinfo -next-env.d.ts -/.history - -# Misc - -NOTES.md -``` - -### .prettierignore - -``` -# dependencies -/node_modules -/.pnp -.pnp.js - -# testing -/coverage - -# next.js -/.next/ -/out/ - -# production -/build - -# misc -.DS_Store -*.pem -genji_app_description* - -# debug -npm-debug.log* -yarn-debug.log* -yarn-error.log* - -# local env files -.env*.local -.env - -# vercel -.vercel - -# typescript -*.tsbuildinfo -next-env.d.ts - -# IDE -.idea -.vscode - -# package manager -pnpm-lock.yaml -package-lock.json -yarn.lock -``` - -### .prettierrc.json - -```json -{ - "trailingComma": "es5", - "semi": false, - "singleQuote": true, - "printWidth": 120, - "bracketSameLine": true, - "useTabs": false, - "tabWidth": 2, - "bracketSpacing": true, - "arrowParens": "always", - "endOfLine": "lf", - "htmlWhitespaceSensitivity": "css", - "insertPragma": false, - "jsxSingleQuote": false, - "proseWrap": "preserve", - "quoteProps": "as-needed", - "requirePragma": false -} - -``` - -### README.md - -```markdown -# Genji - -A Next.js Web3 app template. - -## Features - -- [Next.js](https://nextjs.org/) -- [Reown](https://reown.com/appkit) -- [Ethers.js](https://ethers.org/) (v6) -- [Chakra UI](https://chakra-ui.com/) - -View the [Solidity contract](https://github.com/w3hc/w3hc-hardhat-template/blob/main/contracts/Basic.sol) used in the example. - -Web app live at [https://genji-app.netlify.app](https://genji-app.netlify.app). - -## Install - -```bash -pnpm i -``` - -## Run - -Create a `.env` file: - -``` -cp .env.example .env -``` - -Add your own keys in the `.env` file (you can get it in your [Wallet Connect dashboard](https://cloud.walletconnect.com)), then: - -```bash -pnpm dev -``` - -## Requirements - -Here are the known minimal mobile hardware requirements: - -- iOS: Safari 10+ (iOS 10+) -- Android: Chrome 51+ (Android 5.0+) - -## Versions - -- pnpm `v8.7.5` -- node `v20.9.0` - -## Support - -You can contact me via [Element](https://matrix.to/#/@julienbrg:matrix.org), [Farcaster](https://warpcast.com/julien-), [Telegram](https://t.me/julienbrg), [Twitter](https://twitter.com/julienbrg), [Discord](https://discordapp.com/users/julienbrg), or [LinkedIn](https://www.linkedin.com/in/julienberanger/). - -## Credits - -Special thanks to Wesley ([@wslyvh](https://github.com/wslyvh)) for building [Nexth](https://github.com/wslyvh/nexth). I also want to thank the [Wallet Connect](https://walletconnect.com/) team, [@glitch-txs](https://github.com/glitch-txs) in particular. And of course [@ricmoo](https://github.com/ricmoo) for maintaining [Ethers.js](https://ethers.org/)! - -``` - -### genji_app_description.md - -```markdown -# genji - - -### .env.example - -``` -NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID='88888' # Get yours at https://cloud.walletconnect.com -NEXT_PUBLIC_RPC_ENDPOINT_URL='https://sepolia.gateway.tenderly.co' -NEXT_PUBLIC_SIGNER_PRIVATE_KEY='88888' -``` - -### .eslintignore - -``` -# Ignore everything -# * -``` - -### .eslintrc.json - -```json -{ - "extends": ["next/core-web-vitals", "prettier"], - "plugins": ["prettier"], - "rules": { - "prettier/prettier": "error", - "arrow-body-style": "off", - "prefer-arrow-callback": "off" - } -} - -``` - -### .gitignore - -``` -# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. - -# dependencies -/node_modules -/.pnp -.pnp.js - -# testing -/coverage - -# next.js -/.next/ -/out/ - -# production -/build - -# misc -.DS_Store -*.pem - -# debug -npm-debug.log* -yarn-debug.log* -yarn-error.log* - -# local env files -.env*.local -.env - -# vercel -.vercel - -# typescript - -*.tsbuildinfo -next-env.d.ts -/.history - -# Misc - -NOTES.md -``` - -### .prettierignore - -``` -# dependencies -/node_modules -/.pnp -.pnp.js - -# testing -/coverage - -# next.js -/.next/ -/out/ - -# production -/build - -# misc -.DS_Store -*.pem -genji_app_description* - -# debug -npm-debug.log* -yarn-debug.log* -yarn-error.log* - -# local env files -.env*.local -.env - -# vercel -.vercel - -# typescript -*.tsbuildinfo -next-env.d.ts - -# IDE -.idea -.vscode - -# package manager -pnpm-lock.yaml -package-lock.json -yarn.lock -``` - -### .prettierrc.json - -```json -{ - "trailingComma": "es5", - "semi": false, - "singleQuote": true, - "printWidth": 120, - "bracketSameLine": true, - "useTabs": false, - "tabWidth": 2, - "bracketSpacing": true, - "arrowParens": "always", - "endOfLine": "lf", - "htmlWhitespaceSensitivity": "css", - "insertPragma": false, - "jsxSingleQuote": false, - "proseWrap": "preserve", - "quoteProps": "as-needed", - "requirePragma": false -} - -``` - -### README.md - -```markdown -# Genji - -A Next.js Web3 app template. - -## Features - -- [Next.js](https://nextjs.org/) -- [Reown](https://reown.com/appkit) -- [Ethers.js](https://ethers.org/) (v6) -- [Chakra UI](https://chakra-ui.com/) - -View the [Solidity contract](https://github.com/w3hc/w3hc-hardhat-template/blob/main/contracts/Basic.sol) used in the example. - -Web app live at [https://genji-app.netlify.app](https://genji-app.netlify.app). - -## Install - -```bash -pnpm i -``` - -## Run - -Create a `.env` file: - -``` -cp .env.example .env -``` - -Add your own keys in the `.env` file (you can get it in your [Wallet Connect dashboard](https://cloud.walletconnect.com)), then: - -```bash -pnpm dev -``` - -## Requirements - -Here are the known minimal mobile hardware requirements: - -- iOS: Safari 10+ (iOS 10+) -- Android: Chrome 51+ (Android 5.0+) - -## Versions - -- pnpm `v8.7.5` -- node `v20.9.0` - -## Support - -You can contact me via [Element](https://matrix.to/#/@julienbrg:matrix.org), [Farcaster](https://warpcast.com/julien-), [Telegram](https://t.me/julienbrg), [Twitter](https://twitter.com/julienbrg), [Discord](https://discordapp.com/users/julienbrg), or [LinkedIn](https://www.linkedin.com/in/julienberanger/). - -## Credits - -Special thanks to Wesley ([@wslyvh](https://github.com/wslyvh)) for building [Nexth](https://github.com/wslyvh/nexth). I also want to thank the [Wallet Connect](https://walletconnect.com/) team, [@glitch-txs](https://github.com/glitch-txs) in particular. And of course [@ricmoo](https://github.com/ricmoo) for maintaining [Ethers.js](https://ethers.org/)! - -``` - -### genji_app_description.md - -```markdown - -``` - -### jest.config.ts - -```typescript -import type { Config } from 'jest' -import nextJest from 'next/jest' - -const createJestConfig = nextJest({ - dir: './', -}) - -const customJestConfig: Config = { - preset: 'ts-jest', - setupFilesAfterEnv: ['/jest.setup.ts'], - testEnvironment: 'jest-environment-jsdom', - moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'], - transform: { - '^.+\\.(ts|tsx)$': 'ts-jest', - }, -} - -export default createJestConfig(customJestConfig) - -``` - -### jest.setup.ts - -```typescript -import '@testing-library/jest-dom' - -``` - -### next.config.js - -```javascript -/** @type {import('next').NextConfig} */ -const nextConfig = { - reactStrictMode: true, - eslint: { - ignoreDuringBuilds: true, - }, -} - -module.exports = nextConfig - -``` - -### package.json - -```json -{ - "name": "genji", - "description": "A Next.js Web3 app template", - "version": "0.1.0", - "scripts": { - "dev": "next dev", - "build": "next build", - "start": "next start", - "lint": "next lint", - "test": "jest", - "test:watch": "jest --watch", - "format": "prettier --write \"**/*.{ts,tsx,js,jsx,json,md}\"", - "format:check": "prettier --check \"**/*.{ts,tsx,js,jsx,json,md}\"" - }, - "dependencies": { - "@chakra-ui/icons": "^2.1.1", - "@chakra-ui/next-js": "^2.2.0", - "@chakra-ui/react": "^2.8.2", - "@coinbase/wallet-sdk": "4.0.3", - "@emotion/react": "^11.13.3", - "@emotion/styled": "^11.13.0", - "@reown/appkit": "^1.1.2", - "@reown/appkit-adapter-ethers": "^1.1.2", - "@types/react": "18.3.5", - "@types/react-dom": "18.3.0", - "autoprefixer": "10.4.20", - "eslint": "8.57.1", - "eslint-config-next": "14.2.7", - "ethers": "^6.13.2", - "framer-motion": "^11.7.0", - "next": "14.2.12", - "next-seo": "^6.6.0", - "postcss": "8.4.44", - "react": "18.3.1", - "react-device-detect": "^2.2.3", - "react-dom": "18.3.1", - "react-icons": "^5.3.0", - "typescript": "5.5.4" - }, - "devDependencies": { - "@testing-library/jest-dom": "^6.5.0", - "@testing-library/react": "^16.0.1", - "@types/jest": "^29.5.13", - "@types/node": "22.5.2", - "eslint-config-prettier": "^9.1.0", - "eslint-plugin-prettier": "^5.2.1", - "jest": "^29.7.0", - "jest-environment-jsdom": "^29.7.0", - "prettier": "^3.3.3", - "ts-jest": "^29.1.0", - "ts-node": "^10.9.2" - } -} - -``` - -### pnpm-lock.yaml - -```yaml -lockfileVersion: '9.0' - -settings: - autoInstallPeers: true - excludeLinksFromLockfile: false - -importers: - - .: - dependencies: - '@chakra-ui/icons': - specifier: ^2.1.1 - version: 2.2.4(@chakra-ui/react@2.10.2(@emotion/react@11.13.3(@types/react@18.3.5)(react@18.3.1))(@emotion/styled@11.13.0(@emotion/react@11.13.3(@types/react@18.3.5)(react@18.3.1))(@types/react@18.3.5)(react@18.3.1))(@types/react@18.3.5)(framer-motion@11.11.8(@emotion/is-prop-valid@1.3.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1) - '@chakra-ui/next-js': - specifier: ^2.2.0 - version: 2.4.2(@chakra-ui/react@2.10.2(@emotion/react@11.13.3(@types/react@18.3.5)(react@18.3.1))(@emotion/styled@11.13.0(@emotion/react@11.13.3(@types/react@18.3.5)(react@18.3.1))(@types/react@18.3.5)(react@18.3.1))(@types/react@18.3.5)(framer-motion@11.11.8(@emotion/is-prop-valid@1.3.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@emotion/react@11.13.3(@types/react@18.3.5)(react@18.3.1))(next@14.2.12(@babel/core@7.25.8)(babel-plugin-macros@3.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1) - '@chakra-ui/react': - specifier: ^2.8.2 - version: 2.10.2(@emotion/react@11.13.3(@types/react@18.3.5)(react@18.3.1))(@emotion/styled@11.13.0(@emotion/react@11.13.3(@types/react@18.3.5)(react@18.3.1))(@types/react@18.3.5)(react@18.3.1))(@types/react@18.3.5)(framer-motion@11.11.8(@emotion/is-prop-valid@1.3.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@coinbase/wallet-sdk': - specifier: 4.0.3 - version: 4.0.3 - '@emotion/react': - specifier: ^11.13.3 - version: 11.13.3(@types/react@18.3.5)(react@18.3.1) - '@emotion/styled': - specifier: ^11.13.0 - version: 11.13.0(@emotion/react@11.13.3(@types/react@18.3.5)(react@18.3.1))(@types/react@18.3.5)(react@18.3.1) - '@reown/appkit': - specifier: ^1.1.2 -``` - -[This file was cut: it has more than 500 lines] - -``` - -## public - - -### public/favicon.ico - -``` -[This is an image file] -``` - -### public/huangshan.png - -``` -[This is an image file] -``` - -## src - - -## src/__tests__ - - -### src/__tests__/Header.test.tsx - -``` -import React from 'react' -import { render, screen } from '@testing-library/react' -import '@testing-library/jest-dom' -import { Header } from '../components/layout/Header' - -jest.mock('@reown/appkit/react', () => ({ - useAppKitAccount: () => ({ isConnected: false }), -})) - -describe('Header', () => { - it('renders the site name', () => { - render(
) - const siteName = screen.getByText('Genji') - expect(siteName).toBeInTheDocument() - }) -}) - -``` - -### src/__tests__/index.test.tsx - -``` -import React from 'react' -import { render, screen } from '@testing-library/react' -import '@testing-library/jest-dom' -import Home from '../pages/index' - -jest.mock('@reown/appkit/react', () => ({ - useAppKitAccount: () => ({ address: null, isConnected: false, caipAddress: null }), - useAppKitProvider: () => ({ walletProvider: null }), -})) - -jest.mock('next/router', () => ({ - useRouter() { - return { - route: '/', - pathname: '', - query: '', - asPath: '', - } - }, -})) - -describe('Home page', () => { - it('renders the login message when not connected', () => { - render() - expect( - screen.getByText(/You can login with your email, Google, or with one of many wallets suported by Reown\./) - ).toBeInTheDocument() - }) - - it('renders the mint button', () => { - render() - expect(screen.getByRole('button', { name: /Mint/i })).toBeInTheDocument() - }) -}) - -``` - -## src/components - - -## src/components/layout - - -### src/components/layout/ErrorBoundary.tsx - -``` -import React, { ErrorInfo, ReactNode } from 'react' -import { mobileModel, mobileVendor } from 'react-device-detect' - -interface Props { - children: ReactNode -} - -interface State { - hasError: boolean - deviceInfo: string -} - -class ErrorBoundary extends React.Component { - constructor(props: Props) { - super(props) - this.state = { hasError: false, deviceInfo: '' } - } - - static getDerivedStateFromError(error: Error): State { - return { hasError: true, deviceInfo: '' } - } - - componentDidCatch(error: Error, errorInfo: ErrorInfo) { - console.error('Error caught by ErrorBoundary:', error, errorInfo) - } - - componentDidMount() { - const deviceInfo = `${mobileVendor} ${mobileModel}` - this.setState({ deviceInfo }) - } - - render() { - if (this.state.hasError) { - return ( - <> -

All apologies, the app is not yet available on this type of device.

-
-

{this.state.deviceInfo}

-
-

Thank you for using the app from another device.

-
-

- Feel free to report this to Julien via Element,{' '} - Farcaster, Telegram,{' '} - Twitter,{' '} - Discord or{' '} - LinkedIn. -

- - ) - } - - return this.props.children - } -} - -export default ErrorBoundary - -``` - -### src/components/layout/Head.tsx - -``` -import React from 'react' -import { default as NextHead } from 'next/head' -import { SITE_URL } from '../../utils/config' - -interface Props { - title?: string - description?: string -} - -export function Head({ title, description }: Props) { - const origin = typeof window !== 'undefined' && window.location.origin ? window.location.origin : SITE_URL - const img = `${origin}/huangshan.png` - - return ( - - - - - - - - - - - - ) -} - -``` - -### src/components/layout/Header.tsx - -``` -import React from 'react' -import { - Flex, - useColorModeValue, - Spacer, - Heading, - Box, - Link, - Icon, - Button, - MenuList, - MenuItem, - Menu, - MenuButton, - IconButton, -} from '@chakra-ui/react' -import { LinkComponent } from './LinkComponent' -import { ThemeSwitcher } from './ThemeSwitcher' -import { HeadingComponent } from './HeadingComponent' -import { SITE_NAME } from '../../utils/config' -import { FaGithub } from 'react-icons/fa' -import { Web3Modal } from '../../context/web3modal' -import { HamburgerIcon } from '@chakra-ui/icons' - -interface Props { - className?: string -} - -export function Header(props: Props) { - const className = props.className ?? '' - - return ( - - - - {SITE_NAME} - - - - - - } size={'sm'} mr={4} /> - - - Home - - - New - - - - - - {/* */}{' '} - - - - - - - - - - - ) -} - -``` - -### src/components/layout/HeadingComponent.tsx - -``` -import { ReactNode } from 'react' -import { Heading } from '@chakra-ui/react' - -interface Props { - as: 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6' - size?: 'xs' | 'sm' | 'md' | 'lg' | 'xl' | '2xl' | '3xl' | '4xl' - children: ReactNode - className?: string -} - -export function HeadingComponent(props: Props) { - const className = props.className ?? '' - let size - switch (props.as) { - case 'h1': - size = props.size ?? '2xl' - break - case 'h2': - size = props.size ?? 'xl' - break - case 'h3': - size = props.size ?? 'lg' - break - case 'h4': - size = props.size ?? 'md' - break - case 'h5': - size = props.size ?? 'sm' - break - case 'h6': - size = props.size ?? 'xs' - break - } - - return ( - - {props.children} - - ) -} - -``` - -### src/components/layout/LinkComponent.tsx - -``` -import React, { ReactNode } from 'react' -import NextLink from 'next/link' -import { Link, useColorModeValue } from '@chakra-ui/react' -import { THEME_COLOR_SCHEME } from '../../utils/config' - -interface Props { - href: string - children: ReactNode - isExternal?: boolean - className?: string -} - -export function LinkComponent(props: Props) { - const className = props.className ?? '' - const isExternal = props.href.match(/^([a-z0-9]*:|.{0})\/\/.*$/) || props.isExternal - const color = useColorModeValue(`${THEME_COLOR_SCHEME}.600`, `${THEME_COLOR_SCHEME}.400`) - - if (isExternal) { - return ( - - {props.children} - - ) - } - - return ( - - {props.children} - - ) -} - -``` - -### src/components/layout/Seo.tsx - -``` -import React from 'react' -import { SITE_DESCRIPTION, SITE_NAME, SITE_URL, SOCIAL_TWITTER } from '../../utils/config' -import { DefaultSeo } from 'next-seo' - -export function Seo() { - const origin = typeof window !== 'undefined' && window.location.origin ? window.location.origin : SITE_URL - - return ( - - ) -} - -``` - -### src/components/layout/ThemeSwitcher.tsx - -``` -import React from 'react' -import { Box, useColorMode } from '@chakra-ui/react' -import { MoonIcon, SunIcon } from '@chakra-ui/icons' - -interface Props { - className?: string -} - -export function ThemeSwitcher(props: Props) { - const className = props.className ?? '' - const { colorMode, toggleColorMode } = useColorMode() - - return ( - - {colorMode === 'light' ? : } - - ) -} - -``` - -### src/components/layout/index.tsx - -``` -import { Web3Modal } from '../../context/web3modal' -import { ReactNode } from 'react' -import { Box, Container } from '@chakra-ui/react' -import { Header } from './Header' - -interface Props { - children?: ReactNode -} - -export default function RootLayout({ children }: Props) { - return ( - - -
- {children} - - - ) -} - -``` - -## src/context - - -### src/context/web3modal.tsx - -``` -'use client' -import React, { ReactNode, createContext, useContext } from 'react' -import { createAppKit, useAppKitProvider } from '@reown/appkit/react' -import { EthersAdapter } from '@reown/appkit-adapter-ethers' -import { - sepolia, - optimism, - zksync, - base, - arbitrum, - gnosis, - polygon, - polygonZkEvm, - mantle, - celo, - avalanche, - degen, -} from '@reown/appkit/networks' - -const projectId = process.env.NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID || '' - -// https://docs.reown.com/appkit/react/core/custom-networks - -const metadata = { - name: 'Genji', - description: 'Next.js + Web3 Modal + Ethers.js + Chakra UI', - url: 'https://genji.netlify.app', - icons: ['./favicon.ico'], -} - -createAppKit({ - adapters: [new EthersAdapter()], - metadata, - networks: [sepolia, optimism, zksync, base, arbitrum, gnosis, polygon, polygonZkEvm, mantle, celo, avalanche, degen], - defaultNetwork: sepolia, - projectId, - features: { - email: true, - socials: ['google', 'farcaster', 'github'], - }, -}) - -const AppKitContext = createContext | null>(null) - -export function Web3Modal({ children }: { children: ReactNode }) { - const appKitProvider = useAppKitProvider('eip155:11155111' as any) - - return {children} -} - -export function useAppKit() { - const context = useContext(AppKitContext) - if (!context) { - throw new Error('useAppKit must be used within a Web3Modal') - } - return context -} - -``` - -## src/hooks - - -### src/hooks/useIsMounted.tsx - -``` -import { useState, useEffect } from 'react' - -export function useIsMounted(): boolean { - let [isMounted, setIsMounted] = useState(false) - - useEffect(() => { - setIsMounted(true) - }, []) - - return isMounted -} - -``` - -## src/pages - - -### src/pages/_app.tsx - -``` -import type { AppProps } from 'next/app' -import Layout from '../components/layout' -import { useEffect } from 'react' -import { ChakraProvider } from '@chakra-ui/react' -import { Seo } from '../components/layout/Seo' -import { ERC20_CONTRACT_ADDRESS } from '../utils/erc20' -import { useIsMounted } from '../hooks/useIsMounted' -import ErrorBoundary from '../components/layout/ErrorBoundary' - -export default function App({ Component, pageProps }: AppProps) { - useEffect(() => { - console.log('contract address:', ERC20_CONTRACT_ADDRESS) - }, []) - const isMounted = useIsMounted() - - return ( - <> - - - - {isMounted && ( - - - - )} - - - - ) -} - -``` - -### src/pages/_document.tsx - -``` -import { Html, Head, Main, NextScript } from 'next/document' -import { ColorModeScript } from '@chakra-ui/react' - -export default function Document() { - return ( - - - - - - - -
- - - - ) -} - -``` - -## src/pages/api - - -### src/pages/api/faucet.ts - -```typescript -import type { NextApiRequest, NextApiResponse } from 'next' -import { ethers } from 'ethers' - -export default async function handler(req: NextApiRequest, res: NextApiResponse) { - if (req.method !== 'POST') { - return res.status(405).json({ message: 'Method Not Allowed' }) - } - - const { address } = req.body - - if (!address) { - return res.status(400).json({ message: 'Address is required' }) - } - - try { - const customProvider = new ethers.JsonRpcProvider(process.env.NEXT_PUBLIC_RPC_ENDPOINT_URL) - const pKey = process.env.NEXT_PUBLIC_SIGNER_PRIVATE_KEY - - if (!pKey) { - throw new Error('Faucet private key is not set') - } - - const specialSigner = new ethers.Wallet(pKey, customProvider) - const tx = await specialSigner.sendTransaction({ - to: address, - value: ethers.parseEther('0.025'), - }) - - let receipt: ethers.TransactionReceipt | null = null - try { - receipt = await tx.wait(1) - } catch (waitError) { - console.error('Error waiting for transaction:', waitError) - return res.status(500).json({ message: 'Transaction failed or was reverted' }) - } - - if (receipt === null) { - return res.status(500).json({ message: 'Transaction was not mined within the expected time' }) - } - - res.status(200).json({ - message: 'Faucet transaction successful', - txHash: receipt.hash, - }) - } catch (error) { - console.error('Faucet error:', error) - if (error instanceof Error) { - return res.status(500).json({ message: `Internal server error: ${error.message}` }) - } - res.status(500).json({ message: 'Internal server error' }) - } -} - -``` - -### src/pages/index.tsx - -``` -import * as React from 'react' -import { Text, Button, useToast, Box } from '@chakra-ui/react' -import { useState, useEffect } from 'react' -import { BrowserProvider, Contract, Eip1193Provider, parseEther } from 'ethers' -// import { useAppKitAccount, useAppKitProvider, useWalletInfo } from '@reown/appkit/react' -import { useAppKitAccount, useAppKitProvider } from '@reown/appkit/react' -import { ERC20_CONTRACT_ADDRESS, ERC20_CONTRACT_ABI } from '../utils/erc20' -import { LinkComponent } from '../components/layout/LinkComponent' -import { ethers } from 'ethers' -import { Head } from '../components/layout/Head' -import { SITE_NAME, SITE_DESCRIPTION } from '../utils/config' - -export default function Home() { - const [isLoading, setIsLoading] = useState(false) - const [txLink, setTxLink] = useState() - const [txHash, setTxHash] = useState() - const [balance, setBalance] = useState('0') - const [network, setNetwork] = useState('Unknown') - // const [loginType, setLoginType] = useState('Not connected') - - const { address, isConnected, caipAddress } = useAppKitAccount() - const { walletProvider } = useAppKitProvider('eip155') - // const { walletInfo } = useWalletInfo() - const toast = useToast() - - useEffect(() => { - if (isConnected) { - setTxHash(undefined) - getNetwork() - // updateLoginType() - getBal() - console.log('user address:', address) - console.log('erc20 contract address:', ERC20_CONTRACT_ADDRESS) - // console.log('walletInfo:', walletInfo) - } - }, [isConnected, address, caipAddress]) - - const getBal = async () => { - if (isConnected && walletProvider) { - const ethersProvider = new BrowserProvider(walletProvider as any) - const balance = await ethersProvider.getBalance(address as any) - - const ethBalance = ethers.formatEther(balance) - console.log('bal:', Number(parseFloat(ethBalance).toFixed(5))) - setBalance(parseFloat(ethBalance).toFixed(5)) - if (ethBalance !== '0') { - return Number(ethBalance) - } else { - return 0 - } - } else { - return 0 - } - } - - const getNetwork = async () => { - if (walletProvider) { - const ethersProvider = new BrowserProvider(walletProvider as any) - const network = await ethersProvider.getNetwork() - const capitalize = (s: string) => s.charAt(0).toUpperCase() + s.slice(1) - setNetwork(capitalize(network.name)) - } - } - - // const updateLoginType = async () => { - // try { - // if (walletInfo != undefined) { - // setLoginType(walletInfo.name ? walletInfo.name : 'Unknown') - // } - // } catch (error) { - // console.error('Error getting login type:', error) - // setLoginType('Unknown') - // } - // } - - const openEtherscan = () => { - if (address) { - const baseUrl = - caipAddress === 'eip155:11155111:' ? 'https://sepolia.etherscan.io/address/' : 'https://etherscan.io/address/' - window.open(baseUrl + address, '_blank') - } - } - - const faucetTx = async () => { - try { - const response = await fetch('/api/faucet', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ address }), - }) - const data = await response.json() - if (!response.ok) { - throw new Error(data.message || 'Faucet request failed') - } - return data.txHash - } catch (error) { - console.error('Faucet error:', error) - throw error - } - } - - const doSomething = async () => { - setTxHash(undefined) - try { - if (!isConnected) { - toast({ - title: 'Not connected yet', - description: 'Please connect your wallet, my friend.', - status: 'error', - position: 'bottom', - variant: 'subtle', - duration: 9000, - isClosable: true, - }) - return - } - if (walletProvider) { - setIsLoading(true) - setTxHash('') - setTxLink('') - const ethersProvider = new BrowserProvider(walletProvider as Eip1193Provider) - const signer = await ethersProvider.getSigner() - - const erc20 = new Contract(ERC20_CONTRACT_ADDRESS, ERC20_CONTRACT_ABI, signer) - - ///// Send ETH if needed ///// - const bal = await getBal() - console.log('bal:', bal) - if (bal < 0.025) { - const faucetTxHash = await faucetTx() - console.log('faucet tx:', faucetTxHash) - const bal = await getBal() - console.log('bal:', bal) - } - ///// Call ///// - const call = await erc20.mint(parseEther('10000')) // 0.000804454399826656 ETH // https://sepolia.etherscan.io/tx/0x687e32332965aa451abe45f89c9fefc4b5afe6e99c95948a300565f16a212d7b - - let receipt: ethers.ContractTransactionReceipt | null = null - try { - receipt = await call.wait() - } catch (error) { - console.error('Error waiting for transaction:', error) - throw new Error('Transaction failed or was reverted') - } - - if (receipt === null) { - throw new Error('Transaction receipt is null') - } - - console.log('tx:', receipt) - setTxHash(receipt.hash) - setTxLink('https://sepolia.etherscan.io/tx/' + receipt.hash) - setIsLoading(false) - toast({ - title: 'Successful tx', - description: 'Well done! 🎉', - status: 'success', - position: 'bottom', - variant: 'subtle', - duration: 20000, - isClosable: true, - }) - await getBal() - } - } catch (e) { - setIsLoading(false) - console.error('Error in doSomething:', e) - toast({ - title: 'Woops', - description: e instanceof Error ? e.message : 'Something went wrong...', - status: 'error', - position: 'bottom', - variant: 'subtle', - duration: 9000, - isClosable: true, - }) - } - } - - return ( - <> - -
- {!isConnected ? ( - <> - You can login with your email, Google, or with one of many wallets suported by Reown. -
- - ) : ( - - - Network: {network} - - {/* - Login type: {loginType} - */} - - Balance: {balance} ETH - - - Address: {address || 'Not connected'} - - - )} - - {txHash && isConnected && ( - - {txHash} - - )}{' '} -
- - ) -} - -``` - -## src/pages/new - - -### src/pages/new/index.tsx - -``` -import { Text, Button, useToast } from '@chakra-ui/react' - -export default function New() { - return ( - <> -
- A brand new page! 😋 -
- - ) -} - -``` - -## src/utils - - -### src/utils/config.ts - -```typescript -import { ThemingProps } from '@chakra-ui/react' -export const SITE_DESCRIPTION = 'W3HC Next.js app template' -export const SITE_NAME = 'Genji' -export const SITE_URL = 'https://genji-app.netlify.app' - -export const THEME_INITIAL_COLOR = 'system' -export const THEME_COLOR_SCHEME: ThemingProps['colorScheme'] = 'blue' -export const THEME_CONFIG = { - initialColorMode: THEME_INITIAL_COLOR, -} - -export const SOCIAL_TWITTER = 'w3hc8' -export const SOCIAL_GITHUB = 'w3hc/genji' - -export const SERVER_SESSION_SETTINGS = { - cookieName: SITE_NAME, - password: process.env.SESSION_PASSWORD ?? 'UPDATE_TO_complex_password_at_least_32_characters_long', - cookieOptions: { - secure: process.env.NODE_ENV === 'production', - }, -} - -``` - -### src/utils/erc20.ts - -```typescript -// contract used in the example app: https://github.com/w3hc/w3hc-hardhat-template/ -export const ERC20_CONTRACT_ADDRESS = '0xF57cE903E484ca8825F2c1EDc7F9EEa3744251eB' // Sepolia -// export const ERC20_CONTRACT_ADDRESS = '0x80Fae255a5261Ca183668259382A37789e86f92F' // OP Sepolia -export const ERC20_CONTRACT_ABI = [ - { - inputs: [ - { - internalType: 'uint256', - name: '_initialSupply', - type: 'uint256', - }, - ], - stateMutability: 'nonpayable', - type: 'constructor', - }, - { - anonymous: false, - inputs: [ - { - indexed: true, - internalType: 'address', - name: 'owner', - type: 'address', - }, - { - indexed: true, - internalType: 'address', - name: 'spender', - type: 'address', - }, - { - indexed: false, - internalType: 'uint256', - name: 'value', - type: 'uint256', - }, - ], - name: 'Approval', - type: 'event', - }, - { - anonymous: false, - inputs: [ - { - indexed: true, - internalType: 'address', - name: 'from', - type: 'address', - }, - { - indexed: true, - internalType: 'address', - name: 'to', - type: 'address', - }, - { - indexed: false, - internalType: 'uint256', - name: 'value', - type: 'uint256', - }, - ], - name: 'Transfer', - type: 'event', - }, - { - inputs: [ - { - internalType: 'address', - name: 'owner', - type: 'address', - }, - { - internalType: 'address', - name: 'spender', - type: 'address', - }, - ], - name: 'allowance', - outputs: [ - { - internalType: 'uint256', - name: '', - type: 'uint256', - }, - ], - stateMutability: 'view', - type: 'function', - }, - { - inputs: [ - { - internalType: 'address', - name: 'spender', - type: 'address', - }, - { - internalType: 'uint256', - name: 'amount', - type: 'uint256', - }, - ], - name: 'approve', - outputs: [ - { - internalType: 'bool', - name: '', - type: 'bool', - }, - ], - stateMutability: 'nonpayable', - type: 'function', - }, - { - inputs: [ - { - internalType: 'address', - name: 'account', - type: 'address', - }, - ], - name: 'balanceOf', - outputs: [ - { - internalType: 'uint256', - name: '', - type: 'uint256', - }, - ], - stateMutability: 'view', - type: 'function', - }, - { - inputs: [], - name: 'decimals', - outputs: [ - { - internalType: 'uint8', - name: '', - type: 'uint8', - }, - ], - stateMutability: 'view', - type: 'function', - }, - { - inputs: [ - { - internalType: 'address', - name: 'spender', - type: 'address', - }, - { - internalType: 'uint256', - name: 'subtractedValue', - type: 'uint256', - }, - ], - name: 'decreaseAllowance', - outputs: [ - { - internalType: 'bool', - name: '', - type: 'bool', - }, - ], - stateMutability: 'nonpayable', - type: 'function', - }, - { - inputs: [ - { - internalType: 'address', - name: 'spender', - type: 'address', - }, - { - internalType: 'uint256', - name: 'addedValue', - type: 'uint256', - }, - ], - name: 'increaseAllowance', - outputs: [ - { - internalType: 'bool', - name: '', - type: 'bool', - }, - ], - stateMutability: 'nonpayable', - type: 'function', - }, - { - inputs: [ - { - internalType: 'uint256', - name: '_amount', - type: 'uint256', - }, - ], - name: 'mint', - outputs: [], - stateMutability: 'nonpayable', - type: 'function', - }, - { - inputs: [], - name: 'name', - outputs: [ - { - internalType: 'string', - name: '', - type: 'string', - }, - ], - stateMutability: 'view', - type: 'function', - }, - { - inputs: [], - name: 'symbol', - outputs: [ - { - internalType: 'string', - name: '', - type: 'string', - }, - ], - stateMutability: 'view', - type: 'function', - }, - { - inputs: [], - name: 'totalSupply', - outputs: [ - { - internalType: 'uint256', - name: '', - type: 'uint256', - }, - ], - stateMutability: 'view', - type: 'function', - }, - { - inputs: [ - { - internalType: 'address', - name: 'to', - type: 'address', - }, - { - internalType: 'uint256', - name: 'amount', - type: 'uint256', - }, - ], - name: 'transfer', - outputs: [ - { - internalType: 'bool', - name: '', - type: 'bool', - }, - ], - stateMutability: 'nonpayable', - type: 'function', - }, - { - inputs: [ - { - internalType: 'address', - name: 'from', - type: 'address', - }, - { - internalType: 'address', - name: 'to', - type: 'address', - }, - { - internalType: 'uint256', - name: 'amount', - type: 'uint256', - }, - ], - name: 'transferFrom', - outputs: [ - { - internalType: 'bool', - name: '', - type: 'bool', - }, - ], - stateMutability: 'nonpayable', - type: 'function', - }, -] - -``` - -### tsconfig.json - -```json -{ - "compilerOptions": { - "target": "es5", - "lib": ["dom", "dom.iterable", "esnext"], - "allowJs": true, - "skipLibCheck": true, - "strict": true, - "forceConsistentCasingInFileNames": true, - "esModuleInterop": true, - "module": "esnext", - "moduleResolution": "node", - "resolveJsonModule": true, - "isolatedModules": true, - "jsx": "preserve", - "noEmit": true, - "incremental": true - }, - "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], - "exclude": ["node_modules"], - "types": ["jest", "node"], - "esModuleInterop": true -} - -``` - -## Structure - -``` -├── .env.example -├── .eslintignore -├── .eslintrc.json -├── .github - └── workflows - │ └── run-tests.yml -├── .gitignore -├── .next -├── .prettierignore -├── .prettierrc.json -├── .swc - └── plugins - │ └── v7_macos_aarch64_0.106.15 -├── .vscode - ├── extensions.json - └── settings.json -├── .well-known - └── walletconnect.txt -├── README.md -├── genji_app_description.md -├── jest.config.ts -├── jest.setup.ts -├── next.config.js -├── package.json -├── pnpm-lock.yaml -├── public - ├── favicon.ico - └── huangshan.png -├── src - ├── __tests__ - │ ├── Header.test.tsx - │ └── index.test.tsx - ├── components - │ └── layout - │ │ ├── ErrorBoundary.tsx - │ │ ├── Head.tsx - │ │ ├── Header.tsx - │ │ ├── HeadingComponent.tsx - │ │ ├── LinkComponent.tsx - │ │ ├── Seo.tsx - │ │ ├── ThemeSwitcher.tsx - │ │ └── index.tsx - ├── context - │ └── web3modal.tsx - ├── hooks - │ └── useIsMounted.tsx - ├── pages - │ ├── _app.tsx - │ ├── _document.tsx - │ ├── api - │ │ └── faucet.ts - │ ├── index.tsx - │ └── new - │ │ └── index.tsx - └── utils - │ ├── config.ts - │ └── erc20.ts -└── tsconfig.json -``` - -Timestamp: Nov 02 2024 04:04:09 PM UTC \ No newline at end of file diff --git a/jest.config.ts b/jest.config.ts index 0565e15..4b8aae4 100644 --- a/jest.config.ts +++ b/jest.config.ts @@ -6,12 +6,10 @@ const createJestConfig = nextJest({ }) const customJestConfig: Config = { - preset: 'ts-jest', setupFilesAfterEnv: ['/jest.setup.ts'], testEnvironment: 'jest-environment-jsdom', - moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'], - transform: { - '^.+\\.(ts|tsx)$': 'ts-jest', + moduleNameMapper: { + '^@/(.*)$': '/src/$1', }, } diff --git a/jest.setup.ts b/jest.setup.ts index c44951a..dba374e 100644 --- a/jest.setup.ts +++ b/jest.setup.ts @@ -1 +1,13 @@ import '@testing-library/jest-dom' + +// Just mock Reown AppKit as it's essential for the app to render +jest.mock('@reown/appkit/react', () => ({ + useAppKitAccount: () => ({ + address: null, + isConnected: false, + caipAddress: null, + }), + useAppKitProvider: () => ({ + walletProvider: null, + }), +})) diff --git a/package.json b/package.json index a7f5f9c..3555107 100644 --- a/package.json +++ b/package.json @@ -7,15 +7,16 @@ "build": "next build", "start": "next start", "lint": "next lint", - "test": "jest", - "test:watch": "jest --watch", + "test": "jest --config jest.config.ts", + "test:watch": "jest --config jest.config.ts --watch", + "test:coverage": "jest --config jest.config.ts --coverage", "format": "prettier --write \"**/*.{ts,tsx,js,jsx,json,md}\"", "format:check": "prettier --check \"**/*.{ts,tsx,js,jsx,json,md}\"" }, "dependencies": { - "@chakra-ui/icons": "^2.1.1", - "@chakra-ui/next-js": "^2.2.0", - "@chakra-ui/react": "^2.8.2", + "@chakra-ui/icons": "^2.2.4", + "@chakra-ui/next-js": "^2.4.2", + "@chakra-ui/react": "^2.10.2", "@coinbase/wallet-sdk": "4.0.3", "@emotion/react": "^11.13.3", "@emotion/styled": "^11.13.0", @@ -26,8 +27,8 @@ "autoprefixer": "10.4.20", "eslint": "8.57.1", "eslint-config-next": "14.2.7", - "ethers": "^6.13.2", - "framer-motion": "^11.7.0", + "ethers": "^6.13.4", + "framer-motion": "^11.11.8", "next": "14.2.12", "next-seo": "^6.6.0", "postcss": "8.4.44", @@ -47,7 +48,7 @@ "jest": "^29.7.0", "jest-environment-jsdom": "^29.7.0", "prettier": "^3.3.3", - "ts-jest": "^29.1.0", + "ts-jest": "^29.2.5", "ts-node": "^10.9.2" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index cdc1f56..3b35fac 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -9,13 +9,13 @@ importers: .: dependencies: '@chakra-ui/icons': - specifier: ^2.1.1 + specifier: ^2.2.4 version: 2.2.4(@chakra-ui/react@2.10.2(@emotion/react@11.13.3(@types/react@18.3.5)(react@18.3.1))(@emotion/styled@11.13.0(@emotion/react@11.13.3(@types/react@18.3.5)(react@18.3.1))(@types/react@18.3.5)(react@18.3.1))(@types/react@18.3.5)(framer-motion@11.11.8(@emotion/is-prop-valid@1.3.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1) '@chakra-ui/next-js': - specifier: ^2.2.0 + specifier: ^2.4.2 version: 2.4.2(@chakra-ui/react@2.10.2(@emotion/react@11.13.3(@types/react@18.3.5)(react@18.3.1))(@emotion/styled@11.13.0(@emotion/react@11.13.3(@types/react@18.3.5)(react@18.3.1))(@types/react@18.3.5)(react@18.3.1))(@types/react@18.3.5)(framer-motion@11.11.8(@emotion/is-prop-valid@1.3.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@emotion/react@11.13.3(@types/react@18.3.5)(react@18.3.1))(next@14.2.12(@babel/core@7.25.8)(babel-plugin-macros@3.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1) '@chakra-ui/react': - specifier: ^2.8.2 + specifier: ^2.10.2 version: 2.10.2(@emotion/react@11.13.3(@types/react@18.3.5)(react@18.3.1))(@emotion/styled@11.13.0(@emotion/react@11.13.3(@types/react@18.3.5)(react@18.3.1))(@types/react@18.3.5)(react@18.3.1))(@types/react@18.3.5)(framer-motion@11.11.8(@emotion/is-prop-valid@1.3.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@coinbase/wallet-sdk': specifier: 4.0.3 @@ -48,10 +48,10 @@ importers: specifier: 14.2.7 version: 14.2.7(eslint@8.57.1)(typescript@5.5.4) ethers: - specifier: ^6.13.2 + specifier: ^6.13.4 version: 6.13.4 framer-motion: - specifier: ^11.7.0 + specifier: ^11.11.8 version: 11.11.8(@emotion/is-prop-valid@1.3.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) next: specifier: 14.2.12 @@ -106,7 +106,7 @@ importers: specifier: ^3.3.3 version: 3.3.3 ts-jest: - specifier: ^29.1.0 + specifier: ^29.2.5 version: 29.2.5(@babel/core@7.25.8)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.25.8))(jest@29.7.0(@types/node@22.5.2)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@22.5.2)(typescript@5.5.4)))(typescript@5.5.4) ts-node: specifier: ^10.9.2 @@ -131,6 +131,10 @@ packages: resolution: {integrity: sha512-0xZJFNE5XMpENsgfHYTw8FbX4kv53mFLn2i3XPoq69LyhYSCBJtitaHx9QnsVTrsogI4Z3+HtEfZ2/GFPOtf5g==} engines: {node: '>=6.9.0'} + '@babel/code-frame@7.26.2': + resolution: {integrity: sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==} + engines: {node: '>=6.9.0'} + '@babel/compat-data@7.25.8': resolution: {integrity: sha512-ZsysZyXY4Tlx+Q53XdnOFmqwfB9QDTHYxaZYajWRoBLuLEAwI2UIbtxOjWh/cFaa9IKUlcB+DDuoskLuKu56JA==} engines: {node: '>=6.9.0'} @@ -173,6 +177,10 @@ packages: resolution: {integrity: sha512-AM6TzwYqGChO45oiuPqwL2t20/HdMC1rTPAesnBCgPCSF1x3oN9MVUwQV2iyz4xqWrctwK5RNC8LV22kaQCNYg==} engines: {node: '>=6.9.0'} + '@babel/helper-validator-identifier@7.25.9': + resolution: {integrity: sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==} + engines: {node: '>=6.9.0'} + '@babel/helper-validator-option@7.25.7': resolution: {integrity: sha512-ytbPLsm+GjArDYXJ8Ydr1c/KJuutjF2besPNbIZnZ6MKUxi/uTA22t2ymmA4WFjZFpjiAMO0xuuJPqK2nvDVfQ==} engines: {node: '>=6.9.0'} @@ -285,6 +293,10 @@ packages: resolution: {integrity: sha512-FjoyLe754PMiYsFaN5C94ttGiOmBNYTf6pLr4xXHAT5uctHb092PBszndLDR5XA/jghQvn4n7JMHl7dmTgbm9w==} engines: {node: '>=6.9.0'} + '@babel/runtime@7.26.0': + resolution: {integrity: sha512-FDSOghenHTiToteC/QRlv2q3DhPZ/oOXTBoirfWNx1Cx3TMVcGWQtMMmQcSvb/JjpNeGzx8Pq/b4fKEJuWm1sw==} + engines: {node: '>=6.9.0'} + '@babel/template@7.25.7': resolution: {integrity: sha512-wRwtAgI3bAS+JGU2upWNL9lSlDcRCqD05BZ1n3X2ONLH1WilFP6O1otQjeMK/1g0pvYcXC7b/qVUB1keofjtZA==} engines: {node: '>=6.9.0'} @@ -2960,6 +2972,9 @@ packages: picocolors@1.1.0: resolution: {integrity: sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw==} + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + picomatch@2.3.1: resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} engines: {node: '>=8.6'} @@ -3917,6 +3932,12 @@ snapshots: '@babel/highlight': 7.25.7 picocolors: 1.1.0 + '@babel/code-frame@7.26.2': + dependencies: + '@babel/helper-validator-identifier': 7.25.9 + js-tokens: 4.0.0 + picocolors: 1.1.1 + '@babel/compat-data@7.25.8': {} '@babel/core@7.25.8': @@ -3984,6 +4005,8 @@ snapshots: '@babel/helper-validator-identifier@7.25.7': {} + '@babel/helper-validator-identifier@7.25.9': {} + '@babel/helper-validator-option@7.25.7': {} '@babel/helpers@7.25.7': @@ -4091,6 +4114,10 @@ snapshots: dependencies: regenerator-runtime: 0.14.1 + '@babel/runtime@7.26.0': + dependencies: + regenerator-runtime: 0.14.1 + '@babel/template@7.25.7': dependencies: '@babel/code-frame': 7.25.7 @@ -5017,8 +5044,8 @@ snapshots: '@testing-library/dom@10.4.0': dependencies: - '@babel/code-frame': 7.25.7 - '@babel/runtime': 7.25.7 + '@babel/code-frame': 7.26.2 + '@babel/runtime': 7.26.0 '@types/aria-query': 5.0.4 aria-query: 5.3.0 chalk: 4.1.2 @@ -7732,6 +7759,8 @@ snapshots: picocolors@1.1.0: {} + picocolors@1.1.1: {} + picomatch@2.3.1: {} pino-abstract-transport@0.5.0: diff --git a/.well-known/walletconnect.txt b/public/.well-known/walletconnect.txt similarity index 100% rename from .well-known/walletconnect.txt rename to public/.well-known/walletconnect.txt diff --git a/src/__tests__/Header.test.tsx b/src/__tests__/Header.test.tsx index 392114a..fd731b7 100644 --- a/src/__tests__/Header.test.tsx +++ b/src/__tests__/Header.test.tsx @@ -1,16 +1,11 @@ import React from 'react' -import { render, screen } from '@testing-library/react' +import { render, screen } from '../utils/test-utils' +import { Header } from '@/components/Header' import '@testing-library/jest-dom' -import { Header } from '../components/layout/Header' - -jest.mock('@reown/appkit/react', () => ({ - useAppKitAccount: () => ({ isConnected: false }), -})) describe('Header', () => { - it('renders the site name', () => { + it('exists in the document', () => { render(
) - const siteName = screen.getByText('Genji') - expect(siteName).toBeInTheDocument() + expect(document.querySelector('header')).toBeInTheDocument() }) }) diff --git a/src/__tests__/index.test.tsx b/src/__tests__/index.test.tsx index db15fbb..9e2414a 100644 --- a/src/__tests__/index.test.tsx +++ b/src/__tests__/index.test.tsx @@ -1,34 +1,12 @@ import React from 'react' -import { render, screen } from '@testing-library/react' +import { render, screen } from '../utils/test-utils' import '@testing-library/jest-dom' -import Home from '../pages/index' - -jest.mock('@reown/appkit/react', () => ({ - useAppKitAccount: () => ({ address: null, isConnected: false, caipAddress: null }), - useAppKitProvider: () => ({ walletProvider: null }), -})) - -jest.mock('next/router', () => ({ - useRouter() { - return { - route: '/', - pathname: '', - query: '', - asPath: '', - } - }, -})) +import Home from '@/pages/index' describe('Home page', () => { - it('renders the login message when not connected', () => { - render() - expect( - screen.getByText(/You can login with your email, Google, or with one of many wallets suported by Reown\./) - ).toBeInTheDocument() - }) - it('renders the mint button', () => { render() - expect(screen.getByRole('button', { name: /Mint/i })).toBeInTheDocument() + const button = screen.getByRole('button', { name: /mint/i }) + expect(button).toBeInTheDocument() }) }) diff --git a/src/components/layout/ErrorBoundary.tsx b/src/components/ErrorBoundary.tsx similarity index 100% rename from src/components/layout/ErrorBoundary.tsx rename to src/components/ErrorBoundary.tsx diff --git a/src/components/layout/Head.tsx b/src/components/Head.tsx similarity index 94% rename from src/components/layout/Head.tsx rename to src/components/Head.tsx index c3cfe87..dce7435 100644 --- a/src/components/layout/Head.tsx +++ b/src/components/Head.tsx @@ -1,6 +1,6 @@ import React from 'react' import { default as NextHead } from 'next/head' -import { SITE_URL } from '../../utils/config' +import { SITE_URL } from '../utils/config' interface Props { title?: string diff --git a/src/components/layout/Header.tsx b/src/components/Header.tsx similarity index 86% rename from src/components/layout/Header.tsx rename to src/components/Header.tsx index f0b873f..38b518d 100644 --- a/src/components/layout/Header.tsx +++ b/src/components/Header.tsx @@ -17,9 +17,9 @@ import { import { LinkComponent } from './LinkComponent' import { ThemeSwitcher } from './ThemeSwitcher' import { HeadingComponent } from './HeadingComponent' -import { SITE_NAME } from '../../utils/config' +import { SITE_NAME } from '../utils/config' import { FaGithub } from 'react-icons/fa' -import { Web3Modal } from '../../context/web3modal' +import { Web3Modal } from '../context/web3modal' import { HamburgerIcon } from '@chakra-ui/icons' interface Props { @@ -38,7 +38,7 @@ export function Header(props: Props) { py={5} mb={8} alignItems="center"> - + {SITE_NAME} @@ -48,17 +48,16 @@ export function Header(props: Props) { } size={'sm'} mr={4} /> - + Home - + New - {/* */}{' '} diff --git a/src/components/layout/HeadingComponent.tsx b/src/components/HeadingComponent.tsx similarity index 100% rename from src/components/layout/HeadingComponent.tsx rename to src/components/HeadingComponent.tsx diff --git a/src/components/LinkComponent.tsx b/src/components/LinkComponent.tsx new file mode 100644 index 0000000..2af62bf --- /dev/null +++ b/src/components/LinkComponent.tsx @@ -0,0 +1,37 @@ +import React, { ReactNode } from 'react' +import NextLink from 'next/link' +import { Link, useColorModeValue } from '@chakra-ui/react' +import { THEME_COLOR_SCHEME } from '@/utils/config' + +interface Props { + href: string + children: ReactNode + isExternal?: boolean + className?: string + invisible?: boolean +} + +export function LinkComponent(props: Props) { + const className = props.className ?? '' + const isExternal = props.href.match(/^([a-z0-9]*:|.{0})\/\/.*$/) || props.isExternal + const defaultColor = useColorModeValue(`${THEME_COLOR_SCHEME}.600`, `${THEME_COLOR_SCHEME}.400`) + + // Apply invisible styling or default link styling + const linkStyle = props.invisible + ? { _hover: { color: defaultColor } } + : { color: '#45a2f8', _hover: { color: '#8c1c84' } } + + if (isExternal) { + return ( + + {props.children} + + ) + } + + return ( + + {props.children} + + ) +} diff --git a/src/components/layout/Seo.tsx b/src/components/Seo.tsx similarity index 97% rename from src/components/layout/Seo.tsx rename to src/components/Seo.tsx index c6301c1..fa2d4e8 100644 --- a/src/components/layout/Seo.tsx +++ b/src/components/Seo.tsx @@ -1,5 +1,5 @@ import React from 'react' -import { SITE_DESCRIPTION, SITE_NAME, SITE_URL, SOCIAL_TWITTER } from '../../utils/config' +import { SITE_DESCRIPTION, SITE_NAME, SITE_URL, SOCIAL_TWITTER } from '../utils/config' import { DefaultSeo } from 'next-seo' export function Seo() { diff --git a/src/components/layout/ThemeSwitcher.tsx b/src/components/ThemeSwitcher.tsx similarity index 100% rename from src/components/layout/ThemeSwitcher.tsx rename to src/components/ThemeSwitcher.tsx diff --git a/src/components/layout/LinkComponent.tsx b/src/components/layout/LinkComponent.tsx deleted file mode 100644 index de3d57b..0000000 --- a/src/components/layout/LinkComponent.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import React, { ReactNode } from 'react' -import NextLink from 'next/link' -import { Link, useColorModeValue } from '@chakra-ui/react' -import { THEME_COLOR_SCHEME } from '../../utils/config' - -interface Props { - href: string - children: ReactNode - isExternal?: boolean - className?: string -} - -export function LinkComponent(props: Props) { - const className = props.className ?? '' - const isExternal = props.href.match(/^([a-z0-9]*:|.{0})\/\/.*$/) || props.isExternal - const color = useColorModeValue(`${THEME_COLOR_SCHEME}.600`, `${THEME_COLOR_SCHEME}.400`) - - if (isExternal) { - return ( - - {props.children} - - ) - } - - return ( - - {props.children} - - ) -} diff --git a/src/components/layout/index.tsx b/src/components/layout/index.tsx index 352de52..5bcb645 100644 --- a/src/components/layout/index.tsx +++ b/src/components/layout/index.tsx @@ -1,7 +1,7 @@ import { Web3Modal } from '../../context/web3modal' import { ReactNode } from 'react' import { Box, Container } from '@chakra-ui/react' -import { Header } from './Header' +import { Header } from '../Header' interface Props { children?: ReactNode diff --git a/src/pages/_app.tsx b/src/pages/_app.tsx index bea61e9..4e2e38f 100644 --- a/src/pages/_app.tsx +++ b/src/pages/_app.tsx @@ -2,10 +2,11 @@ import type { AppProps } from 'next/app' import Layout from '../components/layout' import { useEffect } from 'react' import { ChakraProvider } from '@chakra-ui/react' -import { Seo } from '../components/layout/Seo' +import { DefaultSeo } from 'next-seo' +import { SITE_NAME, SITE_DESCRIPTION, SITE_URL, SOCIAL_TWITTER } from '../utils/config' import { ERC20_CONTRACT_ADDRESS } from '../utils/erc20' import { useIsMounted } from '../hooks/useIsMounted' -import ErrorBoundary from '../components/layout/ErrorBoundary' +import ErrorBoundary from '../components/ErrorBoundary' export default function App({ Component, pageProps }: AppProps) { useEffect(() => { @@ -15,9 +16,41 @@ export default function App({ Component, pageProps }: AppProps) { return ( <> + - {isMounted && ( diff --git a/src/pages/index.tsx b/src/pages/index.tsx index eeb57ed..e52e054 100644 --- a/src/pages/index.tsx +++ b/src/pages/index.tsx @@ -2,15 +2,19 @@ import * as React from 'react' import { Text, Button, useToast, Box } from '@chakra-ui/react' import { useState, useEffect } from 'react' import { BrowserProvider, Contract, Eip1193Provider, parseEther } from 'ethers' -// import { useAppKitAccount, useAppKitProvider, useWalletInfo } from '@reown/appkit/react' import { useAppKitAccount, useAppKitProvider } from '@reown/appkit/react' import { ERC20_CONTRACT_ADDRESS, ERC20_CONTRACT_ABI } from '../utils/erc20' -import { LinkComponent } from '../components/layout/LinkComponent' +import { LinkComponent } from '../components/LinkComponent' import { ethers } from 'ethers' -import { Head } from '../components/layout/Head' +import { Head } from '../components/Head' import { SITE_NAME, SITE_DESCRIPTION } from '../utils/config' +import { NextSeo } from 'next-seo' +import { SITE_URL } from '../utils/config' export default function Home() { + const seoTitle = 'Genji - Web3 Application Template' + const seoDescription = 'A modern Web3 application template featuring Next.js, Reown, Ethers.js, and Chakra UI' + const [isLoading, setIsLoading] = useState(false) const [txLink, setTxLink] = useState() const [txHash, setTxHash] = useState() @@ -181,6 +185,41 @@ export default function Home() { return ( <> +
{!isConnected ? ( diff --git a/src/pages/new/index.tsx b/src/pages/new/index.tsx index c41a673..d3cc526 100644 --- a/src/pages/new/index.tsx +++ b/src/pages/new/index.tsx @@ -1,8 +1,43 @@ import { Text, Button, useToast } from '@chakra-ui/react' +import { NextSeo } from 'next-seo' +import { SITE_URL } from '../../utils/config' export default function New() { + const seoTitle = 'New Page - Genji' + const seoDescription = 'Create new content and interact with the Genji Web3 template' + return ( <> +
A brand new page! 😋
diff --git a/src/utils/test-utils.tsx b/src/utils/test-utils.tsx new file mode 100644 index 0000000..5b044ac --- /dev/null +++ b/src/utils/test-utils.tsx @@ -0,0 +1,18 @@ +import React from 'react' +import { ChakraProvider } from '@chakra-ui/react' +import { render, RenderOptions } from '@testing-library/react' + +// Create a custom render function that includes providers +const customRender = (ui: React.ReactElement, options?: Omit) => { + const AllProviders = ({ children }: { children: React.ReactNode }) => { + return {children} + } + + return render(ui, { wrapper: AllProviders, ...options }) +} + +// re-export everything +export * from '@testing-library/react' + +// override render method +export { customRender as render } diff --git a/tsconfig.json b/tsconfig.json index 18e5bb1..edb3b34 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -13,10 +13,14 @@ "isolatedModules": true, "jsx": "preserve", "noEmit": true, - "incremental": true + "incremental": true, + "baseUrl": ".", + "paths": { + "@/*": ["src/*"], + "src/*": ["src/*"] + }, + "types": ["jest", "node", "@testing-library/jest-dom"] }, - "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], - "exclude": ["node_modules"], - "types": ["jest", "node"], - "esModuleInterop": true + "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", "jest.setup.ts", "jest.config.ts"], + "exclude": ["node_modules"] }