├── .eslintrc.json ├── .gitignore ├── README.md ├── eventsdemo.txt ├── file-hash-cache.json ├── next.config.mjs ├── package-lock.json ├── package.json ├── postcss.config.mjs ├── public ├── chats │ ├── 1.png │ ├── 10.png │ ├── 2.png │ ├── 3.png │ ├── 4.png │ ├── 5.png │ ├── 6.png │ ├── 7.png │ ├── 8.png │ ├── 9.png │ └── noimg.svg ├── favicon.ico ├── logo │ ├── Untitled.svg │ ├── ethereum.png │ ├── icon.svg │ ├── logo.svg │ └── wbone.png ├── next.svg ├── seo │ ├── create.jpg │ ├── dash.jpg │ └── home.jpg └── vercel.svg ├── src ├── abi │ ├── BondingCurveManager.json │ ├── ERC20.json │ ├── old_token.json │ └── old_token1.json ├── chain │ └── config.ts ├── components │ ├── PriceLiquidity.tsx │ ├── TokenDetails │ │ ├── Chats.tsx │ │ ├── TokenHolders.tsx │ │ ├── TokenInfo.tsx │ │ └── TransactionHistory.tsx │ ├── auth │ │ └── SiweAuth.tsx │ ├── charts │ │ ├── TradingViewChart.tsx │ │ └── pol │ ├── events │ │ ├── ChristmasEvent.tsx │ │ └── Valentine.tsx │ ├── layout │ │ ├── Footer.tsx │ │ ├── Layout.tsx │ │ └── Navbar.tsx │ ├── notifications │ │ ├── HowItWorksPopup.tsx │ │ ├── LiveNotifications.tsx │ │ ├── Modal.tsx │ │ └── PurchaseConfirmationPopup.tsx │ ├── providers │ │ └── WebSocketProvider.tsx │ ├── seo │ │ ├── OGPreview.tsx │ │ └── SEO.tsx │ ├── token │ │ └── TokenUpdateModal.tsx │ ├── tokens │ │ ├── TokenCard.tsx │ │ └── TokenList.tsx │ └── ui │ │ ├── LoadingBar.tsx │ │ ├── SearchFilter.tsx │ │ ├── ShareButton.tsx │ │ ├── SortOptions.tsx │ │ ├── Spinner.tsx │ │ ├── ldspinner.tsx │ │ └── switch.tsx ├── interface │ └── types.ts ├── pages │ ├── 404.tsx │ ├── FAQ.tsx │ ├── _app.tsx │ ├── _document.tsx │ ├── about.tsx │ ├── api │ │ ├── auth │ │ │ ├── nonce.ts │ │ │ ├── user.ts │ │ │ └── verify.ts │ │ ├── hello.ts │ │ ├── ports │ │ │ ├── addChatMessage.ts │ │ │ ├── getAllTokenAddresses.ts │ │ │ ├── getAllTokens.ts │ │ │ ├── getAllTokensTrends.ts │ │ │ ├── getAllTokensWithoutLiquidity.ts │ │ │ ├── getChatMessages.ts │ │ │ ├── getCurrentPrice.ts │ │ │ ├── getHistoricalPriceData.ts │ │ │ ├── getRecentTokens.ts │ │ │ ├── getTokenByAddress.ts │ │ │ ├── getTokenInfoAndTransactions.ts │ │ │ ├── getTokenLiquidityEvents.ts │ │ │ ├── getTokensByCreator.ts │ │ │ ├── getTokensWithLiquidity.ts │ │ │ ├── getTotalTokenCount.ts │ │ │ ├── getTransactionsByAddress.ts │ │ │ ├── getVolumeRange.ts │ │ │ ├── searchTokens.ts │ │ │ └── updateToken.ts │ │ ├── proxy │ │ │ └── [...path].ts │ │ ├── robots.ts │ │ └── upload-to-ipfs.ts │ ├── create.tsx │ ├── dashboard.tsx │ ├── home.tsx │ ├── index.tsx │ ├── profile │ │ └── [address].tsx │ └── token │ │ └── [address].tsx ├── styles │ └── globals.css ├── types │ ├── contracts │ │ ├── BondingCurveManager.ts │ │ ├── common.ts │ │ ├── factories │ │ │ ├── BondingCurveManager__factory.ts │ │ │ └── index.ts │ │ └── index.ts │ ├── heroicons.d.ts │ └── siwe.d.ts └── utils │ ├── api.ts │ ├── blockchainUtils.ts │ └── chatUtils.ts ├── tailwind.config.ts └── tsconfig.json /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | .yarn/install-state.gz 8 | 9 | # testing 10 | /coverage 11 | 12 | # next.js 13 | /.next/ 14 | /out/ 15 | 16 | # production 17 | /build 18 | 19 | # misc 20 | .DS_Store 21 | *.pem 22 | 23 | # debug 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | 28 | # local env files 29 | .env*.local 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | 38 | .vercel 39 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Pump Fun UI 2 | 3 | Pump Fun UI is a Next.js-based web application for interacting with the Pump Fun platform. 4 | 5 | ## Table of Contents 6 | 7 | - [Getting Started](#getting-started) 8 | - [Prerequisites](#prerequisites) 9 | - [Installation](#installation) 10 | - [Running the Application](#running-the-application) 11 | - [Architecture Overview](#architecture-overview) 12 | - [Environment Variables](#environment-variables) 13 | - [Available Scripts](#available-scripts) 14 | - [Dependencies](#dependencies) 15 | 16 | 17 | ## Getting Started 18 | 19 | ### Prerequisites 20 | 21 | - Node.js (version 14 or later) 22 | - Yarn package manager 23 | 24 | ### Installation 25 | 26 | 1. Clone the repository: 27 | ```bash 28 | git clone https://github.com/redWilly/Pump-UI 29 | cd Pump-UI 30 | ``` 31 | 32 | 2. Install dependencies: 33 | ```bash 34 | yarn install 35 | ``` 36 | 37 | ### Running the Application 38 | 39 | 1. Set up your environment variables (see [Environment Variables](#environment-variables) section). 40 | 41 | 2. Start the development server: 42 | ```bash 43 | yarn dev 44 | ``` 45 | 46 | 3. Open your browser and navigate to `http://localhost:3000`. 47 | 48 | ## Architecture Overview 49 | 50 | Pump Fun UI is built using the following technologies and frameworks: 51 | 52 | - Next.js: React framework for server-side rendering and static site generation 53 | - React: JavaScript library for building user interfaces 54 | - Tailwind CSS: Utility-first CSS framework for styling 55 | - Ethers.js: Library for interacting with Ethereum 56 | - RainbowKit: Ethereum wallet connection library 57 | - Wagmi: React hooks for EVM chains 58 | - lightweight-charts: Charting libraries for data visualization 59 | 60 | The application follows a component-based architecture, with reusable UI components and hooks for managing state and interactions with the blockchain. 61 | 62 | ## Environment Variables 63 | 64 | Create a `.env.local` file in the root directory with the following variables: 65 | 66 | ``` 67 | NEXT_PUBLIC_API_BASE_URL=backend-url 68 | NEXT_PUBLIC_BONDING_CURVE_MANAGER_ADDRESS="contract-address" 69 | NEXT_PUBLIC_WS_BASE_URL=https://backend-url 70 | CHAINSAFE_API_KEY=your_chainsafe_api_key 71 | CHAINSAFE_BUCKET_ID=your_chainsafe_bucket_id 72 | ``` 73 | 74 | ### 🚀 Looking to build a platform like Pump.fun? 75 | 76 | I've made the UI open-source, but the backend and smart contract are closed-source. If you're interested in creating a full-fledged Pump.fun-like platform, let's collaborate! [Contact me on Telegram](https://t.me/rink3y) (RedWilly) for more details. -------------------------------------------------------------------------------- /eventsdemo.txt: -------------------------------------------------------------------------------- 1 | /*************************** 2 | 3 | BEGIN OF FLYING SANTA 4 | 5 | ***************************/ 6 | .santa { 7 | width:20vw; 8 | min-width:175px; 9 | z-index: 600; 10 | cursor: pointer; 11 | -webkit-animation: FlyingSanta 38s infinite linear; 12 | -moz-animation: FlyingSanta 38s infinite linear; 13 | -ms-animation: FlyingSanta 38s infinite linear; 14 | -o-animation: FlyingSanta 38s infinite linear; 15 | animation: FlyingSanta 38s infinite linear; 16 | bottom: 0%; 17 | left: 0%; 18 | position: absolute; 19 | } 20 | @keyframes FlyingSanta { 21 | 25% { 22 | bottom: 80%; 23 | left: 85%; 24 | transform: rotateY(0deg); 25 | } 26 | 26% { 27 | transform: rotateY(180deg); 28 | } 29 | 50% { 30 | bottom: 60%; 31 | left: 0%; 32 | transform: rotateY(180deg); 33 | } 34 | 51% { 35 | transform: rotateY(0deg); 36 | } 37 | 75% { 38 | bottom: 40%; 39 | left: 85%; 40 | transform: rotateY(0deg); 41 | } 42 | 76% { 43 | bottom: 40%; 44 | left: 85%; 45 | transform: rotateY(180deg); 46 | } 47 | 99% { 48 | transform: rotateY(180deg); 49 | } 50 | } 51 | 52 | /*************************** 53 | 54 | //// END OF FLYING SANTA /// 55 | 56 | ***************************/ -------------------------------------------------------------------------------- /file-hash-cache.json: -------------------------------------------------------------------------------- 1 | { 2 | "3bc6f9bf3ba5f163a49b9671f8b6dd41070438e28d6a6f1042cd05d81191679f.webp": "https://1zmn9mofwj.ufs.sh/f/FPA21S8nEiDqvNlXhwAZ2PgTEI37Mjba6mslL0f1uF8NUkXx", 3 | "40998155b9eaeaa802246a1b83ed54f3921a29b572fd00976fcb04d8fc14bd02.webp": "https://1zmn9mofwj.ufs.sh/f/FPA21S8nEiDqFHckPai8nEiDqmwlZH90rSCtcYIpG7azhVkX", 4 | "da314e382260566bb8a3f9af9c1ed5736d3b65205d3630bae13f704d9e205031.webp": "https://1zmn9mofwj.ufs.sh/f/FPA21S8nEiDqYpsNC2Z3d32cm6KxiSzPNGqE1oWBC7LblgaI" 5 | } -------------------------------------------------------------------------------- /next.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | reactStrictMode: true, 4 | images: { 5 | remotePatterns: [ 6 | { 7 | protocol: 'https', 8 | hostname: 'ipfs-chainsafe.dev', 9 | }, 10 | { 11 | protocol: 'https', 12 | hostname: '**', 13 | }, 14 | ], 15 | }, 16 | async rewrites() { 17 | return [ 18 | // { 19 | // source: '/api/:path*', 20 | // destination: '/api/proxy', 21 | // }, 22 | { 23 | source: '/api/proxy/:path*', 24 | destination: `${process.env.NEXT_PUBLIC_API_BASE_URL}/:path*`, 25 | }, 26 | { 27 | source: '/robots.txt', 28 | destination: '/api/robots', 29 | }, 30 | ]; 31 | }, 32 | }; 33 | 34 | export default nextConfig; 35 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pump-fun-ui", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint", 10 | "typechain": "typechain --target ethers-v5 --out-dir src/types/contracts './src/abis/*.json'" 11 | }, 12 | "dependencies": { 13 | "@chakra-ui/icons": "^2.2.4", 14 | "@chakra-ui/react": "^2.10.4", 15 | "@headlessui/react": "^2.1.2", 16 | "@heroicons/react": "^2.1.4", 17 | "@radix-ui/react-switch": "^1.1.0", 18 | "@rainbow-me/rainbowkit": "^2.1.3", 19 | "@tanstack/react-query": "^5.50.1", 20 | "@uploadthing/react": "^7.3.0", 21 | "axios": "^1.7.2", 22 | "chart.js": "^4.4.3", 23 | "chartjs-adapter-date-fns": "^3.0.0", 24 | "chartjs-plugin-zoom": "^2.0.1", 25 | "cookie": "^1.0.2", 26 | "date-fns": "^3.6.0", 27 | "ethers": "5", 28 | "formidable": "^3.5.1", 29 | "framer-motion": "^11.3.21", 30 | "lightweight-charts": "^4.1.7", 31 | "lucide-react": "^0.407.0", 32 | "next": "14.2.4", 33 | "next-auth": "^4.24.8", 34 | "next-http-proxy-middleware": "^1.2.6", 35 | "react": "^18", 36 | "react-chartjs-2": "^5.2.0", 37 | "react-dom": "^18", 38 | "react-hot-toast": "^2.4.1", 39 | "react-toastify": "^10.0.5", 40 | "recharts": "^2.12.7", 41 | "sharp": "^0.33.5", 42 | "siwe": "^2.3.2", 43 | "styled-components": "^6.1.13", 44 | "typewriter-effect": "^2.21.0", 45 | "uploadthing": "^7.5.2", 46 | "use-debounce": "^10.0.1", 47 | "viem": "^2.17.2", 48 | "wagmi": "^2.10.9" 49 | }, 50 | "devDependencies": { 51 | "@typechain/ethers-v5": "^11.1.2", 52 | "@types/cookie": "^0.6.0", 53 | "@types/formidable": "^3.4.5", 54 | "@types/node": "^20", 55 | "@types/react": "^18", 56 | "@types/react-dom": "^18", 57 | "@types/styled-components": "^5.1.34", 58 | "eslint": "^8", 59 | "eslint-config-next": "14.2.4", 60 | "postcss": "^8", 61 | "tailwindcss": "^3.4.1", 62 | "typechain": "^8.3.2", 63 | "typescript": "^5" 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /postcss.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('postcss-load-config').Config} */ 2 | const config = { 3 | plugins: { 4 | tailwindcss: {}, 5 | }, 6 | }; 7 | 8 | export default config; 9 | -------------------------------------------------------------------------------- /public/chats/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RedWilly/Pump-UI/57d80d837986b4c945f643295c830ac024be7707/public/chats/1.png -------------------------------------------------------------------------------- /public/chats/10.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RedWilly/Pump-UI/57d80d837986b4c945f643295c830ac024be7707/public/chats/10.png -------------------------------------------------------------------------------- /public/chats/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RedWilly/Pump-UI/57d80d837986b4c945f643295c830ac024be7707/public/chats/2.png -------------------------------------------------------------------------------- /public/chats/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RedWilly/Pump-UI/57d80d837986b4c945f643295c830ac024be7707/public/chats/3.png -------------------------------------------------------------------------------- /public/chats/4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RedWilly/Pump-UI/57d80d837986b4c945f643295c830ac024be7707/public/chats/4.png -------------------------------------------------------------------------------- /public/chats/5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RedWilly/Pump-UI/57d80d837986b4c945f643295c830ac024be7707/public/chats/5.png -------------------------------------------------------------------------------- /public/chats/6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RedWilly/Pump-UI/57d80d837986b4c945f643295c830ac024be7707/public/chats/6.png -------------------------------------------------------------------------------- /public/chats/7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RedWilly/Pump-UI/57d80d837986b4c945f643295c830ac024be7707/public/chats/7.png -------------------------------------------------------------------------------- /public/chats/8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RedWilly/Pump-UI/57d80d837986b4c945f643295c830ac024be7707/public/chats/8.png -------------------------------------------------------------------------------- /public/chats/9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RedWilly/Pump-UI/57d80d837986b4c945f643295c830ac024be7707/public/chats/9.png -------------------------------------------------------------------------------- /public/chats/noimg.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 9 | 14 | 17 | 19 | 20 | 47 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RedWilly/Pump-UI/57d80d837986b4c945f643295c830ac024be7707/public/favicon.ico -------------------------------------------------------------------------------- /public/logo/Untitled.svg: -------------------------------------------------------------------------------- 1 | 2 | Created with Fabric.js 3.5.0 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /public/logo/ethereum.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RedWilly/Pump-UI/57d80d837986b4c945f643295c830ac024be7707/public/logo/ethereum.png -------------------------------------------------------------------------------- /public/logo/icon.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/logo/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 13 | 14 | Bondle. 15 | 16 | -------------------------------------------------------------------------------- /public/logo/wbone.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RedWilly/Pump-UI/57d80d837986b4c945f643295c830ac024be7707/public/logo/wbone.png -------------------------------------------------------------------------------- /public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/seo/create.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RedWilly/Pump-UI/57d80d837986b4c945f643295c830ac024be7707/public/seo/create.jpg -------------------------------------------------------------------------------- /public/seo/dash.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RedWilly/Pump-UI/57d80d837986b4c945f643295c830ac024be7707/public/seo/dash.jpg -------------------------------------------------------------------------------- /public/seo/home.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RedWilly/Pump-UI/57d80d837986b4c945f643295c830ac024be7707/public/seo/home.jpg -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/abi/BondingCurveManager.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "inputs": [ 4 | { 5 | "internalType": "address", 6 | "name": "tokenAddress", 7 | "type": "address" 8 | } 9 | ], 10 | "name": "addLP", 11 | "outputs": [], 12 | "stateMutability": "nonpayable", 13 | "type": "function" 14 | }, 15 | { 16 | "inputs": [ 17 | { 18 | "internalType": "address", 19 | "name": "tokenAddress", 20 | "type": "address" 21 | } 22 | ], 23 | "name": "buy", 24 | "outputs": [], 25 | "stateMutability": "payable", 26 | "type": "function" 27 | }, 28 | { 29 | "inputs": [ 30 | { 31 | "internalType": "string", 32 | "name": "name", 33 | "type": "string" 34 | }, 35 | { 36 | "internalType": "string", 37 | "name": "symbol", 38 | "type": "string" 39 | } 40 | ], 41 | "name": "create", 42 | "outputs": [], 43 | "stateMutability": "payable", 44 | "type": "function" 45 | }, 46 | { 47 | "inputs": [ 48 | { 49 | "internalType": "address", 50 | "name": "_uniRouter", 51 | "type": "address" 52 | }, 53 | { 54 | "internalType": "address", 55 | "name": "_bancorFormula", 56 | "type": "address" 57 | }, 58 | { 59 | "internalType": "address payable", 60 | "name": "_feeRecipient", 61 | "type": "address" 62 | } 63 | ], 64 | "stateMutability": "nonpayable", 65 | "type": "constructor" 66 | }, 67 | { 68 | "inputs": [], 69 | "name": "FailedToSendEth", 70 | "type": "error" 71 | }, 72 | { 73 | "inputs": [], 74 | "name": "InsufficientPoolbalance", 75 | "type": "error" 76 | }, 77 | { 78 | "inputs": [], 79 | "name": "InvalidLpFeePercentage", 80 | "type": "error" 81 | }, 82 | { 83 | "inputs": [], 84 | "name": "InvalidRecipient", 85 | "type": "error" 86 | }, 87 | { 88 | "inputs": [], 89 | "name": "MaxPoolBalanceReached", 90 | "type": "error" 91 | }, 92 | { 93 | "inputs": [], 94 | "name": "PairCreationFailed", 95 | "type": "error" 96 | }, 97 | { 98 | "inputs": [], 99 | "name": "renounceOwnership", 100 | "outputs": [], 101 | "stateMutability": "nonpayable", 102 | "type": "function" 103 | }, 104 | { 105 | "inputs": [ 106 | { 107 | "internalType": "address", 108 | "name": "tokenAddress", 109 | "type": "address" 110 | }, 111 | { 112 | "internalType": "uint256", 113 | "name": "tokenAmount", 114 | "type": "uint256" 115 | } 116 | ], 117 | "name": "sell", 118 | "outputs": [], 119 | "stateMutability": "nonpayable", 120 | "type": "function" 121 | }, 122 | { 123 | "inputs": [ 124 | { 125 | "internalType": "address", 126 | "name": "_bancorFormula", 127 | "type": "address" 128 | } 129 | ], 130 | "name": "setBancorFormula", 131 | "outputs": [], 132 | "stateMutability": "nonpayable", 133 | "type": "function" 134 | }, 135 | { 136 | "inputs": [ 137 | { 138 | "internalType": "address payable", 139 | "name": "_newRecipient", 140 | "type": "address" 141 | } 142 | ], 143 | "name": "setFeeRecipient", 144 | "outputs": [], 145 | "stateMutability": "nonpayable", 146 | "type": "function" 147 | }, 148 | { 149 | "inputs": [ 150 | { 151 | "internalType": "uint256", 152 | "name": "_lpFeePercentage", 153 | "type": "uint256" 154 | } 155 | ], 156 | "name": "setLpFeePercentage", 157 | "outputs": [], 158 | "stateMutability": "nonpayable", 159 | "type": "function" 160 | }, 161 | { 162 | "inputs": [], 163 | "name": "TokenAlreadyListed", 164 | "type": "error" 165 | }, 166 | { 167 | "inputs": [], 168 | "name": "TokenDoesNotExist", 169 | "type": "error" 170 | }, 171 | { 172 | "inputs": [], 173 | "name": "TokenTransferFailed", 174 | "type": "error" 175 | }, 176 | { 177 | "inputs": [], 178 | "name": "ZeroEthSent", 179 | "type": "error" 180 | }, 181 | { 182 | "inputs": [], 183 | "name": "ZeroTokenAmount", 184 | "type": "error" 185 | }, 186 | { 187 | "anonymous": false, 188 | "inputs": [ 189 | { 190 | "indexed": true, 191 | "internalType": "address", 192 | "name": "token", 193 | "type": "address" 194 | }, 195 | { 196 | "indexed": false, 197 | "internalType": "uint256", 198 | "name": "ethAmount", 199 | "type": "uint256" 200 | }, 201 | { 202 | "indexed": false, 203 | "internalType": "uint256", 204 | "name": "tokenAmount", 205 | "type": "uint256" 206 | } 207 | ], 208 | "name": "LiquidityAdded", 209 | "type": "event" 210 | }, 211 | { 212 | "anonymous": false, 213 | "inputs": [ 214 | { 215 | "indexed": true, 216 | "internalType": "address", 217 | "name": "previousOwner", 218 | "type": "address" 219 | }, 220 | { 221 | "indexed": true, 222 | "internalType": "address", 223 | "name": "newOwner", 224 | "type": "address" 225 | } 226 | ], 227 | "name": "OwnershipTransferred", 228 | "type": "event" 229 | }, 230 | { 231 | "anonymous": false, 232 | "inputs": [ 233 | { 234 | "indexed": true, 235 | "internalType": "address", 236 | "name": "tokenAddress", 237 | "type": "address" 238 | }, 239 | { 240 | "indexed": true, 241 | "internalType": "address", 242 | "name": "creator", 243 | "type": "address" 244 | }, 245 | { 246 | "indexed": false, 247 | "internalType": "string", 248 | "name": "name", 249 | "type": "string" 250 | }, 251 | { 252 | "indexed": false, 253 | "internalType": "string", 254 | "name": "symbol", 255 | "type": "string" 256 | } 257 | ], 258 | "name": "TokenCreated", 259 | "type": "event" 260 | }, 261 | { 262 | "anonymous": false, 263 | "inputs": [ 264 | { 265 | "indexed": true, 266 | "internalType": "address", 267 | "name": "token", 268 | "type": "address" 269 | }, 270 | { 271 | "indexed": true, 272 | "internalType": "address", 273 | "name": "buyer", 274 | "type": "address" 275 | }, 276 | { 277 | "indexed": false, 278 | "internalType": "uint256", 279 | "name": "ethAmount", 280 | "type": "uint256" 281 | }, 282 | { 283 | "indexed": false, 284 | "internalType": "uint256", 285 | "name": "tokenAmount", 286 | "type": "uint256" 287 | } 288 | ], 289 | "name": "TokensBought", 290 | "type": "event" 291 | }, 292 | { 293 | "anonymous": false, 294 | "inputs": [ 295 | { 296 | "indexed": true, 297 | "internalType": "address", 298 | "name": "token", 299 | "type": "address" 300 | }, 301 | { 302 | "indexed": true, 303 | "internalType": "address", 304 | "name": "seller", 305 | "type": "address" 306 | }, 307 | { 308 | "indexed": false, 309 | "internalType": "uint256", 310 | "name": "tokenAmount", 311 | "type": "uint256" 312 | }, 313 | { 314 | "indexed": false, 315 | "internalType": "uint256", 316 | "name": "ethAmount", 317 | "type": "uint256" 318 | } 319 | ], 320 | "name": "TokensSold", 321 | "type": "event" 322 | }, 323 | { 324 | "inputs": [ 325 | { 326 | "internalType": "address", 327 | "name": "newOwner", 328 | "type": "address" 329 | } 330 | ], 331 | "name": "transferOwnership", 332 | "outputs": [], 333 | "stateMutability": "nonpayable", 334 | "type": "function" 335 | }, 336 | { 337 | "stateMutability": "payable", 338 | "type": "receive" 339 | }, 340 | { 341 | "inputs": [ 342 | { 343 | "internalType": "address", 344 | "name": "tokenAddress", 345 | "type": "address" 346 | }, 347 | { 348 | "internalType": "uint256", 349 | "name": "ethAmount", 350 | "type": "uint256" 351 | } 352 | ], 353 | "name": "calculateCurvedBuyReturn", 354 | "outputs": [ 355 | { 356 | "internalType": "uint256", 357 | "name": "", 358 | "type": "uint256" 359 | } 360 | ], 361 | "stateMutability": "view", 362 | "type": "function" 363 | }, 364 | { 365 | "inputs": [ 366 | { 367 | "internalType": "address", 368 | "name": "tokenAddress", 369 | "type": "address" 370 | }, 371 | { 372 | "internalType": "uint256", 373 | "name": "tokenAmount", 374 | "type": "uint256" 375 | } 376 | ], 377 | "name": "calculateCurvedSellReturn", 378 | "outputs": [ 379 | { 380 | "internalType": "uint256", 381 | "name": "", 382 | "type": "uint256" 383 | } 384 | ], 385 | "stateMutability": "view", 386 | "type": "function" 387 | }, 388 | { 389 | "inputs": [ 390 | { 391 | "internalType": "address", 392 | "name": "tokenAddress", 393 | "type": "address" 394 | } 395 | ], 396 | "name": "getCurrentTokenPrice", 397 | "outputs": [ 398 | { 399 | "internalType": "uint256", 400 | "name": "", 401 | "type": "uint256" 402 | } 403 | ], 404 | "stateMutability": "view", 405 | "type": "function" 406 | }, 407 | { 408 | "inputs": [ 409 | { 410 | "internalType": "address", 411 | "name": "tokenAddress", 412 | "type": "address" 413 | } 414 | ], 415 | "name": "getMarketCap", 416 | "outputs": [ 417 | { 418 | "internalType": "uint256", 419 | "name": "", 420 | "type": "uint256" 421 | } 422 | ], 423 | "stateMutability": "view", 424 | "type": "function" 425 | }, 426 | { 427 | "inputs": [ 428 | { 429 | "internalType": "address", 430 | "name": "tokenAddress", 431 | "type": "address" 432 | } 433 | ], 434 | "name": "getTokenEthBalance", 435 | "outputs": [ 436 | { 437 | "internalType": "uint256", 438 | "name": "", 439 | "type": "uint256" 440 | } 441 | ], 442 | "stateMutability": "view", 443 | "type": "function" 444 | }, 445 | { 446 | "inputs": [], 447 | "name": "owner", 448 | "outputs": [ 449 | { 450 | "internalType": "address", 451 | "name": "", 452 | "type": "address" 453 | } 454 | ], 455 | "stateMutability": "view", 456 | "type": "function" 457 | }, 458 | { 459 | "inputs": [ 460 | { 461 | "internalType": "uint256", 462 | "name": "", 463 | "type": "uint256" 464 | } 465 | ], 466 | "name": "tokenList", 467 | "outputs": [ 468 | { 469 | "internalType": "address", 470 | "name": "", 471 | "type": "address" 472 | } 473 | ], 474 | "stateMutability": "view", 475 | "type": "function" 476 | }, 477 | { 478 | "inputs": [ 479 | { 480 | "internalType": "address", 481 | "name": "", 482 | "type": "address" 483 | } 484 | ], 485 | "name": "tokens", 486 | "outputs": [ 487 | { 488 | "internalType": "contract BondingCurveToken", 489 | "name": "token", 490 | "type": "address" 491 | }, 492 | { 493 | "internalType": "uint256", 494 | "name": "tokenbalance", 495 | "type": "uint256" 496 | }, 497 | { 498 | "internalType": "uint256", 499 | "name": "ethBalance", 500 | "type": "uint256" 501 | }, 502 | { 503 | "internalType": "bool", 504 | "name": "isListed", 505 | "type": "bool" 506 | } 507 | ], 508 | "stateMutability": "view", 509 | "type": "function" 510 | } 511 | ] -------------------------------------------------------------------------------- /src/abi/ERC20.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "inputs": [ 4 | { 5 | "internalType": "string", 6 | "name": "name", 7 | "type": "string" 8 | }, 9 | { 10 | "internalType": "string", 11 | "name": "symbol", 12 | "type": "string" 13 | } 14 | ], 15 | "stateMutability": "nonpayable", 16 | "type": "constructor" 17 | }, 18 | { 19 | "anonymous": false, 20 | "inputs": [ 21 | { 22 | "indexed": true, 23 | "internalType": "address", 24 | "name": "owner", 25 | "type": "address" 26 | }, 27 | { 28 | "indexed": true, 29 | "internalType": "address", 30 | "name": "spender", 31 | "type": "address" 32 | }, 33 | { 34 | "indexed": false, 35 | "internalType": "uint256", 36 | "name": "value", 37 | "type": "uint256" 38 | } 39 | ], 40 | "name": "Approval", 41 | "type": "event" 42 | }, 43 | { 44 | "anonymous": false, 45 | "inputs": [ 46 | { 47 | "indexed": true, 48 | "internalType": "address", 49 | "name": "previousOwner", 50 | "type": "address" 51 | }, 52 | { 53 | "indexed": true, 54 | "internalType": "address", 55 | "name": "newOwner", 56 | "type": "address" 57 | } 58 | ], 59 | "name": "OwnershipTransferred", 60 | "type": "event" 61 | }, 62 | { 63 | "anonymous": false, 64 | "inputs": [ 65 | { 66 | "indexed": true, 67 | "internalType": "address", 68 | "name": "from", 69 | "type": "address" 70 | }, 71 | { 72 | "indexed": true, 73 | "internalType": "address", 74 | "name": "to", 75 | "type": "address" 76 | }, 77 | { 78 | "indexed": false, 79 | "internalType": "uint256", 80 | "name": "value", 81 | "type": "uint256" 82 | } 83 | ], 84 | "name": "Transfer", 85 | "type": "event" 86 | }, 87 | { 88 | "inputs": [ 89 | { 90 | "internalType": "address", 91 | "name": "owner", 92 | "type": "address" 93 | }, 94 | { 95 | "internalType": "address", 96 | "name": "spender", 97 | "type": "address" 98 | } 99 | ], 100 | "name": "allowance", 101 | "outputs": [ 102 | { 103 | "internalType": "uint256", 104 | "name": "", 105 | "type": "uint256" 106 | } 107 | ], 108 | "stateMutability": "view", 109 | "type": "function" 110 | }, 111 | { 112 | "inputs": [ 113 | { 114 | "internalType": "address", 115 | "name": "spender", 116 | "type": "address" 117 | }, 118 | { 119 | "internalType": "uint256", 120 | "name": "amount", 121 | "type": "uint256" 122 | } 123 | ], 124 | "name": "approve", 125 | "outputs": [ 126 | { 127 | "internalType": "bool", 128 | "name": "", 129 | "type": "bool" 130 | } 131 | ], 132 | "stateMutability": "nonpayable", 133 | "type": "function" 134 | }, 135 | { 136 | "inputs": [ 137 | { 138 | "internalType": "address", 139 | "name": "account", 140 | "type": "address" 141 | } 142 | ], 143 | "name": "balanceOf", 144 | "outputs": [ 145 | { 146 | "internalType": "uint256", 147 | "name": "", 148 | "type": "uint256" 149 | } 150 | ], 151 | "stateMutability": "view", 152 | "type": "function" 153 | }, 154 | { 155 | "inputs": [ 156 | { 157 | "internalType": "uint256", 158 | "name": "amount", 159 | "type": "uint256" 160 | } 161 | ], 162 | "name": "burn", 163 | "outputs": [], 164 | "stateMutability": "nonpayable", 165 | "type": "function" 166 | }, 167 | { 168 | "inputs": [ 169 | { 170 | "internalType": "address", 171 | "name": "account", 172 | "type": "address" 173 | }, 174 | { 175 | "internalType": "uint256", 176 | "name": "amount", 177 | "type": "uint256" 178 | } 179 | ], 180 | "name": "burnFrom", 181 | "outputs": [], 182 | "stateMutability": "nonpayable", 183 | "type": "function" 184 | }, 185 | { 186 | "inputs": [], 187 | "name": "decimals", 188 | "outputs": [ 189 | { 190 | "internalType": "uint8", 191 | "name": "", 192 | "type": "uint8" 193 | } 194 | ], 195 | "stateMutability": "view", 196 | "type": "function" 197 | }, 198 | { 199 | "inputs": [ 200 | { 201 | "internalType": "address", 202 | "name": "spender", 203 | "type": "address" 204 | }, 205 | { 206 | "internalType": "uint256", 207 | "name": "subtractedValue", 208 | "type": "uint256" 209 | } 210 | ], 211 | "name": "decreaseAllowance", 212 | "outputs": [ 213 | { 214 | "internalType": "bool", 215 | "name": "", 216 | "type": "bool" 217 | } 218 | ], 219 | "stateMutability": "nonpayable", 220 | "type": "function" 221 | }, 222 | { 223 | "inputs": [ 224 | { 225 | "internalType": "address", 226 | "name": "spender", 227 | "type": "address" 228 | }, 229 | { 230 | "internalType": "uint256", 231 | "name": "addedValue", 232 | "type": "uint256" 233 | } 234 | ], 235 | "name": "increaseAllowance", 236 | "outputs": [ 237 | { 238 | "internalType": "bool", 239 | "name": "", 240 | "type": "bool" 241 | } 242 | ], 243 | "stateMutability": "nonpayable", 244 | "type": "function" 245 | }, 246 | { 247 | "inputs": [ 248 | { 249 | "internalType": "address", 250 | "name": "to", 251 | "type": "address" 252 | }, 253 | { 254 | "internalType": "uint256", 255 | "name": "amount", 256 | "type": "uint256" 257 | } 258 | ], 259 | "name": "mint", 260 | "outputs": [], 261 | "stateMutability": "nonpayable", 262 | "type": "function" 263 | }, 264 | { 265 | "inputs": [], 266 | "name": "name", 267 | "outputs": [ 268 | { 269 | "internalType": "string", 270 | "name": "", 271 | "type": "string" 272 | } 273 | ], 274 | "stateMutability": "view", 275 | "type": "function" 276 | }, 277 | { 278 | "inputs": [], 279 | "name": "owner", 280 | "outputs": [ 281 | { 282 | "internalType": "address", 283 | "name": "", 284 | "type": "address" 285 | } 286 | ], 287 | "stateMutability": "view", 288 | "type": "function" 289 | }, 290 | { 291 | "inputs": [], 292 | "name": "renounceOwnership", 293 | "outputs": [], 294 | "stateMutability": "nonpayable", 295 | "type": "function" 296 | }, 297 | { 298 | "inputs": [], 299 | "name": "symbol", 300 | "outputs": [ 301 | { 302 | "internalType": "string", 303 | "name": "", 304 | "type": "string" 305 | } 306 | ], 307 | "stateMutability": "view", 308 | "type": "function" 309 | }, 310 | { 311 | "inputs": [], 312 | "name": "totalSupply", 313 | "outputs": [ 314 | { 315 | "internalType": "uint256", 316 | "name": "", 317 | "type": "uint256" 318 | } 319 | ], 320 | "stateMutability": "view", 321 | "type": "function" 322 | }, 323 | { 324 | "inputs": [ 325 | { 326 | "internalType": "address", 327 | "name": "to", 328 | "type": "address" 329 | }, 330 | { 331 | "internalType": "uint256", 332 | "name": "amount", 333 | "type": "uint256" 334 | } 335 | ], 336 | "name": "transfer", 337 | "outputs": [ 338 | { 339 | "internalType": "bool", 340 | "name": "", 341 | "type": "bool" 342 | } 343 | ], 344 | "stateMutability": "nonpayable", 345 | "type": "function" 346 | }, 347 | { 348 | "inputs": [ 349 | { 350 | "internalType": "address", 351 | "name": "from", 352 | "type": "address" 353 | }, 354 | { 355 | "internalType": "address", 356 | "name": "to", 357 | "type": "address" 358 | }, 359 | { 360 | "internalType": "uint256", 361 | "name": "amount", 362 | "type": "uint256" 363 | } 364 | ], 365 | "name": "transferFrom", 366 | "outputs": [ 367 | { 368 | "internalType": "bool", 369 | "name": "", 370 | "type": "bool" 371 | } 372 | ], 373 | "stateMutability": "nonpayable", 374 | "type": "function" 375 | }, 376 | { 377 | "inputs": [ 378 | { 379 | "internalType": "address", 380 | "name": "newOwner", 381 | "type": "address" 382 | } 383 | ], 384 | "name": "transferOwnership", 385 | "outputs": [], 386 | "stateMutability": "nonpayable", 387 | "type": "function" 388 | } 389 | ] -------------------------------------------------------------------------------- /src/abi/old_token.json: -------------------------------------------------------------------------------- 1 | [ 2 | "0xf88F78347837680e093bcf95BC0528A246989438", 3 | "0x05d28a3bb8510b9Ce14Fd5b46E2DA61355FeCCf5", 4 | "0xBe46E4b092f70ec610239786Dc10c25256a30a1c", 5 | "0x1AE23dCC622D84389Ec35Cb4Cb1046Ce342Be59d", 6 | "0x61F0ab3132cd4010F0F0Faa905EB1fB9a8f58BA4", 7 | "0x24fBCA9c445405cCb8212BD69dDF53Ba606b9607", 8 | "0xFD050b0b6Fc567fA99ca0052f16fb6bcc9451eD5", 9 | "0x8D0E91De64E8478954799b3b0100B7D61c2adC98", 10 | "0x7100707EE9759980753D7D538CAd8689145B4500", 11 | "0x41897f380d1851EDC0D1634aB15443969E2843b8", 12 | "0x87e36ACD9ADD23B4eC8052B71608cfDD725932b9", 13 | "0x7B1F53cAC88CD12A64795Da02b01eA8EbBd45f57", 14 | "0x798825b942bD9081266b95C6B08fBFC805e09f12", 15 | "0x09Ff81cbF1D708742AcAA143Db30e979D1ff4DDC", 16 | "0xC5713Bf81b19b9ACf3d5Db10965c12C3E5d382c5", 17 | "0xD4c5f5B7676e6A01889F5d5F988a8Eac26da2c78", 18 | "0x38AC17A02D79B4DBAcB86ECB428e87422fa00692", 19 | "0xBA4ed7d21795549380fBce5ceA138a9F4A4F5B69", 20 | "0xE1A5047978fbfc352a292932Eb6e282Ed5Cb95a2", 21 | "0xF14d0b49D579409058fFB3ebC387AB1Cc3c8d5D1", 22 | "0xf1dc0544AF6a30701439F24e81Beb35299f32645", 23 | "0xB47f3dE547a17C816dbC0661A79a061053F72D95", 24 | "0x02a7956457A2bE2E08963912F588c1D8644245C6", 25 | "0x3dfa5439BF69183Af1cfd0ccdCA19809Bd67b388", 26 | "0x336c347885d89Aa974c709C4c3B5e699cfe1BdEf" 27 | ] -------------------------------------------------------------------------------- /src/abi/old_token1.json: -------------------------------------------------------------------------------- 1 | [ 2 | "0x87a7007f88d9b1E54129fEBc98F7afdc96Ee3154", 3 | "0xAbddcD32a0E2F4C9E20eb3D4eC371b677442B48C", 4 | "0xc7BBEc3752a3B1CF0A2c81bCd3AEbB7712B4ED1d", 5 | "0x32Fd573a05a46613735662cb1fe10ab545b1e391", 6 | "0xCF373626D00BDbdb1884bCA56EEbed40E78b630F", 7 | "0x064ae3C30bc7B287A8Cfacb67F6270b63a541C8a", 8 | "0xa34d0bdd9f09F03F70048AfBBD62D8d8398A59C2", 9 | "0x664616C8981A3C1b23cAFFF2a80977e477754F33", 10 | "0x770CE958B527C30EF33123f01fCa17E0d78E1c07", 11 | "0xAb6d6a16D8aecE892e4c62060Ef408B83cE359BF", 12 | "0x94Cc1caE09975493E2bbc95eeFE00ea79a48F276", 13 | "0xb90E65Ee21A68dF6509c7248815B0626D59780bc" 14 | ] 15 | 16 | -------------------------------------------------------------------------------- /src/chain/config.ts: -------------------------------------------------------------------------------- 1 | import { Chain } from 'viem' 2 | import { flare, shibarium } from 'wagmi/chains' 3 | 4 | interface ChainConfig { 5 | apiBaseUrl: string 6 | wsBaseUrl: string 7 | blockscoutUrl: string 8 | dexTarget: number 9 | contractAddresses: string[] 10 | } 11 | 12 | interface ChainConfigs { 13 | [chainId: number]: ChainConfig 14 | } 15 | 16 | // Shibarium Chain Configuration 17 | const shibariumConfig: ChainConfig = { 18 | apiBaseUrl: process.env.NEXT_PUBLIC_API_BASE_URL!, 19 | wsBaseUrl: process.env.NEXT_PUBLIC_WS_BASE_URL!, 20 | blockscoutUrl: process.env.NEXT_PUBLIC_BLOCKSCOUT_URL!, 21 | dexTarget: Number(process.env.NEXT_PUBLIC_DEX_TARGET), 22 | contractAddresses: [ 23 | process.env.NEXT_PUBLIC_BONDING_CURVE_MANAGER_ADDRESS!, 24 | process.env.NEXT_PUBLIC_BONDING_CURVE_MANAGER_ADDRESS_OLD!, 25 | process.env.NEXT_PUBLIC_BONDING_CURVE_MANAGER_ADDRESS_OLD1!, 26 | ].filter(Boolean) 27 | } 28 | 29 | // Flare Chain Configuration 30 | // const flareConfig: ChainConfig = { 31 | // apiBaseUrl: process.env.FLARE_NEXT_PUBLIC_API_BASE_URL!, 32 | // wsBaseUrl: process.env.FLARE_NEXT_PUBLIC_WS_BASE_URL!, 33 | // blockscoutUrl: process.env.FLARE_NEXT_PUBLIC_BLOCKSCOUT_URL!, 34 | // dexTarget: Number(process.env.FLARE_NEXT_PUBLIC_DEX_TARGET), 35 | // contractAddresses: [ 36 | // process.env.FLARE_NEXT_PUBLIC_BONDING_CURVE_MANAGER_ADDRESS! 37 | // ].filter(Boolean) 38 | // } 39 | 40 | // Chain configurations mapped by chainId 41 | export const chainConfigs: ChainConfigs = { 42 | [shibarium.id]: shibariumConfig, 43 | // [flare.id]: flareConfig, 44 | } 45 | 46 | // Supported chains for the application 47 | // export const supportedChains: Chain[] = [shibarium, flare] 48 | export const supportedChains: Chain[] = [shibarium] 49 | 50 | // Helper function to get chain configuration by chainId 51 | export const getChainConfig = (chainId: number): ChainConfig | undefined => { 52 | return chainConfigs[chainId] 53 | } 54 | 55 | // Helper function to get current active contract address for a chain //wrong ill fix later on 56 | export const getActiveContractAddress = (chainId: number): string | undefined => { 57 | const config = chainConfigs[chainId] 58 | return config?.contractAddresses[0] // Returns the most recent contract address 59 | } 60 | -------------------------------------------------------------------------------- /src/components/PriceLiquidity.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useCurrentTokenPrice, useTokenLiquidity } from '@/utils/blockchainUtils'; 3 | import { formatAmount, formatAmountV2 } from '@/utils/blockchainUtils'; 4 | 5 | interface PriceLiquidityProps { 6 | address: `0x${string}`; 7 | } 8 | 9 | const PriceLiquidity: React.FC = ({ address }) => { 10 | const { data: currentPrice } = useCurrentTokenPrice(address); 11 | const { data: liquidityData } = useTokenLiquidity(address); 12 | 13 | const calculateProgress = (currentLiquidity: bigint): number => { 14 | const liquidityInEth = parseFloat(formatAmountV2(currentLiquidity.toString())); 15 | const target = Number(process.env.NEXT_PUBLIC_DEX_TARGET); 16 | const progress = (liquidityInEth / target) * 100; 17 | return Math.min(progress, 100); 18 | }; 19 | 20 | return ( 21 |
22 |
23 |

