Add funnel-style TBFF visualization with draggable thresholds
New FundingFunnel component with three distinct zones: - Overflow zone (above MAX): Straight walls, excess redistribution - Healthy zone (MIN to MAX): Straight walls, normal operations - Critical zone (below MIN): Narrowing funnel, restricted outflow Features: - Draggable MIN/MAX threshold lines with real-time updates - Animated inflow particles (blue) dropping from top - Animated outflow particles (pink) from bottom spout - Gradient liquid fill with glow effect - Live balance simulation based on flow rates - Status panel showing current balance, rates, progress - Single/Multi-funnel view toggle - Zone color coding (rose/emerald/amber) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
14ebf0853d
commit
4b0491e999
398
DESIGN.md
398
DESIGN.md
|
|
@ -2,268 +2,232 @@
|
|||
|
||||
## Core Concept: Threshold-Based Flow Funding (TBFF)
|
||||
|
||||
Unlike traditional discrete grants, TBFF enables **continuous resource flows** that respond dynamically to conditions. Funds stream like water through a network, with thresholds acting as gates that open, close, or modulate flow rates.
|
||||
Unlike traditional discrete grants, TBFF enables **continuous resource flows** that respond dynamically to threshold conditions. The funnel visualization makes these dynamics intuitive and interactive.
|
||||
|
||||
---
|
||||
|
||||
## Visual Metaphors
|
||||
## The Funnel Metaphor
|
||||
|
||||
### 1. The River System
|
||||
- **Sources** = Springs/tributaries (funding origins)
|
||||
- **Flows** = Rivers/streams (animated fund movement)
|
||||
- **Recipients** = Lakes/deltas (fund destinations)
|
||||
- **Thresholds** = Dams/locks (control points)
|
||||
The funding funnel has three distinct zones defined by minimum and maximum thresholds:
|
||||
|
||||
### 2. The Circulatory System
|
||||
- **Sources** = Heart (pumping resources)
|
||||
- **Flows** = Arteries/veins (distribution network)
|
||||
- **Recipients** = Organs (entities needing resources)
|
||||
- **Thresholds** = Valves (flow regulation)
|
||||
```
|
||||
════════════════════════
|
||||
│ │ OVERFLOW ZONE
|
||||
│ Excess funds │ (above MAX)
|
||||
│ redistribute │
|
||||
MAX ─ ─ ─ ─ ─ ─ ┼──────────────────────┼ ─ ─ ─ ─ ─ ─
|
||||
│ │
|
||||
│ HEALTHY ZONE │ STRAIGHT WALLS
|
||||
│ Normal operations │ (MIN to MAX)
|
||||
│ │
|
||||
MIN ─ ─ ─ ─ ─ ─ ┼──────────────────────┼ ─ ─ ─ ─ ─ ─
|
||||
╲ ╱
|
||||
╲ CRITICAL ZONE ╱ NARROWING FUNNEL
|
||||
╲ Restricted ╱ (below MIN)
|
||||
╲ outflow ╱
|
||||
╲ ╱
|
||||
╲________╱
|
||||
║║
|
||||
outflow
|
||||
```
|
||||
|
||||
### 3. The Electrical Grid
|
||||
- **Sources** = Power plants (generation)
|
||||
- **Flows** = Transmission lines (distribution)
|
||||
- **Recipients** = Consumers (utilization)
|
||||
- **Thresholds** = Circuit breakers (protection/regulation)
|
||||
### Zone Behaviors
|
||||
|
||||
| Zone | Shape | Behavior |
|
||||
|------|-------|----------|
|
||||
| **Overflow** (above MAX) | Straight walls | Excess funds spill over, can redirect to other pools |
|
||||
| **Healthy** (MIN to MAX) | Straight walls | Normal flow rate, balanced operations |
|
||||
| **Critical** (below MIN) | Narrowing funnel | Outflow restricted, conservation mode |
|
||||
|
||||
---
|
||||
|
||||
## Key Visual Elements
|
||||
## Visual Elements
|
||||
|
||||
### Flow Nodes
|
||||
### Animated Flows
|
||||
|
||||
**Inflow (blue particles)**
|
||||
- Drops falling from above into the funnel
|
||||
- Speed/density indicates inflow rate
|
||||
- Particles merge into the liquid surface
|
||||
|
||||
**Outflow (pink particles)**
|
||||
- Drops falling from the bottom spout
|
||||
- Throttled in critical zone (fewer/slower)
|
||||
- Full rate in healthy zone
|
||||
|
||||
**Liquid Fill**
|
||||
- Gradient fill showing current balance level
|
||||
- Subtle glow/pulse animation
|
||||
- Wave effect at surface
|
||||
|
||||
### Threshold Indicators
|
||||
|
||||
**Minimum Threshold (Rose/Red)**
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ SOURCE NODE │
|
||||
│ ┌─────────────────────────────────────────────────┐ │
|
||||
│ │ ╭──────────────╮ │ │
|
||||
│ │ │ Treasury │ Balance: $50,000 │ │
|
||||
│ │ │ 💰 │ Flow Rate: $100/hr │ │
|
||||
│ │ ╰──────┬───────╯ Status: ● Active │ │
|
||||
│ │ │ │ │
|
||||
│ │ ═════╧═════▶ (animated particles) │ │
|
||||
│ └─────────────────────────────────────────────────┘ │
|
||||
└─────────────────────────────────────────────────────────┘
|
||||
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ RECIPIENT NODE │
|
||||
│ ┌─────────────────────────────────────────────────┐ │
|
||||
│ │ ═════╤═════▶ (incoming flow) │ │
|
||||
│ │ │ │ │
|
||||
│ │ ╭──────┴───────╮ │ │
|
||||
│ │ │ Project │ Received: $12,340 │ │
|
||||
│ │ │ 🎯 │ Rate: $85/hr │ │
|
||||
│ │ ╰──────────────╯ Health: ████████░░ 80% │ │
|
||||
│ └─────────────────────────────────────────────────┘ │
|
||||
└─────────────────────────────────────────────────────────┘
|
||||
════════════╳════════════ ← Draggable handle
|
||||
MIN $20,000
|
||||
```
|
||||
|
||||
### Threshold Visualization
|
||||
|
||||
**Maximum Threshold (Amber/Yellow)**
|
||||
```
|
||||
THRESHOLD STATES:
|
||||
|
||||
Below Threshold At Threshold Above Threshold
|
||||
(Flow Blocked) (Flow Starting) (Full Flow)
|
||||
|
||||
╱╲ ╱╲ ╱╲
|
||||
╱ ╲ ╱ ╲ ╱ ╲
|
||||
╱ ⛔ ╲ ╱ ⚠️ ╲ ╱ ✅ ╲
|
||||
╱──────╲ ╱──────╲ ╱──────╲
|
||||
│░░░░░░│ │▓▓▓░░░│ │██████│
|
||||
│░░░░░░│ │▓▓▓░░░│ │██████│
|
||||
└──────┘ └──────┘ └──────┘
|
||||
$2,000 $4,500 $8,000
|
||||
─────── ─────── ───────
|
||||
Threshold: $5,000 Threshold: $5,000 Threshold: $5,000
|
||||
════════════╳════════════ ← Draggable handle
|
||||
MAX $80,000
|
||||
```
|
||||
|
||||
### Flow Animation Styles
|
||||
### Status Panel
|
||||
|
||||
```
|
||||
1. PARTICLE FLOW (recommended)
|
||||
○───○───○───○───○───▶
|
||||
Dots flowing along the path, speed indicates rate
|
||||
|
||||
2. GRADIENT PULSE
|
||||
████▓▓▒▒░░░░░░░░░░░▶
|
||||
Color gradient pulses along the line
|
||||
|
||||
3. DASHED ANIMATION
|
||||
─ ─ ─ ─ ─ ─ ─ ─ ─ ─▶
|
||||
Animated dashes moving toward recipient
|
||||
|
||||
4. THICKNESS ENCODING
|
||||
═══════════════════▶ High flow
|
||||
────────────────────▶ Medium flow
|
||||
╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌▶ Low flow
|
||||
┌─────────────────────────────┐
|
||||
│ Current Balance │
|
||||
│ ████████████████░░░░░░░░ │
|
||||
│ $45,000 │
|
||||
│ ● Healthy Range │
|
||||
├─────────────────────────────┤
|
||||
│ Flow Rates │
|
||||
│ ↓ Inflow: +$500/hr │
|
||||
│ ↑ Outflow: -$300/hr │
|
||||
│ ═ Net: +$200/hr │
|
||||
├─────────────────────────────┤
|
||||
│ Progress │
|
||||
│ To MIN: ████████░░ 80% │
|
||||
│ To MAX: ██████░░░░ 60% │
|
||||
└─────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Interactive Features
|
||||
## Interaction Design
|
||||
|
||||
### 1. Threshold Slider
|
||||
Users can drag thresholds to see how changes affect flow dynamics:
|
||||
### Dragging Thresholds
|
||||
|
||||
1. **Hover** over threshold line → cursor changes to resize
|
||||
2. **Click & drag** up/down to adjust value
|
||||
3. **Real-time preview** of zone changes
|
||||
4. **Constraints**: MIN cannot exceed MAX, minimum gap enforced
|
||||
|
||||
### Visual Feedback During Drag
|
||||
|
||||
```
|
||||
┌────────────────────────────────────────────────────────┐
|
||||
│ THRESHOLD CONTROL │
|
||||
│ │
|
||||
│ Min ├────────────●────────────────────────────┤ Max │
|
||||
│ $0 $5,000 $50,000 │
|
||||
│ ▲ │
|
||||
│ │ │
|
||||
│ Current: $5,000 │
|
||||
│ │
|
||||
│ Flow activates when source balance > threshold │
|
||||
└────────────────────────────────────────────────────────┘
|
||||
Before drag: During drag: After release:
|
||||
════════════ ════════════════ ════════════
|
||||
│ ║ (thicker) │
|
||||
│ ║ │
|
||||
│ New value shown │
|
||||
```
|
||||
|
||||
### 2. Flow Rate Dial
|
||||
Adjust how fast funds flow when threshold is met:
|
||||
### Responsive Behaviors
|
||||
|
||||
```
|
||||
╭─────────────╮
|
||||
╱ │ ╲
|
||||
│ ────●──── │
|
||||
│ ╱ ╲ │
|
||||
╲ ╱ │ ╲ ╱
|
||||
╰──────┴──────╯
|
||||
$10 $100 $1000
|
||||
/hr
|
||||
|
||||
Current Rate: $50/hr
|
||||
```
|
||||
|
||||
### 3. Time Simulation
|
||||
Scrub through time to see how flows evolve:
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ ◀◀ ◀ ▶ ▶▶ │▒▒▒▒▒▒▒▒▒▒▒░░░░░░░░░░░░░░░░░░│ │
|
||||
│ ▲ │
|
||||
│ NOW │
|
||||
│ │
|
||||
│ Jan 2025 ─────────────────────────────────── Dec 2025 │
|
||||
└─────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Screen Layouts
|
||||
|
||||
### Main Canvas View
|
||||
|
||||
```
|
||||
┌──────────────────────────────────────────────────────────────────┐
|
||||
│ FLOW FUNDING [+] [⚙] [?] │
|
||||
├──────────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ ╭─────────╮ ╭─────────╮ │
|
||||
│ │ Treasury│ │Project A│ │
|
||||
│ │ $50K │══════════════════════════│ $12K │ │
|
||||
│ ╰────┬────╯ ╱ ╰─────────╯ │
|
||||
│ │ ╱ │
|
||||
│ │ ┌──────┐ │
|
||||
│ ├═════│ GATE │═══════╗ ╭─────────╮ │
|
||||
│ │ │$5K TH│ ║ │Project B│ │
|
||||
│ │ └──────┘ ╚══════════│ $8K │ │
|
||||
│ │ ╰─────────╯ │
|
||||
│ │ │
|
||||
│ │ ╭─────────╮ │
|
||||
│ ╰═══════════════════════════════│Project C│ │
|
||||
│ │ $5K │ │
|
||||
│ ╰─────────╯ │
|
||||
│ │
|
||||
├──────────────────────────────────────────────────────────────────┤
|
||||
│ Total Flowing: $300/hr │ Active Flows: 3 │ Blocked: 1 │
|
||||
└──────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### Detail Panel (on node click)
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────┐
|
||||
│ PROJECT A [×] │
|
||||
├─────────────────────────────────────┤
|
||||
│ │
|
||||
│ Current Balance $12,340.50 │
|
||||
│ Incoming Rate $85.00/hr │
|
||||
│ ──────────────────────────────── │
|
||||
│ │
|
||||
│ THRESHOLD STATUS │
|
||||
│ ████████████████░░░░ 80% │
|
||||
│ $12,340 / $15,000 threshold │
|
||||
│ │
|
||||
│ When threshold met: │
|
||||
│ → Unlock outgoing flow to Proj D │
|
||||
│ → Increase rate to $150/hr │
|
||||
│ │
|
||||
│ ──────────────────────────────── │
|
||||
│ FLOW HISTORY │
|
||||
│ ┌─────────────────────────────┐ │
|
||||
│ │ ╱╲ ╱╲ │ │
|
||||
│ │ ╱ ╲ ╱ ╲ ╱╲ │ │
|
||||
│ │ ╱ ╲╱ ╲ ╱ ╲ │ │
|
||||
│ │ ╱ ╲╱ ╲ │ │
|
||||
│ └─────────────────────────────┘ │
|
||||
│ Jan Feb Mar Apr May Jun │
|
||||
│ │
|
||||
└─────────────────────────────────────┘
|
||||
```
|
||||
| Balance State | Visual Response |
|
||||
|---------------|-----------------|
|
||||
| Approaching MIN | Rose tint intensifies, warning pulse |
|
||||
| Crossing MIN ↓ | Funnel narrows animation, outflow slows |
|
||||
| Crossing MIN ↑ | Funnel widens animation, outflow normalizes |
|
||||
| Approaching MAX | Amber tint at top, overflow warning |
|
||||
| Crossing MAX ↑ | Overflow particles, excess redistribution |
|
||||
|
||||
---
|
||||
|
||||
## Color System
|
||||
|
||||
| State | Color | Hex | Usage |
|
||||
|-------|-------|-----|-------|
|
||||
| Active Flow | Electric Blue | `#3B82F6` | Flowing connections |
|
||||
| Threshold Met | Emerald | `#10B981` | Success states |
|
||||
| Near Threshold | Amber | `#F59E0B` | Warning/attention |
|
||||
| Below Threshold | Rose | `#F43F5E` | Blocked/inactive |
|
||||
| Pending | Purple | `#8B5CF6` | Processing states |
|
||||
| Background | Slate | `#0F172A` | Canvas background |
|
||||
| Element | Color | Hex | Usage |
|
||||
|---------|-------|-----|-------|
|
||||
| Inflow | Blue | `#3B82F6` | Incoming fund particles |
|
||||
| Outflow | Pink | `#EC4899` | Outgoing fund particles |
|
||||
| Fill Gradient | Blue→Purple→Pink | gradient | Liquid level |
|
||||
| Critical Zone | Rose | `#F43F5E` | Below minimum |
|
||||
| Healthy Zone | Emerald | `#10B981` | Normal range |
|
||||
| Overflow Zone | Amber | `#F59E0B` | Above maximum |
|
||||
| Background | Slate | `#0F172A` | Canvas |
|
||||
|
||||
---
|
||||
|
||||
## Interaction Patterns
|
||||
## Multi-Funnel View
|
||||
|
||||
### Creating a New Flow
|
||||
1. Click source node → drag to recipient
|
||||
2. Release to open threshold configuration
|
||||
3. Set threshold amount and flow rate
|
||||
4. Flow appears with animation
|
||||
When displaying multiple funding pools:
|
||||
|
||||
### Adjusting Threshold
|
||||
1. Click on threshold gate icon
|
||||
2. Drag slider or enter exact value
|
||||
3. See real-time preview of flow state change
|
||||
4. Changes apply immediately or on confirm
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ Threshold-Based Flow Funding │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ ╔═══════╗ ╔═══════╗ ╔═══════╗ │
|
||||
│ ║ ║ ║▓▓▓▓▓▓▓║ ║███████║ │
|
||||
│ ║ ║ ║▓▓▓▓▓▓▓║ ║███████║ │
|
||||
│ ║░░░░░░░║ ║▓▓▓▓▓▓▓║ ║███████║ │
|
||||
│ ║░░░░░░░║ ╚═══╤═══╝ ╚═══╤═══╝ │
|
||||
│ ╚═══╤═══╝ │ │ │
|
||||
│ │ │ │ │
|
||||
│ Public Goods Research Emergency │
|
||||
│ $12K / $30K $45K / $60K $85K / $70K │
|
||||
│ ● Critical ● Healthy ● Overflow │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### Time Travel
|
||||
1. Pause current state
|
||||
2. Drag timeline scrubber
|
||||
3. See historical or projected flow states
|
||||
4. Resume real-time or stay at selected time
|
||||
### Interconnected Flows
|
||||
|
||||
Future enhancement: Connect funnels so overflow from one feeds into another:
|
||||
|
||||
```
|
||||
Fund A Fund B
|
||||
╔═════╗ ╔═════╗
|
||||
║█████║ ═══overflow═══▶ ║░░░░░║
|
||||
║█████║ ║░░░░░║
|
||||
╚══╤══╝ ╚══╤══╝
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Technical Implementation
|
||||
|
||||
### React Flow Integration
|
||||
- Custom node types: `SourceNode`, `RecipientNode`, `ThresholdGate`
|
||||
- Custom edge type: `AnimatedFlowEdge` with particle animation
|
||||
- Minimap for large networks
|
||||
- Pan/zoom controls
|
||||
### Component Architecture
|
||||
|
||||
### Animation System
|
||||
- Canvas-based particle system for flow animation
|
||||
- 60fps smooth animations
|
||||
- Performance-optimized for 100+ nodes
|
||||
```
|
||||
<FundingFunnel>
|
||||
├── <SVG Canvas>
|
||||
│ ├── Zone backgrounds (overflow, healthy, critical)
|
||||
│ ├── Funnel outline path
|
||||
│ ├── Liquid fill path (animated)
|
||||
│ ├── Inflow particles (animated)
|
||||
│ ├── Outflow particles (animated)
|
||||
│ ├── Threshold lines (draggable)
|
||||
│ └── Balance indicator
|
||||
└── <Stats Panel>
|
||||
├── Current balance display
|
||||
├── Flow rates
|
||||
└── Progress bars
|
||||
```
|
||||
|
||||
### State Management
|
||||
- Real-time flow calculations
|
||||
- Threshold state machine
|
||||
- Time-series data for history/projections
|
||||
|
||||
```typescript
|
||||
interface FunnelState {
|
||||
balance: number // Current fund level
|
||||
minThreshold: number // Minimum threshold (draggable)
|
||||
maxThreshold: number // Maximum threshold (draggable)
|
||||
inflowRate: number // Funds per hour coming in
|
||||
outflowRate: number // Funds per hour going out
|
||||
maxCapacity: number // Visual ceiling
|
||||
}
|
||||
```
|
||||
|
||||
### Animation Loop
|
||||
|
||||
```typescript
|
||||
// Simulation runs at 10Hz
|
||||
setInterval(() => {
|
||||
const netFlow = (inflowRate - outflowRate) / 3600 // per second
|
||||
balance = clamp(balance + netFlow, 0, maxCapacity * 1.2)
|
||||
}, 100)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
1. **Cascading Thresholds** — When one fund overflows, automatically route to connected funds
|
||||
2. **Historical Playback** — Scrub through time to see past flow dynamics
|
||||
3. **Prediction Mode** — Project future states based on current rates
|
||||
4. **Smart Routing** — AI-suggested threshold adjustments for optimal distribution
|
||||
5. **Real Data Integration** — Connect to actual treasury/DAO data sources
|
||||
6. **Export/Share** — Save configurations and share visualizations
|
||||
|
|
|
|||
151
app/page.tsx
151
app/page.tsx
|
|
@ -1,45 +1,154 @@
|
|||
'use client'
|
||||
|
||||
import { useState } from 'react'
|
||||
import dynamic from 'next/dynamic'
|
||||
|
||||
// Dynamic import to avoid SSR issues with React Flow
|
||||
const FlowCanvas = dynamic(() => import('@/components/FlowCanvas'), {
|
||||
const FundingFunnel = dynamic(() => import('@/components/FundingFunnel'), {
|
||||
ssr: false,
|
||||
loading: () => (
|
||||
<div className="w-full h-full flex items-center justify-center bg-slate-950">
|
||||
<div className="text-slate-400 animate-pulse">Loading flow visualization...</div>
|
||||
<div className="w-[500px] h-[600px] flex items-center justify-center bg-slate-900 rounded-2xl">
|
||||
<div className="text-slate-400 animate-pulse">Loading visualization...</div>
|
||||
</div>
|
||||
),
|
||||
})
|
||||
|
||||
export default function Home() {
|
||||
const [view, setView] = useState<'single' | 'multi'>('single')
|
||||
|
||||
return (
|
||||
<main className="h-screen w-screen overflow-hidden">
|
||||
<main className="min-h-screen bg-gradient-to-br from-slate-950 via-slate-900 to-slate-950 p-8">
|
||||
{/* Header */}
|
||||
<header className="absolute top-0 left-0 right-0 z-10 bg-slate-900/80 backdrop-blur border-b border-slate-700">
|
||||
<div className="px-6 py-4 flex items-center justify-between">
|
||||
<div className="flex items-center gap-4">
|
||||
<h1 className="text-xl font-bold bg-gradient-to-r from-blue-400 via-purple-500 to-emerald-400 bg-clip-text text-transparent">
|
||||
Flow Funding
|
||||
<header className="mb-8">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold bg-gradient-to-r from-blue-400 via-purple-500 to-pink-500 bg-clip-text text-transparent">
|
||||
Threshold-Based Flow Funding
|
||||
</h1>
|
||||
<span className="text-xs text-slate-500 border border-slate-700 px-2 py-0.5 rounded">
|
||||
TBFF Demo
|
||||
</span>
|
||||
<p className="text-slate-400 mt-2">
|
||||
Interactive visualization of funding flows with minimum and maximum thresholds
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-4">
|
||||
<button className="px-3 py-1.5 text-sm text-slate-300 hover:text-white transition">
|
||||
Reset
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
onClick={() => setView('single')}
|
||||
className={`px-4 py-2 rounded-lg text-sm transition ${
|
||||
view === 'single'
|
||||
? 'bg-purple-600 text-white'
|
||||
: 'bg-slate-800 text-slate-300 hover:bg-slate-700'
|
||||
}`}
|
||||
>
|
||||
Single Funnel
|
||||
</button>
|
||||
<button className="px-3 py-1.5 text-sm bg-blue-600 hover:bg-blue-500 text-white rounded-lg transition">
|
||||
+ Add Node
|
||||
<button
|
||||
onClick={() => setView('multi')}
|
||||
className={`px-4 py-2 rounded-lg text-sm transition ${
|
||||
view === 'multi'
|
||||
? 'bg-purple-600 text-white'
|
||||
: 'bg-slate-800 text-slate-300 hover:bg-slate-700'
|
||||
}`}
|
||||
>
|
||||
Multi-Funnel
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{/* Flow Canvas */}
|
||||
<div className="pt-16 h-full">
|
||||
<FlowCanvas />
|
||||
{/* Legend */}
|
||||
<div className="mb-8 flex items-center gap-8 text-sm">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-4 h-4 rounded bg-rose-500/30 border border-rose-500" />
|
||||
<span className="text-slate-400">Critical Zone (below min)</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-4 h-4 rounded bg-emerald-500/30 border border-emerald-500" />
|
||||
<span className="text-slate-400">Healthy Zone (min to max)</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-4 h-4 rounded bg-amber-500/30 border border-amber-500" />
|
||||
<span className="text-slate-400">Overflow Zone (above max)</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Funnel Visualizations */}
|
||||
{view === 'single' ? (
|
||||
<div className="flex justify-center">
|
||||
<FundingFunnel
|
||||
name="Community Treasury"
|
||||
currentBalance={35000}
|
||||
minThreshold={20000}
|
||||
maxThreshold={80000}
|
||||
inflowRate={500}
|
||||
outflowRate={300}
|
||||
maxCapacity={100000}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-3 gap-8 justify-items-center">
|
||||
<FundingFunnel
|
||||
name="Public Goods Fund"
|
||||
currentBalance={45000}
|
||||
minThreshold={25000}
|
||||
maxThreshold={75000}
|
||||
inflowRate={400}
|
||||
outflowRate={350}
|
||||
maxCapacity={100000}
|
||||
/>
|
||||
<FundingFunnel
|
||||
name="Research Grant Pool"
|
||||
currentBalance={12000}
|
||||
minThreshold={30000}
|
||||
maxThreshold={60000}
|
||||
inflowRate={200}
|
||||
outflowRate={150}
|
||||
maxCapacity={80000}
|
||||
/>
|
||||
<FundingFunnel
|
||||
name="Emergency Reserve"
|
||||
currentBalance={85000}
|
||||
minThreshold={50000}
|
||||
maxThreshold={70000}
|
||||
inflowRate={300}
|
||||
outflowRate={100}
|
||||
maxCapacity={100000}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Instructions */}
|
||||
<div className="mt-12 max-w-2xl mx-auto">
|
||||
<div className="bg-slate-800/50 rounded-xl p-6 border border-slate-700">
|
||||
<h3 className="text-lg font-semibold text-white mb-4">How It Works</h3>
|
||||
<ul className="space-y-3 text-slate-300">
|
||||
<li className="flex items-start gap-3">
|
||||
<span className="text-rose-400 font-bold">1.</span>
|
||||
<span>
|
||||
<strong className="text-rose-400">Minimum Threshold</strong> — Below this level,
|
||||
the funnel narrows, restricting outflow. Funds are conserved until the minimum is reached.
|
||||
</span>
|
||||
</li>
|
||||
<li className="flex items-start gap-3">
|
||||
<span className="text-emerald-400 font-bold">2.</span>
|
||||
<span>
|
||||
<strong className="text-emerald-400">Healthy Range</strong> — Between min and max,
|
||||
the funnel has straight walls. Normal operations with balanced in/out flows.
|
||||
</span>
|
||||
</li>
|
||||
<li className="flex items-start gap-3">
|
||||
<span className="text-amber-400 font-bold">3.</span>
|
||||
<span>
|
||||
<strong className="text-amber-400">Maximum Threshold</strong> — Above this level,
|
||||
excess funds overflow and can be redistributed to other pools or purposes.
|
||||
</span>
|
||||
</li>
|
||||
<li className="flex items-start gap-3">
|
||||
<span className="text-purple-400 font-bold">↕</span>
|
||||
<span>
|
||||
<strong className="text-purple-400">Drag the threshold lines</strong> to adjust
|
||||
minimum and maximum levels interactively.
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,576 @@
|
|||
'use client'
|
||||
|
||||
import { useState, useEffect, useRef, useCallback } from 'react'
|
||||
|
||||
interface FundingFunnelProps {
|
||||
name: string
|
||||
currentBalance: number
|
||||
minThreshold: number
|
||||
maxThreshold: number
|
||||
inflowRate: number // per hour
|
||||
outflowRate: number // per hour
|
||||
maxCapacity?: number
|
||||
onMinThresholdChange?: (value: number) => void
|
||||
onMaxThresholdChange?: (value: number) => void
|
||||
}
|
||||
|
||||
export default function FundingFunnel({
|
||||
name,
|
||||
currentBalance,
|
||||
minThreshold: initialMin,
|
||||
maxThreshold: initialMax,
|
||||
inflowRate,
|
||||
outflowRate,
|
||||
maxCapacity = 100000,
|
||||
onMinThresholdChange,
|
||||
onMaxThresholdChange,
|
||||
}: FundingFunnelProps) {
|
||||
const [minThreshold, setMinThreshold] = useState(initialMin)
|
||||
const [maxThreshold, setMaxThreshold] = useState(initialMax)
|
||||
const [balance, setBalance] = useState(currentBalance)
|
||||
const [isDraggingMin, setIsDraggingMin] = useState(false)
|
||||
const [isDraggingMax, setIsDraggingMax] = useState(false)
|
||||
const containerRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
// Funnel dimensions
|
||||
const width = 300
|
||||
const height = 500
|
||||
const funnelTopWidth = 280
|
||||
const funnelNarrowWidth = 80
|
||||
const padding = 10
|
||||
|
||||
// Calculate Y positions for thresholds (inverted - 0 is at bottom)
|
||||
const minY = height - (minThreshold / maxCapacity) * height
|
||||
const maxY = height - (maxThreshold / maxCapacity) * height
|
||||
const balanceY = height - (balance / maxCapacity) * height
|
||||
|
||||
// Funnel shape points
|
||||
const funnelPath = `
|
||||
M ${padding} ${maxY}
|
||||
L ${padding} ${minY}
|
||||
L ${(width - funnelNarrowWidth) / 2} ${height - padding}
|
||||
L ${(width + funnelNarrowWidth) / 2} ${height - padding}
|
||||
L ${width - padding} ${minY}
|
||||
L ${width - padding} ${maxY}
|
||||
L ${padding} ${maxY}
|
||||
`
|
||||
|
||||
// Overflow zone (above max)
|
||||
const overflowPath = `
|
||||
M ${padding} ${padding}
|
||||
L ${padding} ${maxY}
|
||||
L ${width - padding} ${maxY}
|
||||
L ${width - padding} ${padding}
|
||||
Z
|
||||
`
|
||||
|
||||
// Calculate fill path based on current balance
|
||||
const getFillPath = () => {
|
||||
if (balance <= 0) return ''
|
||||
|
||||
const fillY = Math.max(balanceY, padding)
|
||||
|
||||
if (balance >= maxThreshold) {
|
||||
// Overflow zone - straight walls above max
|
||||
const overflowY = Math.max(fillY, padding)
|
||||
return `
|
||||
M ${padding} ${minY}
|
||||
L ${(width - funnelNarrowWidth) / 2} ${height - padding}
|
||||
L ${(width + funnelNarrowWidth) / 2} ${height - padding}
|
||||
L ${width - padding} ${minY}
|
||||
L ${width - padding} ${overflowY}
|
||||
L ${padding} ${overflowY}
|
||||
Z
|
||||
`
|
||||
} else if (balance >= minThreshold) {
|
||||
// Between min and max - straight walls
|
||||
return `
|
||||
M ${padding} ${minY}
|
||||
L ${(width - funnelNarrowWidth) / 2} ${height - padding}
|
||||
L ${(width + funnelNarrowWidth) / 2} ${height - padding}
|
||||
L ${width - padding} ${minY}
|
||||
L ${width - padding} ${fillY}
|
||||
L ${padding} ${fillY}
|
||||
Z
|
||||
`
|
||||
} else {
|
||||
// Below min - in the funnel narrowing section
|
||||
const ratio = balance / minThreshold
|
||||
const bottomWidth = funnelNarrowWidth
|
||||
const topWidth = funnelTopWidth - 2 * padding
|
||||
const currentWidth = bottomWidth + (topWidth - bottomWidth) * ratio
|
||||
const leftX = (width - currentWidth) / 2
|
||||
const rightX = (width + currentWidth) / 2
|
||||
|
||||
return `
|
||||
M ${(width - funnelNarrowWidth) / 2} ${height - padding}
|
||||
L ${(width + funnelNarrowWidth) / 2} ${height - padding}
|
||||
L ${rightX} ${fillY}
|
||||
L ${leftX} ${fillY}
|
||||
Z
|
||||
`
|
||||
}
|
||||
}
|
||||
|
||||
// Simulate balance changes
|
||||
useEffect(() => {
|
||||
const interval = setInterval(() => {
|
||||
setBalance((prev) => {
|
||||
const netFlow = (inflowRate - outflowRate) / 3600 // per second
|
||||
const newBalance = prev + netFlow
|
||||
return Math.max(0, Math.min(maxCapacity * 1.2, newBalance))
|
||||
})
|
||||
}, 100)
|
||||
return () => clearInterval(interval)
|
||||
}, [inflowRate, outflowRate, maxCapacity])
|
||||
|
||||
// Handle threshold dragging
|
||||
const handleMouseMove = useCallback(
|
||||
(e: MouseEvent) => {
|
||||
if (!containerRef.current) return
|
||||
const rect = containerRef.current.getBoundingClientRect()
|
||||
const y = e.clientY - rect.top
|
||||
const value = Math.max(0, Math.min(maxCapacity, ((height - y) / height) * maxCapacity))
|
||||
|
||||
if (isDraggingMin) {
|
||||
const newMin = Math.min(value, maxThreshold - 1000)
|
||||
setMinThreshold(Math.max(0, newMin))
|
||||
onMinThresholdChange?.(Math.max(0, newMin))
|
||||
} else if (isDraggingMax) {
|
||||
const newMax = Math.max(value, minThreshold + 1000)
|
||||
setMaxThreshold(Math.min(maxCapacity, newMax))
|
||||
onMaxThresholdChange?.(Math.min(maxCapacity, newMax))
|
||||
}
|
||||
},
|
||||
[isDraggingMin, isDraggingMax, maxThreshold, minThreshold, maxCapacity, onMinThresholdChange, onMaxThresholdChange]
|
||||
)
|
||||
|
||||
const handleMouseUp = useCallback(() => {
|
||||
setIsDraggingMin(false)
|
||||
setIsDraggingMax(false)
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (isDraggingMin || isDraggingMax) {
|
||||
window.addEventListener('mousemove', handleMouseMove)
|
||||
window.addEventListener('mouseup', handleMouseUp)
|
||||
return () => {
|
||||
window.removeEventListener('mousemove', handleMouseMove)
|
||||
window.removeEventListener('mouseup', handleMouseUp)
|
||||
}
|
||||
}
|
||||
}, [isDraggingMin, isDraggingMax, handleMouseMove, handleMouseUp])
|
||||
|
||||
// Determine zone status
|
||||
const getZoneStatus = () => {
|
||||
if (balance < minThreshold) return { zone: 'critical', color: '#F43F5E', label: 'Below Minimum' }
|
||||
if (balance > maxThreshold) return { zone: 'overflow', color: '#F59E0B', label: 'Overflow' }
|
||||
return { zone: 'healthy', color: '#10B981', label: 'Healthy Range' }
|
||||
}
|
||||
|
||||
const status = getZoneStatus()
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-center">
|
||||
<h3 className="text-xl font-bold text-white mb-2">{name}</h3>
|
||||
|
||||
<div className="flex gap-8">
|
||||
{/* Main Funnel Visualization */}
|
||||
<div
|
||||
ref={containerRef}
|
||||
className="relative bg-slate-900 rounded-2xl p-4 border border-slate-700"
|
||||
style={{ width: width + 80, height: height + 40 }}
|
||||
>
|
||||
<svg width={width} height={height} className="overflow-visible">
|
||||
<defs>
|
||||
{/* Gradient for the fill */}
|
||||
<linearGradient id={`fill-gradient-${name}`} x1="0%" y1="100%" x2="0%" y2="0%">
|
||||
<stop offset="0%" stopColor="#3B82F6" stopOpacity="0.9" />
|
||||
<stop offset="50%" stopColor="#8B5CF6" stopOpacity="0.8" />
|
||||
<stop offset="100%" stopColor="#EC4899" stopOpacity="0.7" />
|
||||
</linearGradient>
|
||||
|
||||
{/* Glow filter */}
|
||||
<filter id="glow">
|
||||
<feGaussianBlur stdDeviation="3" result="coloredBlur" />
|
||||
<feMerge>
|
||||
<feMergeNode in="coloredBlur" />
|
||||
<feMergeNode in="SourceGraphic" />
|
||||
</feMerge>
|
||||
</filter>
|
||||
|
||||
{/* Wave pattern for liquid effect */}
|
||||
<pattern id="wave" x="0" y="0" width="20" height="10" patternUnits="userSpaceOnUse">
|
||||
<path
|
||||
d="M0 5 Q5 0 10 5 T20 5"
|
||||
fill="none"
|
||||
stroke="rgba(255,255,255,0.1)"
|
||||
strokeWidth="1"
|
||||
/>
|
||||
</pattern>
|
||||
</defs>
|
||||
|
||||
{/* Background zones */}
|
||||
{/* Overflow zone (above max) */}
|
||||
<rect
|
||||
x={padding}
|
||||
y={padding}
|
||||
width={width - 2 * padding}
|
||||
height={maxY - padding}
|
||||
fill="#F59E0B"
|
||||
fillOpacity="0.1"
|
||||
stroke="#F59E0B"
|
||||
strokeWidth="1"
|
||||
strokeDasharray="4 4"
|
||||
/>
|
||||
|
||||
{/* Healthy zone (between min and max) - straight walls */}
|
||||
<rect
|
||||
x={padding}
|
||||
y={maxY}
|
||||
width={width - 2 * padding}
|
||||
height={minY - maxY}
|
||||
fill="#10B981"
|
||||
fillOpacity="0.1"
|
||||
/>
|
||||
|
||||
{/* Funnel zone (below min) */}
|
||||
<path
|
||||
d={`
|
||||
M ${padding} ${minY}
|
||||
L ${(width - funnelNarrowWidth) / 2} ${height - padding}
|
||||
L ${(width + funnelNarrowWidth) / 2} ${height - padding}
|
||||
L ${width - padding} ${minY}
|
||||
Z
|
||||
`}
|
||||
fill="#F43F5E"
|
||||
fillOpacity="0.1"
|
||||
/>
|
||||
|
||||
{/* Funnel outline */}
|
||||
<path
|
||||
d={`
|
||||
M ${padding} ${padding}
|
||||
L ${padding} ${minY}
|
||||
L ${(width - funnelNarrowWidth) / 2} ${height - padding}
|
||||
L ${(width + funnelNarrowWidth) / 2} ${height - padding}
|
||||
L ${width - padding} ${minY}
|
||||
L ${width - padding} ${padding}
|
||||
`}
|
||||
fill="none"
|
||||
stroke="#475569"
|
||||
strokeWidth="2"
|
||||
/>
|
||||
|
||||
{/* Fill (current balance) */}
|
||||
<path
|
||||
d={getFillPath()}
|
||||
fill={`url(#fill-gradient-${name})`}
|
||||
filter="url(#glow)"
|
||||
>
|
||||
<animate
|
||||
attributeName="opacity"
|
||||
values="0.8;1;0.8"
|
||||
dur="2s"
|
||||
repeatCount="indefinite"
|
||||
/>
|
||||
</path>
|
||||
|
||||
{/* Animated inflow particles */}
|
||||
{inflowRate > 0 && (
|
||||
<>
|
||||
{[...Array(5)].map((_, i) => (
|
||||
<circle
|
||||
key={`inflow-${i}`}
|
||||
r="4"
|
||||
fill="#3B82F6"
|
||||
opacity="0.8"
|
||||
>
|
||||
<animate
|
||||
attributeName="cy"
|
||||
values={`-10;${balanceY}`}
|
||||
dur={`${1 + i * 0.2}s`}
|
||||
repeatCount="indefinite"
|
||||
begin={`${i * 0.2}s`}
|
||||
/>
|
||||
<animate
|
||||
attributeName="cx"
|
||||
values={`${width / 2 - 20 + i * 10};${width / 2 - 10 + i * 5}`}
|
||||
dur={`${1 + i * 0.2}s`}
|
||||
repeatCount="indefinite"
|
||||
begin={`${i * 0.2}s`}
|
||||
/>
|
||||
<animate
|
||||
attributeName="opacity"
|
||||
values="0.8;0.8;0"
|
||||
dur={`${1 + i * 0.2}s`}
|
||||
repeatCount="indefinite"
|
||||
begin={`${i * 0.2}s`}
|
||||
/>
|
||||
</circle>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Animated outflow particles */}
|
||||
{outflowRate > 0 && balance > 0 && (
|
||||
<>
|
||||
{[...Array(3)].map((_, i) => (
|
||||
<circle
|
||||
key={`outflow-${i}`}
|
||||
r="3"
|
||||
fill="#EC4899"
|
||||
opacity="0.8"
|
||||
>
|
||||
<animate
|
||||
attributeName="cy"
|
||||
values={`${height - padding};${height + 30}`}
|
||||
dur={`${0.8 + i * 0.15}s`}
|
||||
repeatCount="indefinite"
|
||||
begin={`${i * 0.25}s`}
|
||||
/>
|
||||
<animate
|
||||
attributeName="cx"
|
||||
values={`${width / 2 - 10 + i * 10};${width / 2 - 15 + i * 15}`}
|
||||
dur={`${0.8 + i * 0.15}s`}
|
||||
repeatCount="indefinite"
|
||||
begin={`${i * 0.25}s`}
|
||||
/>
|
||||
<animate
|
||||
attributeName="opacity"
|
||||
values="0.8;0.6;0"
|
||||
dur={`${0.8 + i * 0.15}s`}
|
||||
repeatCount="indefinite"
|
||||
begin={`${i * 0.25}s`}
|
||||
/>
|
||||
</circle>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Max threshold line (draggable) */}
|
||||
<g
|
||||
className="cursor-ns-resize"
|
||||
onMouseDown={() => setIsDraggingMax(true)}
|
||||
>
|
||||
<line
|
||||
x1={0}
|
||||
y1={maxY}
|
||||
x2={width}
|
||||
y2={maxY}
|
||||
stroke="#F59E0B"
|
||||
strokeWidth={isDraggingMax ? 4 : 2}
|
||||
strokeDasharray="8 4"
|
||||
/>
|
||||
<rect
|
||||
x={width - 8}
|
||||
y={maxY - 12}
|
||||
width={16}
|
||||
height={24}
|
||||
rx={4}
|
||||
fill="#F59E0B"
|
||||
className="cursor-ns-resize"
|
||||
/>
|
||||
<text
|
||||
x={width + 12}
|
||||
y={maxY + 5}
|
||||
fill="#F59E0B"
|
||||
fontSize="12"
|
||||
fontFamily="monospace"
|
||||
>
|
||||
MAX ${maxThreshold.toLocaleString()}
|
||||
</text>
|
||||
</g>
|
||||
|
||||
{/* Min threshold line (draggable) */}
|
||||
<g
|
||||
className="cursor-ns-resize"
|
||||
onMouseDown={() => setIsDraggingMin(true)}
|
||||
>
|
||||
<line
|
||||
x1={padding}
|
||||
y1={minY}
|
||||
x2={width - padding}
|
||||
y2={minY}
|
||||
stroke="#F43F5E"
|
||||
strokeWidth={isDraggingMin ? 4 : 2}
|
||||
strokeDasharray="8 4"
|
||||
/>
|
||||
<rect
|
||||
x={width - 8}
|
||||
y={minY - 12}
|
||||
width={16}
|
||||
height={24}
|
||||
rx={4}
|
||||
fill="#F43F5E"
|
||||
className="cursor-ns-resize"
|
||||
/>
|
||||
<text
|
||||
x={width + 12}
|
||||
y={minY + 5}
|
||||
fill="#F43F5E"
|
||||
fontSize="12"
|
||||
fontFamily="monospace"
|
||||
>
|
||||
MIN ${minThreshold.toLocaleString()}
|
||||
</text>
|
||||
</g>
|
||||
|
||||
{/* Current balance indicator */}
|
||||
<g>
|
||||
<line
|
||||
x1={0}
|
||||
y1={balanceY}
|
||||
x2={padding - 2}
|
||||
y2={balanceY}
|
||||
stroke={status.color}
|
||||
strokeWidth={3}
|
||||
/>
|
||||
<polygon
|
||||
points={`${padding - 2},${balanceY - 6} ${padding - 2},${balanceY + 6} ${padding + 6},${balanceY}`}
|
||||
fill={status.color}
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
|
||||
{/* Zone labels */}
|
||||
<div
|
||||
className="absolute text-xs text-amber-400/60 font-medium"
|
||||
style={{ right: 8, top: maxY / 2 + 20 }}
|
||||
>
|
||||
OVERFLOW
|
||||
</div>
|
||||
<div
|
||||
className="absolute text-xs text-emerald-400/60 font-medium"
|
||||
style={{ right: 8, top: (maxY + minY) / 2 + 20 }}
|
||||
>
|
||||
HEALTHY
|
||||
</div>
|
||||
<div
|
||||
className="absolute text-xs text-rose-400/60 font-medium"
|
||||
style={{ right: 8, top: (minY + height) / 2 + 10 }}
|
||||
>
|
||||
CRITICAL
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Stats Panel */}
|
||||
<div className="flex flex-col gap-4 min-w-[200px]">
|
||||
{/* Current Balance */}
|
||||
<div className="bg-slate-800 rounded-xl p-4 border border-slate-700">
|
||||
<div className="text-sm text-slate-400 mb-1">Current Balance</div>
|
||||
<div className="text-3xl font-bold font-mono" style={{ color: status.color }}>
|
||||
${balance.toLocaleString(undefined, { maximumFractionDigits: 0 })}
|
||||
</div>
|
||||
<div
|
||||
className="text-sm mt-2 px-2 py-1 rounded-full inline-block"
|
||||
style={{ backgroundColor: `${status.color}20`, color: status.color }}
|
||||
>
|
||||
{status.label}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Flow Rates */}
|
||||
<div className="bg-slate-800 rounded-xl p-4 border border-slate-700">
|
||||
<div className="text-sm text-slate-400 mb-3">Flow Rates</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-2 h-2 rounded-full bg-blue-500" />
|
||||
<span className="text-slate-300">Inflow</span>
|
||||
</div>
|
||||
<span className="font-mono text-blue-400">+${inflowRate}/hr</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-2 h-2 rounded-full bg-pink-500" />
|
||||
<span className="text-slate-300">Outflow</span>
|
||||
</div>
|
||||
<span className="font-mono text-pink-400">-${outflowRate}/hr</span>
|
||||
</div>
|
||||
|
||||
<div className="border-t border-slate-600 pt-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-slate-300">Net Flow</span>
|
||||
<span
|
||||
className={`font-mono font-bold ${
|
||||
inflowRate - outflowRate >= 0 ? 'text-emerald-400' : 'text-rose-400'
|
||||
}`}
|
||||
>
|
||||
{inflowRate - outflowRate >= 0 ? '+' : ''}${inflowRate - outflowRate}/hr
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Thresholds */}
|
||||
<div className="bg-slate-800 rounded-xl p-4 border border-slate-700">
|
||||
<div className="text-sm text-slate-400 mb-3">Thresholds</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
<div>
|
||||
<div className="flex items-center justify-between text-sm mb-1">
|
||||
<span className="text-rose-400">Minimum</span>
|
||||
<span className="font-mono text-rose-400">${minThreshold.toLocaleString()}</span>
|
||||
</div>
|
||||
<div className="text-xs text-slate-500">
|
||||
Drag the red line to adjust
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className="flex items-center justify-between text-sm mb-1">
|
||||
<span className="text-amber-400">Maximum</span>
|
||||
<span className="font-mono text-amber-400">${maxThreshold.toLocaleString()}</span>
|
||||
</div>
|
||||
<div className="text-xs text-slate-500">
|
||||
Drag the yellow line to adjust
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Progress to thresholds */}
|
||||
<div className="bg-slate-800 rounded-xl p-4 border border-slate-700">
|
||||
<div className="text-sm text-slate-400 mb-3">Progress</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
<div>
|
||||
<div className="flex justify-between text-xs mb-1">
|
||||
<span className="text-slate-400">To Minimum</span>
|
||||
<span className="text-slate-400">
|
||||
{Math.min(100, (balance / minThreshold) * 100).toFixed(0)}%
|
||||
</span>
|
||||
</div>
|
||||
<div className="h-2 bg-slate-700 rounded-full overflow-hidden">
|
||||
<div
|
||||
className="h-full bg-rose-500 transition-all duration-300"
|
||||
style={{ width: `${Math.min(100, (balance / minThreshold) * 100)}%` }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className="flex justify-between text-xs mb-1">
|
||||
<span className="text-slate-400">To Maximum</span>
|
||||
<span className="text-slate-400">
|
||||
{Math.min(100, (balance / maxThreshold) * 100).toFixed(0)}%
|
||||
</span>
|
||||
</div>
|
||||
<div className="h-2 bg-slate-700 rounded-full overflow-hidden">
|
||||
<div
|
||||
className="h-full bg-amber-500 transition-all duration-300"
|
||||
style={{ width: `${Math.min(100, (balance / maxThreshold) * 100)}%` }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Loading…
Reference in New Issue