🎉 init:

This commit is contained in:
john 2024-06-18 10:56:57 +08:00
commit bdaa469836
36 changed files with 9212 additions and 0 deletions

0
.env Normal file
View File

1
.env.development Normal file
View File

@ -0,0 +1 @@
VITE_BASE_API_URL=/dev

18
.eslintrc.cjs Normal file
View File

@ -0,0 +1,18 @@
module.exports = {
root: true,
env: { browser: true, es2020: true },
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
'plugin:react-hooks/recommended',
],
ignorePatterns: ['dist', '.eslintrc.cjs'],
parser: '@typescript-eslint/parser',
plugins: ['react-refresh'],
rules: {
'react-refresh/only-export-components': [
'warn',
{ allowConstantExport: true },
],
},
}

24
.gitignore vendored Normal file
View File

@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.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?

BIN
.yarn/install-state.gz Normal file

Binary file not shown.

1
.yarnrc.yml Normal file
View File

@ -0,0 +1 @@
nodeLinker: node-modules

30
README.md Normal file
View File

@ -0,0 +1,30 @@
# React + TypeScript + Vite
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
Currently, two official plugins are available:
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
## Expanding the ESLint configuration
If you are developing a production application, we recommend updating the configuration to enable type aware lint rules:
- Configure the top-level `parserOptions` property like this:
```js
export default {
// other rules...
parserOptions: {
ecmaVersion: 'latest',
sourceType: 'module',
project: ['./tsconfig.json', './tsconfig.node.json'],
tsconfigRootDir: __dirname,
},
}
```
- Replace `plugin:@typescript-eslint/recommended` to `plugin:@typescript-eslint/recommended-type-checked` or `plugin:@typescript-eslint/strict-type-checked`
- Optionally add `plugin:@typescript-eslint/stylistic-type-checked`
- Install [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) and add `plugin:react/recommended` & `plugin:react/jsx-runtime` to the `extends` list

8
iconfont.json Normal file
View File

@ -0,0 +1,8 @@
{
"symbol_url": "请参考README.md复制官网提供的JS链接",
"use_typescript": false,
"save_dir": "./src/components/iconfont",
"trim_icon_prefix": "icon",
"unit": "px",
"default_icon_size": 18
}

22
index.html Normal file
View File

@ -0,0 +1,22 @@
<!--
* @LastEditors: John
* @Date: 2024-06-17 17:20:03
* @LastEditTime: 2024-06-18 10:51:36
* @Author: John
-->
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta
name="viewport"
content="width=device-width, initial-scale=1.0,maximum-scale=1, minimum-scale=1, user-scalable=no"
/>
<title>red devils</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

49
package.json Normal file
View File

@ -0,0 +1,49 @@
{
"name": "red-devils",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
"preview": "vite preview",
"iconfont": "npx iconfont-h5"
},
"dependencies": {
"@hyper-fetch/core": "^5.7.5",
"@hyper-fetch/react": "^5.7.5",
"@tanstack/react-query": "^5.45.1",
"@web3modal/wagmi": "^5.0.2",
"antd-mobile": "^5.36.1",
"i18next": "^23.11.5",
"normalize.css": "^8.0.1",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-i18next": "^14.1.2",
"react-router-dom": "^6.23.1",
"viem": "^2.14.2",
"wagmi": "^2.10.2",
"zustand": "^4.5.2"
},
"devDependencies": {
"@types/node": "^20.14.2",
"@types/postcss-pxtorem": "^6",
"@types/react": "^18.2.66",
"@types/react-dom": "^18.2.22",
"@typescript-eslint/eslint-plugin": "^7.2.0",
"@typescript-eslint/parser": "^7.2.0",
"@vitejs/plugin-react": "^4.2.1",
"autoprefixer": "^10.4.19",
"eslint": "^8.57.0",
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-react-refresh": "^0.4.6",
"postcss": "^8.4.38",
"postcss-pxtorem": "5.1.1",
"react-iconfont-cli": "^2.0.2",
"typescript": "^5.2.2",
"vite": "^5.2.0",
"vite-plugin-compression": "^0.5.1",
"vite-plugin-node-polyfills": "^0.22.0"
}
}

22
postcss.config.js Normal file
View File

@ -0,0 +1,22 @@
/*
* @LastEditors: John
* @Date: 2024-06-17 18:25:10
* @LastEditTime: 2024-06-17 18:45:37
* @Author: John
*/
export default {
plugins: {
autoprefixer: {},
"postcss-pxtorem": {
rootValue: 37.5,
propList: ["*"],
exclude: (e) => {
if (/.module\.css$/.test(e)) {
console.log(e);
return false;
}
return true;
},
},
},
};