Current Price

24 |

25 | {currentPrice ? formatAmount(currentPrice.toString()) : 'Loading...'} BONE 26 |

27 |
28 |
29 |

Current Liquidity

30 |

31 | {liquidityData && liquidityData[2] ? `${formatAmountV2(liquidityData[2].toString())} BONE` : '0 BONE'} 32 |

33 | {liquidityData && liquidityData[2] && ( 34 | <> 35 |
36 |
40 |
43 | {calculateProgress(liquidityData[2]).toFixed(2)}% 44 |
45 |
46 | 47 | )} 48 |
49 |
50 | ); 51 | }; 52 | 53 | export default PriceLiquidity; -------------------------------------------------------------------------------- /src/components/TokenDetails/Chats.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect, useMemo } from 'react'; 2 | import Image from 'next/image'; 3 | import { getChatMessages, addChatMessage } from '@/utils/api'; 4 | import { useAccount } from 'wagmi'; 5 | import { toast } from 'react-toastify'; 6 | import { formatTimestamp, getRandomAvatarImage, shortenAddress } from '@/utils/chatUtils'; 7 | import { motion, AnimatePresence } from 'framer-motion'; 8 | import { TokenWithTransactions } from '@/interface/types'; 9 | import SiweAuth from '@/components/auth/SiweAuth'; 10 | import { Reply, X } from 'lucide-react'; 11 | 12 | interface ChatMessage { 13 | id: number; 14 | user: string; 15 | token: string; 16 | message: string; 17 | reply_to: number | null; 18 | timestamp: string; 19 | } 20 | 21 | interface ChatsProps { 22 | tokenAddress: string; 23 | tokenInfo: TokenWithTransactions; 24 | } 25 | 26 | const Chats: React.FC = ({ tokenAddress, tokenInfo }) => { 27 | const [messages, setMessages] = useState([]); 28 | const [newMessage, setNewMessage] = useState(''); 29 | const [isAuthenticated, setIsAuthenticated] = useState(false); 30 | const [replyingTo, setReplyingTo] = useState(null); 31 | const { address } = useAccount(); 32 | 33 | const userAvatars = useMemo(() => { 34 | const avatars: { [key: string]: string } = {}; 35 | messages.forEach(msg => { 36 | if (!avatars[msg.user]) { 37 | avatars[msg.user] = getRandomAvatarImage(); 38 | } 39 | }); 40 | return avatars; 41 | }, [messages]); 42 | 43 | useEffect(() => { 44 | checkAuth(); 45 | fetchMessages(); 46 | const interval = setInterval(fetchMessages, 30000); 47 | return () => clearInterval(interval); 48 | }, [tokenAddress]); 49 | 50 | const checkAuth = async () => { 51 | try { 52 | const response = await fetch('/api/auth/user'); 53 | setIsAuthenticated(response.ok); 54 | } catch (error) { 55 | setIsAuthenticated(false); 56 | } 57 | }; 58 | 59 | const fetchMessages = async () => { 60 | try { 61 | const fetchedMessages = await getChatMessages(tokenAddress); 62 | setMessages(fetchedMessages); 63 | } catch (error) { 64 | console.error('Error fetching messages:', error); 65 | } 66 | }; 67 | 68 | const handleSendMessage = async (e: React.FormEvent) => { 69 | e.preventDefault(); 70 | if (!isAuthenticated) { 71 | toast.error('Please sign in to chat'); 72 | return; 73 | } 74 | if (!newMessage.trim()) return; 75 | try { 76 | await addChatMessage(address!, tokenAddress, newMessage, replyingTo?.id); 77 | setNewMessage(''); 78 | setReplyingTo(null); 79 | fetchMessages(); 80 | } catch (error) { 81 | console.error('Error sending message:', error); 82 | toast.error('Failed to send message'); 83 | } 84 | }; 85 | 86 | const handleReply = (message: ChatMessage) => { 87 | setReplyingTo(message); 88 | }; 89 | 90 | const cancelReply = () => { 91 | setReplyingTo(null); 92 | }; 93 | 94 | const findParentMessage = (replyId: number | null) => { 95 | if (!replyId) return null; 96 | return messages.find(m => m.id === replyId); 97 | }; 98 | 99 | if (!isAuthenticated) { 100 | return ( 101 |
102 | setIsAuthenticated(true)} /> 103 |
104 | ); 105 | } 106 | 107 | return ( 108 |
109 |
110 | 111 | {messages.map((message) => { 112 | const parentMessage = findParentMessage(message.reply_to); 113 | return ( 114 | 121 | {parentMessage && ( 122 |
123 | {shortenAddress(parentMessage.user)}: 124 | {parentMessage.message} 125 |
126 | )} 127 |
128 | Avatar 135 |
136 |
137 | 138 | {shortenAddress(message.user)} 139 | {message.user.toLowerCase() === tokenInfo.creatorAddress.toLowerCase() && 140 | (dev) 141 | } 142 | 143 | {formatTimestamp(message.timestamp)} 144 |
145 |

{message.message}

146 | 153 |
154 |
155 |
156 | ); 157 | })} 158 |
159 |
160 | 161 |
162 | {replyingTo && ( 163 |
164 | 165 | Replying to {shortenAddress(replyingTo.user)} 166 | 167 | 174 |
175 | )} 176 |
177 | setNewMessage(e.target.value)} 181 | placeholder="Type a message..." 182 | className="flex-grow bg-[var(--card2)] text-white rounded-lg px-3 py-1.5 sm:px-4 sm:py-2 text-xs sm:text-sm focus:outline-none focus:ring-1 focus:ring-[var(--primary)]" 183 | /> 184 | 191 |
192 |
193 |
194 | ); 195 | }; 196 | 197 | export default Chats; 198 | -------------------------------------------------------------------------------- /src/components/TokenDetails/TokenHolders.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { ChevronLeftIcon, ChevronRightIcon, ExternalLinkIcon } from 'lucide-react'; 3 | import { TokenHolder } from '@/interface/types'; 4 | import { formatAmountV3, shortenAddress, getBondingCurveAddress } from '@/utils/blockchainUtils'; 5 | 6 | interface TokenHoldersProps { 7 | tokenHolders: TokenHolder[]; 8 | currentPage: number; 9 | totalPages: number; 10 | tokenSymbol: string; 11 | creatorAddress: string; 12 | tokenAddress: string; 13 | onPageChange: (page: number) => void; 14 | allHolders: TokenHolder[]; 15 | } 16 | 17 | const TokenHolders: React.FC = ({ 18 | tokenHolders, 19 | currentPage, 20 | totalPages, 21 | tokenSymbol, 22 | creatorAddress, 23 | tokenAddress, 24 | onPageChange, 25 | allHolders, 26 | }) => { 27 | const bondingCurveAddress = getBondingCurveAddress(tokenAddress as `0x${string}`); 28 | 29 | // Calculate total supply excluding only the token contract 30 | const totalSupply = allHolders.reduce((sum, holder) => { 31 | if (holder.address.toLowerCase() === tokenAddress.toLowerCase()) { 32 | return sum; 33 | } 34 | return sum + BigInt(holder.balance); 35 | }, BigInt(0)); 36 | 37 | // Calculate percentage for a holder 38 | const calculatePercentage = (balance: string, address: string): string => { 39 | if (address.toLowerCase() === tokenAddress.toLowerCase()) { 40 | return '0%'; 41 | } 42 | 43 | if (totalSupply === BigInt(0)) return '0%'; 44 | 45 | const percentage = (BigInt(balance) * BigInt(10000) / totalSupply); 46 | const percentageNumber = Number(percentage) / 100; 47 | 48 | if (percentageNumber < 0.001) { 49 | return '<0.001%'; 50 | } else if (percentageNumber < 0.01) { 51 | return percentageNumber.toFixed(3) + '%'; 52 | } else if (percentageNumber < 0.1) { 53 | return percentageNumber.toFixed(2) + '%'; 54 | } else { 55 | return percentageNumber.toFixed(2) + '%'; 56 | } 57 | }; 58 | 59 | // Find bonding curve holder 60 | const bondingCurveHolder = allHolders.find( 61 | holder => holder.address.toLowerCase() === bondingCurveAddress.toLowerCase() 62 | ); 63 | 64 | // Filter holders (excluding token contract AND bonding curve address) and paginate 65 | const filteredHolders = allHolders.filter(holder => 66 | holder.address.toLowerCase() !== tokenAddress.toLowerCase() && 67 | holder.address.toLowerCase() !== bondingCurveAddress.toLowerCase() 68 | ); 69 | 70 | // Calculate pagination 71 | const holdersPerPage = 10; 72 | const startIndex = (currentPage - 1) * holdersPerPage; 73 | const endIndex = startIndex + holdersPerPage; 74 | const paginatedHolders = filteredHolders.slice(startIndex, endIndex); 75 | const actualTotalPages = Math.ceil(filteredHolders.length / holdersPerPage); 76 | 77 | return ( 78 |
79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | {/* Bonding Curve Manager as the first entry */} 88 | 89 | 99 | 102 | 103 | {paginatedHolders.map((holder, index) => ( 104 | 105 | 126 | 129 | 130 | ))} 131 | 132 |
HolderPercentage
90 | 96 | Bonding Curve 97 | 98 | 100 | {bondingCurveHolder ? calculatePercentage(bondingCurveHolder.balance, bondingCurveHolder.address) : '0%'} 101 |
106 | {holder.address === creatorAddress ? ( 107 | 113 | Creator 114 | 115 | ) : ( 116 | 122 | {shortenAddress(holder.address)} 123 | 124 | )} 125 | 127 | {calculatePercentage(holder.balance, holder.address)} 128 |
133 | 134 | {filteredHolders.length === 0 && ( 135 |
136 | No token holder data available 137 |
138 | )} 139 | 140 | {actualTotalPages > 1 && ( 141 |
142 | 149 | {Array.from({ length: actualTotalPages }, (_, i) => i + 1).map((page) => ( 150 | 161 | ))} 162 | 169 |
170 | )} 171 |
172 | ); 173 | }; 174 | 175 | export default TokenHolders; -------------------------------------------------------------------------------- /src/components/TokenDetails/TransactionHistory.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { ChevronLeftIcon, ChevronRightIcon, ExternalLinkIcon, ChevronDownIcon } from 'lucide-react'; 3 | import { formatTimestamp, formatAmountV3, shortenAddress } from '@/utils/blockchainUtils'; 4 | import { Transaction } from '@/interface/types'; 5 | 6 | interface TransactionHistoryProps { 7 | transactions: Transaction[]; 8 | transactionPage: number; 9 | totalTransactionPages: number; 10 | tokenSymbol: string; 11 | handlePageChange: (page: number) => void; 12 | } 13 | 14 | const TransactionHistory: React.FC = ({ 15 | transactions, 16 | transactionPage, 17 | totalTransactionPages, 18 | tokenSymbol, 19 | handlePageChange, 20 | }) => { 21 | const [expandedRow, setExpandedRow] = useState(null); 22 | 23 | const getPaginationRange = (current: number, total: number) => { 24 | if (total <= 5) { 25 | return Array.from({ length: total }, (_, i) => i + 1); 26 | } 27 | 28 | if (current <= 2) { 29 | return [1, 2, 3, '...', total]; 30 | } 31 | 32 | if (current >= total - 1) { 33 | return [1, '...', total - 2, total - 1, total]; 34 | } 35 | 36 | return [ 37 | 1, 38 | '...', 39 | current, 40 | '...', 41 | total 42 | ]; 43 | }; 44 | 45 | // Desktop view table 46 | const DesktopTable = () => ( 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | {transactions.map((tx) => ( 60 | 61 | 71 | 72 | 73 | 74 | 75 | 85 | 86 | ))} 87 | 88 |
MakerTypeBONE{tokenSymbol}DateTx
62 | 68 | {shortenAddress(tx.senderAddress)} 69 | 70 | {tx.type}{formatAmountV3(tx.ethAmount)}{formatAmountV3(tx.tokenAmount)}{formatTimestamp(tx.timestamp)} 76 | 82 | {tx.txHash.slice(0, 8)} 83 | 84 |
89 | ); 90 | 91 | // Mobile view table 92 | const MobileTable = () => ( 93 |
94 | {transactions.map((tx) => ( 95 |
96 |
setExpandedRow(expandedRow === tx.id ? null : tx.id)} 99 | > 100 |
101 |
102 |
103 | {tx.type} 104 | 110 |
111 |
112 | {formatAmountV3(tx.ethAmount)} BONE 113 | {formatAmountV3(tx.tokenAmount)} {tokenSymbol} 114 |
115 |
116 |
117 | 118 | {expandedRow === tx.id && ( 119 |
120 | 131 |
132 | Date: 133 | {formatTimestamp(tx.timestamp)} 134 |
135 |
136 | Transaction: 137 | 143 | View 144 | 145 |
146 |
147 | )} 148 |
149 |
150 | ))} 151 |
152 | ); 153 | 154 | return ( 155 |
156 | 157 | 158 | 159 | {transactions.length === 0 && ( 160 |
161 | No transactions yet 162 |
163 | )} 164 | 165 | {totalTransactionPages > 1 && ( 166 |
167 | 174 | {getPaginationRange(transactionPage, totalTransactionPages).map((page, index) => ( 175 | 176 | {page === '...' ? ( 177 | ... 178 | ) : ( 179 | 189 | )} 190 | 191 | ))} 192 | 199 |
200 | )} 201 |
202 | ); 203 | }; 204 | 205 | export default TransactionHistory; -------------------------------------------------------------------------------- /src/components/auth/SiweAuth.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { useAccount, useSignMessage } from 'wagmi'; 3 | import { SiweMessage } from 'siwe'; 4 | import { toast } from 'react-toastify'; 5 | 6 | interface SiweAuthProps { 7 | onAuthSuccess: () => void; 8 | } 9 | 10 | const SiweAuth: React.FC = ({ onAuthSuccess }) => { 11 | const { address } = useAccount(); 12 | const { signMessageAsync } = useSignMessage(); 13 | const [loading, setLoading] = useState(false); 14 | 15 | const handleSignIn = async () => { 16 | try { 17 | setLoading(true); 18 | 19 | const nonceRes = await fetch('/api/auth/nonce'); 20 | const { nonce } = await nonceRes.json(); 21 | 22 | const message = new SiweMessage({ 23 | domain: process.env.NEXT_PUBLIC_DOMAIN || window.location.host, 24 | address: address, 25 | statement: 'Sign in to post a message', 26 | uri: window.location.origin, 27 | version: '1', 28 | chainId: 1, 29 | nonce: nonce 30 | }); 31 | const messageToSign = message.prepareMessage(); 32 | const signature = await signMessageAsync({ message: messageToSign }); 33 | 34 | const verifyRes = await fetch('/api/auth/verify', { 35 | method: 'POST', 36 | headers: { 37 | 'Content-Type': 'application/json', 38 | }, 39 | body: JSON.stringify({ message, signature }), 40 | }); 41 | 42 | if (!verifyRes.ok) throw new Error('Error verifying message'); 43 | 44 | // Notify parent component of successful authentication 45 | onAuthSuccess(); 46 | toast.success('Successfully signed in!', { 47 | position: "top-right", 48 | autoClose: 3000, 49 | hideProgressBar: false, 50 | closeOnClick: true, 51 | pauseOnHover: true, 52 | draggable: true, 53 | }); 54 | } catch (error) { 55 | console.error(error); 56 | toast.error('Failed to sign. try again.', { 57 | position: "top-right", 58 | autoClose: 3000, 59 | hideProgressBar: false, 60 | closeOnClick: true, 61 | pauseOnHover: true, 62 | draggable: true, 63 | }); 64 | } finally { 65 | setLoading(false); 66 | } 67 | }; 68 | 69 | if (!address) return null; 70 | 71 | return ( 72 |
73 | 90 |
91 | ); 92 | }; 93 | 94 | export default SiweAuth; 95 | -------------------------------------------------------------------------------- /src/components/charts/TradingViewChart.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useRef, useState } from 'react'; 2 | import { createChart, CrosshairMode, IChartApi, Time } from 'lightweight-charts'; 3 | import Image from 'next/image'; 4 | import { formatAmountV3 } from '@/utils/blockchainUtils'; 5 | import Spinner from '@/components/ui/Spinner'; 6 | 7 | // TODO: add different chart types (bars, line, area, etc) 8 | 9 | interface ChartDataPoint { 10 | time: number; 11 | open: number; 12 | high: number; 13 | low: number; 14 | close: number; 15 | } 16 | 17 | interface PriceChartProps { 18 | data: ChartDataPoint[]; 19 | liquidityEvents: any; 20 | tokenInfo: any; 21 | } 22 | 23 | const PriceChart: React.FC = ({ data, liquidityEvents, tokenInfo }) => { 24 | const chartContainerRef = useRef(null); 25 | const [chart, setChart] = useState(null); 26 | const [showUniswapInfo, setShowUniswapInfo] = useState(null); 27 | 28 | useEffect(() => { 29 | if (liquidityEvents) { 30 | setShowUniswapInfo(liquidityEvents.liquidityEvents.length > 0); 31 | } 32 | }, [liquidityEvents]); 33 | 34 | useEffect(() => { 35 | if (chartContainerRef.current && data.length >= 2 && showUniswapInfo === false) { 36 | const newChart: IChartApi = createChart(chartContainerRef.current, { 37 | width: chartContainerRef.current.clientWidth, 38 | height: 500, 39 | layout: { 40 | background: { color: '#1f2937' }, 41 | textColor: '#d1d5db', 42 | }, 43 | grid: { 44 | vertLines: { color: 'rgba(255, 255, 255, 0.1)' }, 45 | horzLines: { color: 'rgba(255, 255, 255, 0.1)' }, 46 | }, 47 | rightPriceScale: { 48 | borderColor: 'rgba(255, 255, 255, 0.2)', 49 | visible: true, 50 | borderVisible: true, 51 | alignLabels: true, 52 | scaleMargins: { 53 | top: 0.1, 54 | bottom: 0.1, 55 | }, 56 | autoScale: false, 57 | }, 58 | timeScale: { 59 | borderColor: 'rgba(255, 255, 255, 0.2)', 60 | timeVisible: true, 61 | secondsVisible: false, 62 | }, 63 | crosshair: { 64 | mode: CrosshairMode.Normal, 65 | }, 66 | watermark: { 67 | color: 'rgba(255, 255, 255, 0.1)', 68 | visible: true, 69 | text: 'Bondle.xyz', 70 | fontSize: 28, 71 | horzAlign: 'center', 72 | vertAlign: 'center', 73 | }, 74 | }); 75 | 76 | const candleSeries = newChart.addCandlestickSeries({ 77 | upColor: '#26a69a', 78 | downColor: '#ef5350', 79 | borderVisible: false, 80 | wickUpColor: '#26a69a', 81 | wickDownColor: '#ef5350' 82 | }); 83 | 84 | //const enhancedChartData = enhanceSmallCandles(data); 85 | 86 | // Sort and deduplicate data 87 | const sortedData = [...data].sort((a, b) => { 88 | if (a.time === b.time) { 89 | // For same timestamps, maintain original order 90 | return data.indexOf(a) - data.indexOf(b); 91 | } 92 | return a.time - b.time; 93 | }); 94 | 95 | // Remove duplicates by slightly incrementing timestamps 96 | // const processedData = sortedData.reduce((acc: ChartDataPoint[], curr, idx) => { 97 | // if (idx > 0 && curr.time === acc[acc.length - 1].time) { 98 | // // Add 1 second to duplicate timestamps 99 | // curr = { ...curr, time: curr.time + 1 }; 100 | // } 101 | // acc.push(curr); 102 | // return acc; 103 | // }, []); 104 | 105 | // Remove duplicates and ensure timestamps are unique 106 | const uniqueData = sortedData.reduce((acc: ChartDataPoint[], curr) => { 107 | const lastItem = acc[acc.length - 1]; 108 | if (!lastItem || lastItem.time !== curr.time) { 109 | acc.push(curr); 110 | } 111 | return acc; 112 | }, []); 113 | 114 | const enhancedChartData = enhanceSmallCandles(uniqueData); 115 | // TODO: add different chart types (bars, line, area, etc) 116 | 117 | 118 | candleSeries.setData(enhancedChartData.map(item => ({ 119 | time: item.time as Time, 120 | open: item.open, 121 | high: item.high, 122 | low: item.low, 123 | close: item.close 124 | }))); 125 | 126 | candleSeries.applyOptions({ 127 | priceFormat: { 128 | type: 'custom', 129 | formatter: formatPrice, 130 | minMove: 1e-9, 131 | }, 132 | }); 133 | 134 | const prices = enhancedChartData.flatMap(item => [item.open, item.high, item.low, item.close]); 135 | const minPrice = Math.min(...prices); 136 | const maxPrice = Math.max(...prices); 137 | 138 | const zoomFactor = 0.8; 139 | const priceRange = maxPrice - minPrice; 140 | const zoomedMinPrice = Math.max(0, minPrice - priceRange * (1 - zoomFactor) / 2); 141 | const zoomedMaxPrice = maxPrice + priceRange * (1 - zoomFactor) / 2; 142 | 143 | newChart.priceScale('right').applyOptions({ 144 | autoScale: false, 145 | scaleMargins: { 146 | top: 0.1, 147 | bottom: 0.1, 148 | }, 149 | }); 150 | 151 | newChart.timeScale().setVisibleRange({ 152 | from: enhancedChartData[0].time as Time, 153 | to: enhancedChartData[enhancedChartData.length - 1].time as Time, 154 | }); 155 | 156 | setChart(newChart); 157 | 158 | return () => { 159 | newChart.remove(); 160 | }; 161 | } 162 | }, [data, showUniswapInfo]); 163 | 164 | useEffect(() => { 165 | const handleResize = () => { 166 | if (chart && chartContainerRef.current) { 167 | chart.applyOptions({ width: chartContainerRef.current.clientWidth }); 168 | } 169 | }; 170 | 171 | window.addEventListener('resize', handleResize); 172 | 173 | return () => { 174 | window.removeEventListener('resize', handleResize); 175 | }; 176 | }, [chart]); 177 | 178 | if (showUniswapInfo === null) { 179 | return ( 180 |
181 | 182 |
183 | ); 184 | } 185 | 186 | if (showUniswapInfo && liquidityEvents.liquidityEvents.length > 0) { 187 | const event = liquidityEvents.liquidityEvents[0]; 188 | return ( 189 |
190 | {tokenInfo.name} 191 |

{tokenInfo.name} Listed on Chewyswap

192 |
193 |
194 |
195 |

Token

196 |

{formatAmountV3(event.tokenAmount)} {tokenInfo.symbol}

197 |
198 |
199 |

BONE

200 |

{formatAmountV3(event.ethAmount)} BONE

201 |
202 |
203 | 221 |
222 | ); 223 | } 224 | 225 | if (data.length < 2) { 226 | return ( 227 |
228 |

Not enough data to display chart

229 |
230 | ); 231 | } 232 | 233 | return ( 234 |
235 | ); 236 | }; 237 | 238 | function enhanceSmallCandles(data: ChartDataPoint[]): ChartDataPoint[] { 239 | const minCandleSize = 1e-9; 240 | return data.map(item => { 241 | const bodySize = Math.abs(item.open - item.close); 242 | if (bodySize < minCandleSize) { 243 | const midPoint = (item.open + item.close) / 2; 244 | const adjustment = minCandleSize / 2; 245 | return { 246 | ...item, 247 | open: midPoint - adjustment, 248 | close: midPoint + adjustment, 249 | high: Math.max(item.high, midPoint + adjustment), 250 | low: Math.min(item.low, midPoint - adjustment) 251 | }; 252 | } 253 | return item; 254 | }); 255 | } 256 | 257 | function formatPrice(price: number) { 258 | return price.toFixed(9); 259 | } 260 | 261 | export default PriceChart; -------------------------------------------------------------------------------- /src/components/events/ChristmasEvent.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from 'styled-components'; 3 | 4 | const ChristmasEventWrapper = styled.div` 5 | position: fixed; 6 | top: 0; 7 | left: 0; 8 | width: 100%; 9 | height: 100%; 10 | pointer-events: none; 11 | z-index: 1000; 12 | overflow: hidden; 13 | `; 14 | 15 | const Snow = styled.div` 16 | position: fixed; 17 | top: 0; 18 | left: 0; 19 | width: 100%; 20 | height: 100%; 21 | z-index: 1000; 22 | pointer-events: none; 23 | background-image: 24 | url("https://s3-us-west-2.amazonaws.com/s.cdpn.io/191814/flake1.png"), 25 | url("https://s3-us-west-2.amazonaws.com/s.cdpn.io/191814/flake2.png"), 26 | url("https://s3-us-west-2.amazonaws.com/s.cdpn.io/191814/flake3.png"); 27 | animation: snow 60s linear infinite; 28 | 29 | @keyframes snow { 30 | 0% { 31 | background-position: 0px 0px, 0px 0px, 0px 0px; 32 | } 33 | 50% { 34 | background-position: 500px 500px, 100px 200px, -100px 150px; 35 | } 36 | 100% { 37 | background-position: 1000px 1000px, 200px 400px, -200px 300px; 38 | } 39 | } 40 | `; 41 | 42 | const Santa = styled.img` 43 | width: 150px; 44 | height: auto; 45 | position: fixed; 46 | z-index: 1001; 47 | animation: flyingSanta 60s linear infinite; 48 | transform-origin: center; 49 | 50 | @media (max-width: 768px) { 51 | width: 120px; 52 | } 53 | 54 | @media (max-width: 480px) { 55 | width: 90px; 56 | } 57 | 58 | @keyframes flyingSanta { 59 | 0% { 60 | left: -150px; 61 | top: 50%; 62 | transform: scaleX(1); 63 | } 64 | /* Straight left to right */ 65 | 12% { 66 | left: 100%; 67 | top: 50%; 68 | transform: scaleX(1); 69 | } 70 | /* Reset position for diagonal up right to left */ 71 | 13% { 72 | left: 100%; 73 | top: 80%; 74 | transform: scaleX(-1); 75 | } 76 | /* Diagonal up right to left */ 77 | 25% { 78 | left: -150px; 79 | top: 20%; 80 | transform: scaleX(-1); 81 | } 82 | /* Reset for straight right to left */ 83 | 26% { 84 | left: 100%; 85 | top: 30%; 86 | transform: scaleX(-1); 87 | } 88 | /* Straight right to left */ 89 | 38% { 90 | left: -150px; 91 | top: 30%; 92 | transform: scaleX(-1); 93 | } 94 | /* Reset for diagonal down left to right */ 95 | 39% { 96 | left: -150px; 97 | top: 20%; 98 | transform: scaleX(1); 99 | } 100 | /* Diagonal down left to right */ 101 | 51% { 102 | left: 100%; 103 | top: 70%; 104 | transform: scaleX(1); 105 | } 106 | /* Reset for diagonal up left to right */ 107 | 52% { 108 | left: -150px; 109 | top: 70%; 110 | transform: scaleX(1); 111 | } 112 | /* Diagonal up left to right */ 113 | 64% { 114 | left: 100%; 115 | top: 20%; 116 | transform: scaleX(1); 117 | } 118 | /* Reset for diagonal down right to left */ 119 | 65% { 120 | left: 100%; 121 | top: 20%; 122 | transform: scaleX(-1); 123 | } 124 | /* Diagonal down right to left */ 125 | 77% { 126 | left: -150px; 127 | top: 70%; 128 | transform: scaleX(-1); 129 | } 130 | /* Reset for straight left to right high */ 131 | 78% { 132 | left: -150px; 133 | top: 15%; 134 | transform: scaleX(1); 135 | } 136 | /* Straight left to right high */ 137 | 90% { 138 | left: 100%; 139 | top: 15%; 140 | transform: scaleX(1); 141 | } 142 | /* Reset for straight right to left low */ 143 | 91% { 144 | left: 100%; 145 | top: 85%; 146 | transform: scaleX(-1); 147 | } 148 | /* Straight right to left low */ 149 | 99% { 150 | left: -150px; 151 | top: 85%; 152 | transform: scaleX(-1); 153 | } 154 | 100% { 155 | left: -150px; 156 | top: 50%; 157 | transform: scaleX(1); 158 | } 159 | } 160 | `; 161 | 162 | const ChristmasEvent = () => { 163 | return ( 164 | 165 | 166 | 170 | 171 | ); 172 | }; 173 | 174 | export default ChristmasEvent; 175 | -------------------------------------------------------------------------------- /src/components/events/Valentine.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled, { keyframes } from 'styled-components'; 3 | 4 | /* 5 | The main full-screen container. 6 | (Note: The background color has been removed.) 7 | */ 8 | const EventWrapper = styled.div` 9 | position: fixed; 10 | top: 0; 11 | left: 0; 12 | width: 100%; 13 | height: 100%; 14 | pointer-events: none; 15 | overflow: hidden; 16 | z-index: 1000; 17 | `; 18 | 19 | /* 20 | The falling heart drops from above, passes through the target (center), 21 | and disappears below. The keyframes include a “hit” (a brief shrink) effect. 22 | */ 23 | const fallAnimation = keyframes` 24 | 0% { 25 | top: -10vh; 26 | opacity: 0; 27 | transform: scale(1); 28 | } 29 | 10% { 30 | opacity: 1; 31 | } 32 | 50% { 33 | top: 50vh; /* at the target */ 34 | opacity: 1; 35 | transform: scale(1); 36 | } 37 | 55% { 38 | transform: scale(0.8); /* simulate a hit impact */ 39 | } 40 | 100% { 41 | top: 110vh; 42 | opacity: 0; 43 | transform: scale(1); 44 | } 45 | `; 46 | 47 | const FallingHeart = styled.div` 48 | position: absolute; 49 | left: 50vw; 50 | width: 40px; 51 | height: 40px; 52 | margin-left: -20px; /* center horizontally */ 53 | animation: ${fallAnimation} 4s linear infinite; 54 | 55 | /* Render the heart using an inline SVG as a background */ 56 | &::before { 57 | content: ''; 58 | display: block; 59 | width: 100%; 60 | height: 100%; 61 | background-image: url("data:image/svg+xml,%3Csvg width='40' height='40' viewBox='0 0 32 29.6' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath fill='%23FF3366' d='M23.6,0c-2.9,0-5.6,1.4-7.6,3.6C13.9,1.4,11.2,0,8.4,0C3.8,0,0,3.8,0,8.4c0,4.3,3.4,7.8,8.6,12.4l7,6.1c0.4,0.3,1,0.3,1.4,0l7-6.1C28.6,16.2,32,12.7,32,8.4C32,3.8,28.2,0,23.6,0z'/%3E%3C/svg%3E"); 62 | background-size: contain; 63 | background-repeat: no-repeat; 64 | } 65 | `; 66 | 67 | /* 68 | The target indicator shows where the arrow is meant to hit. 69 | Here it’s a dashed circle at the center of the viewport. 70 | */ 71 | const TargetIndicator = styled.div` 72 | position: absolute; 73 | top: 50vh; 74 | left: 50vw; 75 | width: 60px; 76 | height: 60px; 77 | margin-left: -30px; 78 | margin-top: -30px; 79 | border: 2px dashed #FF3366; 80 | border-radius: 50%; 81 | pointer-events: none; 82 | `; 83 | 84 | /* 85 | Cupid’s container is positioned in the lower-left quadrant 86 | so that its arrow can travel to the target. 87 | A gentle floating animation gives it a zero‑gravity feel. 88 | */ 89 | const CupidContainer = styled.div` 90 | position: absolute; 91 | left: 25vw; 92 | top: 70vh; 93 | width: 80px; 94 | height: 80px; 95 | animation: float 3s ease-in-out infinite; 96 | 97 | @keyframes float { 98 | 0% { transform: translateY(0); } 99 | 50% { transform: translateY(-15px); } 100 | 100% { transform: translateY(0); } 101 | } 102 | `; 103 | 104 | /* 105 | CupidIconWrapper sets the size for our Cupid graphic. 106 | */ 107 | const CupidIconWrapper = styled.div` 108 | width: 100%; 109 | height: 100%; 110 | `; 111 | 112 | /* 113 | A refined Cupid SVG drawn with basic shapes. 114 | (Feel free to improve this graphic or replace it with your own.) 115 | */ 116 | const CupidIcon: React.FC = () => ( 117 | 118 | {/* Head */} 119 | 120 | {/* Torso as a heart‐like shape */} 121 | 122 | {/* Wings */} 123 | 124 | 125 | {/* Bow */} 126 | 127 | {/* A resting arrow on the bow */} 128 | 129 | 130 | ); 131 | 132 | /* 133 | The arrow fires from Cupid toward the target. 134 | Using viewport units in the translation ensures the arrow reaches the target (center of the screen). 135 | In this animation, the arrow appears briefly, moves from (0,0) to (25vw, -20vh) (relative to Cupid), 136 | then fades out. 137 | */ 138 | const shootArrow = keyframes` 139 | 0% { 140 | opacity: 0; 141 | transform: translate(0, 0) rotate(0deg); 142 | } 143 | 10% { 144 | opacity: 1; 145 | } 146 | 45% { 147 | opacity: 1; 148 | transform: translate(25vw, -20vh) rotate(45deg); 149 | } 150 | 50% { 151 | opacity: 1; 152 | transform: translate(25vw, -20vh) rotate(45deg); 153 | } 154 | 55% { 155 | opacity: 0; 156 | } 157 | 100% { 158 | opacity: 0; 159 | } 160 | `; 161 | 162 | const ArrowWrapper = styled.div` 163 | position: absolute; 164 | /* Start the arrow at Cupid’s approximate center */ 165 | top: 30px; 166 | left: 40px; 167 | transform-origin: left center; 168 | animation: ${shootArrow} 4s linear infinite; 169 | `; 170 | 171 | /* 172 | A simple arrow drawn with SVG. 173 | */ 174 | const ArrowSVG: React.FC = () => ( 175 | 176 | 177 | 178 | 179 | ); 180 | 181 | /* 182 | The ValentineEvent component brings everything together. 183 | The falling heart, the target indicator, Cupid (with a floating animation), and the arrow firing in sync. 184 | */ 185 | const ValentineEvent: React.FC = () => { 186 | return ( 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | ); 200 | }; 201 | 202 | export default ValentineEvent; 203 | -------------------------------------------------------------------------------- /src/components/layout/Footer.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Link from 'next/link'; 3 | 4 | const Footer: React.FC = () => { 5 | return ( 6 |
7 |
8 |
9 |
10 |

11 | © {new Date().getFullYear()} Bondle. All rights reserved. 12 |

13 |
14 | 28 |
29 |
30 |
31 | ); 32 | }; 33 | 34 | export default Footer; -------------------------------------------------------------------------------- /src/components/layout/Layout.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Navbar from './Navbar' 3 | import Footer from './Footer' 4 | import LiveNotifications from '../notifications/LiveNotifications' 5 | // import ChristmasEvent from '../events/ChristmasEvent' 6 | 7 | interface LayoutProps { 8 | children: React.ReactNode 9 | } 10 | 11 | const Layout: React.FC = ({ children }) => { 12 | return ( 13 |
14 | {/* */} 15 | 16 | 17 |
18 | {children} 19 |
20 |
21 |
22 | ) 23 | } 24 | 25 | export default Layout -------------------------------------------------------------------------------- /src/components/layout/Navbar.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react' 2 | import Link from 'next/link' 3 | import { ConnectButton } from '@rainbow-me/rainbowkit' 4 | import { Bars3Icon, XMarkIcon } from '@heroicons/react/24/outline' 5 | import { shortenAddress } from '@/utils/blockchainUtils' 6 | import { useAccount } from 'wagmi' 7 | import { useRouter } from 'next/router' 8 | import { toast } from 'react-toastify' 9 | import HowItWorksPopup from '@/components/notifications/HowItWorksPopup' 10 | 11 | const CustomConnectButton = () => { 12 | return ( 13 | 14 | {({ 15 | account, 16 | chain, 17 | openAccountModal, 18 | openChainModal, 19 | openConnectModal, 20 | mounted, 21 | }) => { 22 | const ready = mounted 23 | const connected = ready && account && chain 24 | 25 | return ( 26 |
36 | {(() => { 37 | if (!connected) { 38 | return ( 39 | 42 | ) 43 | } 44 | 45 | if (chain.unsupported) { 46 | return ( 47 | 50 | ) 51 | } 52 | 53 | return ( 54 |
55 | 82 | 83 | 89 |
90 | ) 91 | })()} 92 |
93 | ) 94 | }} 95 |
96 | ) 97 | } 98 | 99 | const Navbar: React.FC = () => { 100 | const [isOpen, setIsOpen] = useState(false) 101 | const [showHowItWorks, setShowHowItWorks] = useState(false) 102 | const { address } = useAccount() 103 | const router = useRouter() 104 | 105 | const handleProfileClick = (e: React.MouseEvent) => { 106 | e.preventDefault() 107 | if (!address) { 108 | toast.error('Please connect your wallet first', { 109 | position: "top-right", 110 | autoClose: 3000, 111 | hideProgressBar: false, 112 | closeOnClick: true, 113 | pauseOnHover: true, 114 | draggable: true, 115 | }) 116 | return 117 | } 118 | router.push(`/profile/${address}`) 119 | } 120 | 121 | return ( 122 | <> 123 | 234 | 235 | setShowHowItWorks(false)} 238 | /> 239 | 240 | ) 241 | } 242 | 243 | export default Navbar -------------------------------------------------------------------------------- /src/components/notifications/HowItWorksPopup.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { XMarkIcon } from '@heroicons/react/24/outline'; 3 | import { 4 | Rocket, 5 | TrendingUp, 6 | DollarSign, 7 | BarChart2, 8 | Zap 9 | } from 'lucide-react'; 10 | 11 | interface HowItWorksPopupProps { 12 | isVisible: boolean; 13 | onClose: () => void; 14 | } 15 | 16 | const HowItWorksPopup: React.FC = ({ isVisible, onClose }) => { 17 | const steps = [ 18 | { icon: Rocket, text: "Launch your token instantly" }, 19 | { icon: TrendingUp, text: "Get discovered by early traders" }, 20 | { icon: DollarSign, text: "Trade with zero slippage" }, 21 | { icon: BarChart2, text: "Track your portfolio" }, 22 | { icon: Zap, text: "List on DEX at 2500 BONE" } 23 | ]; 24 | 25 | if (!isVisible) return null; 26 | 27 | return ( 28 |
29 |
30 | 36 | 37 |
38 |

How It Works

39 | 40 |
41 | {steps.map((step, index) => ( 42 |
43 |
44 | 45 |
46 |

{step.text}

47 |
48 | ))} 49 |
50 | 51 | 57 |
58 |
59 |
60 | ); 61 | }; 62 | 63 | export default HowItWorksPopup; 64 | -------------------------------------------------------------------------------- /src/components/notifications/LiveNotifications.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect, useRef } from 'react'; 2 | import Image from 'next/image'; 3 | import { useWebSocket } from '@/components/providers/WebSocketProvider'; 4 | import { formatAmountV2 } from '@/utils/blockchainUtils'; 5 | 6 | interface Notification { 7 | message: string; 8 | secondPart: string; 9 | type: 'buy' | 'sell' | 'tokenCreated'; 10 | logo?: string; 11 | } 12 | 13 | const LiveNotifications: React.FC = () => { 14 | const [currentNotification, setCurrentNotification] = useState(null); 15 | const [isVisible, setIsVisible] = useState(false); 16 | const containerRef = useRef(null); 17 | const animationRef = useRef(null); 18 | const timeoutRef = useRef(null); 19 | const { newTokens, newTransactions } = useWebSocket(); 20 | 21 | const createNotificationMessage = (data: { type: string; data: any }): Notification => { 22 | const addressEnd = data.data.senderAddress?.slice(-6) || data.data.creatorAddress?.slice(-6) || 'Unknown'; 23 | 24 | switch (data.type) { 25 | case 'buy': 26 | return { 27 | message: `${addressEnd} Bought ${formatAmountV2(data.data.tokenAmount)} ${data.data.symbol}`, 28 | secondPart: `with ${formatAmountV2(data.data.ethAmount)} BONE`, 29 | type: 'buy', 30 | logo: data.data.logo 31 | }; 32 | case 'sell': 33 | return { 34 | message: `${addressEnd} Sold ${formatAmountV2(data.data.tokenAmount)} ${data.data.symbol}`, 35 | secondPart: `for ${formatAmountV2(data.data.ethAmount)} BONE`, 36 | type: 'sell', 37 | logo: data.data.logo 38 | }; 39 | case 'tokenCreated': 40 | return { 41 | message: `${data.data.symbol}`, 42 | secondPart: `Created by ${addressEnd}`, 43 | type: 'tokenCreated', 44 | logo: data.data.logo 45 | }; 46 | default: 47 | return { 48 | message: 'New activity', 49 | secondPart: '', 50 | type: 'buy', 51 | logo: undefined 52 | }; 53 | } 54 | }; 55 | 56 | const closeNotification = () => { 57 | if (timeoutRef.current) { 58 | clearTimeout(timeoutRef.current); 59 | } 60 | timeoutRef.current = setTimeout(() => { 61 | setIsVisible(false); 62 | setCurrentNotification(null); 63 | }, 3000); // 3 seconds delay 64 | }; 65 | 66 | useEffect(() => { 67 | const handleNewNotification = (notification: Notification) => { 68 | setCurrentNotification(notification); 69 | setIsVisible(true); 70 | if (timeoutRef.current) { 71 | clearTimeout(timeoutRef.current); 72 | timeoutRef.current = null; 73 | } 74 | }; 75 | 76 | if (newTokens.length > 0) { 77 | console.log(newTokens) 78 | const notification = createNotificationMessage({ type: 'tokenCreated', data: newTokens[0] }); 79 | handleNewNotification(notification); 80 | } 81 | 82 | if (newTransactions.length > 0) { 83 | console.log(newTransactions) 84 | const notification = createNotificationMessage({ type: newTransactions[0].type, data: newTransactions[0] }); 85 | handleNewNotification(notification); 86 | } 87 | }, [newTokens, newTransactions]); 88 | 89 | useEffect(() => { 90 | if (containerRef.current && currentNotification && isVisible) { 91 | const container = containerRef.current; 92 | const totalWidth = container.scrollWidth; 93 | const viewportWidth = container.offsetWidth; 94 | const duration = 15000; // 15 seconds for a complete cycle 95 | 96 | if (animationRef.current) { 97 | animationRef.current.cancel(); 98 | } 99 | 100 | animationRef.current = container.animate( 101 | [ 102 | { transform: 'translateX(100%)' }, 103 | { transform: `translateX(-${totalWidth - viewportWidth}px)` } 104 | ], 105 | { 106 | duration: duration, 107 | easing: 'linear' 108 | } 109 | ); 110 | 111 | animationRef.current.onfinish = closeNotification; 112 | 113 | return () => { 114 | if (animationRef.current) { 115 | animationRef.current.cancel(); 116 | } 117 | if (timeoutRef.current) { 118 | clearTimeout(timeoutRef.current); 119 | } 120 | }; 121 | } 122 | }, [currentNotification, isVisible]); 123 | 124 | if (!isVisible || !currentNotification) return null; 125 | 126 | return ( 127 |
128 |
129 |
130 | 131 | {currentNotification.message} 132 | {currentNotification.logo && ( 133 | 134 | Token Logo 141 | 142 | )} 143 | {currentNotification.secondPart} 144 | 145 |
146 |
147 |
148 | ); 149 | }; 150 | 151 | export default LiveNotifications; -------------------------------------------------------------------------------- /src/components/notifications/Modal.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | interface ModalProps { 4 | isOpen: boolean; 5 | onClose: () => void; 6 | children: React.ReactNode; 7 | } 8 | 9 | const Modal: React.FC = ({ isOpen, onClose, children }) => { 10 | if (!isOpen) return null; 11 | 12 | return ( 13 |
14 |
15 |
16 | {children} 17 |
18 |
19 |
20 | ); 21 | }; 22 | 23 | export default Modal; -------------------------------------------------------------------------------- /src/components/notifications/PurchaseConfirmationPopup.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { X as XIcon } from 'lucide-react'; 3 | import { parseUnits } from 'viem'; 4 | 5 | interface PurchaseConfirmationPopupProps { 6 | onConfirm: (amount: bigint) => void; 7 | onCancel: () => void; 8 | tokenSymbol: string; 9 | } 10 | 11 | const PurchaseConfirmationPopup: React.FC = ({ onConfirm, onCancel, tokenSymbol }) => { 12 | const [purchaseAmount, setPurchaseAmount] = useState(''); 13 | 14 | const handleConfirm = () => { 15 | const amount = parseFloat(purchaseAmount); 16 | onConfirm(amount ? parseUnits(purchaseAmount, 18) : BigInt(0)); 17 | }; 18 | 19 | return ( 20 |
21 |
22 | 29 |

How many {tokenSymbol} do you want to buy? - amount in Bone

30 |

31 | Tip: It's optional, but buying a small amount helps protect your coin from snipers. 32 | When creating, creators can buy up to 5% of the trading supply; any excess BONE is refunded. 33 |

34 | setPurchaseAmount(e.target.value)} 38 | className="w-full py-2 px-3 bg-[var(--card2)] border border-[var(--card-boarder)] rounded-md text-white mb-3 text-xs focus:outline-none focus:ring-2 focus:ring-[var(--primary)] focus:border-[var(--primary)] transition-colors" 39 | placeholder={`0.0 (optional)`} 40 | /> 41 |
42 | 48 | 54 |
55 |

56 | Cost to deploy: ~0 BONE 57 |

58 |
59 |
60 | ); 61 | }; 62 | 63 | export default PurchaseConfirmationPopup; -------------------------------------------------------------------------------- /src/components/providers/WebSocketProvider.tsx: -------------------------------------------------------------------------------- 1 | import React, { createContext, useContext, useState, useEffect, ReactNode } from 'react'; 2 | import { Token } from '@/interface/types'; 3 | 4 | interface WebSocketContextType { 5 | newTokens: Token[]; 6 | newTransactions: any[]; 7 | } 8 | 9 | const WebSocketContext = createContext(undefined); 10 | 11 | export const useWebSocket = () => { 12 | const context = useContext(WebSocketContext); 13 | if (context === undefined) { 14 | throw new Error('useWebSocket must be used within a WebSocketProvider'); 15 | } 16 | return context; 17 | }; 18 | 19 | export const WebSocketProvider: React.FC<{ children: ReactNode }> = ({ children }) => { 20 | const [newTokens, setNewTokens] = useState([]); 21 | const [newTransactions, setNewTransactions] = useState([]); 22 | 23 | useEffect(() => { 24 | const socket = new WebSocket(process.env.NEXT_PUBLIC_WS_BASE_URL as string); 25 | 26 | socket.onopen = () => { 27 | console.log('WebSocket connected'); 28 | }; 29 | 30 | socket.onmessage = (event) => { 31 | const data = JSON.parse(event.data); 32 | if (data.type === 'tokenCreated') { 33 | setNewTokens(prev => [data.data, ...prev]); 34 | } else if (data.type === 'tokensBought' || data.type === 'tokensSold') { 35 | setNewTransactions(prev => [data.data, ...prev]); 36 | } 37 | }; 38 | 39 | return () => { 40 | socket.close(); 41 | }; 42 | }, []); 43 | 44 | return ( 45 | 46 | {children} 47 | 48 | ); 49 | }; -------------------------------------------------------------------------------- /src/components/seo/OGPreview.tsx: -------------------------------------------------------------------------------- 1 | //just another test for Open Graph Preview in the ui 2 | 3 | import React from 'react'; 4 | import Head from 'next/head'; 5 | 6 | const OGPreview = () => { 7 | const [ogData, setOgData] = React.useState({ 8 | title: '', 9 | description: '', 10 | image: '', 11 | url: '', 12 | }); 13 | 14 | React.useEffect(() => { 15 | const title = document.querySelector('meta[property="og:title"]')?.getAttribute('content') || ''; 16 | const description = document.querySelector('meta[property="og:description"]')?.getAttribute('content') || ''; 17 | const image = document.querySelector('meta[property="og:image"]')?.getAttribute('content') || ''; 18 | const url = document.querySelector('meta[property="og:url"]')?.getAttribute('content') || ''; 19 | 20 | setOgData({ title, description, image, url }); 21 | }, []); 22 | 23 | return ( 24 |
25 |
Open Graph Preview
26 |
27 | {ogData.image && ( 28 | OG Image 29 | )} 30 |

