Upload project config and lib files
Browse files- .env.example +4 -0
- .gitignore +4 -0
- next-env.d.ts +2 -0
- next.config.js +3 -0
- package.json +28 -0
- postcss.config.js +1 -0
- src/app/globals.css +16 -0
- src/app/layout.tsx +20 -0
- src/lib/types.ts +13 -0
- src/lib/utils.ts +28 -0
- tailwind.config.js +51 -0
- tsconfig.json +20 -0
.env.example
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
TORQUE_API_KEY=your_torque_api_key_here
|
| 2 |
+
TORQUE_API_URL=https://api.torque.so/v1
|
| 3 |
+
HELIUS_API_KEY=your_helius_api_key_here
|
| 4 |
+
SOLANA_RPC_URL=https://api.mainnet-beta.solana.com
|
.gitignore
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
node_modules/
|
| 2 |
+
.next/
|
| 3 |
+
.env
|
| 4 |
+
.env.local
|
next-env.d.ts
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/// <reference types="next" />
|
| 2 |
+
/// <reference types="next/image-types/global" />
|
next.config.js
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/** @type {import('next').NextConfig} */
|
| 2 |
+
const nextConfig = { reactStrictMode: true }
|
| 3 |
+
module.exports = nextConfig
|
package.json
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"name": "flowstate",
|
| 3 |
+
"version": "1.0.0",
|
| 4 |
+
"scripts": {
|
| 5 |
+
"dev": "next dev",
|
| 6 |
+
"build": "next build",
|
| 7 |
+
"start": "next start"
|
| 8 |
+
},
|
| 9 |
+
"dependencies": {
|
| 10 |
+
"@types/node": "^20.0.0",
|
| 11 |
+
"@types/react": "^18.3.0",
|
| 12 |
+
"@types/react-dom": "^18.3.0",
|
| 13 |
+
"autoprefixer": "^10.4.0",
|
| 14 |
+
"clsx": "^2.1.0",
|
| 15 |
+
"date-fns": "^4.1.0",
|
| 16 |
+
"framer-motion": "^12.0.0",
|
| 17 |
+
"lucide-react": "^1.0.0",
|
| 18 |
+
"next": "^14.2.0",
|
| 19 |
+
"postcss": "^8.5.0",
|
| 20 |
+
"react": "^18.3.0",
|
| 21 |
+
"react-dom": "^18.3.0",
|
| 22 |
+
"recharts": "^2.12.0",
|
| 23 |
+
"tailwind-merge": "^2.3.0",
|
| 24 |
+
"tailwindcss": "^3.4.0",
|
| 25 |
+
"typescript": "^5.4.0",
|
| 26 |
+
"zustand": "^5.0.0"
|
| 27 |
+
}
|
| 28 |
+
}
|
postcss.config.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
module.exports = { plugins: { tailwindcss: {}, autoprefixer: {} } }
|
src/app/globals.css
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
@tailwind base;
|
| 2 |
+
@tailwind components;
|
| 3 |
+
@tailwind utilities;
|
| 4 |
+
|
| 5 |
+
@layer base {
|
| 6 |
+
* { border-color: theme('colors.hairline.dark'); }
|
| 7 |
+
body { @apply bg-canvas-dark text-[#eaecef] antialiased; }
|
| 8 |
+
::selection { @apply bg-brand-yellow/20 text-brand-yellow; }
|
| 9 |
+
::-webkit-scrollbar { @apply w-1.5; }
|
| 10 |
+
::-webkit-scrollbar-track { @apply bg-canvas-dark; }
|
| 11 |
+
::-webkit-scrollbar-thumb { @apply bg-surface-elevated rounded-full; }
|
| 12 |
+
}
|
| 13 |
+
|
| 14 |
+
@layer utilities {
|
| 15 |
+
.glow-yellow { box-shadow: 0 0 20px rgba(252, 213, 53, 0.15); }
|
| 16 |
+
}
|
src/app/layout.tsx
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import type { Metadata } from 'next'
|
| 2 |
+
import { Inter, IBM_Plex_Mono } from 'next/font/google'
|
| 3 |
+
import { cn } from '@/lib/utils'
|
| 4 |
+
import './globals.css'
|
| 5 |
+
|
| 6 |
+
const inter = Inter({ subsets: ['latin'], variable: '--font-inter', display: 'swap', weight: ['400', '500', '600', '700'] })
|
| 7 |
+
const ibmPlexMono = IBM_Plex_Mono({ subsets: ['latin'], variable: '--font-ibm-plex-mono', display: 'swap', weight: ['400', '500', '600', '700'] })
|
| 8 |
+
|
| 9 |
+
export const metadata: Metadata = {
|
| 10 |
+
title: 'FlowState — AI-Powered Anti-Churn Engine',
|
| 11 |
+
description: 'The autonomous retention layer for Solana protocols. Powered by Torque MCP.',
|
| 12 |
+
}
|
| 13 |
+
|
| 14 |
+
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
| 15 |
+
return (
|
| 16 |
+
<html lang="en" className={cn('dark', inter.variable, ibmPlexMono.variable)}>
|
| 17 |
+
<body className="bg-canvas-dark text-[#eaecef] font-sans antialiased min-h-screen">{children}</body>
|
| 18 |
+
</html>
|
| 19 |
+
)
|
| 20 |
+
}
|
src/lib/types.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
export type ChurnRisk = 'critical' | 'high' | 'medium' | 'low' | 'safe'
|
| 2 |
+
export type CampaignType = 'leaderboard' | 'rebate' | 'raffle' | 'gift'
|
| 3 |
+
export type CampaignStatus = 'draft' | 'active' | 'ended' | 'distributed'
|
| 4 |
+
|
| 5 |
+
export interface Wallet { address: string; churnRisk: ChurnRisk; riskScore: number; lastActive: Date; totalVolume: number; streak: number; protocols: string[]; joinedAt: Date; savedCount: number; campaignsParticipated: number }
|
| 6 |
+
export interface Campaign { id: string; name: string; type: CampaignType; status: CampaignStatus; description: string; budget: number; tokenMint: string; startTime: Date; endTime: Date; participantCount: number; eventsProcessed: number; rewardsDistributed: number; formula?: string; createdBy: 'ai-agent' | 'manual' }
|
| 7 |
+
export interface LeaderboardEntry { rank: number; wallet: string; score: number; change24h: number; volume: number; streak: number; protocols: string[]; rewards: number }
|
| 8 |
+
export interface AgentAction { id: string; timestamp: Date; actionType: string; description: string; walletTarget?: string; campaignId?: string; success: boolean; metadata: Record<string, unknown> }
|
| 9 |
+
export interface ChurnEvent { id: string; wallet: string; eventType: string; timestamp: Date; metadata: Record<string, unknown>; campaignTriggered?: string; resolved: boolean }
|
| 10 |
+
export interface AnalyticsMetric { date: string; value: number }
|
| 11 |
+
export interface RetentionCohort { week: string; day1: number; day7: number; day14: number; day30: number; day60: number }
|
| 12 |
+
export interface ProtocolMetric { protocol: string; volume: number; users: number; churnRate: number; retentionRate: number; avgStreak: number; color: string }
|
| 13 |
+
export interface DashboardStats { totalWallets: number; activeWallets: number; walletsAtRisk: number; walletsSaved: number; totalCampaigns: number; activeCampaigns: number; totalEventsToday: number; rewardsDistributed: number; avgRetention: number; churnRate: number; agentActionsToday: number; roi: number }
|
src/lib/utils.ts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { clsx, type ClassValue } from 'clsx'
|
| 2 |
+
import { twMerge } from 'tailwind-merge'
|
| 3 |
+
|
| 4 |
+
export function cn(...inputs: ClassValue[]) { return twMerge(clsx(inputs)) }
|
| 5 |
+
|
| 6 |
+
export function formatNumber(n: number): string {
|
| 7 |
+
if (n >= 1_000_000_000) return `${(n / 1_000_000_000).toFixed(2)}B`
|
| 8 |
+
if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(2)}M`
|
| 9 |
+
if (n >= 1_000) return `${(n / 1_000).toFixed(1)}K`
|
| 10 |
+
return n.toFixed(0)
|
| 11 |
+
}
|
| 12 |
+
|
| 13 |
+
export function formatCurrency(n: number): string {
|
| 14 |
+
return new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD', maximumFractionDigits: 0 }).format(n)
|
| 15 |
+
}
|
| 16 |
+
|
| 17 |
+
export function shortenAddress(addr: string): string {
|
| 18 |
+
if (addr.length < 10) return addr
|
| 19 |
+
return `${addr.slice(0, 4)}...${addr.slice(-4)}`
|
| 20 |
+
}
|
| 21 |
+
|
| 22 |
+
export function getTimeAgo(date: Date): string {
|
| 23 |
+
const s = Math.floor((Date.now() - date.getTime()) / 1000)
|
| 24 |
+
if (s < 60) return `${s}s ago`
|
| 25 |
+
if (s < 3600) return `${Math.floor(s / 60)}m ago`
|
| 26 |
+
if (s < 86400) return `${Math.floor(s / 3600)}h ago`
|
| 27 |
+
return `${Math.floor(s / 86400)}d ago`
|
| 28 |
+
}
|
tailwind.config.js
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/** @type {import('tailwindcss').Config} */
|
| 2 |
+
module.exports = {
|
| 3 |
+
darkMode: 'class',
|
| 4 |
+
content: ['./src/**/*.{ts,tsx}'],
|
| 5 |
+
theme: {
|
| 6 |
+
extend: {
|
| 7 |
+
colors: {
|
| 8 |
+
canvas: { dark: '#0b0e11', light: '#ffffff' },
|
| 9 |
+
surface: { card: '#1e2329', elevated: '#2b3139', soft: '#fafafa', hover: '#252c35' },
|
| 10 |
+
brand: { yellow: '#FCD535', 'yellow-active': '#f0b90b', turquoise: '#2dbdb6' },
|
| 11 |
+
hairline: { light: '#eaecef', dark: '#2b3139' },
|
| 12 |
+
ink: '#181a20',
|
| 13 |
+
muted: { DEFAULT: '#707a8a', strong: '#929aa5' },
|
| 14 |
+
trading: { up: '#0ecb81', down: '#f6465d' },
|
| 15 |
+
info: '#3b82f6',
|
| 16 |
+
},
|
| 17 |
+
fontFamily: {
|
| 18 |
+
sans: ['var(--font-inter)', 'system-ui', 'sans-serif'],
|
| 19 |
+
mono: ['var(--font-ibm-plex-mono)', 'Consolas', 'monospace'],
|
| 20 |
+
},
|
| 21 |
+
borderRadius: { xs: '2px', sm: '4px', md: '6px', lg: '8px', xl: '12px', pill: '9999px' },
|
| 22 |
+
fontSize: {
|
| 23 |
+
'display-sm': ['32px', { lineHeight: '1.2', fontWeight: '600' }],
|
| 24 |
+
'title-lg': ['24px', { lineHeight: '1.3', fontWeight: '600' }],
|
| 25 |
+
'title-md': ['20px', { lineHeight: '1.35', fontWeight: '600' }],
|
| 26 |
+
'title-sm': ['16px', { lineHeight: '1.4', fontWeight: '600' }],
|
| 27 |
+
'num-display': ['40px', { lineHeight: '1.1', fontWeight: '700' }],
|
| 28 |
+
'num-md': ['16px', { lineHeight: '1.4', fontWeight: '500' }],
|
| 29 |
+
'num-sm': ['14px', { lineHeight: '1.4', fontWeight: '500' }],
|
| 30 |
+
'body-md': ['14px', { lineHeight: '1.5', fontWeight: '400' }],
|
| 31 |
+
'body-sm': ['13px', { lineHeight: '1.5', fontWeight: '400' }],
|
| 32 |
+
'caption': ['12px', { lineHeight: '1.4', fontWeight: '500' }],
|
| 33 |
+
'button': ['14px', { lineHeight: '1', fontWeight: '600' }],
|
| 34 |
+
'nav': ['14px', { lineHeight: '1.4', fontWeight: '500' }],
|
| 35 |
+
},
|
| 36 |
+
keyframes: {
|
| 37 |
+
'flash-green': { '0%,100%': { backgroundColor: 'transparent' }, '50%': { backgroundColor: 'rgba(14,203,129,0.15)' } },
|
| 38 |
+
'pulse-glow': { '0%,100%': { boxShadow: '0 0 0 0 rgba(252,213,53,0.4)' }, '50%': { boxShadow: '0 0 20px 4px rgba(252,213,53,0.15)' } },
|
| 39 |
+
'slide-up': { '0%': { transform: 'translateY(10px)', opacity: '0' }, '100%': { transform: 'translateY(0)', opacity: '1' } },
|
| 40 |
+
'slide-in-right': { '0%': { transform: 'translateX(20px)', opacity: '0' }, '100%': { transform: 'translateX(0)', opacity: '1' } },
|
| 41 |
+
},
|
| 42 |
+
animation: {
|
| 43 |
+
'flash-green': 'flash-green 0.6s ease-in-out',
|
| 44 |
+
'pulse-glow': 'pulse-glow 2s ease-in-out infinite',
|
| 45 |
+
'slide-up': 'slide-up 0.5s ease-out',
|
| 46 |
+
'slide-in-right': 'slide-in-right 0.4s ease-out',
|
| 47 |
+
},
|
| 48 |
+
},
|
| 49 |
+
},
|
| 50 |
+
plugins: [],
|
| 51 |
+
}
|
tsconfig.json
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
+
"plugins": [{ "name": "next" }],
|
| 16 |
+
"paths": { "@/*": ["./src/*"] }
|
| 17 |
+
},
|
| 18 |
+
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
| 19 |
+
"exclude": ["node_modules"]
|
| 20 |
+
}
|