1
public/favicon.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 8.5 KiB

4
src/App.css Normal file
View File

@ -0,0 +1,4 @@
body {
width: 100%;
height: 100%;
}

20
src/App.tsx Normal file
View File

@ -0,0 +1,20 @@
/*
* @LastEditors: John
* @Date: 2024-06-17 17:20:03
* @LastEditTime: 2024-06-17 17:36:20
* @Author: John
*/
import { Route, Routes } from "react-router-dom";
import "./App.css";
import Home from "./pages/Home";
function App() {
return (
<>
<Routes>
<Route path="/" element={<Home />} />
</Routes>
</>
);
}
export default App;

View File

@ -0,0 +1,103 @@
/*
* @LastEditors: John
* @Date: 2024-06-17 18:01:43
* @LastEditTime: 2024-06-17 18:16:57
* @Author: John
*/
/*
* @LastEditors: John
* @Date: 2024-06-17 18:01:43
* @LastEditTime: 2024-06-17 18:05:15
* @Author: John
*/
/*
* @LastEditors: John
* @Date: 2024-03-06 11:26:45
* @LastEditTime: 2024-05-27 16:32:26
* @Author: John
*/
import { createWeb3Modal } from "@web3modal/wagmi/react";
import { defaultWagmiConfig } from "@web3modal/wagmi/react/config";
import { WagmiProvider } from "wagmi";
import { bsc } from "wagmi/chains";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { bnbTestNetwork } from "@/constants/wallet";
import { PropsWithChildren } from "react";
// 0. Setup queryClient
const queryClient = new QueryClient();
// 1. Get projectId at https://cloud.walletconnect.com
const projectId = "10834bf2b5d92d012e4b0b05e1150246";
// 2. Create wagmiConfig
const metadata = {
name: "red-devils",
description: "red devils dapp",
url: import.meta.env.BASE_URL, // origin must match your domain & subdomain
icons: [`${import.meta.env.BASE_URL}/favicon.svg`],
};
let chains = import.meta.env.PROD
? ([bsc] as const)
: ([bnbTestNetwork] as const);
export const config = defaultWagmiConfig({
chains, // required
projectId, // required
metadata, // required
enableWalletConnect: true, // Optional - true by default
enableInjected: true, // Optional - true by default
enableEIP6963: true, // Optional - true by default
enableCoinbase: false, // Optional - true by default
multiInjectedProviderDiscovery: true,
});
// console.log(window.ethereum);
// 3. Create modal
createWeb3Modal({
wagmiConfig: config,
projectId,
themeVariables: {
"--w3m-accent": "#ea6d28",
},
featuredWalletIds: [
...(window.ethereum
? []
: ["c57ca95b47569778a828d19178114f4db188b89b763c899ba0be274e97267d96"]),
...(window.okxwallet
? []
: ["971e689d0a5be527bac79629b4ee9b925e82208e5168b733496a09c0faed0709"]),
...(window.bitkeep?.ethereum
? []
: ["38f5d18bd8522c244bdd70cb4a68e0e718865155811c043f052fb9f1c51de662"]),
...(window.ethereum?.isTokenPocket
? []
: ["20459438007b75f4f4acb98bf29aa3b800550309646d375da5fd4aac6c2a2c66"]),
],
allWallets: "HIDE",
});
export function WalletProvider({ children }: PropsWithChildren) {
return (
<>
<WagmiProvider config={config}>
<QueryClientProvider client={queryClient}>
{children}
</QueryClientProvider>
</WagmiProvider>
</>
);
}
declare global {
interface Window {
ethereum?: { isTokenPocket?: boolean };
okxwallet: {
bitcoin: any;
};
unisat: any;
bitkeep?: { ethereum?: any };
}
}

9
src/constants/index.ts Normal file
View File

@ -0,0 +1,9 @@
/*
* @LastEditors: John
* @Date: 2024-06-17 17:57:13
* @LastEditTime: 2024-06-17 17:57:30
* @Author: John
*/
export enum ASYNC_STORAGE_KEY {
Token = "user.token",
}

24
src/constants/wallet.ts Normal file
View File

@ -0,0 +1,24 @@
import { defineChain } from "viem/utils";
export const bnbTestNetwork = defineChain({
id: 97,
name: "BNB-test",
nativeCurrency: {
name: "tBNB",
symbol: "tBNB",
decimals: 18,
},
rpcUrls: {
default: {
http: ["https://data-seed-prebsc-1-s3.binance.org:8545"],
webSocket: undefined,
},
},
blockExplorers: {
default: {
name: "BNB-test",
url: "https://testnet.bscscan.com",
},
},
testnet: true,
});

View File

@ -0,0 +1,78 @@
/*
* @LastEditors: John
* @Date: 2024-06-17 17:37:21
* @LastEditTime: 2024-06-18 09:54:18
* @Author: John
*/
import { createContext, useContext, useState, PropsWithChildren } from "react";
type EventName = "TEST";
export type EventCallBackData = {
TEST: {};
};
// 定义事件的类型
type EventType = {
[eventName in EventName]?: ((data: EventCallBackData[EventName]) => void)[];
};
// 定义事件总线上下文的类型
type EventBusContextType = {
subscribe: <T extends EventName>(
eventName: T,
callback: (data: EventCallBackData[T]) => void
) => void;
unsubscribe: <T extends EventName>(
eventName: T,
callback: (data: EventCallBackData[T]) => void
) => void;
emit: <T extends EventName>(eventName: T, data: EventCallBackData[T]) => void;
};
const EventBusContext = createContext<EventBusContextType | undefined>(
undefined
);
export const useEventBus = () => {
const context = useContext(EventBusContext);
if (!context) {
throw new Error("useEventBus must be used within an EventBusProvider");
}
return context;
};
const EventBusProvider = ({ children }: PropsWithChildren) => {
const [events, setEvents] = useState<EventType>({});
const subscribe = (eventName: EventName, callback: (data: any) => void) => {
setEvents((prevEvents) => ({
...prevEvents,
[eventName]: [...(prevEvents[eventName] || []), callback],
}));
};
const unsubscribe = (eventName: EventName, callback: (data: any) => void) => {
setEvents((prevEvents) => ({
...prevEvents,
[eventName]: prevEvents[eventName]?.filter((cb) => cb !== callback),
}));
};
const emit = (eventName: EventName, data: any) => {
const eventCallbacks = events[eventName] || [];
eventCallbacks.forEach((callback) => callback(data));
};
const eventBusContextValue: EventBusContextType = {
subscribe,
unsubscribe,
emit,
};
return (
<EventBusContext.Provider value={eventBusContextValue}>
{children}
</EventBusContext.Provider>
);
};
export default EventBusProvider;

23
src/i18n/init.ts Normal file
View File

@ -0,0 +1,23 @@
/*
* @LastEditors: John
* @Date: 2024-06-18 10:30:43
* @LastEditTime: 2024-06-18 10:34:19
* @Author: John
*/
import i18next from "i18next";
import { initReactI18next } from "react-i18next";
import en from "./translation/en.json";
i18next.use(initReactI18next).init({
compatibilityJSON: "v3", // <--- add this line.
lng: "en", // if you're using a language detector, do not define the lng option
// lng: LANGUAGE["en-US"],
debug: false,
resources: {
en: {
translation: en,
},
},
// if you see an error like: "Argument of type 'DefaultTFuncReturn' is not assignable to parameter of type xyz"
// set returnNull to false (and also in the i18next.d.ts options)
// returnNull: false,
});

View File

@ -0,0 +1,3 @@
{
"AppName": "red devils"
}

0
src/index.css Normal file
View File

29
src/main.tsx Normal file
View File

@ -0,0 +1,29 @@
/*
* @LastEditors: John
* @Date: 2024-06-17 17:20:03
* @LastEditTime: 2024-06-18 10:46:11
* @Author: John
*/
import "@/i18n/init.ts";
import "antd-mobile/es/global";
import ReactDOM from "react-dom/client";
import App from "./App.tsx";
import "normalize.css";
import "./index.css";
import { HashRouter } from "react-router-dom";
import EventBusProvider from "./context/EventBusContext.tsx";
import { WalletProvider } from "./components/WalletProvider.tsx";
import flexible from "./utils/flexible.ts";
flexible(window, document);
ReactDOM.createRoot(document.getElementById("root")!).render(
<>
<WalletProvider>
<EventBusProvider>
<HashRouter>
<App />
</HashRouter>
</EventBusProvider>
</WalletProvider>
</>
);

10
src/pages/Home.module.css Normal file
View File

@ -0,0 +1,10 @@
.Home {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 10px;
.text {
font-size: 20px;
}
}

46
src/pages/Home.tsx Normal file
View File

@ -0,0 +1,46 @@
import classes from "./Home.module.css";
import useUserStore from "@/store/User";
import { copyText } from "@/utils";
import { useWeb3Modal } from "@web3modal/wagmi/react";
import Button from "antd-mobile/es/components/button";
import { useEffect } from "react";
import { useTranslation } from "react-i18next";
export default function () {
const { Token, UpdateToken } = useUserStore();
const { open } = useWeb3Modal();
const { t } = useTranslation();
useEffect(() => {
UpdateToken("user token abc");
return () => {};
}, []);
useEffect(() => {
console.log("user token:", Token);
return () => {};
}, [Token]);
return (
<>
<div className={classes.Home}>
<span className={classes.text}>{t("AppName")}</span>
<Button
onClick={() => {
open();
}}
>
Connect Wallet
</Button>
<Button
onClick={() => {
copyText("123");
}}
>
copy text
</Button>
</div>
</>
);
}

0
src/server/api.ts Normal file
View File

34
src/server/client.ts Normal file
View File

@ -0,0 +1,34 @@
/*
* @LastEditors: John
* @Date: 2024-06-18 10:09:21
* @LastEditTime: 2024-06-18 10:27:04
* @Author: John
*/
import { Client } from "@hyper-fetch/core";
import { BASE_RESPONSE } from "./module";
const client = new Client({ url: import.meta.env.VITE_BASE_API_URL })
.onAuth((req) => {
return req;
})
.onRequest((req) => {
req.setHeaders({});
return req;
})
.onResponse((res) => {
return res;
});
export const POST = <P, R = any>({ url }: { url: string }) => {
return client.createRequest<BASE_RESPONSE<R>, P>()({
method: "POST",
endpoint: url,
});
};
export const GET = <P, R = any>({ url }: { url: string }) => {
return client.createRequest<BASE_RESPONSE<R>, any, any, P>()({
method: "GET",
endpoint: url,
});
};

6
src/server/module.d.ts vendored Normal file
View File

@ -0,0 +1,6 @@
export type BASE_RESPONSE<T = any> = {
code: 0 | 200;
data: T | null;
msg: string;
timeMillis: number;
}; // What's returned from request

29
src/store/User.ts Normal file
View File

@ -0,0 +1,29 @@
/*
* @LastEditors: John
* @Date: 2024-06-17 17:45:43
* @LastEditTime: 2024-06-17 17:57:42
* @Author: John
*/
import { ASYNC_STORAGE_KEY } from "@/constants";
import { create } from "zustand";
import { createJSONStorage, persist } from "zustand/middleware";
interface UserState {
Token: string;
UpdateToken: (t: string) => void;
}
export const useUserStore = create<UserState>()(
persist(
(set, _get) => ({
Token: "",
UpdateToken: (t) => set({ Token: t }),
}),
{
name: ASYNC_STORAGE_KEY.Token, // name of item in the storage (must be unique)
storage: createJSONStorage(() => localStorage), // (optional) by default the 'localStorage' is used
}
)
);
export default useUserStore;

23
src/types/i18next.d.ts vendored Normal file
View File

@ -0,0 +1,23 @@
/*
* @LastEditors: John
* @Date: 2024-01-23 10:39:17
* @LastEditTime: 2024-06-18 10:35:21
* @Author: John
*/
// import the original type declarations
import "i18next";
// import all namespaces (for the default language, only)
import en from "../i18n/translation/en.json";
declare module "i18next" {
// Extend CustomTypeOptions
interface CustomTypeOptions {
// custom namespace type, if you changed it
defaultNS: "en";
// custom resources type
resources: {
en: typeof en;
};
// other
}
}

51
src/utils/flexible.ts Normal file
View File