{ogData.title}

31 |

{ogData.description}

32 |

{ogData.url}

33 |
34 |
35 | ); 36 | }; 37 | 38 | export default OGPreview; -------------------------------------------------------------------------------- /src/components/seo/SEO.tsx: -------------------------------------------------------------------------------- 1 | import Head from 'next/head'; 2 | import { useRouter } from 'next/router'; 3 | 4 | interface SEOProps { 5 | title?: string; 6 | description?: string; 7 | image?: string; 8 | token?: { 9 | name: string; 10 | symbol: string; 11 | description: string; 12 | logo: string; 13 | }; 14 | } 15 | 16 | const SEO: React.FC = ({ title, description, image, token }) => { 17 | const router = useRouter(); 18 | const domain = process.env.NEXT_PUBLIC_DOMAIN || 'https://bondle.xyz'; 19 | 20 | const seo = { 21 | title: token ? `${token.name} (${token.symbol}) - Bondle` : title || 'Bondle - Explore and Trade Tokens', 22 | description: token?.description || description || 'Explore, create, and trade tokens on the Bondle platform', 23 | image: token?.logo || image || `${domain}/default-og-image.jpg`, 24 | url: `${domain}${router.asPath}`, 25 | }; 26 | 27 | return ( 28 | 29 | {seo.title} 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | ); 42 | }; 43 | 44 | export default SEO; -------------------------------------------------------------------------------- /src/components/tokens/TokenCard.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import Link from 'next/link'; 3 | import { Token, TokenWithLiquidityEvents } from '@/interface/types'; 4 | import { useTokenLiquidity, formatTimestampV1, formatTimestamp, formatAmountV2 } from '@/utils/blockchainUtils'; 5 | import { useRouter } from 'next/router'; 6 | import { Globe, Twitter, Send as Telegram, Clock, Youtube, MessageCircle as Discord } from 'lucide-react'; 7 | import LoadingBar from '@/components/ui/LoadingBar'; 8 | 9 | interface TokenCardProps { 10 | token: Token | TokenWithLiquidityEvents; 11 | isEnded: boolean; 12 | onTokenClick: (address: string) => void; 13 | onLiquidityUpdate?: (liquidityAmount: bigint) => void; 14 | } 15 | 16 | const TokenCard: React.FC = ({ token, isEnded, onTokenClick, onLiquidityUpdate }) => { 17 | const [currentLiquidity, setCurrentLiquidity] = useState('0'); 18 | const tokenAddress = token.address as `0x${string}`; 19 | const shouldFetchLiquidity = !token._count?.liquidityEvents; 20 | const { data: liquidityData } = useTokenLiquidity(shouldFetchLiquidity ? tokenAddress : null); 21 | const router = useRouter(); 22 | 23 | useEffect(() => { 24 | if (shouldFetchLiquidity && 25 | liquidityData && 26 | liquidityData[2] && 27 | liquidityData[2].toString() !== currentLiquidity) { 28 | 29 | const newLiquidity = liquidityData[2].toString(); 30 | setCurrentLiquidity(newLiquidity); 31 | 32 | if (onLiquidityUpdate) { 33 | onLiquidityUpdate(liquidityData[2]); 34 | } 35 | } 36 | }, [ 37 | liquidityData, 38 | shouldFetchLiquidity, 39 | onLiquidityUpdate, 40 | currentLiquidity, 41 | token.address 42 | ]); 43 | 44 | const calculateProgress = (liquidity: string): number => { 45 | if (token._count?.liquidityEvents > 0) { 46 | return 100; 47 | } 48 | // Convert from Wei to Ether (divide by 10^18) 49 | const currentValue = Number(liquidity) / 10**18; 50 | const target = Number(process.env.NEXT_PUBLIC_DEX_TARGET); 51 | const percentage = (currentValue / target) * 100; 52 | return Math.min(percentage, 100); 53 | }; 54 | 55 | const progress = calculateProgress(currentLiquidity); 56 | const isCompleted = token._count?.liquidityEvents > 0; 57 | 58 | const SocialLinks = () => ( 59 |
e.stopPropagation()}> 60 | {token.website && ( 61 | 67 | 68 | 69 | )} 70 | {token.twitter && ( 71 | e.stopPropagation()} 77 | > 78 | 79 | 80 | )} 81 | {token.telegram && ( 82 | e.stopPropagation()} 88 | > 89 | 90 | 91 | )} 92 | {token.discord && ( 93 | e.stopPropagation()} 99 | > 100 | 101 | 102 | )} 103 | {token.youtube && ( 104 | e.stopPropagation()} 110 | > 111 | 112 | 113 | )} 114 |
115 | ); 116 | 117 | const handleClick = () => { 118 | onTokenClick(token.address); 119 | }; 120 | 121 | if (isEnded && 'liquidityEvents' in token && token.liquidityEvents.length > 0) { 122 | const liquidityEvent = token.liquidityEvents[0]; 123 | const uniswapLink = `https://chewyswap.dog/swap/?outputCurrency=${token.address}&chain=shibarium`; 124 | 125 | return ( 126 |
127 |
128 |
129 |
130 |
131 | {token.name} 136 |
137 |
138 |

139 | {token.name} {token.symbol} 140 |

141 |

{token.description}

142 |
143 |
144 | 145 |
146 |
147 | Progress to DEX 148 | Completed 149 |
150 |
151 |
154 |
155 |
156 | 157 |
158 | e.stopPropagation()} 164 | > 165 | Trade 166 | 167 | 171 | View 172 | 173 |
174 |
175 |
176 |
177 | ); 178 | } 179 | 180 | return ( 181 |
182 |
183 | 184 |
185 |
186 |
187 | {token.name} 192 |
193 |
194 |

195 | {token.name} {token.symbol} 196 |

197 |

{token.description}

198 |
199 |
200 | 201 |
202 |
203 | 204 | {formatTimestampV1(token.createdAt)} 205 |
206 | 207 |
208 |
209 | Progress to DEX 210 | 211 | {isCompleted 212 | ? 'Completed' 213 | : liquidityData && liquidityData[2] 214 | ? `${calculateProgress(liquidityData[2].toString()).toFixed(2)}%` 215 | : '0%'} 216 | 217 |
218 |
219 |
223 |
224 |
225 |
226 |
227 |
228 |
229 | ); 230 | }; 231 | 232 | export default TokenCard; -------------------------------------------------------------------------------- /src/components/tokens/TokenList.tsx: -------------------------------------------------------------------------------- 1 | import React, { useMemo, useState } from 'react'; 2 | import TokenCard from './TokenCard'; 3 | import { Token, TokenWithLiquidityEvents } from '@/interface/types'; 4 | import { ChevronLeftIcon, ChevronRightIcon } from '@heroicons/react/24/outline'; 5 | import { useRouter } from 'next/router'; 6 | import LoadingBar from '@/components/ui/LoadingBar'; 7 | import { SortOption } from '../ui/SortOptions'; 8 | 9 | interface TokenListProps { 10 | tokens: (Token | TokenWithLiquidityEvents)[]; 11 | currentPage: number; 12 | totalPages: number; 13 | onPageChange: (page: number) => void; 14 | isEnded: boolean; 15 | sortType: SortOption; 16 | itemsPerPage: number; 17 | isFullList?: boolean; 18 | } 19 | 20 | interface TokenLiquidityData { 21 | [key: string]: bigint; 22 | } 23 | 24 | const TokenList: React.FC = ({ 25 | tokens, 26 | currentPage, 27 | totalPages, 28 | onPageChange, 29 | isEnded, 30 | sortType, 31 | itemsPerPage, 32 | isFullList 33 | }) => { 34 | const router = useRouter(); 35 | const [isLoading, setIsLoading] = useState(false); 36 | const [liquidityData, setLiquidityData] = useState({}); 37 | 38 | const handleTokenClick = async (tokenAddress: string) => { 39 | setIsLoading(true); 40 | await router.push(`/token/${tokenAddress}`); 41 | setIsLoading(false); 42 | }; 43 | 44 | const updateLiquidityData = (tokenAddress: string, amount: bigint) => { 45 | setLiquidityData(prev => ({ 46 | ...prev, 47 | [tokenAddress]: amount 48 | })); 49 | }; 50 | 51 | // Sort and paginate tokens 52 | const displayTokens = useMemo(() => { 53 | let sortedTokens = [...tokens]; 54 | 55 | if (sortType === 'marketcap') { 56 | sortedTokens.sort((a, b) => { 57 | const liquidityA = liquidityData[a.address] || BigInt(0); 58 | const liquidityB = liquidityData[b.address] || BigInt(0); 59 | return liquidityB > liquidityA ? 1 : -1; 60 | }); 61 | } 62 | 63 | // If we're handling the full list, paginate here 64 | if (isFullList) { 65 | const startIndex = (currentPage - 1) * itemsPerPage; 66 | const endIndex = startIndex + itemsPerPage; 67 | return sortedTokens.slice(startIndex, endIndex); 68 | } 69 | 70 | return sortedTokens; 71 | }, [tokens, sortType, liquidityData, currentPage, itemsPerPage, isFullList]); 72 | 73 | return ( 74 | <> 75 |
76 | {displayTokens.map((token) => ( 77 | updateLiquidityData(token.address, amount)} 83 | /> 84 | ))} 85 |
86 | 87 | {isLoading && ( 88 |
89 | 90 |
91 | )} 92 | 93 | {totalPages > 1 && ( 94 |
95 | 102 | 103 |
104 | {Array.from({ length: totalPages }, (_, i) => i + 1).map((page) => ( 105 | 116 | ))} 117 |
118 | 119 | 126 |
127 | )} 128 | 129 | ); 130 | }; 131 | 132 | export default TokenList; -------------------------------------------------------------------------------- /src/components/ui/LoadingBar.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | interface LoadingBarProps { 4 | size?: 'small' | 'medium' | 'large'; 5 | color?: string; 6 | } 7 | 8 | const LoadingBar: React.FC = ({ 9 | size = 'medium', 10 | color = 'var(--primary)' 11 | }) => { 12 | const sizeClasses = { 13 | small: 'w-32 h-1', 14 | medium: 'w-48 h-1.5', 15 | large: 'w-64 h-2' 16 | }; 17 | 18 | return ( 19 |
20 |
21 |
28 |
29 | Loading... 30 |
31 | ); 32 | }; 33 | 34 | export default LoadingBar; -------------------------------------------------------------------------------- /src/components/ui/SearchFilter.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect, forwardRef } from 'react'; 2 | import { MagnifyingGlassIcon } from '@heroicons/react/24/outline'; 3 | import { useDebounce } from 'use-debounce'; 4 | 5 | interface SearchFilterProps { 6 | onSearch: (query: string) => void; 7 | } 8 | 9 | const SearchFilter = forwardRef(({ onSearch }, ref) => { 10 | const [searchInput, setSearchInput] = useState(''); 11 | const [debouncedValue] = useDebounce(searchInput, 500); 12 | 13 | useEffect(() => { 14 | onSearch(debouncedValue); 15 | }, [debouncedValue, onSearch]); 16 | 17 | const handleInputChange = (e: React.ChangeEvent) => { 18 | setSearchInput(e.target.value); 19 | }; 20 | 21 | return ( 22 |
23 | 31 | 32 |
33 | ); 34 | }); 35 | 36 | SearchFilter.displayName = 'SearchFilter'; 37 | 38 | export default SearchFilter; -------------------------------------------------------------------------------- /src/components/ui/ShareButton.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import { Share2Icon, X } from 'lucide-react'; 3 | import { 4 | TwitterIcon, 5 | FacebookIcon, 6 | InstagramIcon, 7 | LinkedinIcon, 8 | Send as TelegramIcon, 9 | MessageCircle as DiscordIcon 10 | } from 'lucide-react'; 11 | 12 | interface TokenInfo { 13 | name: string; 14 | description: string; 15 | logo: string; 16 | } 17 | 18 | interface ShareModalProps { 19 | tokenInfo: TokenInfo; 20 | isOpen: boolean; 21 | onClose: () => void; 22 | } 23 | 24 | interface SocialPlatform { 25 | name: string; 26 | icon: React.ElementType; 27 | color: string; 28 | getShareUrl: (shareText: string, shareUrl: string, logoUrl: string) => string; 29 | } 30 | 31 | const ShareModal: React.FC = ({ tokenInfo, isOpen, onClose }) => { 32 | const [mounted, setMounted] = useState(false); 33 | 34 | useEffect(() => { 35 | setMounted(true); 36 | }, []); 37 | 38 | if (!mounted || !isOpen) return null; 39 | 40 | const shareUrl = typeof window !== 'undefined' ? window.location.href : ''; 41 | const shareText = `Check out ${tokenInfo.name} on our platform!\n\n${tokenInfo.description}\n\n`; 42 | 43 | const socialPlatforms: SocialPlatform[] = [ 44 | { 45 | name: 'Twitter', 46 | icon: TwitterIcon, 47 | color: 'bg-[#1DA1F2]', 48 | getShareUrl: (text, url, logo) => `https://twitter.com/intent/tweet?text=${encodeURIComponent(text)}&url=${encodeURIComponent(url)}&image=${encodeURIComponent(logo)}` 49 | }, 50 | { 51 | name: 'Facebook', 52 | icon: FacebookIcon, 53 | color: 'bg-[#4267B2]', 54 | getShareUrl: (text, url, logo) => `https://www.facebook.com/sharer/sharer.php?u=${encodeURIComponent(url)}` 55 | }, 56 | { 57 | name: 'Telegram', 58 | icon: TelegramIcon, 59 | color: 'bg-[#0088cc]', 60 | getShareUrl: (text, url, logo) => `https://t.me/share/url?url=${encodeURIComponent(url)}&text=${encodeURIComponent(text)}` 61 | }, 62 | { 63 | name: 'LinkedIn', 64 | icon: LinkedinIcon, 65 | color: 'bg-[#0077B5]', 66 | getShareUrl: (text, url, logo) => `https://www.linkedin.com/shareArticle?mini=true&url=${encodeURIComponent(url)}&title=${encodeURIComponent(tokenInfo.name)}&summary=${encodeURIComponent(text)}` 67 | }, 68 | { 69 | name: 'Instagram', 70 | icon: InstagramIcon, 71 | color: 'bg-gradient-to-r from-[#833AB4] via-[#FD1D1D] to-[#FCAF45]', 72 | getShareUrl: (text, url, logo) => `https://www.instagram.com/` 73 | }, 74 | { 75 | name: 'Discord', 76 | icon: DiscordIcon, 77 | color: 'bg-[#7289DA]', 78 | getShareUrl: (text, url, logo) => `https://discord.com/` 79 | } 80 | ]; 81 | 82 | return ( 83 |
84 |
85 |
86 |

Share {tokenInfo.name} on

87 | 94 |
95 |
96 | {`${tokenInfo.name} 101 |
102 |
103 | {socialPlatforms.map((platform) => ( 104 | 112 | 113 | 114 | ))} 115 |
116 |
117 |
118 | ); 119 | }; 120 | 121 | interface ShareButtonProps { 122 | tokenInfo: TokenInfo; 123 | className?: string; 124 | } 125 | 126 | const ShareButton: React.FC = ({ tokenInfo, className }) => { 127 | const [isModalOpen, setIsModalOpen] = useState(false); 128 | 129 | return ( 130 | <> 131 |
132 | 139 |
140 | setIsModalOpen(false)} 144 | /> 145 | 146 | ); 147 | }; 148 | 149 | export default ShareButton; -------------------------------------------------------------------------------- /src/components/ui/SortOptions.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export type SortOption = 'trending' | 'new' | 'finalized' | 'marketcap'; 4 | 5 | interface SortOptionsProps { 6 | onSort: (option: SortOption) => void; 7 | currentSort: SortOption; 8 | } 9 | 10 | const SortOptions: React.FC = ({ onSort, currentSort }) => { 11 | // Function to handle the trending/marketcap toggle 12 | const handleTrendingClick = () => { 13 | if (currentSort === 'trending') { 14 | onSort('marketcap'); 15 | } else { 16 | onSort('trending'); 17 | } 18 | }; 19 | 20 | return ( 21 |
22 | {/* Trending Section with Market Cap */} 23 |
24 | 34 | 35 | {/* Market Cap option only shows when Trending is selected */} 36 | {(currentSort === 'trending' || currentSort === 'marketcap') && ( 37 | 47 | )} 48 |
49 | 50 | {/* New tokens */} 51 | 61 | 62 | {/* Finalized = listed */} 63 | 73 |
74 | ); 75 | }; 76 | 77 | export default SortOptions; -------------------------------------------------------------------------------- /src/components/ui/Spinner.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | interface SpinnerProps { 4 | size?: 'small' | 'medium' | 'large'; 5 | } 6 | 7 | const Spinner: React.FC = ({ size = 'medium' }) => { 8 | const sizeClasses = { 9 | small: 'w-4 h-4', 10 | medium: 'w-6 h-6', 11 | large: 'w-8 h-8' 12 | }; 13 | 14 | return ( 15 |
16 |
22 |
23 | ); 24 | }; 25 | 26 | export default Spinner; -------------------------------------------------------------------------------- /src/components/ui/ldspinner.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | interface LdSpinnerProps { 4 | size?: 'small' | 'medium' | 'large'; 5 | color?: string; 6 | } 7 | 8 | const LdSpinner: React.FC = ({ 9 | size = 'medium', 10 | color = '#ffffff' 11 | }) => { 12 | const sizeMap = { 13 | small: 'w-4 h-4 border', 14 | medium: 'w-6 h-6 border-2', 15 | large: 'w-8 h-8 border-2' 16 | }; 17 | 18 | return ( 19 |
20 |
27 |
28 | ); 29 | }; 30 | 31 | export default LdSpinner; -------------------------------------------------------------------------------- /src/components/ui/switch.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import * as SwitchPrimitives from "@radix-ui/react-switch" 3 | 4 | const Switch = React.forwardRef< 5 | React.ElementRef, 6 | React.ComponentPropsWithoutRef 7 | >(({ className, ...props }, ref) => ( 8 | 13 | 14 | 15 | )) 16 | Switch.displayName = SwitchPrimitives.Root.displayName 17 | 18 | export { Switch } -------------------------------------------------------------------------------- /src/interface/types.ts: -------------------------------------------------------------------------------- 1 | // types.ts 2 | 3 | export interface LiquidityEvent { 4 | id: string; 5 | ethAmount: string; 6 | tokenAmount: string; 7 | timestamp: string; 8 | } 9 | 10 | export interface Token { 11 | map: any; 12 | id: string; 13 | chainId: number; 14 | address: string; 15 | creatorAddress: string; 16 | name: string; 17 | symbol: string; 18 | logo: string; 19 | description: string; 20 | createdAt: string; 21 | updatedAt: string; 22 | website: string; 23 | youtube: string; 24 | discord: string; 25 | twitter: string; 26 | telegram: string; 27 | latestTransactionTimestamp: string; 28 | _count: { 29 | liquidityEvents: number; 30 | }; 31 | } 32 | 33 | export interface TokenWithLiquidityEvents extends Token { 34 | liquidityEvents: LiquidityEvent[]; 35 | } 36 | 37 | export interface PaginatedResponse { 38 | [x: string]: any;//wad added 39 | tokens: never[]; 40 | data: T[]; 41 | totalCount: number; 42 | currentPage: number; 43 | totalPages: number; 44 | } 45 | 46 | export interface TokenWithTransactions extends Token { 47 | transactions: { 48 | data: Transaction[]; 49 | pagination: { 50 | currentPage: number; 51 | pageSize: number; 52 | totalCount: number; 53 | totalPages: number; 54 | }; 55 | }; 56 | } 57 | 58 | export interface Transaction { 59 | id: string; 60 | type: string; 61 | senderAddress: string; 62 | recipientAddress: string; 63 | ethAmount: string; 64 | tokenAmount: string; 65 | tokenPrice: string; 66 | txHash: string; 67 | timestamp: string; 68 | } 69 | 70 | 71 | export interface PriceResponse { 72 | price: string; 73 | } 74 | 75 | export interface HistoricalPrice { 76 | tokenPrice: string; 77 | timestamp: string; 78 | } 79 | 80 | export interface USDHistoricalPrice { 81 | tokenPriceUSD: string; 82 | timestamp: string; 83 | } 84 | 85 | export interface TokenHolder { 86 | address: string; 87 | balance: string; 88 | } 89 | 90 | export interface TransactionResponse extends Omit, 'data'> { 91 | transactions: Transaction[]; 92 | } 93 | 94 | export interface PriceCache { 95 | price: string; 96 | timestamp: number; 97 | } -------------------------------------------------------------------------------- /src/pages/404.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Link from 'next/link'; 3 | import Layout from '@/components/layout/Layout'; 4 | import SEO from '@/components/seo/SEO'; 5 | import { Ban } from 'lucide-react'; 6 | 7 | const Custom404: React.FC = () => { 8 | return ( 9 | 10 | 14 |
15 | 16 |

Not Found

17 |

18 | Wait and try again later. It seems like what you are looking for doesn't exist or is still indexing. 19 |

20 | 21 | 24 | 25 |
26 |
27 | ); 28 | }; 29 | 30 | export default Custom404; -------------------------------------------------------------------------------- /src/pages/FAQ.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import Layout from '@/components/layout/Layout'; 3 | import { ChevronDownIcon } from '@heroicons/react/24/solid'; 4 | 5 | const FAQPage: React.FC = () => { 6 | const [openIndex, setOpenIndex] = useState(null); 7 | 8 | const faqs = [ 9 | { 10 | question: "What is a bonding curve?", 11 | answer: "A bonding curve is a mathematical function that defines the relationship between a token's price and its supply. It creates a dynamic pricing mechanism that automatically adjusts based on demand.\n\nKey points:\n• As supply increases, price increases\n• As supply decreases, price decreases\n• This creates a dynamic pricing mechanism that automatically adjusts based on demand" 12 | }, 13 | { 14 | question: "How do I create a token?", 15 | answer: "To create a token on Bondle:\n\n1. Go to 'Create Token' page\n2. Fill in token details (name, symbol, etc.)\n3. Upload an image (optional)\n4. Add social links (optional)\n5. Review details\n6. Pay small fee in BONE\n7. Wait for confirmation\n\nYour token will then be live and tradable!" 16 | }, 17 | { 18 | question: "How is the token price determined?", 19 | answer: "Token price is determined dynamically by the bonding curve.\n\n• Buying tokens: Price increases\n• Selling tokens: Price decreases\n\nThis creates a fair and transparent pricing mechanism reflecting real-time supply and demand." 20 | }, 21 | { 22 | question: "Can I sell my tokens at any time?", 23 | answer: "Yes, you can sell your tokens back to the contract at any time.\n\n• Sell price: Determined by current position on the bonding curve\n• Ensures continuous liquidity\n• Allows you to exit your position whenever you choose" 24 | }, 25 | { 26 | question: "Is there a fee for buying or selling tokens?", 27 | answer: "Yes, there's a small fee (typically 1%) for buying and selling.\n\nPurposes of the fee:\n1. Incentivize long-term holding\n2. Prevent market manipulation\n3. Contribute to platform sustainability\n4. Potentially reward token holders or fund development" 28 | } 29 | ]; 30 | 31 | const toggleFAQ = (index: number) => { 32 | setOpenIndex(openIndex === index ? null : index); 33 | }; 34 | 35 | return ( 36 | 37 |
38 |

Frequently Asked Questions

39 | 40 |
41 | {faqs.map((faq, index) => ( 42 |
43 | 54 |
59 |
60 |

{faq.answer}

61 |
62 |
63 |
64 | ))} 65 |
66 |
67 |
68 | ); 69 | }; 70 | 71 | export default FAQPage; -------------------------------------------------------------------------------- /src/pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import '@/styles/globals.css' 2 | import type { AppProps } from 'next/app' 3 | import { WagmiConfig, createConfig, WagmiProvider } from 'wagmi' 4 | import { supportedChains } from '@/chain/config' 5 | import { QueryClient, QueryClientProvider } from '@tanstack/react-query' 6 | import { getDefaultConfig, RainbowKitProvider } from '@rainbow-me/rainbowkit' 7 | import '@rainbow-me/rainbowkit/styles.css' 8 | import { ToastContainer } from 'react-toastify'; 9 | import 'react-toastify/dist/ReactToastify.css'; 10 | import { WebSocketProvider } from '@/components/providers/WebSocketProvider'; 11 | 12 | 13 | 14 | const config = getDefaultConfig({ 15 | appName: "Pump Fun", 16 | projectId: "YOUR_PROJECT_ID", 17 | chains: supportedChains as any, 18 | ssr: true, 19 | }); 20 | 21 | 22 | const queryClient = new QueryClient() 23 | 24 | export default function App({ Component, pageProps }: AppProps) { 25 | return ( 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | ) 37 | } -------------------------------------------------------------------------------- /src/pages/_document.tsx: -------------------------------------------------------------------------------- 1 | import { Html, Head, Main, NextScript } from "next/document"; 2 | 3 | export default function Document() { 4 | return ( 5 | 6 | 7 | 8 |
9 | 10 | 11 | 12 | ); 13 | } 14 | -------------------------------------------------------------------------------- /src/pages/about.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Layout from '@/components/layout/Layout'; 3 | import { CubeTransparentIcon, ChartBarIcon, CurrencyDollarIcon, ArrowPathIcon, BeakerIcon } from '@heroicons/react/24/outline'; 4 | 5 | const AboutPage: React.FC = () => { 6 | return ( 7 | 8 |
9 |

About Bondle

10 | 11 |
12 |
13 |

What is Bondle?

14 |
15 |

16 | Bondle is a revolutionary decentralized platform that empowers individuals to create and trade tokens using innovative bonding curve technology. 17 |

18 |

19 | Our mission is to democratize token creation and provide a dynamic, fair trading environment for the crypto community. 20 |

21 |

22 | Our mission is to democratize token creation for the crypto community. 23 |

24 |

25 | We provide a dynamic and fair trading environment using bonding curve technology. 26 |

27 |
28 |
29 |
30 | 31 |
32 |

How It Works

33 |
34 | {[ 35 | { icon: CubeTransparentIcon, title: "Token Creation", description: "Users easily create tokens by setting a name, symbol, and description." }, 36 | { icon: ChartBarIcon, title: "Bonding Curve", description: "Each token's price is determined by its unique bonding curve." }, 37 | { icon: CurrencyDollarIcon, title: "Trading", description: "Users can buy and sell tokens, with prices dynamically adjusted by the curve." }, 38 | { icon: ArrowPathIcon, title: "Liquidity Pool", description: "Each token has its own liquidity pool that grows and shrinks with trades." }, 39 | { icon: BeakerIcon, title: "Customization", description: "Various curve shapes allow for different token economics." }, 40 | ].map((item, index) => ( 41 |
42 | 43 |

{item.title}

44 |

{item.description}

45 |
46 | ))} 47 |
48 |
49 | 50 |
51 |

Benefits of Bonding Curves

52 |
53 |
    54 | {[ 55 | { title: "Continuous Liquidity", description: "Tokens can always be bought or sold, ensuring a fluid market." }, 56 | { title: "Algorithmic Price Discovery", description: "Market price is determined automatically based on supply and demand." }, 57 | { title: "Incentivized Participation", description: "Early supporters benefit from potential price appreciation as demand grows." }, 58 | { title: "Flexible Token Economics", description: "Different curve shapes allow for various economic models to suit project needs." }, 59 | ].map((item, index) => ( 60 |
  • 61 | 62 | 63 | 64 |
    65 | {item.title}: {item.description} 66 |
    67 |
  • 68 | ))} 69 |
70 |
71 |
72 |
73 |
74 | ); 75 | }; 76 | 77 | export default AboutPage; -------------------------------------------------------------------------------- /src/pages/api/auth/nonce.ts: -------------------------------------------------------------------------------- 1 | import { NextApiRequest, NextApiResponse } from 'next'; 2 | import { randomBytes } from 'crypto'; 3 | 4 | export default async function handler(req: NextApiRequest, res: NextApiResponse) { 5 | if (req.method !== 'GET') { 6 | return res.status(405).json({ error: 'Method not allowed' }); 7 | } 8 | 9 | /* 10 | Expires in 5 minutes 11 | */ 12 | const nonce = randomBytes(32).toString('hex'); 13 | res.setHeader('Set-Cookie', `siwe_nonce=${nonce}; HttpOnly; Path=/; Max-Age=300`); 14 | res.status(200).json({ nonce }); 15 | } 16 | -------------------------------------------------------------------------------- /src/pages/api/auth/user.ts: -------------------------------------------------------------------------------- 1 | import { NextApiRequest, NextApiResponse } from 'next'; 2 | import { parse } from 'cookie'; 3 | 4 | export default async function handler(req: NextApiRequest, res: NextApiResponse) { 5 | if (req.method !== 'GET') { 6 | return res.status(405).json({ error: 'Method not allowed' }); 7 | } 8 | 9 | const cookies = parse(req.headers.cookie || ''); 10 | const address = cookies.siwe_session; 11 | 12 | if (address) { 13 | res.status(200).json({ address }); 14 | } else { 15 | res.status(401).json({ error: 'Not authenticated' }); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/pages/api/auth/verify.ts: -------------------------------------------------------------------------------- 1 | import { NextApiRequest, NextApiResponse } from 'next'; 2 | import { SiweMessage } from 'siwe'; 3 | import { parse } from 'cookie'; 4 | 5 | export default async function handler(req: NextApiRequest, res: NextApiResponse) { 6 | if (req.method !== 'POST') { 7 | return res.status(405).json({ error: 'Method not allowed' }); 8 | } 9 | 10 | try { 11 | const { message, signature } = req.body; 12 | 13 | const siweMessage = new SiweMessage(message); 14 | const fields = await siweMessage.validate(signature); 15 | 16 | const cookies = parse(req.headers.cookie || ''); 17 | const storedNonce = cookies.siwe_nonce; 18 | 19 | if (fields.nonce !== storedNonce) { 20 | return res.status(422).json({ error: 'Invalid nonce' }); 21 | } 22 | 23 | const sessionExpiration = 5 * 24 * 60 * 60; 24 | res.setHeader('Set-Cookie', `siwe_session=${fields.address}; HttpOnly; Path=/; Max-Age=${sessionExpiration}`); 25 | 26 | res.status(200).json({ ok: true }); 27 | } catch (error) { 28 | console.error('Verification failed:', error); 29 | res.status(400).json({ error: 'Verification failed' }); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/pages/api/hello.ts: -------------------------------------------------------------------------------- 1 | // Next.js API route support: https://nextjs.org/docs/api-routes/introduction 2 | import type { NextApiRequest, NextApiResponse } from "next"; 3 | 4 | type Data = { 5 | name: string; 6 | }; 7 | 8 | export default function handler( 9 | req: NextApiRequest, 10 | res: NextApiResponse, 11 | ) { 12 | res.status(200).json({ name: "John Doe" }); 13 | } 14 | -------------------------------------------------------------------------------- /src/pages/api/ports/addChatMessage.ts: -------------------------------------------------------------------------------- 1 | import type { NextApiRequest, NextApiResponse } from 'next'; 2 | import axios from 'axios'; 3 | 4 | const API_BASE_URL = process.env.NEXT_PUBLIC_API_BASE_URL; 5 | 6 | export default async function handler( 7 | req: NextApiRequest, 8 | res: NextApiResponse<{ id: number }> 9 | ) { 10 | if (req.method !== 'POST') { 11 | return res.status(405).end(); 12 | } 13 | 14 | try { 15 | const { user, token, message, replyTo } = req.body; 16 | if (!user || !token || !message) { 17 | return res.status(400).json({ error: 'User, token, and message are required' } as any); 18 | } 19 | 20 | const response = await axios.post(`${API_BASE_URL}/chats`, { 21 | user, 22 | token, 23 | message, 24 | reply_to: replyTo 25 | }); 26 | res.status(200).json(response.data); 27 | } catch (error) { 28 | res.status(500).json({ error: 'Failed to add chat message' } as any); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/pages/api/ports/getAllTokenAddresses.ts: -------------------------------------------------------------------------------- 1 | import type { NextApiRequest, NextApiResponse } from 'next'; 2 | import axios from 'axios'; 3 | 4 | const API_BASE_URL = process.env.NEXT_PUBLIC_API_BASE_URL; 5 | 6 | interface TokenAddress { 7 | address: string; 8 | symbol: string; 9 | } 10 | 11 | export default async function handler( 12 | req: NextApiRequest, 13 | res: NextApiResponse 14 | ) { 15 | if (req.method !== 'GET') { 16 | return res.status(405).end(); 17 | } 18 | 19 | try { 20 | const response = await axios.get(`${API_BASE_URL}/api/tokens/addresses`); 21 | res.status(200).json(response.data); 22 | } catch (error) { 23 | res.status(500).json([]); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/pages/api/ports/getAllTokens.ts: -------------------------------------------------------------------------------- 1 | import type { NextApiRequest, NextApiResponse } from 'next'; 2 | import axios from 'axios'; 3 | import { Token, PaginatedResponse } from '@/interface/types'; 4 | 5 | const API_BASE_URL = process.env.NEXT_PUBLIC_API_BASE_URL; 6 | 7 | export default async function handler( 8 | req: NextApiRequest, 9 | res: NextApiResponse> 10 | ) { 11 | if (req.method !== 'GET') { 12 | return res.status(405).end(); 13 | } 14 | 15 | try { 16 | const { page = 1, pageSize = 13 } = req.query; 17 | 18 | const response = await axios.get(`${API_BASE_URL}/api/tokens`, { 19 | params: { 20 | page: Number(page), 21 | pageSize: Number(pageSize) 22 | } 23 | }); 24 | 25 | res.status(200).json(response.data); 26 | } catch (error) { 27 | res.status(500).json({ 28 | tokens: [], 29 | data: [], 30 | totalCount: 0, 31 | currentPage: 1, 32 | totalPages: 1 33 | }); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/pages/api/ports/getAllTokensTrends.ts: -------------------------------------------------------------------------------- 1 | import type { NextApiRequest, NextApiResponse } from 'next'; 2 | import axios from 'axios'; 3 | import { Token } from '@/interface/types'; 4 | 5 | const API_BASE_URL = process.env.NEXT_PUBLIC_API_BASE_URL; 6 | 7 | export default async function handler( 8 | req: NextApiRequest, 9 | res: NextApiResponse 10 | ) { 11 | if (req.method !== 'GET') { 12 | return res.status(405).end(); 13 | } 14 | 15 | try { 16 | const response = await axios.get(`${API_BASE_URL}/api/tokens/trending`); 17 | res.status(200).json(response.data); 18 | } catch (error) { 19 | res.status(500).json([]); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/pages/api/ports/getAllTokensWithoutLiquidity.ts: -------------------------------------------------------------------------------- 1 | import type { NextApiRequest, NextApiResponse } from 'next'; 2 | import axios from 'axios'; 3 | import { Token } from '@/interface/types'; 4 | 5 | const API_BASE_URL = process.env.NEXT_PUBLIC_API_BASE_URL; 6 | 7 | export default async function handler( 8 | req: NextApiRequest, 9 | res: NextApiResponse 10 | ) { 11 | if (req.method !== 'GET') { 12 | return res.status(405).end(); 13 | } 14 | 15 | try { 16 | const response = await axios.get(`${API_BASE_URL}/api/tokens/without-liquidityEvent`); 17 | res.status(200).json(response.data); 18 | } catch (error) { 19 | res.status(500).json([]); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/pages/api/ports/getChatMessages.ts: -------------------------------------------------------------------------------- 1 | import type { NextApiRequest, NextApiResponse } from 'next'; 2 | import axios from 'axios'; 3 | 4 | const API_BASE_URL = process.env.NEXT_PUBLIC_API_BASE_URL; 5 | 6 | interface ChatMessage { 7 | id: number; 8 | user: string; 9 | token: string; 10 | message: string; 11 | reply_to: number | null; 12 | timestamp: string; 13 | } 14 | 15 | export default async function handler( 16 | req: NextApiRequest, 17 | res: NextApiResponse 18 | ) { 19 | if (req.method !== 'GET') { 20 | return res.status(405).end(); 21 | } 22 | 23 | try { 24 | const { token } = req.query; 25 | if (!token || typeof token !== 'string') { 26 | return res.status(400).json([]); 27 | } 28 | 29 | const response = await axios.get(`${API_BASE_URL}/chats`, { 30 | params: { token } 31 | }); 32 | res.status(200).json(response.data); 33 | } catch (error) { 34 | res.status(500).json([]); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/pages/api/ports/getCurrentPrice.ts: -------------------------------------------------------------------------------- 1 | import type { NextApiRequest, NextApiResponse } from 'next'; 2 | import axios from 'axios'; 3 | import { PriceResponse } from '@/interface/types'; 4 | 5 | const API_BASE_URL = process.env.NEXT_PUBLIC_API_BASE_URL; 6 | 7 | export default async function handler( 8 | req: NextApiRequest, 9 | res: NextApiResponse 10 | ) { 11 | if (req.method !== 'GET') { 12 | return res.status(405).end(); 13 | } 14 | 15 | try { 16 | const response = await axios.get(`${API_BASE_URL}/api/price`); 17 | res.status(200).json(response.data); 18 | } catch (error) { 19 | res.status(500).json({ price: '0' }); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/pages/api/ports/getHistoricalPriceData.ts: -------------------------------------------------------------------------------- 1 | import type { NextApiRequest, NextApiResponse } from 'next'; 2 | import axios from 'axios'; 3 | import { Token } from '@/interface/types'; 4 | 5 | const API_BASE_URL = process.env.NEXT_PUBLIC_API_BASE_URL; 6 | 7 | export default async function handler( 8 | req: NextApiRequest, 9 | res: NextApiResponse 10 | ) { 11 | if (req.method !== 'GET') { 12 | return res.status(405).end(); 13 | } 14 | 15 | try { 16 | const { address } = req.query; 17 | if (!address || typeof address !== 'string') { 18 | return res.status(400).json({ error: 'Address is required' } as any); 19 | } 20 | 21 | const response = await axios.get(`${API_BASE_URL}/api/tokens/address/${address}/historical-prices`); 22 | res.status(200).json(response.data); 23 | } catch (error) { 24 | res.status(500).json({ error: 'Failed to fetch historical price data' } as any); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/pages/api/ports/getRecentTokens.ts: -------------------------------------------------------------------------------- 1 | import type { NextApiRequest, NextApiResponse } from 'next'; 2 | import axios from 'axios'; 3 | import { Token, PaginatedResponse } from '@/interface/types'; 4 | 5 | const API_BASE_URL = process.env.NEXT_PUBLIC_API_BASE_URL; 6 | 7 | export default async function handler( 8 | req: NextApiRequest, 9 | res: NextApiResponse | null> 10 | ) { 11 | if (req.method !== 'GET') { 12 | return res.status(405).end(); 13 | } 14 | 15 | try { 16 | const { page = 1, pageSize = 20, hours = 24 } = req.query; 17 | const response = await axios.get(`${API_BASE_URL}/api/tokens/recent`, { 18 | params: { 19 | page: Number(page), 20 | pageSize: Number(pageSize), 21 | hours: Number(hours) 22 | } 23 | }); 24 | 25 | // If we get an empty array of tokens, return 404 26 | if (!response.data.tokens || response.data.tokens.length === 0) { 27 | return res.status(404).json(null); 28 | } 29 | 30 | res.status(200).json(response.data); 31 | } catch (error) { 32 | console.error('Error in getRecentTokens:', error); 33 | if (axios.isAxiosError(error) && error.response?.status === 404) { 34 | return res.status(404).json(null); 35 | } 36 | res.status(500).json(null); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/pages/api/ports/getTokenByAddress.ts: -------------------------------------------------------------------------------- 1 | import type { NextApiRequest, NextApiResponse } from 'next'; 2 | import axios from 'axios'; 3 | import { Token } from '@/interface/types'; 4 | 5 | const API_BASE_URL = process.env.NEXT_PUBLIC_API_BASE_URL; 6 | 7 | export default async function handler( 8 | req: NextApiRequest, 9 | res: NextApiResponse 10 | ) { 11 | if (req.method !== 'GET') { 12 | return res.status(405).end(); 13 | } 14 | 15 | try { 16 | const { address } = req.query; 17 | if (!address || typeof address !== 'string') { 18 | return res.status(400).json({ error: 'Address is required' } as any); 19 | } 20 | 21 | const response = await axios.get(`${API_BASE_URL}/api/tokens/address/${address}`); 22 | res.status(200).json(response.data); 23 | } catch (error) { 24 | console.error('Error in getTokenByAddress:', error); 25 | res.status(500).json({ error: 'Failed to fetch token' } as any); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/pages/api/ports/getTokenInfoAndTransactions.ts: -------------------------------------------------------------------------------- 1 | import type { NextApiRequest, NextApiResponse } from 'next'; 2 | import axios from 'axios'; 3 | import { TokenWithTransactions, Token } from '@/interface/types'; 4 | 5 | const API_BASE_URL = process.env.NEXT_PUBLIC_API_BASE_URL; 6 | 7 | // Empty token object for error cases 8 | const emptyToken: Token = { 9 | id: '', 10 | chainId: 0, 11 | address: '', 12 | creatorAddress: '', 13 | name: '', 14 | symbol: '', 15 | logo: '', 16 | description: '', 17 | createdAt: '', 18 | updatedAt: '', 19 | website: '', 20 | youtube: '', 21 | discord: '', 22 | twitter: '', 23 | telegram: '', 24 | latestTransactionTimestamp: '', 25 | _count: { 26 | liquidityEvents: 0 27 | }, 28 | // liquidityEvents: 0, 29 | map: null 30 | }; 31 | 32 | export default async function handler( 33 | req: NextApiRequest, 34 | res: NextApiResponse 35 | ) { 36 | if (req.method !== 'GET') { 37 | return res.status(405).end(); 38 | } 39 | 40 | try { 41 | const { address, transactionPage = 1, transactionPageSize = 10 } = req.query; 42 | if (!address || typeof address !== 'string') { 43 | return res.status(400).json({ 44 | ...emptyToken, 45 | transactions: { 46 | data: [], 47 | pagination: { 48 | currentPage: 1, 49 | pageSize: 10, 50 | totalCount: 0, 51 | totalPages: 0 52 | } 53 | } 54 | }); 55 | } 56 | 57 | const response = await axios.get( 58 | `${API_BASE_URL}/api/tokens/address/${address}/info-and-transactions`, 59 | { 60 | params: { 61 | transactionPage: Number(transactionPage), 62 | transactionPageSize: Number(transactionPageSize) 63 | } 64 | } 65 | ); 66 | res.status(200).json(response.data); 67 | } catch (error) { 68 | console.error('Server-side error in getTokenInfoAndTransactions:', error); 69 | res.status(500).json({ 70 | ...emptyToken, 71 | transactions: { 72 | data: [], 73 | pagination: { 74 | currentPage: 1, 75 | pageSize: 10, 76 | totalCount: 0, 77 | totalPages: 0 78 | } 79 | } 80 | }); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/pages/api/ports/getTokenLiquidityEvents.ts: -------------------------------------------------------------------------------- 1 | import type { NextApiRequest, NextApiResponse } from 'next'; 2 | import axios from 'axios'; 3 | import { LiquidityEvent, PaginatedResponse } from '@/interface/types'; 4 | 5 | const API_BASE_URL = process.env.NEXT_PUBLIC_API_BASE_URL; 6 | 7 | export default async function handler( 8 | req: NextApiRequest, 9 | res: NextApiResponse> 10 | ) { 11 | if (req.method !== 'GET') { 12 | return res.status(405).end(); 13 | } 14 | 15 | try { 16 | const { tokenId, page = 1, pageSize = 20 } = req.query; 17 | if (!tokenId || typeof tokenId !== 'string') { 18 | return res.status(400).json({ 19 | tokens: [], 20 | data: [], 21 | totalCount: 0, 22 | currentPage: 1, 23 | totalPages: 1 24 | }); 25 | } 26 | 27 | const response = await axios.get(`${API_BASE_URL}/api/liquidity/token/${tokenId}`, { 28 | params: { 29 | page: Number(page), 30 | pageSize: Number(pageSize) 31 | } 32 | }); 33 | res.status(200).json(response.data); 34 | } catch (error) { 35 | res.status(500).json({ 36 | tokens: [], 37 | data: [], 38 | totalCount: 0, 39 | currentPage: 1, 40 | totalPages: 1 41 | }); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/pages/api/ports/getTokensByCreator.ts: -------------------------------------------------------------------------------- 1 | import type { NextApiRequest, NextApiResponse } from 'next'; 2 | import axios from 'axios'; 3 | import { Token, PaginatedResponse } from '@/interface/types'; 4 | 5 | const API_BASE_URL = process.env.NEXT_PUBLIC_API_BASE_URL; 6 | 7 | export default async function handler( 8 | req: NextApiRequest, 9 | res: NextApiResponse> 10 | ) { 11 | if (req.method !== 'GET') { 12 | return res.status(405).end(); 13 | } 14 | 15 | try { 16 | const { creatorAddress, page = 1, pageSize = 20 } = req.query; 17 | if (!creatorAddress || typeof creatorAddress !== 'string') { 18 | return res.status(400).json({ 19 | tokens: [], 20 | data: [], 21 | totalCount: 0, 22 | currentPage: 1, 23 | totalPages: 1 24 | }); 25 | } 26 | 27 | const response = await axios.get( 28 | `${API_BASE_URL}/api/tokens/creator/${creatorAddress}`, 29 | { 30 | params: { 31 | page: Number(page), 32 | pageSize: Number(pageSize) 33 | } 34 | } 35 | ); 36 | res.status(200).json(response.data); 37 | } catch (error) { 38 | res.status(500).json({ 39 | tokens: [], 40 | data: [], 41 | totalCount: 0, 42 | currentPage: 1, 43 | totalPages: 1 44 | }); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/pages/api/ports/getTokensWithLiquidity.ts: -------------------------------------------------------------------------------- 1 | import type { NextApiRequest, NextApiResponse } from 'next'; 2 | import axios from 'axios'; 3 | import { TokenWithLiquidityEvents, PaginatedResponse } from '@/interface/types'; 4 | 5 | const API_BASE_URL = process.env.NEXT_PUBLIC_API_BASE_URL; 6 | 7 | export default async function handler( 8 | req: NextApiRequest, 9 | res: NextApiResponse> 10 | ) { 11 | if (req.method !== 'GET') { 12 | return res.status(405).end(); 13 | } 14 | 15 | try { 16 | const { page = 1, pageSize = 20 } = req.query; 17 | const response = await axios.get(`${API_BASE_URL}/api/tokens/with-liquidityEvent`, { 18 | params: { 19 | page: Number(page), 20 | pageSize: Number(pageSize) 21 | } 22 | }); 23 | res.status(200).json(response.data); 24 | } catch (error) { 25 | res.status(500).json({ 26 | tokens: [], 27 | data: [], 28 | totalCount: 0, 29 | currentPage: 1, 30 | totalPages: 1 31 | }); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/pages/api/ports/getTotalTokenCount.ts: -------------------------------------------------------------------------------- 1 | import type { NextApiRequest, NextApiResponse } from 'next'; 2 | import axios from 'axios'; 3 | 4 | const API_BASE_URL = process.env.NEXT_PUBLIC_API_BASE_URL; 5 | 6 | export default async function handler( 7 | req: NextApiRequest, 8 | res: NextApiResponse<{ totalTokens: number }> 9 | ) { 10 | if (req.method !== 'GET') { 11 | return res.status(405).end(); 12 | } 13 | 14 | try { 15 | const response = await axios.get(`${API_BASE_URL}/api/tokens/total-count`); 16 | res.status(200).json(response.data); 17 | } catch (error) { 18 | res.status(500).json({ totalTokens: 0 }); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/pages/api/ports/getTransactionsByAddress.ts: -------------------------------------------------------------------------------- 1 | import type { NextApiRequest, NextApiResponse } from 'next'; 2 | import axios from 'axios'; 3 | import { TransactionResponse } from '@/interface/types'; 4 | 5 | const API_BASE_URL = process.env.NEXT_PUBLIC_API_BASE_URL; 6 | 7 | export default async function handler( 8 | req: NextApiRequest, 9 | res: NextApiResponse 10 | ) { 11 | if (req.method !== 'GET') { 12 | return res.status(405).end(); 13 | } 14 | 15 | try { 16 | const { address, page = 1, pageSize = 10 } = req.query; 17 | if (!address || typeof address !== 'string') { 18 | return res.status(400).json({ error: 'Address is required' } as any); 19 | } 20 | 21 | const response = await axios.get( 22 | `${API_BASE_URL}/api/transactions/address/${address}`, 23 | { 24 | params: { 25 | page: Number(page), 26 | pageSize: Number(pageSize) 27 | } 28 | } 29 | ); 30 | res.status(200).json(response.data); 31 | } catch (error) { 32 | res.status(500).json({ error: 'Failed to fetch transactions' } as any); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/pages/api/ports/getVolumeRange.ts: -------------------------------------------------------------------------------- 1 | import type { NextApiRequest, NextApiResponse } from 'next'; 2 | import axios from 'axios'; 3 | 4 | const API_BASE_URL = process.env.NEXT_PUBLIC_API_BASE_URL; 5 | 6 | export default async function handler( 7 | req: NextApiRequest, 8 | res: NextApiResponse<{ totalVolume: number }> 9 | ) { 10 | if (req.method !== 'GET') { 11 | return res.status(405).end(); 12 | } 13 | 14 | try { 15 | const { hours = 24 } = req.query; 16 | const response = await axios.get(`${API_BASE_URL}/api/volume/range`, { 17 | params: { hours: Number(hours) } 18 | }); 19 | res.status(200).json(response.data); 20 | } catch (error) { 21 | res.status(500).json({ totalVolume: 0 }); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/pages/api/ports/searchTokens.ts: -------------------------------------------------------------------------------- 1 | import type { NextApiRequest, NextApiResponse } from 'next'; 2 | import axios from 'axios'; 3 | import { Token, PaginatedResponse } from '@/interface/types'; 4 | 5 | const API_BASE_URL = process.env.NEXT_PUBLIC_API_BASE_URL; 6 | 7 | export default async function handler( 8 | req: NextApiRequest, 9 | res: NextApiResponse> 10 | ) { 11 | if (req.method !== 'GET') { 12 | return res.status(405).end(); 13 | } 14 | 15 | try { 16 | const { q, page = 1, pageSize = 20 } = req.query; 17 | const response = await axios.get(`${API_BASE_URL}/api/tokens/search`, { 18 | params: { 19 | q, 20 | page: Number(page), 21 | pageSize: Number(pageSize) 22 | } 23 | }); 24 | res.status(200).json(response.data); 25 | } catch (error) { 26 | res.status(500).json({ 27 | tokens: [], 28 | data: [], 29 | totalCount: 0, 30 | currentPage: 1, 31 | totalPages: 1 32 | }); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/pages/api/ports/updateToken.ts: -------------------------------------------------------------------------------- 1 | import type { NextApiRequest, NextApiResponse } from 'next'; 2 | import axios from 'axios'; 3 | import { Token } from '@/interface/types'; 4 | 5 | const API_BASE_URL = process.env.NEXT_PUBLIC_API_BASE_URL; 6 | 7 | export default async function handler( 8 | req: NextApiRequest, 9 | res: NextApiResponse 10 | ) { 11 | if (req.method !== 'PATCH') { 12 | return res.status(405).end(); 13 | } 14 | 15 | try { 16 | const { address, data } = req.body; 17 | if (!address || !data) { 18 | return res.status(400).json({ error: 'Address and data are required' } as any); 19 | } 20 | 21 | const response = await axios.patch( 22 | `${API_BASE_URL}/api/tokens/update/${address}`, 23 | data 24 | ); 25 | res.status(200).json(response.data); 26 | } catch (error) { 27 | res.status(500).json({ error: 'Failed to update token' } as any); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/pages/api/proxy/[...path].ts: -------------------------------------------------------------------------------- 1 | import { NextApiRequest, NextApiResponse } from 'next'; 2 | import axios from 'axios'; 3 | 4 | export default async function handler(req: NextApiRequest, res: NextApiResponse) { 5 | if (!process.env.API_BASE_URL) { 6 | console.error('API_BASE_URL is not configured'); 7 | return res.status(500).json({ error: 'API_BASE_URL is not configured' }); 8 | } 9 | 10 | const { path } = req.query; 11 | 12 | if (!path) { 13 | console.error('No path provided in request'); 14 | return res.status(400).json({ error: 'No path provided' }); 15 | } 16 | 17 | const apiPath = Array.isArray(path) ? path.join('/') : path; 18 | const url = `${process.env.API_BASE_URL}/api/${apiPath}`; 19 | 20 | try { 21 | console.log(`Proxying ${req.method} request to:`, url); 22 | 23 | const response = await axios({ 24 | method: req.method, 25 | url: url, 26 | params: req.query, 27 | data: req.body, 28 | headers: { 29 | ...req.headers, 30 | host: new URL(process.env.API_BASE_URL).host, 31 | }, 32 | }); 33 | 34 | return res.status(response.status).json(response.data); 35 | } catch (error: any) { 36 | console.error('Proxy Error:', { 37 | method: req.method, 38 | url, 39 | error: error.message, 40 | status: error.response?.status, 41 | data: error.response?.data 42 | }); 43 | 44 | return res.status(error.response?.status || 500).json(error.response?.data || { 45 | error: 'Internal Server Error', 46 | message: error.message 47 | }); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/pages/api/robots.ts: -------------------------------------------------------------------------------- 1 | import { NextApiRequest, NextApiResponse } from 'next'; 2 | 3 | export const maxAge = 24 * 60 * 60; 4 | export default function handler(req: NextApiRequest, res: NextApiResponse) { 5 | res.setHeader('Content-Type', 'text/plain'); 6 | res.setHeader('Cache-Control', `public, max-age=${maxAge}`); 7 | res.status(200).send(` 8 | User-agent: * 9 | Sitemap: ${process.env.NEXT_PUBLIC_API_BASE_URL}/sitemap.xml 10 | Disallow: /admin 11 | `); 12 | } -------------------------------------------------------------------------------- /src/pages/home.tsx: -------------------------------------------------------------------------------- 1 | // pages/index.tsx (future) 2 | import React from 'react'; 3 | import Link from 'next/link'; 4 | import Image from 'next/image'; 5 | import { motion } from 'framer-motion'; 6 | import { ArrowRightIcon, CubeTransparentIcon, ChartBarIcon, CurrencyDollarIcon } from '@heroicons/react/24/outline'; 7 | import Layout from '@/components/layout/Layout'; 8 | import SEO from '@/components/seo/SEO'; 9 | 10 | const FeatureCard: React.FC<{ icon: React.ElementType; title: string; description: string }> = ({ icon: Icon, title, description }) => ( 11 | 16 | 17 |

{title}

18 |

{description}

19 |
20 | ); 21 | 22 | const HomePage: React.FC = () => { 23 | return ( 24 | 25 | 30 |
31 | {/* Hero Section */} 32 |
33 |
34 |
35 | 41 | Welcome to Bondle 42 | 43 | 49 | Create, trade, and grow your tokens with bonding curves 50 | 51 | 56 | 57 | Enter App 58 | 61 |
62 |
63 |
64 | Hero background 65 |
66 |
67 |
68 | 69 | {/* Features Section */} 70 |
71 |
72 |

Why Choose Bondle?

73 |
74 | 79 | 84 | 89 |
90 |
91 |
92 | 93 | {/* How It Works Section */} 94 |
95 |
96 |

How It Works

97 |
98 | {[ 99 | "Create your token", 100 | "Set bonding curve parameters", 101 | "Users buy and sell tokens", 102 | "Watch your token grow" 103 | ].map((step, index) => ( 104 | 110 |
111 | {index + 1} 112 |
113 |

{step}

114 |
115 | ))} 116 |
117 |
118 |
119 | 120 | {/* Call to Action */} 121 |
122 |
123 |

Ready to revolutionize token trading?

124 |

Join Bondle today and experience the future of decentralized finance.

125 | 126 | Launch Your Token Now 127 |
130 |
131 |
132 |
133 | ); 134 | }; 135 | 136 | export default HomePage; -------------------------------------------------------------------------------- /src/styles/globals.css: -------------------------------------------------------------------------------- 1 | /* @import url('https://fonts.googleapis.com/css2?family=Press+Start+2P&family=Space+Grotesk:wght@300..700&display=swap'); */ 2 | @import url('https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;500;600;700&display=swap'); 3 | 4 | 5 | @tailwind base; 6 | @tailwind components; 7 | @tailwind utilities; 8 | 9 | :root { 10 | --background: #111111; 11 | --foreground: #ffffff; 12 | --primary: #60A5FA; 13 | --primary-hover: #4B82EC; 14 | --card: #222222; 15 | --card2: #1a1a1a; 16 | --card-hover: #2a2a2a; 17 | --card-boarder: #333333; 18 | } 19 | 20 | body { 21 | background-color: var(--background); 22 | color: var(--foreground); 23 | font-family: 'Space Grotesk', sans-serif; 24 | } 25 | 26 | @layer base { 27 | h1, h2, h3, h4, h5, h6 { 28 | @apply font-bold mb-4; 29 | } 30 | 31 | h1 { 32 | @apply text-3xl md:text-4xl; 33 | } 34 | 35 | h2 { 36 | @apply text-2xl md:text-3xl; 37 | } 38 | 39 | h3 { 40 | @apply text-xl md:text-2xl; 41 | } 42 | } 43 | 44 | @layer components { 45 | .btn { 46 | @apply px-4 py-2 rounded-md font-bold transition-colors duration-200; 47 | } 48 | 49 | .btn-primary { 50 | @apply bg-[var(--primary)] text-black px-4 py-2 rounded-lg hover:bg-[var(--primary-hover)] transition-colors; 51 | } 52 | 53 | .btn-secondary { 54 | @apply bg-[var(--card)] text-white px-4 py-2 rounded-lg hover:bg-[var(--card-hover)] transition-colors; 55 | } 56 | 57 | .card { 58 | @apply bg-gray-800 rounded-lg shadow-lg p-6; 59 | } 60 | } 61 | 62 | @layer utilities { 63 | .text-balance { 64 | text-wrap: balance; 65 | } 66 | 67 | .neon-text { 68 | text-shadow: 0 0 5px rgb(var(--accent-color)), 0 0 10px rgb(var(--accent-color)), 0 0 15px rgb(var(--accent-color)); 69 | } 70 | 71 | .neon-border { 72 | box-shadow: 0 0 5px var(--primary), 73 | 0 0 10px var(--primary), 74 | 0 0 15px var(--primary); 75 | } 76 | } 77 | 78 | 79 | .custom-scrollbar::-webkit-scrollbar { 80 | width: 6px; 81 | } 82 | 83 | .custom-scrollbar::-webkit-scrollbar-track { 84 | background: var(--card-hover); 85 | } 86 | 87 | .custom-scrollbar::-webkit-scrollbar-thumb { 88 | background: #444444; 89 | border-radius: 3px; 90 | } 91 | 92 | .custom-scrollbar::-webkit-scrollbar-thumb:hover { 93 | background: #555555; 94 | } -------------------------------------------------------------------------------- /src/types/contracts/common.ts: -------------------------------------------------------------------------------- 1 | /* Autogenerated file. Do not edit manually. */ 2 | /* tslint:disable */ 3 | /* eslint-disable */ 4 | import type { Listener } from "@ethersproject/providers"; 5 | import type { Event, EventFilter } from "ethers"; 6 | 7 | export interface TypedEvent< 8 | TArgsArray extends Array = any, 9 | TArgsObject = any 10 | > extends Event { 11 | args: TArgsArray & TArgsObject; 12 | } 13 | 14 | export interface TypedEventFilter<_TEvent extends TypedEvent> 15 | extends EventFilter {} 16 | 17 | export interface TypedListener { 18 | (...listenerArg: [...__TypechainArgsArray, TEvent]): void; 19 | } 20 | 21 | type __TypechainArgsArray = T extends TypedEvent ? U : never; 22 | 23 | export interface OnEvent { 24 | ( 25 | eventFilter: TypedEventFilter, 26 | listener: TypedListener 27 | ): TRes; 28 | (eventName: string, listener: Listener): TRes; 29 | } 30 | 31 | export type MinEthersFactory = { 32 | deploy(...a: ARGS[]): Promise; 33 | }; 34 | 35 | export type GetContractTypeFromFactory = F extends MinEthersFactory< 36 | infer C, 37 | any 38 | > 39 | ? C 40 | : never; 41 | 42 | export type GetARGsTypeFromFactory = F extends MinEthersFactory 43 | ? Parameters 44 | : never; 45 | -------------------------------------------------------------------------------- /src/types/contracts/factories/index.ts: -------------------------------------------------------------------------------- 1 | /* Autogenerated file. Do not edit manually. */ 2 | /* tslint:disable */ 3 | /* eslint-disable */ 4 | export { BondingCurveManager__factory } from "./BondingCurveManager__factory"; 5 | -------------------------------------------------------------------------------- /src/types/contracts/index.ts: -------------------------------------------------------------------------------- 1 | /* Autogenerated file. Do not edit manually. */ 2 | /* tslint:disable */ 3 | /* eslint-disable */ 4 | export type { BondingCurveManager } from "./BondingCurveManager"; 5 | export * as factories from "./factories"; 6 | export { BondingCurveManager__factory } from "./factories/BondingCurveManager__factory"; 7 | -------------------------------------------------------------------------------- /src/types/heroicons.d.ts: -------------------------------------------------------------------------------- 1 | declare module '@@heroicons/react/24/outline' { 2 | import * as React from 'react'; 3 | 4 | export const Bars3Icon: React.FC>; 5 | export const XMarkIcon: React.FC>; 6 | 7 | // Add other icons here as needed 8 | } 9 | -------------------------------------------------------------------------------- /src/types/siwe.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'siwe'; -------------------------------------------------------------------------------- /src/utils/api.ts: -------------------------------------------------------------------------------- 1 | // api.ts 2 | 3 | import axios from 'axios'; 4 | import { Token, TokenWithLiquidityEvents, PaginatedResponse, LiquidityEvent, TokenWithTransactions, PriceResponse, HistoricalPrice, USDHistoricalPrice, TokenHolder, TransactionResponse } from '@/interface/types'; 5 | import { ethers } from 'ethers'; 6 | 7 | 8 | export async function getAllTokens(page: number = 1, pageSize: number = 13): Promise> { 9 | const response = await axios.get('/api/ports/getAllTokens', { 10 | params: { page, pageSize } 11 | }); 12 | return response.data; 13 | } 14 | 15 | export async function getAllTokensTrends(): Promise { 16 | const response = await axios.get('/api/ports/getAllTokensTrends'); 17 | return response.data; 18 | } 19 | 20 | export async function getAllTokensWithoutLiquidity(): Promise { //chewyswap aggregator use 21 | const response = await axios.get('/api/ports/getAllTokensWithoutLiquidity'); 22 | return response.data; 23 | } 24 | 25 | //GET /api/volume/total 26 | export async function getTotalVolume(): Promise<{ totalVolume: number }> { 27 | const response = await axios.get('/api/ports/getTotalVolume'); 28 | return response.data; 29 | } 30 | 31 | //GET /api/volume/range?hours=24 32 | export async function getVolumeRange(hours: number): Promise<{ totalVolume: number }> { 33 | const response = await axios.get('/api/ports/getVolumeRange', { 34 | params: { hours } 35 | }); 36 | return response.data; 37 | } 38 | 39 | //GET /api/tokens/total-count 40 | export async function getTotalTokenCount(): Promise<{ totalTokens: number }> { 41 | const response = await axios.get('/api/ports/getTotalTokenCount'); 42 | return response.data; 43 | } 44 | 45 | 46 | export async function getRecentTokens(page: number = 1, pageSize: number = 20, hours: number = 24): Promise | null> { 47 | try { 48 | const response = await axios.get('/api/ports/getRecentTokens', { 49 | params: { page, pageSize, hours } 50 | }); 51 | return response.data; 52 | } catch (error) { 53 | if (axios.isAxiosError(error) && error.response?.status === 404) { 54 | // Return null to indicate no recent tokens found 55 | return null; 56 | } 57 | throw error; // Re-throw other errors 58 | } 59 | } 60 | 61 | export async function searchTokens( 62 | query: string, 63 | page: number = 1, 64 | pageSize: number = 20 65 | ): Promise> { 66 | try { 67 | const response = await axios.get('/api/ports/searchTokens', { 68 | params: { q: query, page, pageSize } 69 | }); 70 | return response.data; 71 | } catch (error) { 72 | console.error('Error searching tokens:', error); 73 | throw new Error('Failed to search tokens'); 74 | } 75 | } 76 | 77 | export async function getTokensWithLiquidity(page: number = 1, pageSize: number = 20): Promise> { 78 | const response = await axios.get('/api/ports/getTokensWithLiquidity', { 79 | params: { page, pageSize } 80 | }); 81 | return response.data; 82 | } 83 | 84 | export async function getTokenByAddress(address: string): Promise { 85 | const response = await axios.get('/api/ports/getTokenByAddress', { 86 | params: { address } 87 | }); 88 | return response.data; 89 | } 90 | 91 | export async function getTokenLiquidityEvents(tokenId: string, page: number = 1, pageSize: number = 20): Promise> { 92 | const response = await axios.get('/api/ports/getTokenLiquidityEvents', { 93 | params: { tokenId, page, pageSize } 94 | }); 95 | return response.data; 96 | } 97 | 98 | export async function getTokenInfoAndTransactions( 99 | address: string, 100 | transactionPage: number = 1, 101 | transactionPageSize: number = 10 102 | ): Promise { 103 | try { 104 | const baseUrl = typeof window === 'undefined' 105 | ? process.env.NEXT_VERCEL_URL 106 | ? `https://${process.env.NEXT_VERCEL_URL}` 107 | : 'http://localhost:3000' 108 | : ''; 109 | 110 | const response = await axios.get(`${baseUrl}/api/ports/getTokenInfoAndTransactions`, { 111 | params: { address, transactionPage, transactionPageSize } 112 | }); 113 | return response.data; 114 | } catch (error) { 115 | console.error('Error in getTokenInfoAndTransactions:', error); 116 | throw error; 117 | } 118 | } 119 | 120 | 121 | //historical price 122 | export async function getHistoricalPriceData(address: string): Promise { 123 | const response = await axios.get('/api/ports/getHistoricalPriceData', { 124 | params: { address } 125 | }); 126 | return response.data; 127 | } 128 | 129 | //eth price usd 130 | export async function getCurrentPrice(): Promise { 131 | try { 132 | const response = await axios.get('/api/ports/getCurrentPrice'); 133 | return response.data.price; 134 | } catch (error) { 135 | console.error('Error fetching current price:', error); 136 | throw new Error('Failed to fetch current price'); 137 | } 138 | } 139 | 140 | 141 | export async function getTokenUSDPriceHistory(address: string): Promise { 142 | try { 143 | const [ethPrice, historicalPrices] = await Promise.all([ 144 | getCurrentPrice(), 145 | getHistoricalPriceData(address) 146 | ]); 147 | 148 | return historicalPrices.map((price: HistoricalPrice) => { 149 | const tokenPriceInWei = ethers.BigNumber.from(price.tokenPrice); 150 | const tokenPriceInETH = ethers.utils.formatEther(tokenPriceInWei); 151 | const tokenPriceUSD = parseFloat(tokenPriceInETH) * parseFloat(ethPrice); 152 | 153 | return { 154 | tokenPriceUSD: tokenPriceUSD.toFixed(9), // Adjust decimal places as needed 155 | timestamp: price.timestamp 156 | }; 157 | }); 158 | } catch (error) { 159 | console.error('Error calculating USD price history:', error); 160 | throw new Error('Failed to calculate USD price history'); 161 | } 162 | } 163 | 164 | 165 | export async function updateToken( 166 | address: string, 167 | data: { 168 | logo?: string; 169 | description?: string; 170 | website?: string; 171 | telegram?: string; 172 | discord?: string; 173 | twitter?: string; 174 | youtube?: string; 175 | } 176 | ): Promise { 177 | try { 178 | const response = await axios.patch('/api/ports/updateToken', { 179 | address, 180 | data 181 | }); 182 | return response.data; 183 | } catch (error) { 184 | console.error('Error updating token:', error); 185 | throw new Error('Failed to update token'); 186 | } 187 | } 188 | 189 | // get all transaction associated with a particular address 190 | export async function getTransactionsByAddress( 191 | address: string, 192 | page: number = 1, 193 | pageSize: number = 10 194 | ): Promise { 195 | try { 196 | const response = await axios.get('/api/ports/getTransactionsByAddress', { 197 | params: { address, page, pageSize } 198 | }); 199 | return response.data; 200 | } catch (error) { 201 | console.error('Error fetching transactions:', error); 202 | throw new Error('Failed to fetch transactions'); 203 | } 204 | } 205 | 206 | // POST /chats: Add a new chat message with optional reply_to 207 | export async function addChatMessage( 208 | user: string, 209 | token: string, 210 | message: string, 211 | replyTo?: number 212 | ): Promise<{ id: number }> { 213 | try { 214 | const response = await axios.post('/api/ports/addChatMessage', { 215 | user, 216 | token, 217 | message, 218 | reply_to: replyTo // Optional: ID of the message being replied to 219 | }); 220 | return response.data; 221 | } catch (error) { 222 | console.error('Error adding chat message:', error); 223 | throw new Error('Failed to add chat message'); 224 | } 225 | } 226 | 227 | // GET /chats: Get chat messages for a specific token 228 | export async function getChatMessages(token: string): Promise> { 236 | try { 237 | const response = await axios.get('/api/ports/getChatMessages', { 238 | params: { token } 239 | }); 240 | return response.data; 241 | } catch (error) { 242 | console.error('Error fetching chat messages:', error); 243 | throw new Error('Failed to fetch chat messages'); 244 | } 245 | } 246 | 247 | //get all token address 248 | export async function getAllTokenAddresses(): Promise> { 249 | try { 250 | const response = await axios.get('/api/ports/getAllTokenAddresses'); 251 | return response.data; 252 | } catch (error) { 253 | console.error('Error fetching token addresses and symbols:', error); 254 | throw new Error('Failed to fetch token addresses and symbols'); 255 | } 256 | } 257 | 258 | export async function getTokensByCreator( 259 | creatorAddress: string, 260 | page: number = 1, 261 | pageSize: number = 20 262 | ): Promise> { 263 | try { 264 | const response = await axios.get('/api/ports/getTokensByCreator', { 265 | params: { creatorAddress, page, pageSize } 266 | }); 267 | // console.log('getTokensByCreator', response.data); 268 | return response.data; 269 | } catch (error) { 270 | console.error('Error fetching tokens by creator:', error); 271 | throw new Error('Failed to fetch tokens by creator'); 272 | } 273 | } 274 | 275 | 276 | //blockexplorer Get token Holders 277 | export async function getTokenHolders(tokenAddress: string): Promise { 278 | try { 279 | const response = await axios.get(`https://www.shibariumscan.io/api/v2/tokens/${tokenAddress}/holders`); 280 | const data = response.data; 281 | 282 | return data.items.map((item: any) => { 283 | return { 284 | address: item.address.hash, 285 | balance: item.value 286 | }; 287 | }); 288 | } catch (error) { 289 | console.error('Error fetching token holders:', error); 290 | throw new Error('Failed to fetch token holders'); 291 | } 292 | } 293 | -------------------------------------------------------------------------------- /src/utils/chatUtils.ts: -------------------------------------------------------------------------------- 1 | export function formatTimestamp(timestamp: string): string { 2 | const now = new Date(); 3 | const messageDate = new Date(timestamp); 4 | const diffInSeconds = Math.floor((now.getTime() - messageDate.getTime()) / 1000); 5 | 6 | if (diffInSeconds < 60) return `${diffInSeconds} sec ago`; 7 | if (diffInSeconds < 3600) return `${Math.floor(diffInSeconds / 60)} min ago`; 8 | if (diffInSeconds < 86400) return `${Math.floor(diffInSeconds / 3600)} hour ago`; 9 | if (diffInSeconds < 2592000) return `${Math.floor(diffInSeconds / 86400)} day ago`; 10 | if (diffInSeconds < 31536000) return `${Math.floor(diffInSeconds / 2592000)} month ago`; 11 | return `${Math.floor(diffInSeconds / 31536000)} year ago`; 12 | } 13 | 14 | export function getRandomAvatarImage(): string { 15 | const randomNumber = Math.floor(Math.random() * 10) + 1; 16 | return `/chats/${randomNumber}.png`; 17 | } 18 | 19 | export function shortenAddress(address: string): string { 20 | return address.slice(2, 8); 21 | } -------------------------------------------------------------------------------- /tailwind.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from "tailwindcss"; 2 | 3 | const config: Config = { 4 | content: [ 5 | "./src/pages/**/*.{js,ts,jsx,tsx,mdx}", 6 | "./src/components/**/*.{js,ts,jsx,tsx,mdx}", 7 | "./src/app/**/*.{js,ts,jsx,tsx,mdx}", 8 | ], 9 | theme: { 10 | extend: { 11 | backgroundImage: { 12 | "gradient-radial": "radial-gradient(var(--tw-gradient-stops))", 13 | "gradient-conic": 14 | "conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))", 15 | }, 16 | animation: { 17 | crawl: 'crawl 20s linear infinite', 18 | 'loading-bar': 'loading-bar 1.5s ease-in-out infinite', 19 | ldspinner: 'ldspinner 2.5s linear infinite', 20 | }, 21 | keyframes: { 22 | crawl: { 23 | '0%': { transform: 'translateX(100%)' }, 24 | '100%': { transform: 'translateX(-100%)' }, 25 | }, 26 | 'loading-bar': { 27 | '0%': { transform: 'translateX(-100%)' }, 28 | '50%': { transform: 'translateX(100%)' }, 29 | '100%': { transform: 'translateX(-100%)' }, 30 | }, 31 | ldspinner: { 32 | '0%': { transform: 'rotate(0deg)' }, 33 | '100%': { transform: 'rotate(360deg)' } 34 | }, 35 | }, 36 | }, 37 | }, 38 | plugins: [], 39 | }; 40 | 41 | export default config; -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": ["dom", "dom.iterable", "esnext"], 4 | "allowJs": true, 5 | "skipLibCheck": true, 6 | "strict": true, 7 | "noEmit": true, 8 | "esModuleInterop": true, 9 | "module": "esnext", 10 | "moduleResolution": "bundler", 11 | "resolveJsonModule": true, 12 | "isolatedModules": true, 13 | "jsx": "preserve", 14 | "incremental": true, 15 | "paths": { 16 | "@/*": ["./src/*"] 17 | } 18 | }, 19 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], 20 | "exclude": ["node_modules"] 21 | } --------------------------------------------------------------------------------