├── src ├── index.css ├── components │ └── ui │ │ ├── use-theme.tsx │ │ ├── label.tsx │ │ ├── input.tsx │ │ ├── card.tsx │ │ ├── alert.tsx │ │ ├── theme-provider.tsx │ │ ├── button.tsx │ │ ├── form.tsx │ │ └── select.tsx ├── types.d.ts ├── App.tsx ├── lib │ └── utils.ts ├── react.svg ├── index.html ├── frontend.tsx ├── index.tsx ├── APITester.tsx ├── logo.svg └── GasFeeApp.tsx ├── bunfig.toml ├── tsconfig.json ├── components.json ├── .gitignore ├── package.json ├── LICENSE ├── README.md ├── styles └── globals.css └── bun.lock /src/index.css: -------------------------------------------------------------------------------- 1 | @import "../styles/globals.css"; 2 | -------------------------------------------------------------------------------- /src/components/ui/use-theme.tsx: -------------------------------------------------------------------------------- 1 | export { useTheme } from "./theme-provider"; 2 | -------------------------------------------------------------------------------- /bunfig.toml: -------------------------------------------------------------------------------- 1 | [serve.static] 2 | plugins = ["bun-plugin-tailwind"] 3 | env = "BUN_PUBLIC_*" 4 | -------------------------------------------------------------------------------- /src/types.d.ts: -------------------------------------------------------------------------------- 1 | declare module "*.svg" { 2 | const content: string; 3 | export default content; 4 | } 5 | -------------------------------------------------------------------------------- /src/App.tsx: -------------------------------------------------------------------------------- 1 | import "./index.css"; 2 | import GasFeeApp from "./GasFeeApp"; 3 | 4 | export function App() { 5 | return ; 6 | } 7 | 8 | export default App; 9 | -------------------------------------------------------------------------------- /src/lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { type ClassValue, clsx } from "clsx"; 2 | import { twMerge } from "tailwind-merge"; 3 | 4 | export function cn(...inputs: ClassValue[]) { 5 | return twMerge(clsx(inputs)); 6 | } 7 | -------------------------------------------------------------------------------- /src/react.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "jsx": "react-jsx", 4 | "allowJs": true, 5 | 6 | // Bundler mode 7 | "moduleResolution": "bundler", 8 | "module": "preserve", 9 | "allowImportingTsExtensions": true, 10 | "verbatimModuleSyntax": true, 11 | "noEmit": true, 12 | 13 | "baseUrl": ".", 14 | "paths": { 15 | "@/*": ["./src/*"] 16 | } 17 | }, 18 | "include": ["**/*.ts", "**/*.tsx"] 19 | } 20 | -------------------------------------------------------------------------------- /components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "new-york", 4 | "rsc": false, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "", 8 | "css": "styles/globals.css", 9 | "baseColor": "zinc", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils", 16 | "ui": "@/components/ui", 17 | "lib": "@/lib", 18 | "hooks": "@/hooks" 19 | }, 20 | "iconLibrary": "lucide" 21 | } 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # dependencies (bun install) 2 | node_modules 3 | 4 | # output 5 | out 6 | dist 7 | *.tgz 8 | 9 | # code coverage 10 | coverage 11 | *.lcov 12 | 13 | # logs 14 | logs 15 | _.log 16 | report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json 17 | 18 | # dotenv environment variable files 19 | .env 20 | .env.development.local 21 | .env.test.local 22 | .env.production.local 23 | .env.local 24 | 25 | # caches 26 | .eslintcache 27 | .cache 28 | *.tsbuildinfo 29 | 30 | # IntelliJ based IDEs 31 | .idea 32 | 33 | # Finder (MacOS) folder config 34 | .DS_Store 35 | -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 11 | Blockchain Gas Fee Predictor 12 | 13 | 14 | 15 |
16 | 17 | 18 | -------------------------------------------------------------------------------- /src/components/ui/label.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as LabelPrimitive from "@radix-ui/react-label" 5 | 6 | import { cn } from "@/lib/utils" 7 | 8 | function Label({ 9 | className, 10 | ...props 11 | }: React.ComponentProps) { 12 | return ( 13 | 21 | ) 22 | } 23 | 24 | export { Label } 25 | -------------------------------------------------------------------------------- /src/frontend.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * This file is the entry point for the React app, it sets up the root 3 | * element and renders the App component to the DOM. 4 | * 5 | * It is included in `src/index.html`. 6 | */ 7 | 8 | import { createRoot } from "react-dom/client"; 9 | import { StrictMode } from "react"; 10 | import { App } from "./App"; 11 | 12 | const elem = document.getElementById("root")!; 13 | const app = ( 14 | 15 | 16 | 17 | ); 18 | 19 | if (import.meta.hot) { 20 | // With hot module reloading, `import.meta.hot.data` is persisted. 21 | const root = (import.meta.hot.data.root ??= createRoot(elem)); 22 | root.render(app); 23 | } else { 24 | // The hot module reloading API is not available in production. 25 | createRoot(elem).render(app); 26 | } 27 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import { serve } from "bun"; 2 | import index from "./index.html"; 3 | 4 | const server = serve({ 5 | routes: { 6 | // Serve index.html for all unmatched routes. 7 | "/*": index, 8 | 9 | "/api/hello": { 10 | async GET(req) { 11 | return Response.json({ 12 | message: "Hello, world!", 13 | method: "GET", 14 | }); 15 | }, 16 | async PUT(req) { 17 | return Response.json({ 18 | message: "Hello, world!", 19 | method: "PUT", 20 | }); 21 | }, 22 | }, 23 | 24 | "/api/hello/:name": async (req) => { 25 | const name = req.params.name; 26 | return Response.json({ 27 | message: `Hello, ${name}!`, 28 | }); 29 | }, 30 | }, 31 | 32 | development: process.env.NODE_ENV !== "production", 33 | }); 34 | 35 | console.log(`🚀 Server running at ${server.url}`); 36 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bun-react-template", 3 | "version": "0.1.0", 4 | "private": true, 5 | "type": "module", 6 | "main": "src/index.tsx", 7 | "module": "src/index.tsx", 8 | "scripts": { 9 | "dev": "bun --hot src/index.tsx", 10 | "start": "NODE_ENV=production bun src/index.tsx", 11 | "build": "bun run build.ts --outdir=public" 12 | }, 13 | "dependencies": { 14 | "@hookform/resolvers": "^4.1.0", 15 | "@radix-ui/react-label": "^2.1.2", 16 | "@radix-ui/react-select": "^2.1.6", 17 | "@radix-ui/react-slot": "^1.1.2", 18 | "bun-plugin-tailwind": "^0.0.14", 19 | "class-variance-authority": "^0.7.1", 20 | "clsx": "^2.1.1", 21 | "lucide-react": "^0.477.0", 22 | "react": "^19", 23 | "react-dom": "^19", 24 | "react-hook-form": "^7.54.2", 25 | "recharts": "^2.15.1", 26 | "tailwind-merge": "^3.0.1", 27 | "tailwindcss": "^4.0.6", 28 | "tailwindcss-animate": "^1.0.7", 29 | "zod": "^3.24.2" 30 | }, 31 | "devDependencies": { 32 | "@types/react": "^19", 33 | "@types/react-dom": "^19", 34 | "@types/bun": "latest" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 aliezza hn 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/components/ui/input.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | import { cn } from "@/lib/utils" 4 | 5 | function Input({ className, type, ...props }: React.ComponentProps<"input">) { 6 | return ( 7 | 16 | ) 17 | } 18 | 19 | export { Input } 20 | -------------------------------------------------------------------------------- /src/components/ui/card.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | import { cn } from "@/lib/utils" 4 | 5 | function Card({ className, ...props }: React.ComponentProps<"div">) { 6 | return ( 7 |
15 | ) 16 | } 17 | 18 | function CardHeader({ className, ...props }: React.ComponentProps<"div">) { 19 | return ( 20 |
25 | ) 26 | } 27 | 28 | function CardTitle({ className, ...props }: React.ComponentProps<"div">) { 29 | return ( 30 |
35 | ) 36 | } 37 | 38 | function CardDescription({ className, ...props }: React.ComponentProps<"div">) { 39 | return ( 40 |
45 | ) 46 | } 47 | 48 | function CardContent({ className, ...props }: React.ComponentProps<"div">) { 49 | return ( 50 |
55 | ) 56 | } 57 | 58 | function CardFooter({ className, ...props }: React.ComponentProps<"div">) { 59 | return ( 60 |
65 | ) 66 | } 67 | 68 | export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent } 69 | -------------------------------------------------------------------------------- /src/components/ui/alert.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { cva, type VariantProps } from "class-variance-authority" 3 | 4 | import { cn } from "@/lib/utils" 5 | 6 | const alertVariants = cva( 7 | "relative w-full rounded-lg border px-4 py-3 text-sm grid has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] grid-cols-[0_1fr] has-[>svg]:gap-x-3 gap-y-0.5 items-start [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current", 8 | { 9 | variants: { 10 | variant: { 11 | default: "bg-background text-foreground", 12 | destructive: 13 | "text-destructive-foreground [&>svg]:text-current *:data-[slot=alert-description]:text-destructive-foreground/80", 14 | }, 15 | }, 16 | defaultVariants: { 17 | variant: "default", 18 | }, 19 | } 20 | ) 21 | 22 | function Alert({ 23 | className, 24 | variant, 25 | ...props 26 | }: React.ComponentProps<"div"> & VariantProps) { 27 | return ( 28 |
34 | ) 35 | } 36 | 37 | function AlertTitle({ className, ...props }: React.ComponentProps<"div">) { 38 | return ( 39 |
47 | ) 48 | } 49 | 50 | function AlertDescription({ 51 | className, 52 | ...props 53 | }: React.ComponentProps<"div">) { 54 | return ( 55 |
63 | ) 64 | } 65 | 66 | export { Alert, AlertTitle, AlertDescription } 67 | -------------------------------------------------------------------------------- /src/components/ui/theme-provider.tsx: -------------------------------------------------------------------------------- 1 | import React, { createContext, useContext, useEffect, useState } from "react"; 2 | 3 | type Theme = "dark" | "light" | "system"; 4 | 5 | type ThemeProviderProps = { 6 | children: React.ReactNode; 7 | defaultTheme?: Theme; 8 | storageKey?: string; 9 | }; 10 | 11 | type ThemeProviderState = { 12 | theme: Theme; 13 | setTheme: (theme: Theme) => void; 14 | }; 15 | 16 | const initialState: ThemeProviderState = { 17 | theme: "system", 18 | setTheme: () => null, 19 | }; 20 | 21 | const ThemeProviderContext = createContext(initialState); 22 | 23 | export function ThemeProvider({ 24 | children, 25 | defaultTheme = "system", 26 | storageKey = "vite-ui-theme", 27 | ...props 28 | }: ThemeProviderProps) { 29 | const [theme, setTheme] = useState( 30 | () => (localStorage.getItem(storageKey) as Theme) || defaultTheme 31 | ); 32 | 33 | useEffect(() => { 34 | const root = window.document.documentElement; 35 | root.classList.remove("light", "dark"); 36 | 37 | if (theme === "system") { 38 | const systemTheme = window.matchMedia("(prefers-color-scheme: dark)") 39 | .matches 40 | ? "dark" 41 | : "light"; 42 | 43 | root.classList.add(systemTheme); 44 | return; 45 | } 46 | 47 | root.classList.add(theme); 48 | localStorage.setItem(storageKey, theme); 49 | }, [theme]); 50 | 51 | const value = { 52 | theme, 53 | setTheme: (theme: Theme) => { 54 | localStorage.setItem(storageKey, theme); 55 | setTheme(theme); 56 | }, 57 | }; 58 | 59 | return ( 60 | 61 | {children} 62 | 63 | ); 64 | } 65 | 66 | export const useTheme = () => { 67 | const context = useContext(ThemeProviderContext); 68 | 69 | if (context === undefined) 70 | throw new Error("useTheme must be used within a ThemeProvider"); 71 | 72 | return context; 73 | }; 74 | -------------------------------------------------------------------------------- /src/components/ui/button.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { Slot } from "@radix-ui/react-slot" 3 | import { cva, type VariantProps } from "class-variance-authority" 4 | 5 | import { cn } from "@/lib/utils" 6 | 7 | const buttonVariants = cva( 8 | "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-[color,box-shadow] disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 [&_svg]:shrink-0 ring-ring/10 dark:ring-ring/20 dark:outline-ring/40 outline-ring/50 focus-visible:ring-4 focus-visible:outline-1 aria-invalid:focus-visible:ring-0", 9 | { 10 | variants: { 11 | variant: { 12 | default: 13 | "bg-primary text-primary-foreground shadow-sm hover:bg-primary/90", 14 | destructive: 15 | "bg-destructive text-destructive-foreground shadow-xs hover:bg-destructive/90", 16 | outline: 17 | "border border-input bg-background shadow-xs hover:bg-accent hover:text-accent-foreground", 18 | secondary: 19 | "bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80", 20 | ghost: "hover:bg-accent hover:text-accent-foreground", 21 | link: "text-primary underline-offset-4 hover:underline", 22 | }, 23 | size: { 24 | default: "h-9 px-4 py-2 has-[>svg]:px-3", 25 | sm: "h-8 rounded-md px-3 has-[>svg]:px-2.5", 26 | lg: "h-10 rounded-md px-6 has-[>svg]:px-4", 27 | icon: "size-9", 28 | }, 29 | }, 30 | defaultVariants: { 31 | variant: "default", 32 | size: "default", 33 | }, 34 | } 35 | ) 36 | 37 | function Button({ 38 | className, 39 | variant, 40 | size, 41 | asChild = false, 42 | ...props 43 | }: React.ComponentProps<"button"> & 44 | VariantProps & { 45 | asChild?: boolean 46 | }) { 47 | const Comp = asChild ? Slot : "button" 48 | 49 | return ( 50 | 55 | ) 56 | } 57 | 58 | export { Button, buttonVariants } 59 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Gas Fee Predictor 2 | 3 | ## Overview 4 | 5 | Gas Fee Predictor is a comprehensive blockchain analytics dashboard designed to help cryptocurrency users and blockchain enthusiasts understand and optimize their transaction strategies. The application provides real-time insights into Ethereum network performance, gas prices, transaction volumes, and more. 6 | 7 | ## Features 8 | 9 | ### 1. Trends Tab 10 | 11 | - **Gas Price Trends**: A dynamic line chart displaying real-time gas price fluctuations over the past 24 hours. 12 | - **Transaction Volume**: A bar chart showing the number of transactions and their distribution across different time periods. 13 | 14 | ### 2. Insights Tab 15 | 16 | - **Network Performance**: A line chart visualizing block times and network efficiency. 17 | - **Transaction Types**: A pie chart breaking down transaction categories including: 18 | - DeFi Transactions 19 | - NFT Interactions 20 | - Smart Contracts 21 | - Token Transfers 22 | 23 | ### 3. Timing Tab 24 | 25 | - **Optimal Transaction Time**: Identifies the most cost-effective moment for executing blockchain transactions. 26 | - **Cost Saving Recommendations**: Provides actionable advice on when to perform transactions based on current gas prices. 27 | - Low gas prices: Recommended time for transactions 28 | - Moderate gas prices: Suggests timing for non-urgent transactions 29 | - High gas prices: Advises delaying non-critical interactions 30 | 31 | ### 4. Settings Tab 32 | 33 | - **Theme Customization**: Toggle between dark and light modes 34 | - **Network Information**: Currently configured for Ethereum Mainnet 35 | 36 | ## User Interface 37 | 38 | The application features a clean, modern design with: 39 | 40 | - Responsive layout adapting to different screen sizes 41 | - Intuitive bottom navigation for easy tab switching 42 | - Top navigation with theme toggle 43 | - Recharts-powered interactive visualizations 44 | - Shadcn UI components for consistent styling 45 | 46 | ## Technical Highlights 47 | 48 | - Built with React 49 | - Uses Recharts for data visualization 50 | - Implements responsive design 51 | - Supports dark and light themes 52 | - Generates mock blockchain data for demonstration 53 | 54 | ## Use Cases 55 | 56 | - Blockchain Investors 57 | - Cryptocurrency Traders 58 | - Smart Contract Developers 59 | - Network Analysts 60 | - DeFi Enthusiasts 61 | 62 | Perfect for anyone looking to optimize transaction timing and understand blockchain network dynamics. 63 | -------------------------------------------------------------------------------- /src/APITester.tsx: -------------------------------------------------------------------------------- 1 | import React, { useRef, type FormEvent } from "react"; 2 | import { Button } from "@/components/ui/button"; 3 | import { Input } from "@/components/ui/input"; 4 | import { 5 | Select, 6 | SelectContent, 7 | SelectItem, 8 | SelectTrigger, 9 | SelectValue, 10 | } from "@/components/ui/select"; 11 | import { cn } from "@/lib/utils"; 12 | 13 | export function APITester() { 14 | const responseInputRef = useRef(null); 15 | 16 | const testEndpoint = async (e: FormEvent) => { 17 | e.preventDefault(); 18 | 19 | try { 20 | const form = e.currentTarget; 21 | const formData = new FormData(form); 22 | const endpoint = formData.get("endpoint") as string; 23 | const url = new URL(endpoint, location.href); 24 | const method = formData.get("method") as string; 25 | const res = await fetch(url, { method }); 26 | 27 | const data = await res.json(); 28 | responseInputRef.current!.value = JSON.stringify(data, null, 2); 29 | } catch (error) { 30 | responseInputRef.current!.value = String(error); 31 | } 32 | }; 33 | 34 | return ( 35 |
36 |
40 | 49 | 50 | 61 | 62 | 65 |
66 | 67 |