@ -0,0 +1,51 @@
/*
* @LastEditors: John
* @Date: 2024-01-09 09:34:24
* @LastEditTime: 2024-06-17 18:37:01
* @Author: John
*/
export default function flexible(window: Window, document: Document) {
var docEl = document.documentElement;
var dpr = window.devicePixelRatio || 1;
// adjust body font size
function setBodyFontSize() {
if (document.body) {
document.body.style.fontSize = 12 * dpr + "px";
} else {
document.addEventListener("DOMContentLoaded", setBodyFontSize);
}
}
setBodyFontSize();
// set 1rem = viewWidth / 10
function setRemUnit() {
var rem = docEl.clientWidth / 10;
// console.log("rem:", rem);
docEl.style.fontSize = rem + "px";
}
setRemUnit();
// reset rem unit on page resize
window.addEventListener("resize", setRemUnit);
window.addEventListener("pageshow", function (e) {
if (e.persisted) {
setRemUnit();
}
});
// detect 0.5px supports
if (dpr >= 2) {
var fakeBody = document.createElement("body");
var testElement = document.createElement("div");
testElement.style.border = ".5px solid transparent";
fakeBody.appendChild(testElement);
docEl.appendChild(fakeBody);
if (testElement.offsetHeight === 1) {
docEl.classList.add("hairlines");
}
docEl.removeChild(fakeBody);
}
}

79
src/utils/index.ts Normal file
View File

@ -0,0 +1,79 @@
/*
* @LastEditors: John
* @Date: 2024-06-17 18:19:27
* @LastEditTime: 2024-06-18 10:48:05
* @Author: John
*/
import Toast from "antd-mobile/es/components/toast";
export const ua = navigator.userAgent;
export const isIOS = /iphone|ipad|ipod|ios/i.test(ua);
export const isAndroid = /android|XiaoMi|MiuiBrowser/i.test(ua);
export const isMobile = isIOS || isAndroid;
export const isOKApp = /OKApp/i.test(ua);
export function shortenString(
inputString: string,
startLength: number,
endLength: number
) {
if (inputString.length <= startLength + endLength) {
return inputString; // 如果字符串长度小于等于要保留的前后字符数之和,直接返回原字符串
}
const startPart = inputString.slice(0, startLength);
const endPart = inputString.slice(-endLength);
return `${startPart}...${endPart}`;
}
// 定义一个函数,用于获取指定参数的值
export function getUrlQueryParam(key: string): string | undefined {
console.log(window.location);
const query: Map<string, string> = new Map();
const queryStr = window.location.href.split("?")[1];
if (queryStr) {
const queryStrArr = queryStr.split("&");
queryStrArr.forEach((v) => {
const queryArr = v.split("=");
query.set(queryArr[0], queryArr[1]);
});
}
return query.get(key);
}
export function copyText(text: string) {
const value = text;
// 1、创建DOM input框
const input = document.createElement("input");
// 2、隐藏input
input.setAttribute(
"style",
`
opacity: 0;
z-index: 999;
position: fixed;
top: 0;
`
);
// 3、将指定文本赋值给input
input.value = value;
// 4、将input插入文档
document.body.appendChild(input);
// 5、选中文本
// @ts-ignore
input.select();
// 6、复制到剪切板
const isCopySuccess = document.execCommand("copy");
// 7、复制成功后提示
isCopySuccess &&
Toast.show({
icon: "success",
content: "Copy successful",
});
// 8、 销毁DOM
document.body.removeChild(input);
}

7
src/vite-env.d.ts vendored Normal file
View File

@ -0,0 +1,7 @@
/// <reference types="vite/client" />
interface ImportMetaEnv {
readonly VITE_BASE_API_URL: string;
// 更多环境变量...
readonly MODE: "development" | "production" | "test";
}

36
tsconfig.json Normal file
View File

@ -0,0 +1,36 @@
/*
* @LastEditors: John
* @Date: 2024-06-17 17:20:03
* @LastEditTime: 2024-06-17 17:41:09
* @Author: John
*/
{
"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 */
"allowSyntheticDefaultImports": true,
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"paths": {
"@/*": ["./src/*"]
}
},
"include": ["src"],
"references": [{ "path": "./tsconfig.node.json" }]
}

11
tsconfig.node.json Normal file
View File

@ -0,0 +1,11 @@
{
"compilerOptions": {
"composite": true,
"skipLibCheck": true,
"module": "ESNext",
"moduleResolution": "bundler",
"allowSyntheticDefaultImports": true,
"strict": true
},
"include": ["vite.config.ts"]
}

20
vite.config.ts Normal file
View File

@ -0,0 +1,20 @@
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import path from "path";
import viteCompression from "vite-plugin-compression";
import { nodePolyfills } from "vite-plugin-node-polyfills";
// https://vitejs.dev/config/
export default defineConfig({
plugins: [
react(),
viteCompression({ deleteOriginFile: false }),
nodePolyfills(),
],
resolve: {
alias: {
"@": path.resolve(__dirname, "./src"),
},
},
css: {},
});

8391
yarn.lock Normal file

File diff suppressed because it is too large Load Diff