Commit 259db7d7 authored by web's avatar web

up

parents
Pipeline #837 failed with stages
File added
MIT License
Copyright (c) 2025 CaduceusMetaverseProtocol
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
# dexchain-frontend
dexchain-frontend
/// <reference types="next" />
/// <reference types="next/image-types/global" />
// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information.
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
/* config options here */
experimental: {
optimizePackageImports: ["@chakra-ui/react"],
},
};
export default nextConfig;
{
"name": "dex-dapp",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev --turbopack",
"build": "next build",
"start": "next start",
"lint": "next lint"
},
"dependencies": {
"@rainbow-me/rainbowkit": "^2.2.1",
"@tanstack/react-query": "^5.62.2",
"antd": "^5.24.1",
"axios": "^1.7.9",
"dayjs": "^1.11.13",
"ethers": "^6.13.4",
"next": "15.0.3",
"react": "19.0.0-rc-66855b96-20241106",
"react-dom": "19.0.0-rc-66855b96-20241106",
"react-tradingview-widget": "^1.3.2",
"viem": "2.x",
"wagmi": "^2.13.3"
},
"devDependencies": {
"@chakra-ui/cli": "^3.2.3",
"@types/node": "^20",
"@types/react": "^18",
"@types/react-dom": "^18",
"postcss": "^8",
"tailwindcss": "3.3.1",
"typescript": "^5"
}
}
/** @type {import('postcss-load-config').Config} */
const config = {
plugins: {
tailwindcss: {},
},
};
export default config;
This diff is collapsed.
<svg fill="none" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><path d="M14.5 13.5V5.41a1 1 0 0 0-.3-.7L9.8.29A1 1 0 0 0 9.08 0H1.5v13.5A2.5 2.5 0 0 0 4 16h8a2.5 2.5 0 0 0 2.5-2.5m-1.5 0v-7H8v-5H3v12a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1M9.5 5V2.12L12.38 5zM5.13 5h-.62v1.25h2.12V5zm-.62 3h7.12v1.25H4.5zm.62 3h-.62v1.25h7.12V11z" clip-rule="evenodd" fill="#666" fill-rule="evenodd"/></svg>
\ No newline at end of file
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><g clip-path="url(#a)"><path fill-rule="evenodd" clip-rule="evenodd" d="M10.27 14.1a6.5 6.5 0 0 0 3.67-3.45q-1.24.21-2.7.34-.31 1.83-.97 3.1M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16m.48-1.52a7 7 0 0 1-.96 0H7.5a4 4 0 0 1-.84-1.32q-.38-.89-.63-2.08a40 40 0 0 0 3.92 0q-.25 1.2-.63 2.08a4 4 0 0 1-.84 1.31zm2.94-4.76q1.66-.15 2.95-.43a7 7 0 0 0 0-2.58q-1.3-.27-2.95-.43a18 18 0 0 1 0 3.44m-1.27-3.54a17 17 0 0 1 0 3.64 39 39 0 0 1-4.3 0 17 17 0 0 1 0-3.64 39 39 0 0 1 4.3 0m1.1-1.17q1.45.13 2.69.34a6.5 6.5 0 0 0-3.67-3.44q.65 1.26.98 3.1M8.48 1.5l.01.02q.41.37.84 1.31.38.89.63 2.08a40 40 0 0 0-3.92 0q.25-1.2.63-2.08a4 4 0 0 1 .85-1.32 7 7 0 0 1 .96 0m-2.75.4a6.5 6.5 0 0 0-3.67 3.44 29 29 0 0 1 2.7-.34q.31-1.83.97-3.1M4.58 6.28q-1.66.16-2.95.43a7 7 0 0 0 0 2.58q1.3.27 2.95.43a18 18 0 0 1 0-3.44m.17 4.71q-1.45-.12-2.69-.34a6.5 6.5 0 0 0 3.67 3.44q-.65-1.27-.98-3.1" fill="#666"/></g><defs><clipPath id="a"><path fill="#fff" d="M0 0h16v16H0z"/></clipPath></defs></svg>
\ No newline at end of file
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 394 80"><path fill="#000" d="M262 0h68.5v12.7h-27.2v66.6h-13.6V12.7H262V0ZM149 0v12.7H94v20.4h44.3v12.6H94v21h55v12.6H80.5V0h68.7zm34.3 0h-17.8l63.8 79.4h17.9l-32-39.7 32-39.6h-17.9l-23 28.6-23-28.6zm18.3 56.7-9-11-27.1 33.7h17.8l18.3-22.7z"/><path fill="#000" d="M81 79.3 17 0H0v79.3h13.6V17l50.2 62.3H81Zm252.6-.4c-1 0-1.8-.4-2.5-1s-1.1-1.6-1.1-2.6.3-1.8 1-2.5 1.6-1 2.6-1 1.8.3 2.5 1a3.4 3.4 0 0 1 .6 4.3 3.7 3.7 0 0 1-3 1.8zm23.2-33.5h6v23.3c0 2.1-.4 4-1.3 5.5a9.1 9.1 0 0 1-3.8 3.5c-1.6.8-3.5 1.3-5.7 1.3-2 0-3.7-.4-5.3-1s-2.8-1.8-3.7-3.2c-.9-1.3-1.4-3-1.4-5h6c.1.8.3 1.6.7 2.2s1 1.2 1.6 1.5c.7.4 1.5.5 2.4.5 1 0 1.8-.2 2.4-.6a4 4 0 0 0 1.6-1.8c.3-.8.5-1.8.5-3V45.5zm30.9 9.1a4.4 4.4 0 0 0-2-3.3 7.5 7.5 0 0 0-4.3-1.1c-1.3 0-2.4.2-3.3.5-.9.4-1.6 1-2 1.6a3.5 3.5 0 0 0-.3 4c.3.5.7.9 1.3 1.2l1.8 1 2 .5 3.2.8c1.3.3 2.5.7 3.7 1.2a13 13 0 0 1 3.2 1.8 8.1 8.1 0 0 1 3 6.5c0 2-.5 3.7-1.5 5.1a10 10 0 0 1-4.4 3.5c-1.8.8-4.1 1.2-6.8 1.2-2.6 0-4.9-.4-6.8-1.2-2-.8-3.4-2-4.5-3.5a10 10 0 0 1-1.7-5.6h6a5 5 0 0 0 3.5 4.6c1 .4 2.2.6 3.4.6 1.3 0 2.5-.2 3.5-.6 1-.4 1.8-1 2.4-1.7a4 4 0 0 0 .8-2.4c0-.9-.2-1.6-.7-2.2a11 11 0 0 0-2.1-1.4l-3.2-1-3.8-1c-2.8-.7-5-1.7-6.6-3.2a7.2 7.2 0 0 1-2.4-5.7 8 8 0 0 1 1.7-5 10 10 0 0 1 4.3-3.5c2-.8 4-1.2 6.4-1.2 2.3 0 4.4.4 6.2 1.2 1.8.8 3.2 2 4.3 3.4 1 1.4 1.5 3 1.5 5h-5.8z"/></svg>
\ No newline at end of file
This diff is collapsed.
This diff is collapsed.
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1155 1000"><path d="m577.3 0 577.4 1000H0z" fill="#fff"/></svg>
\ No newline at end of file
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill-rule="evenodd" clip-rule="evenodd" d="M1.5 2.5h13v10a1 1 0 0 1-1 1h-11a1 1 0 0 1-1-1zM0 1h16v11.5a2.5 2.5 0 0 1-2.5 2.5h-11A2.5 2.5 0 0 1 0 12.5zm3.75 4.5a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5M7 4.75a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0m1.75.75a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5" fill="#666"/></svg>
\ No newline at end of file
File added
@tailwind base;
@tailwind components;
@tailwind utilities;
:root {
--background: #ffffff;
--foreground: #000;
--foreground-rgb: 0, 0, 0;
--background-start-rgb: 214, 219, 220;
--background-end-rgb: 255, 255, 255;
}
@media (prefers-color-scheme: dark) {
:root {
--background: #0a0a0a;
--foreground: #ededed;
}
}
body {
color: var(--foreground);
background: linear-gradient(to bottom, transparent, rgb(var(--background-end-rgb))) rgb(var(--background-start-rgb));
font-family: Arial, Helvetica, sans-serif;
}
.text-inherit {
color: inherit
}
.text-primary {
--tw-text-opacity: 1;
color: rgb(255 196 49/var(--tw-text-opacity))
}
.text-red-500 {
--tw-text-opacity: 1;
color: rgb(239 68 68/var(--tw-text-opacity))
}
.text-success {
--tw-text-opacity: 1;
color: rgb(53 178 111/var(--tw-text-opacity))
}
.text-white {
--tw-text-opacity: 1;
color: rgb(255 255 255/var(--tw-text-opacity))
}
.text-yellow-800 {
--tw-text-opacity: 1;
color: rgb(133 77 14/var(--tw-text-opacity))
}
.underline {
text-decoration-line: underline
}
.accent-primary {
accent-color: #ffc431
}
.\!border-primary {
--tw-border-opacity: 1!important;
border-color: rgb(255 196 49/var(--tw-border-opacity))!important
}
.border-danger {
--tw-border-opacity: 1;
border-color: rgb(242 77 91/var(--tw-border-opacity))
}
.border-gray-200 {
--tw-border-opacity: 1;
border-color: rgb(135 134 139/var(--tw-border-opacity))
}
.border-gray-300 {
--tw-border-opacity: 1;
border-color: rgb(62 61 64/var(--tw-border-opacity))
}
.border-gray-600 {
--tw-border-opacity: 1;
border-color: rgb(32 32 32/var(--tw-border-opacity))
}
.border-neutral-700 {
--tw-border-opacity: 1;
border-color: rgb(64 64 64/var(--tw-border-opacity))
}
.border-primary {
--tw-border-opacity: 1;
border-color: rgb(255 196 49/var(--tw-border-opacity))
}
.border-transparent {
border-color: transparent
}
.border-white\/20 {
border-color: hsla(0,0%,100%,.2)
}
.\!bg-gray-800 {
--tw-bg-opacity: 1!important;
background-color: rgb(20 20 20/var(--tw-bg-opacity))!important
}
.bg-\[\#202020b3\] {
background-color: #202020b3
}
.bg-\[\#312300\] {
--tw-bg-opacity: 1;
background-color: rgb(49 35 0/var(--tw-bg-opacity))
}
.bg-\[\#343332\] {
--tw-bg-opacity: 1;
background-color: rgb(52 51 50/var(--tw-bg-opacity))
}
.bg-\[\#681A21\] {
--tw-bg-opacity: 1;
background-color: rgb(104 26 33/var(--tw-bg-opacity))
}
.bg-\[\#ffc43133\] {
background-color: #ffc43133
}
.bg-black {
--tw-bg-opacity: 1;
background-color: rgb(0 0 0/var(--tw-bg-opacity))
}
.bg-black\/25 {
background-color: rgba(0,0,0,.25)
}
.bg-black\/30 {
background-color: rgba(0,0,0,.3)
}
.bg-danger {
--tw-bg-opacity: 1;
background-color: rgb(242 77 91/var(--tw-bg-opacity))
}
.bg-danger-disabled {
--tw-bg-opacity: 1;
background-color: rgb(52 51 50/var(--tw-bg-opacity))
}
.bg-danger-hover {
--tw-bg-opacity: 1;
background-color: rgb(217 69 81/var(--tw-bg-opacity))
}
.bg-gray-200 {
--tw-bg-opacity: 1;
background-color: rgb(135 134 139/var(--tw-bg-opacity))
}
.bg-gray-300 {
--tw-bg-opacity: 1;
background-color: rgb(62 61 64/var(--tw-bg-opacity))
}
.bg-gray-400 {
--tw-bg-opacity: 1;
background-color: rgb(52 51 50/var(--tw-bg-opacity))
}
.bg-gray-500 {
--tw-bg-opacity: 1;
background-color: rgb(39 39 43/var(--tw-bg-opacity))
}
.bg-gray-700 {
--tw-bg-opacity: 1;
background-color: rgb(26 26 26/var(--tw-bg-opacity))
}
.bg-gray-800 {
--tw-bg-opacity: 1;
background-color: rgb(20 20 20/var(--tw-bg-opacity))
}
.bg-gray-900 {
--tw-bg-opacity: 1;
background-color: rgb(16 16 16/var(--tw-bg-opacity))
}
.bg-primary {
--tw-bg-opacity: 1;
background-color: rgb(255 196 49/var(--tw-bg-opacity))
}
.bg-primary-500,.bg-primary-darker {
--tw-bg-opacity: 1;
background-color: rgb(92 66 0/var(--tw-bg-opacity));
}
.bg-primary-disabled {
--tw-bg-opacity: 1;
background-color: rgb(52 51 50/var(--tw-bg-opacity))
}
.bg-success {
--tw-bg-opacity: 1;
background-color: rgb(53 178 111/var(--tw-bg-opacity))
}
.bg-success-500 {
--tw-bg-opacity: 1;
background-color: rgb(11 77 41/var(--tw-bg-opacity))
}
.bg-transparent {
background-color: transparent
}
.bg-yellow-100 {
--tw-bg-opacity: 1;
background-color: rgb(254 249 195/var(--tw-bg-opacity))
}
.bg-gradient-to-l {
background-image: linear-gradient(to left,var(--tw-gradient-stops))
}
.bg-gradient-to-r {
background-image: linear-gradient(to right,var(--tw-gradient-stops))
}
.from-gray-500 {
--tw-gradient-from: #27272b var(--tw-gradient-from-position);
--tw-gradient-from-position: ;
--tw-gradient-to: rgba(39,39,43,0) var(--tw-gradient-from-position);
--tw-gradient-to-position: ;
--tw-gradient-stops: var(--tw-gradient-from),var(--tw-gradient-to)
}
.from-20\% {
--tw-gradient-from-position: 20%
}
.to-gray-500\/0 {
--tw-gradient-to: rgba(39,39,43,0) var(--tw-gradient-to-position);
--tw-gradient-to-position:
}
.to-100\% {
--tw-gradient-to-position: 100%
}
.fill-black {
fill: #000
}
.fill-primary {
fill: #ffc431
}
.button-danger {
--tw-bg-opacity: 1;
background-color: rgb(242 77 91/var(--tw-bg-opacity));
--tw-text-opacity: 1;
color: rgb(255 255 255/var(--tw-text-opacity));
transition-property: color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,-webkit-backdrop-filter;
transition-property: color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;
transition-property: color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter,-webkit-backdrop-filter;
transition-timing-function: cubic-bezier(.4,0,.2,1);
transition-duration: .2s
}
.button-danger:hover {
transition-timing-function: cubic-bezier(0,0,.2,1);
--tw-bg-opacity: 1;
background-color: rgb(217 69 81/var(--tw-bg-opacity))
}
.button-danger:active,.button-danger:focus {
--tw-bg-opacity: 1;
background-color: rgb(242 97 108/var(--tw-bg-opacity))
}
.button-success {
--tw-bg-opacity: 1;
background-color: rgb(53 178 111/var(--tw-bg-opacity));
--tw-text-opacity: 1;
color: rgb(255 255 255/var(--tw-text-opacity));
transition-property: color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,-webkit-backdrop-filter;
transition-property: color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;
transition-property: color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter,-webkit-backdrop-filter;
transition-timing-function: cubic-bezier(.4,0,.2,1);
transition-duration: .2s
}
.button-success:hover {
transition-timing-function: cubic-bezier(0,0,.2,1);
--tw-bg-opacity: 1;
background-color: rgb(46 153 95/var(--tw-bg-opacity))
}
.button-success:active,.button-success:focus {
--tw-bg-opacity: 1;
background-color: rgb(76 191 129/var(--tw-bg-opacity))
}
.button-outline {
border-width: 1px;
--tw-border-opacity: 1;
border-color: rgb(255 196 49/var(--tw-border-opacity));
background-color: transparent;
--tw-text-opacity: 1;
color: rgb(255 255 255/var(--tw-text-opacity));
transition-property: color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,-webkit-backdrop-filter;
transition-property: color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;
transition-property: color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter,-webkit-backdrop-filter;
transition-timing-function: cubic-bezier(.4,0,.2,1);
transition-duration: .2s
}
.button-outline:hover {
transition-timing-function: cubic-bezier(0,0,.2,1);
background-color: rgb(255 255 255/var(--tw-bg-opacity));
--tw-bg-opacity: 0.05
}
.button-outline:active,.button-outline:focus {
--tw-bg-opacity: 1;
background-color: rgb(255 196 49/var(--tw-bg-opacity));
--tw-text-opacity: 1;
color: rgb(16 16 16/var(--tw-text-opacity))
}
.button-outline-primary {
--tw-bg-opacity: 1;
background-color: rgb(49 35 0/var(--tw-bg-opacity));
--tw-text-opacity: 1;
color: rgb(255 196 49/var(--tw-text-opacity));
transition-property: color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,-webkit-backdrop-filter;
transition-property: color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;
transition-property: color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter,-webkit-backdrop-filter;
transition-timing-function: cubic-bezier(.4,0,.2,1);
transition-duration: .2s
}
.button-outline-primary:hover {
transition-timing-function: cubic-bezier(0,0,.2,1)
}
.button-danger:disabled,.button-danger:disabled:hover,.button-outline-primary:disabled,.button-outline-primary:disabled:hover,.button-outline:disabled,.button-outline:disabled:hover,.button-primary:disabled,.button-primary:disabled:hover,.button-secondary:disabled,.button-secondary:disabled:hover,.button-success:disabled,.button-success:disabled:hover {
border-style: none;
--tw-bg-opacity: 1;
background-color: rgb(62 61 64/var(--tw-bg-opacity));
--tw-text-opacity: 1;
color: rgb(135 134 139/var(--tw-text-opacity))
}
\ No newline at end of file
import "./globals.css";
import '@rainbow-me/rainbowkit/styles.css';
import { Providers } from '@/providers/wagmiProvider';
// import { ChakraProvider, extendTheme } from '@chakra-ui/react';
// const theme = extendTheme({
// // 使用默认主题的扩展
// });
import Header from '@/components/Header';
function RootLayout({ children }: { children: React.ReactNode }) {
return (
// <ChakraProvider theme={theme}>
<html lang="en" suppressHydrationWarning>
<head>
</head>
<body>
<main className='flex min-h-screen flex-col bg-gray-900'>
<Providers>
<Header />
{children}
</Providers>
</main>
</body>
</html>
// </ChakraProvider>
);
}
export default RootLayout;
This diff is collapsed.
'use client';
import { ConnectButton } from '@rainbow-me/rainbowkit';
import { useEffect, useState } from 'react';
import { useGetPairStat, useGetPairList } from '@/utils/api';
function Page() {
const pair = '0x8b1c2430908d719bff0d880e54eb2b1c498d06e6'
const { data, error, isLoading, refetch } = useGetPairStat(pair);
const { data: pairListData, error: pairListError, isLoading: pairListLoading } = useGetPairList();
const [showPairList, setShowPairList] = useState(false);
useEffect(() => {
const intervalId = setInterval(() => {
refetch(); // 每隔 3 秒触发一次请求
}, 3000);
if(error instanceof Error) clearInterval(intervalId);
// 清除定时器,防止内存泄漏
return () => {
clearInterval(intervalId);
};
}, [refetch, error]);
// if (isLoading) {
// return <div>Loading...</div>;
// }
// if (error instanceof Error) {
// return <div>Error: {error.message}</div>;
// }
const pairList = pairListData?.data.data;
let statData = data?.data.data[0] || {
pair: '',
price: '0.00',
high: '0.00',
low: '0.00',
base_volume: '0.00',
quote_volume: '0.00',
change: '0.00',
};
console.log(statData)
return (
<>
<div
className='flex items-center justify-between bg-black px-4 py-[14px] md:max-h-16 md:px-6 md:py-3'
>
<div className='flex h-10 items-center'>
<div className="relative mt-4 hidden items-center md:mt-0 md:flex">
<div className="relative mr-4 ml-4 pr-4 shrink-0">
<div className="flex cursor-pointer items-center rounded-md px-0.5 hover:bg-gray-hover" onClick={() => setShowPairList(!showPairList)}>
<div className="flex flex-col items-start font-bold text-white">
<div className="flex space-x-2">
BTC/USDT
</div>
{/* <div className="rounded-[4px] bg-primary-500 px-2 py-1 text-xxs leading-none text-primary">spot</div> */}
</div>
</div>
{
showPairList && <div className="absolute left-0 top-14 z-40 h-[400px] flex-col border-gray-300 bg-gray-500 p-4 md:w-[560px] ">
{/* <div className="flex items-center gap-x-4">
<p className="cursor-pointer text-sm font-bold text-primary">Search crypto</p>
</div> */}
<div className="flex items-center rounded bg-gray-700 px-4 py-2 md:bg-gray-700 mt-3 md:bg-gray-700">
<input type="text" className='w-full bg-transparent text-gray-200 outline-none' placeholder="Search crypto" />
</div>
<div className="mt-3 flex items-center space-x-2">
<div className="w-2/4 text-left text-xs text-gray-200">Pair</div>
<div className="w-1/4 text-left text-xs text-gray-200">Last Price</div>
<div className="w-1/4 text-left text-xs text-gray-200">Change</div>
{/* <div className="w-20 pr-2 text-right text-xs text-gray-200">Chain</div> */}
</div>
<div className="-mr-4 mt-2 flex h-[210px] flex-col overflow-y-scroll pr-4">
{
pairList?.map((item: any, index: number) => {
return (
<div key={index} className="flex cursor-pointer items-center space-x-2 py-1 hover:opacity-80">
<div className="flex w-2/4 items-center text-left text-xs">
<a href="" className='flex-1 font-bold text-white'>{item.pair}</a>
</div>
<a href="" className="w-1/4 text-left text-xs text-white">${item.last_price}</a>
<a href="" className="w-1/4 text-left text-xs text-white">{item.change}%</a>
{/* <a href="" className="flex w-20 items-center justify-end pr-2 text-white">T</a> */}
</div>
)
})
}
</div>
</div>
}
</div>
<div className="mr-4 flex flex-col items-start gap-y-2 text-right md:mr-6 md:gap-y-0">
<p className="text-xs text-gray-200">Price</p>
<p className={`text-xs font-bold text-white md:text-[14px]`}>{statData.price} </p>
</div>
<div className="mr-4 flex flex-col items-start gap-y-2 text-right md:mr-6 md:gap-y-0">
<p className="text-xs text-gray-200">24h change</p>
<p className={`text-xs font-bold md:text-[14px] ${statData && statData.change.toString().includes('-') ? 'text-red-500' : 'text-success'}`}>{statData.change}%</p>
</div>
<div className="mr-4 flex flex-col items-start gap-y-2 text-right md:mr-6 md:gap-y-0">
<p className="text-xs text-gray-200">24h High</p>
<p className='text-xs font-bold text-white md:text-[14px]'>{statData.high}</p>
</div>
<div className="mr-4 flex flex-col items-start gap-y-2 text-right md:mr-6 md:gap-y-0">
<p className="text-xs text-gray-200">24h Low</p>
<p className='text-xs font-bold text-white md:text-[14px]'>{statData.low}</p>
</div>
<div className="mr-4 flex flex-col items-start gap-y-2 text-right md:mr-6 md:gap-y-0">
<p className="text-xs text-gray-200">24h Volume ({statData.base_token})</p>
<p className='text-xs font-bold text-white md:text-[14px]'>{statData.base_volume}</p>
</div>
<div className="mr-4 flex flex-col items-start gap-y-2 text-right md:mr-6 md:gap-y-0">
<p className="text-xs text-gray-200">24h Volume</p>
<p className='text-xs font-bold text-white md:text-[14px]'>{statData.quote_volume}</p>
</div>
</div>
</div>
<ConnectButton showBalance={false} />
</div>
{showPairList && <div className="fixed left-0 top-[64px] z-30 h-full w-full bg-black opacity-60" onClick={() => setShowPairList(false)}></div>}
</>
);
}
export default Page;
"use client";
import dayjs from 'dayjs'
import { useGetPairTradeHistory } from '@/utils/api'
export default function MarketTrade() {
const pair = '0x8b1c2430908d719bff0d880e54eb2b1c498d06e6'
const { data, error, isLoading, refetch } = useGetPairTradeHistory(pair);
// if (isLoading) {
// return <div className='no-scrollbar mt-2 flex w-full flex-col overflow-y-scroll'>Loading...</div>;
// }
// if (error instanceof Error) {
// return <div>Error: {error.message}</div>;
// }
let tradData = data?.data.data && data?.data.data.slice(0, 40);
function formatTime(timestamp: number) {
return dayjs(timestamp * 1000).format('MM-DD HH:mm:ss')
}
return (
<div className="no-scrollbar mt-2 flex w-full flex-col overflow-y-scroll min-h-[calc(100vh-300px)]">
{tradData && tradData.map((item: any, index: number) =>
<div key={index} className="item-center flex h-[26px] w-full cursor-pointer py-1 text-xs hover:opacity-80 md:h-[28px] green-blink">
<p className={`w-1/3 text-left ${item.side === 0 ? 'text-success' : 'text-red-500'}`}>{item.price}</p>
<p className="w-1/3 text-center text-white">{item.amount && item.amount.slice(0, 8)}</p>
<a href="#" className="w-1/3 text-right text-white">{formatTime(item.time)}</a>
</div>)}
</div>
)
}
\ No newline at end of file
"use client";
import { useAccount } from 'wagmi';
import { useEffect } from 'react';
import { useGetUserOpenOrders } from '@/utils/api';
import dayjs from 'dayjs'
export default function OpenOrders() {
const { address } = useAccount();
console.log(address)
const params = {
"address": '0x2D4E6b96bD85248d13020D392e99558abFb4f74C',
"limit": 20,
"offset": 0,
"orderBy": "date", //price
"orderDirection": "desc",
"pair": "0x8b1c2430908d719bff0d880e54eb2b1c498d06e6"
}
const { mutate, isPending, isError, error, isSuccess, data } = useGetUserOpenOrders()
useEffect(() => {
mutate(params)
}, [])
if(isPending) return <p>Loading...</p>
// if(isError) return <p>Error: {error.message}</p>
// if(isSuccess && data && data.length === 0) return <p>No open orders</p>
const orderData = isSuccess && data && data.data.data
console.log(orderData)
function formatTime(timestamp: number) {
return dayjs(timestamp).format('YYYY-MM-DD HH:mm:ss')
}
return (
<div className="no-scrollbar flex flex-1 flex-col overflow-scroll">
<div className="mt-4 flex w-full items-center border-b border-gray-300 pb-2 text-xs text-white">
<p className="flex w-1/4 cursor-pointer items-center gap-x-1 text-left md:w-[16%]">Date </p>
<p className="w-1/4 text-center ">Pair</p>
<p className="flex w-1/4 cursor-pointer items-center gap-x-1 text-center md:w-[8%]">Side </p>
<p className="hidden w-[10%] cursor-pointer items-center gap-x-1 text-left md:flex">Price </p>
<p className="hidden w-[10%] cursor-pointer items-center gap-x-1 text-left md:flex">Amount </p>
<p className="hidden w-[10%] cursor-pointer items-center gap-x-1 text-left md:flex">executed </p>
<p className="hidden w-[10%] cursor-pointer items-center gap-x-1 text-left md:flex">Filled </p>
</div>
{isSuccess && orderData && orderData?.map((order: any, index: number) => {
return <div key={index}>
<div className="flex w-full items-center border-b border-gray-300 py-2 text-xs text-white">
<p className="flex w-1/4 items-center gap-x-1 text-left md:w-[16%]">{formatTime(order.date)}</p>
<p className="w-1/4 text-center">{order.pairName}</p>
<p className="flex w-1/4 items-center gap-x-1 text-center md:w-[8%]">{order.side}</p>
<p className="hidden w-[10%] items-center gap-x-1 text-left md:flex">{order.price}</p>
<p className="hidden w-[10%] items-center gap-x-1 text-left md:flex">{order.total}</p>
<p className="hidden w-[10%] items-center gap-x-1 text-left md:flex">{order.executed}</p>
<p className="hidden w-[10%] items-center gap-x-1 text-left md:flex">{order.filled}%</p>
</div>
</div>
})}
</div>
)
}
\ No newline at end of file
"use client";
import dayjs from 'dayjs'
import { useGetPairOrderList } from '@/utils/api'
export default function OrderBook() {
const pair = '0x8b1c2430908d719bff0d880e54eb2b1c498d06e6'
const { data, error, isLoading, refetch } = useGetPairOrderList(pair);
// if (isLoading) {
// return <div>Loading...</div>;
// }
// if (error instanceof Error) {
// return <div>Error: {error.message}</div>;
// }
let bitsData = data?.data.data && data?.data.data[0].bids;
let asksData = data?.data.data && data?.data.data[0].asks;
return (
<div className="no-scrollbar flex flex-col w-full items-start overflow-y-scroll min-h-[calc(100vh-300px)]">
<div className="mt-2 flex w-full flex-col">
{asksData && asksData.map((item: any, index: number) =>
<div key={index} className="item-center flex h-[26px] w-full cursor-pointer py-1 text-xs hover:opacity-80 md:h-[28px]">
<p className={`mr-2 w-1/2 text-left text-red-500`}>{item.price}</p>
<p className="mr-2 w-1/2 text-right text-white">{item.amount && item.amount.slice(0, 8)}</p>
<p className="mr-2 w-1/2 text-right text-white">{item.price && item.amount && (Number(item.price) * Number(item.amount)).toFixed(6)}</p>
</div>)}
</div>
<div className="mt-2 flex w-full flex-col">
{bitsData && bitsData.map((item: any, index: number) =>
<div key={index} className="item-center flex h-[26px] w-full cursor-pointer py-1 text-xs hover:opacity-80 md:h-[28px]">
<p className="mr-2 w-1/2 text-left text-success">{item.price}</p>
<p className={`mr-2 w-1/2 text-right text-white`}>{item.amount && item.amount}</p>
<p className="mr-2 w-1/2 text-right text-white">{item.price && item.amount && (Number(item.price) * Number(item.amount)).toFixed(6)}</p>
</div>)}
</div>
</div>
)
}
\ No newline at end of file
"use client";
import { useAccount } from 'wagmi';
import { useEffect } from 'react';
import { useGetUserOrderHistory } from '@/utils/api';
import dayjs from 'dayjs'
export default function OrderHistory() {
const { address } = useAccount();
console.log(address)
const params = {
"address": '0x2D4E6b96bD85248d13020D392e99558abFb4f74C',
"limit": 20,
"offset": 0,
"orderBy": "date", //price
"orderDirection": "desc",
"pair": "0x8b1c2430908d719bff0d880e54eb2b1c498d06e6"
}
const { mutate, isPending, isError, error, isSuccess, data } = useGetUserOrderHistory()
useEffect(() => {
mutate(params)
}, [])
if(isPending) return <p>Loading...</p>
// if(isError) return <p>Error: {error.message}</p>
// if(isSuccess && data && data.length === 0) return <p>No open orders</p>
const orderData = isSuccess && data && data.data.data
console.log(orderData)
function formatTime(timestamp: number) {
return dayjs(timestamp).format('YYYY-MM-DD HH:mm:ss')
}
return (
<div className="no-scrollbar flex flex-1 flex-col overflow-scroll">
<div className="mt-4 flex w-full items-center border-b border-gray-300 pb-2 text-xs text-white">
<p className="flex cursor-pointer flex-row items-center gap-x-1 text-left md:w-[16%] ">Order Time </p>
<p className="w-1/4 text-center">Pair</p>
<p className="flex w-1/4 cursor-pointer items-center gap-x-1 text-center md:w-[8%]">Side </p>
<p className="hidden w-[10%] cursor-pointer items-center gap-x-1 text-left md:flex">Price </p>
<p className="hidden w-[10%] cursor-pointer items-center gap-x-1 text-left md:flex">Amount </p>
<p className="hidden w-[10%] cursor-pointer items-center gap-x-1 text-left md:flex">Executed </p>
<p className="hidden w-[10%] cursor-pointer items-center gap-x-1 text-left md:flex">Status </p>
</div>
{isSuccess && orderData && orderData?.map((order: any, index: number) => {
return <div key={index}>
<div className="flex w-full items-center border-b border-gray-300 py-2 text-xs text-white">
<p className="flex w-1/4 items-center gap-x-1 text-left md:w-[16%]">{formatTime(order.date)}</p>
<p className="w-1/4 text-center">{order.pairName}</p>
<p className="flex w-1/4 items-center gap-x-1 text-center md:w-[8%]">{order.side}</p>
<p className="hidden w-[10%] items-center gap-x-1 text-left md:flex">{order.price}</p>
<p className="hidden w-[10%] items-center gap-x-1 text-left md:flex">{order.total}</p>
<p className="hidden w-[10%] items-center gap-x-1 text-left md:flex">{order.executed}</p>
<p className="hidden w-[10%] items-center gap-x-1 text-left md:flex">{order.status}</p>
</div>
</div>
})}
</div>
)
}
\ No newline at end of file
"use client";
import { useAccount } from 'wagmi';
import { useEffect } from 'react';
import { useGetUserTradeHistory } from '@/utils/api';
import dayjs from 'dayjs'
export default function TradHistory() {
const { address } = useAccount();
console.log(address)
const params = {
"address": '0x2D4E6b96bD85248d13020D392e99558abFb4f74C',
"limit": 20,
"offset": 0,
"orderBy": "date", //price
"orderDirection": "desc",
"pair": "0x8b1c2430908d719bff0d880e54eb2b1c498d06e6"
}
const { mutate, isPending, isError, error, isSuccess, data } = useGetUserTradeHistory()
useEffect(() => {
mutate(params)
}, [])
if (isPending) return <p>Loading...</p>
// if (isError) return <p>Error: {error.message}</p>
// if (isSuccess && data && data.length === 0) return <p>No open orders</p>
const orderData = isSuccess && data && data.data.data
console.log(orderData)
function formatTime(timestamp: number) {
return dayjs(timestamp).format('YYYY-MM-DD HH:mm:ss')
}
return (
<div className="no-scrollbar flex flex-1 flex-col overflow-scroll">
<div className="mt-4 flex w-full items-center border-b border-gray-300 pb-2 text-xs text-white">
<p className="flex w-1/4 cursor-pointer items-center gap-x-1 text-left md:w-[16%]">Date </p>
<p className="w-1/4 text-center">Pair</p>
<p className="flex w-1/4 cursor-pointer items-center gap-x-1 text-center md:w-[8%]">Side </p>
<p className="hidden w-[10%] cursor-pointer items-center gap-x-1 text-left md:flex">Price </p>
<p className="hidden w-[10%] cursor-pointer items-center gap-x-1 text-left md:flex">Executed </p>
{/* <p className="hidden w-[10%] cursor-pointer items-center gap-x-1 text-left md:flex">Total </p> */}
{/* <p className="hidden w-[10%] cursor-pointer items-center gap-x-1 text-left md:flex">Role </p> */}
<p className="hidden w-[10%] cursor-pointer items-center gap-x-1 text-left md:flex">Fee </p>
</div>
{isSuccess && orderData && orderData?.map((order: any, index: number) => {
return <div key={index}>
<div className="flex w-full items-center border-b border-gray-300 py-2 text-xs text-white">
<p className="flex w-1/4 items-center gap-x-1 text-left md:w-[16%]">{formatTime(order.date)}</p>
<p className="w-1/4 text-center">{order.pairName}</p>
<p className="flex w-1/4 items-center gap-x-1 text-center md:w-[8%]">{order.side}</p>
<p className="hidden w-[10%] items-center gap-x-1 text-left md:flex">{order.price}</p>
<p className="hidden w-[10%] items-center gap-x-1 text-left md:flex">{order.executed}</p>
{/* <p className="hidden w-[10%] items-center gap-x-1 text-left md:flex">{order.total}</p> */}
{/* <p className="hidden w-[10%] items-center gap-x-1 text-left md:flex">{order.role}</p> */}
<p className="hidden w-[10%] items-center gap-x-1 text-left md:flex">{order.fee}</p>
</div>
</div>
})}
</div>
)
}
\ No newline at end of file
// components/TradingViewChart.tsx
import { useEffect } from 'react';
import { BinanceDataFeed } from '../utils/BianceDatafeed';
interface TradingViewChartProps {
symbol: string;
interval?: string;
}
const TradingViewChart: React.FC<TradingViewChartProps> = ({ symbol = 'BTCUSDT', interval = 'D' }) => {
useEffect(() => {
if (typeof window !== "undefined") {
const script = document.createElement('script');
script.src = "https://s3.tradingview.com/tv.js";
script.async = true;
script.onload = () => {
console.log(123)
new window.TradingView.widget({
autosize: true,
symbol: symbol || "BTCUSDT",
interval: interval,
container_id: "tradingview_chart",
theme: 'dark',
datafeed: new BinanceDataFeed(),
library_path: "/charting_library/",
style: '1',
locale: 'en',
toolbar_bg: '#f1f3f6',
enable_publishing: false,
withdateranges: true,
// allow_symbol_change: true,s
hide_side_toolbar: false,
details: true,
studies: [],
});
};
document.body.appendChild(script);
// 清理函数
return () => {
// 在组件卸载时移除脚本
// document.body.removeChild(script);
script.remove();
};
}
}, [symbol, interval]);
return <div id="tradingview_chart" style={{ height: '100%', width: '100%' }}></div>;
};
export default TradingViewChart;
// global.d.ts
interface Window {
TradingView: any;
Datafeeds: any;
}
\ No newline at end of file
'use client';
import React from 'react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { WagmiProvider } from 'wagmi';
// import { useRouter } from 'next/router';
import { RainbowKitProvider, type Locale, darkTheme } from '@rainbow-me/rainbowkit';
import { config } from '../wagmi';
const queryClient = new QueryClient();
export function Providers({ children }: { children: React.ReactNode }) {
// const { locale } = useRouter() as { locale: Locale };
return (
<WagmiProvider config={config}>
<QueryClientProvider client={queryClient} >
<RainbowKitProvider locale={'en'} theme={darkTheme()}>{children}</RainbowKitProvider>
</QueryClientProvider>
</WagmiProvider>
);
}
export class BinanceDataFeed {
onReady(callback: (config: any) => void) {
console.log('onReady called');
const config = {
supports_marks: false,
supports_time: true,
supported_resolutions: ['1', '5', '15', '30', '60', 'D', 'W', 'M'],
};
setTimeout(() => callback(config), 0);
}
resolveSymbol(symbolName: string, onSymbolResolvedCallback: (symbolInfo: any) => void) {
console.log('resolveSymbol called with symbol:')
const symbolInfo = {
name: symbolName,
ticker: symbolName,
description: `${symbolName} on Binance`,
type: 'crypto',
session: '24x7',
exchange: 'Binance',
minmov: 1,
pricescale: 100,
has_intraday: true,
supported_resolutions: ['1', '5', '15', '30', '60', 'D', 'W', 'M'],
volume_precision: 2,
data_status: 'streaming',
};
setTimeout(() => onSymbolResolvedCallback(symbolInfo), 0);
}
getBars(
symbolInfo: any,
resolution: string,
from: number,
to: number,
onHistoryCallback: (bars: any[], { noData }: { noData: boolean }) => void,
onErrorCallback: (reason: string) => void
) {
console.log(`getBars called with symbol: ${symbolInfo.ticker}, resolution: ${resolution}, from: ${from}, to: ${to}`)
const interval = this.mapResolution(resolution);
fetch(
`https://api.binance.com/api/v3/klines?symbol=${symbolInfo.ticker}&interval=${interval}&startTime=${from * 1000}&endTime=${to * 1000}`
)
.then((response) => response.json())
.then((data) => {
const bars = data.map((item: any) => ({
time: item[0], // 开盘时间 (毫秒)
open: parseFloat(item[1]),
high: parseFloat(item[2]),
low: parseFloat(item[3]),
close: parseFloat(item[4]),
volume: parseFloat(item[5]),
}));
onHistoryCallback(bars, { noData: bars.length === 0 });
})
.catch((error) => {
console.error('Error fetching Binance data:', error);
onErrorCallback('Error fetching data');
});
}
subscribeBars(
symbolInfo: any,
resolution: string,
onRealtimeCallback: (bar: any) => void,
subscriberUID: string
) {
const interval = this.mapResolution(resolution);
const ws = new WebSocket(
`wss://stream.binance.com:9443/ws/${symbolInfo.ticker.toLowerCase()}@kline_${interval}`
);
ws.onmessage = (event) => {
const message = JSON.parse(event.data);
if (message.k.x) {
const bar = {
time: message.k.t,
open: parseFloat(message.k.o),
high: parseFloat(message.k.h),
low: parseFloat(message.k.l),
close: parseFloat(message.k.c),
volume: parseFloat(message.k.v),
};
onRealtimeCallback(bar);
}
};
ws.onerror = (error) => {
console.error('WebSocket error:', error);
};
}
mapResolution(resolution: string): string {
const resolutionMap: { [key: string]: string } = {
'1': '1m',
'5': '5m',
'15': '15m',
'30': '30m',
'60': '1h',
'D': '1d',
'W': '1w',
'M': '1M',
};
return resolutionMap[resolution] || '1m';
}
}
\ No newline at end of file
import axios, { AxiosResponse } from 'axios';
import { useQuery, useMutation, UseQueryResult, UseMutationResult } from '@tanstack/react-query';
const BASE_URL = 'http://3.114.44.103:38088/api/v1';
const api = {
getPairList: () => axios.get(`${BASE_URL}/pair-list`),
getPairStat: (pair: string) => axios.get(`${BASE_URL}/pair-trade-info/${pair}`),
getPairOrderList: (pair: string) => axios.get(`${BASE_URL}/pair-order-list/${pair}`),
getPairTradeHistory: (pair: string) => axios.get(`${BASE_URL}/pair-trade-history/${pair}`),
makeOrder: (orderData: any) => axios.post(`${BASE_URL}/user-make-order`, orderData),
getUserOpenOrders: (data: any) => axios.post(`${BASE_URL}/user-open-orders`, data),
getUserOrderHistory: (data: any) => axios.post(`${BASE_URL}/user-order-history`, data),
getUserTradeHistory: (data: any) => axios.post(`${BASE_URL}/user-trade-history`, data),
};
// 封装的 get 请求,使用 react-query 的 useQuery 钩子
export const useGetPairList = (): UseQueryResult<AxiosResponse<any>, Error> => {
return useQuery({ queryKey: ['pairList'], queryFn: () => api.getPairList() });
};
export const useGetPairStat = (pair: string): UseQueryResult<any, Error> => {
return useQuery({ queryKey: ['pairStat', pair], queryFn: () => api.getPairStat(pair) });
};
export const useGetPairOrderList = (pair: string): UseQueryResult<any, Error> => {
return useQuery({ queryKey: ['pairOrderList', pair], queryFn: () => api.getPairOrderList(pair) });
};
export const useGetPairTradeHistory = (pair: string): UseQueryResult<any, Error> => {
return useQuery({ queryKey: ['pairTradeHistory', pair], queryFn: () => api.getPairTradeHistory(pair) });
};
// 封装的 post 请求,使用 react-query 的 useMutation 钩子
export const useMakeOrder = (onSuccessCallback: (data: any) => void): UseMutationResult<any, Error, any> => {
return useMutation({
mutationFn: (orderData: any) => api.makeOrder(orderData),
onSuccess: (data) => {
// 请求成功后执行的操作
console.log('Order placed successfully:', data);
onSuccessCallback(data); // 调用传入的回调函数
},
onError: (error) => {
// 请求失败时的操作
console.error('Error placing order:', error);
},
});
};
export const useGetUserOpenOrders = (): UseMutationResult<any, Error, any> => {
return useMutation({ mutationFn: (data: any) => api.getUserOpenOrders(data) });
};
export const useGetUserOrderHistory = (): UseMutationResult<any, Error, any> => {
return useMutation({ mutationFn: (data: any) => api.getUserOrderHistory(data) });
};
export const useGetUserTradeHistory = (): UseMutationResult<any, Error, any> => {
return useMutation({ mutationFn: (data:any) => api.getUserTradeHistory(data) });
};
import { getDefaultConfig, Chain } from '@rainbow-me/rainbowkit';
import {
arbitrum,
base,
mainnet,
optimism,
polygon,
sepolia,
} from 'wagmi/chains';
const dexTestnet = {
id: 100199,
name: 'DEX Testnet',
iconBackground: '#ffc431',
nativeCurrency: { name: 'Ether', symbol: 'ETH', decimals: 18 },
rpcUrls: {
default: { http: ['http://18.168.16.120:30088'] },
},
blockExplorers: {
default: { name: 'dex', url: 'https://snowtrace.io' },
},
} as const satisfies Chain;
export const config = getDefaultConfig({
appName: 'RainbowKit demo',
projectId: 'YOUR_PROJECT_ID',
chains: [
dexTestnet,
mainnet,
polygon,
optimism,
arbitrum,
base,
...(process.env.NEXT_PUBLIC_ENABLE_TESTNETS === 'true' ? [sepolia] : []),
],
ssr: true,
});
import type { Config } from "tailwindcss";
export default {
content: [
"./src/pages/**/*.{js,ts,jsx,tsx,mdx}",
"./src/components/**/*.{js,ts,jsx,tsx,mdx}",
"./src/app/**/*.{js,ts,jsx,tsx,mdx}",
],
theme: {
extend: {
colors: {
background: "var(--background)",
foreground: "var(--foreground)",
},
data: {
'md-trade-footer-h': '24px',
'md-header-h': '64px',
},
},
},
plugins: [],
} satisfies Config;
{
"compilerOptions": {
"target": "ES2017",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"plugins": [
{
"name": "next"
}
],
"paths": {
"@/*": ["./src/*"]
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
}
This diff is collapsed.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment