mirror of
https://github.com/laoxong/nofx.git
synced 2026-06-04 01:48:22 +08:00
feat: add SQLite/PostgreSQL database switching support
This commit is contained in:
@@ -14,6 +14,9 @@ import (
|
||||
"nofx/manager"
|
||||
"nofx/market"
|
||||
"nofx/provider/alpaca"
|
||||
"nofx/provider/coinank/coinank_api"
|
||||
"nofx/provider/coinank/coinank_enum"
|
||||
"nofx/provider/hyperliquid"
|
||||
"nofx/provider/twelvedata"
|
||||
"nofx/store"
|
||||
"nofx/trader"
|
||||
|
||||
@@ -56,6 +56,7 @@ require (
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
|
||||
github.com/leodido/go-urn v1.4.0 // indirect
|
||||
github.com/lib/pq v1.10.9 // indirect
|
||||
github.com/mailru/easyjson v0.9.1 // indirect
|
||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
|
||||
@@ -133,6 +133,8 @@ github.com/leanovate/gopter v0.2.11 h1:vRjThO1EKPb/1NsDXuDrzldR28RLkBflWYcU9CvzW
|
||||
github.com/leanovate/gopter v0.2.11/go.mod h1:aK3tzZP/C+p1m3SPRE4SYZFGP7jjkuSI4f7Xvpt0S9c=
|
||||
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
||||
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
||||
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
|
||||
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||
github.com/mailru/easyjson v0.9.1 h1:LbtsOm5WAswyWbvTEOqhypdPeZzHavpZx96/n553mR8=
|
||||
github.com/mailru/easyjson v0.9.1/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU=
|
||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||
|
||||
@@ -36,21 +36,30 @@ func main() {
|
||||
cfg := config.Get()
|
||||
logger.Info("✅ Configuration loaded")
|
||||
|
||||
// Initialize database
|
||||
// Default path is data/data.db to work with Docker volume mount (/app/data)
|
||||
dbPath := "data/data.db"
|
||||
// Initialize database from environment variables
|
||||
// DB_TYPE: sqlite (default) or postgres
|
||||
// For SQLite: DB_PATH (default: data/data.db)
|
||||
// For PostgreSQL: DB_HOST, DB_PORT, DB_USER, DB_PASSWORD, DB_NAME, DB_SSLMODE
|
||||
dbPath := os.Getenv("DB_PATH")
|
||||
if dbPath == "" {
|
||||
dbPath = "data/data.db"
|
||||
}
|
||||
// For backward compatibility: command line arg overrides env var (SQLite only)
|
||||
if len(os.Args) > 1 {
|
||||
dbPath = os.Args[1]
|
||||
os.Setenv("DB_PATH", dbPath)
|
||||
}
|
||||
// Ensure data directory exists
|
||||
if dir := filepath.Dir(dbPath); dir != "." {
|
||||
if err := os.MkdirAll(dir, 0755); err != nil {
|
||||
logger.Errorf("Failed to create data directory: %v", err)
|
||||
// Ensure data directory exists (for SQLite)
|
||||
if os.Getenv("DB_TYPE") == "" || os.Getenv("DB_TYPE") == "sqlite" {
|
||||
if dir := filepath.Dir(dbPath); dir != "." {
|
||||
if err := os.MkdirAll(dir, 0755); err != nil {
|
||||
logger.Errorf("Failed to create data directory: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
logger.Infof("📋 Initializing database: %s", dbPath)
|
||||
st, err := store.New(dbPath)
|
||||
logger.Info("📋 Initializing database...")
|
||||
st, err := store.NewFromEnv()
|
||||
if err != nil {
|
||||
logger.Fatalf("❌ Failed to initialize database: %v", err)
|
||||
}
|
||||
|
||||
+240
@@ -0,0 +1,240 @@
|
||||
// Package store provides database driver abstraction
|
||||
package store
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
_ "github.com/lib/pq" // PostgreSQL driver
|
||||
_ "modernc.org/sqlite" // SQLite driver
|
||||
)
|
||||
|
||||
// DBType represents database type
|
||||
type DBType string
|
||||
|
||||
const (
|
||||
DBTypeSQLite DBType = "sqlite"
|
||||
DBTypePostgres DBType = "postgres"
|
||||
)
|
||||
|
||||
// DBConfig database configuration
|
||||
type DBConfig struct {
|
||||
Type DBType // sqlite or postgres
|
||||
Path string // SQLite file path (for sqlite)
|
||||
Host string // PostgreSQL host (for postgres)
|
||||
Port int // PostgreSQL port (for postgres)
|
||||
User string // PostgreSQL user (for postgres)
|
||||
Password string // PostgreSQL password (for postgres)
|
||||
DBName string // PostgreSQL database name (for postgres)
|
||||
SSLMode string // PostgreSQL SSL mode (for postgres)
|
||||
}
|
||||
|
||||
// DBDriver database driver abstraction
|
||||
type DBDriver struct {
|
||||
Type DBType
|
||||
db *sql.DB
|
||||
}
|
||||
|
||||
// NewDBDriver creates database driver from config
|
||||
func NewDBDriver(cfg DBConfig) (*DBDriver, error) {
|
||||
var db *sql.DB
|
||||
var err error
|
||||
|
||||
switch cfg.Type {
|
||||
case DBTypeSQLite:
|
||||
db, err = openSQLite(cfg.Path)
|
||||
case DBTypePostgres:
|
||||
db, err = openPostgres(cfg)
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported database type: %s", cfg.Type)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &DBDriver{Type: cfg.Type, db: db}, nil
|
||||
}
|
||||
|
||||
// NewDBDriverFromEnv creates database driver from environment variables
|
||||
// DB_TYPE: sqlite (default) or postgres
|
||||
// For SQLite: DB_PATH (default: data/data.db)
|
||||
// For PostgreSQL: DB_HOST, DB_PORT, DB_USER, DB_PASSWORD, DB_NAME, DB_SSLMODE
|
||||
func NewDBDriverFromEnv() (*DBDriver, error) {
|
||||
dbType := DBType(strings.ToLower(getEnv("DB_TYPE", "sqlite")))
|
||||
|
||||
switch dbType {
|
||||
case DBTypeSQLite:
|
||||
path := getEnv("DB_PATH", "data/data.db")
|
||||
return NewDBDriver(DBConfig{Type: DBTypeSQLite, Path: path})
|
||||
|
||||
case DBTypePostgres:
|
||||
port := 5432
|
||||
if p := os.Getenv("DB_PORT"); p != "" {
|
||||
fmt.Sscanf(p, "%d", &port)
|
||||
}
|
||||
return NewDBDriver(DBConfig{
|
||||
Type: DBTypePostgres,
|
||||
Host: getEnv("DB_HOST", "localhost"),
|
||||
Port: port,
|
||||
User: getEnv("DB_USER", "postgres"),
|
||||
Password: os.Getenv("DB_PASSWORD"),
|
||||
DBName: getEnv("DB_NAME", "nofx"),
|
||||
SSLMode: getEnv("DB_SSLMODE", "disable"),
|
||||
})
|
||||
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported DB_TYPE: %s (use 'sqlite' or 'postgres')", dbType)
|
||||
}
|
||||
}
|
||||
|
||||
// DB returns underlying database connection
|
||||
func (d *DBDriver) DB() *sql.DB {
|
||||
return d.db
|
||||
}
|
||||
|
||||
// Close closes database connection
|
||||
func (d *DBDriver) Close() error {
|
||||
return d.db.Close()
|
||||
}
|
||||
|
||||
// AutoIncrement returns auto-increment syntax for current database
|
||||
func (d *DBDriver) AutoIncrement() string {
|
||||
switch d.Type {
|
||||
case DBTypePostgres:
|
||||
return "SERIAL"
|
||||
default:
|
||||
return "INTEGER PRIMARY KEY AUTOINCREMENT"
|
||||
}
|
||||
}
|
||||
|
||||
// Placeholder returns placeholder for parameterized queries
|
||||
// SQLite uses ?, PostgreSQL uses $1, $2, etc.
|
||||
func (d *DBDriver) Placeholder(index int) string {
|
||||
switch d.Type {
|
||||
case DBTypePostgres:
|
||||
return fmt.Sprintf("$%d", index)
|
||||
default:
|
||||
return "?"
|
||||
}
|
||||
}
|
||||
|
||||
// ConvertPlaceholders converts ? placeholders to database-specific format
|
||||
func (d *DBDriver) ConvertPlaceholders(query string) string {
|
||||
if d.Type != DBTypePostgres {
|
||||
return query
|
||||
}
|
||||
|
||||
// Convert ? to $1, $2, etc. for PostgreSQL
|
||||
result := query
|
||||
index := 1
|
||||
for strings.Contains(result, "?") {
|
||||
result = strings.Replace(result, "?", fmt.Sprintf("$%d", index), 1)
|
||||
index++
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// TableExists checks if a table exists
|
||||
func (d *DBDriver) TableExists(tableName string) (bool, error) {
|
||||
var exists bool
|
||||
var query string
|
||||
|
||||
switch d.Type {
|
||||
case DBTypePostgres:
|
||||
query = `SELECT EXISTS (
|
||||
SELECT FROM information_schema.tables
|
||||
WHERE table_schema = 'public' AND table_name = $1
|
||||
)`
|
||||
default:
|
||||
query = `SELECT EXISTS (
|
||||
SELECT 1 FROM sqlite_master
|
||||
WHERE type = 'table' AND name = ?
|
||||
)`
|
||||
}
|
||||
|
||||
query = d.ConvertPlaceholders(query)
|
||||
err := d.db.QueryRow(query, tableName).Scan(&exists)
|
||||
return exists, err
|
||||
}
|
||||
|
||||
// UpsertSyntax returns the upsert syntax for current database
|
||||
// SQLite: INSERT ... ON CONFLICT(...) DO UPDATE SET ...
|
||||
// PostgreSQL: INSERT ... ON CONFLICT(...) DO UPDATE SET ...
|
||||
// Both use the same syntax in modern versions
|
||||
func (d *DBDriver) UpsertSyntax() string {
|
||||
return "ON CONFLICT"
|
||||
}
|
||||
|
||||
// openSQLite opens SQLite database
|
||||
func openSQLite(path string) (*sql.DB, error) {
|
||||
db, err := sql.Open("sqlite", path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to open SQLite database: %w", err)
|
||||
}
|
||||
|
||||
// SQLite configuration
|
||||
db.SetMaxOpenConns(1)
|
||||
db.SetMaxIdleConns(1)
|
||||
|
||||
// Enable foreign key constraints
|
||||
if _, err := db.Exec(`PRAGMA foreign_keys = ON`); err != nil {
|
||||
db.Close()
|
||||
return nil, fmt.Errorf("failed to enable foreign keys: %w", err)
|
||||
}
|
||||
|
||||
// Use DELETE mode for Docker compatibility
|
||||
if _, err := db.Exec("PRAGMA journal_mode=DELETE"); err != nil {
|
||||
db.Close()
|
||||
return nil, fmt.Errorf("failed to set journal_mode: %w", err)
|
||||
}
|
||||
|
||||
// Set synchronous=FULL
|
||||
if _, err := db.Exec("PRAGMA synchronous=FULL"); err != nil {
|
||||
db.Close()
|
||||
return nil, fmt.Errorf("failed to set synchronous: %w", err)
|
||||
}
|
||||
|
||||
// Set busy_timeout
|
||||
if _, err := db.Exec("PRAGMA busy_timeout = 5000"); err != nil {
|
||||
db.Close()
|
||||
return nil, fmt.Errorf("failed to set busy_timeout: %w", err)
|
||||
}
|
||||
|
||||
return db, nil
|
||||
}
|
||||
|
||||
// openPostgres opens PostgreSQL database
|
||||
func openPostgres(cfg DBConfig) (*sql.DB, error) {
|
||||
connStr := fmt.Sprintf(
|
||||
"host=%s port=%d user=%s password=%s dbname=%s sslmode=%s",
|
||||
cfg.Host, cfg.Port, cfg.User, cfg.Password, cfg.DBName, cfg.SSLMode,
|
||||
)
|
||||
|
||||
db, err := sql.Open("postgres", connStr)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to open PostgreSQL database: %w", err)
|
||||
}
|
||||
|
||||
// PostgreSQL configuration
|
||||
db.SetMaxOpenConns(25)
|
||||
db.SetMaxIdleConns(5)
|
||||
|
||||
// Test connection
|
||||
if err := db.Ping(); err != nil {
|
||||
db.Close()
|
||||
return nil, fmt.Errorf("failed to connect to PostgreSQL: %w", err)
|
||||
}
|
||||
|
||||
return db, nil
|
||||
}
|
||||
|
||||
// getEnv gets environment variable with default value
|
||||
func getEnv(key, defaultValue string) string {
|
||||
if value := os.Getenv(key); value != "" {
|
||||
return value
|
||||
}
|
||||
return defaultValue
|
||||
}
|
||||
+52
-38
@@ -7,13 +7,12 @@ import (
|
||||
"fmt"
|
||||
"nofx/logger"
|
||||
"sync"
|
||||
|
||||
_ "modernc.org/sqlite"
|
||||
)
|
||||
|
||||
// Store unified data storage interface
|
||||
type Store struct {
|
||||
db *sql.DB
|
||||
db *sql.DB
|
||||
driver *DBDriver // Database driver for abstraction
|
||||
|
||||
// Sub-stores (lazy initialization)
|
||||
user *UserStore
|
||||
@@ -34,57 +33,56 @@ type Store struct {
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
// New creates new Store instance
|
||||
// New creates new Store instance (SQLite mode for backward compatibility)
|
||||
func New(dbPath string) (*Store, error) {
|
||||
db, err := sql.Open("sqlite", dbPath)
|
||||
driver, err := NewDBDriver(DBConfig{Type: DBTypeSQLite, Path: dbPath})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to open database: %w", err)
|
||||
}
|
||||
|
||||
// SQLite configuration
|
||||
db.SetMaxOpenConns(1)
|
||||
db.SetMaxIdleConns(1)
|
||||
|
||||
// Enable foreign key constraints
|
||||
if _, err := db.Exec(`PRAGMA foreign_keys = ON`); err != nil {
|
||||
db.Close()
|
||||
return nil, fmt.Errorf("failed to enable foreign keys: %w", err)
|
||||
}
|
||||
|
||||
// Use DELETE mode (traditional mode) to ensure Docker bind mount compatibility
|
||||
// Note: WAL mode causes data sync issues on macOS Docker
|
||||
if _, err := db.Exec("PRAGMA journal_mode=DELETE"); err != nil {
|
||||
db.Close()
|
||||
return nil, fmt.Errorf("failed to set journal_mode: %w", err)
|
||||
}
|
||||
|
||||
// Set synchronous=FULL
|
||||
if _, err := db.Exec("PRAGMA synchronous=FULL"); err != nil {
|
||||
db.Close()
|
||||
return nil, fmt.Errorf("failed to set synchronous: %w", err)
|
||||
}
|
||||
|
||||
// Set busy_timeout
|
||||
if _, err := db.Exec("PRAGMA busy_timeout = 5000"); err != nil {
|
||||
db.Close()
|
||||
return nil, fmt.Errorf("failed to set busy_timeout: %w", err)
|
||||
}
|
||||
|
||||
s := &Store{db: db}
|
||||
s := &Store{db: driver.DB(), driver: driver}
|
||||
|
||||
// Initialize all table structures
|
||||
if err := s.initTables(); err != nil {
|
||||
db.Close()
|
||||
driver.Close()
|
||||
return nil, fmt.Errorf("failed to initialize table structure: %w", err)
|
||||
}
|
||||
|
||||
// Initialize default data
|
||||
if err := s.initDefaultData(); err != nil {
|
||||
db.Close()
|
||||
driver.Close()
|
||||
return nil, fmt.Errorf("failed to initialize default data: %w", err)
|
||||
}
|
||||
|
||||
logger.Info("✅ Database enabled DELETE mode and FULL sync")
|
||||
logger.Infof("✅ Database initialized (type: %s)", driver.Type)
|
||||
return s, nil
|
||||
}
|
||||
|
||||
// NewFromEnv creates new Store instance from environment variables
|
||||
// DB_TYPE: sqlite (default) or postgres
|
||||
// For SQLite: DB_PATH (default: data/data.db)
|
||||
// For PostgreSQL: DB_HOST, DB_PORT, DB_USER, DB_PASSWORD, DB_NAME, DB_SSLMODE
|
||||
func NewFromEnv() (*Store, error) {
|
||||
driver, err := NewDBDriverFromEnv()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to open database: %w", err)
|
||||
}
|
||||
|
||||
s := &Store{db: driver.DB(), driver: driver}
|
||||
|
||||
// Initialize all table structures
|
||||
if err := s.initTables(); err != nil {
|
||||
driver.Close()
|
||||
return nil, fmt.Errorf("failed to initialize table structure: %w", err)
|
||||
}
|
||||
|
||||
// Initialize default data
|
||||
if err := s.initDefaultData(); err != nil {
|
||||
driver.Close()
|
||||
return nil, fmt.Errorf("failed to initialize default data: %w", err)
|
||||
}
|
||||
|
||||
logger.Infof("✅ Database initialized (type: %s)", driver.Type)
|
||||
return s, nil
|
||||
}
|
||||
|
||||
@@ -293,9 +291,25 @@ func (s *Store) Order() *OrderStore {
|
||||
|
||||
// Close closes database connection
|
||||
func (s *Store) Close() error {
|
||||
if s.driver != nil {
|
||||
return s.driver.Close()
|
||||
}
|
||||
return s.db.Close()
|
||||
}
|
||||
|
||||
// Driver returns database driver for abstraction
|
||||
func (s *Store) Driver() *DBDriver {
|
||||
return s.driver
|
||||
}
|
||||
|
||||
// DBType returns current database type
|
||||
func (s *Store) DBType() DBType {
|
||||
if s.driver != nil {
|
||||
return s.driver.Type
|
||||
}
|
||||
return DBTypeSQLite
|
||||
}
|
||||
|
||||
// DB gets underlying database connection (for legacy code compatibility, gradually deprecated)
|
||||
// Deprecated: use Store methods instead
|
||||
func (s *Store) DB() *sql.DB {
|
||||
|
||||
@@ -54,10 +54,10 @@ export default function AgentGrid() {
|
||||
<div className="flex flex-col md:flex-row justify-between items-end mb-16 gap-6">
|
||||
<div>
|
||||
<div className="flex items-center gap-2 text-nofx-gold font-mono text-xs mb-2 tracking-widest uppercase">
|
||||
<Crosshair className="w-4 h-4" /> Operator Select
|
||||
<Crosshair className="w-4 h-4" /> MARKET SELECT
|
||||
</div>
|
||||
<h2 className="text-4xl md:text-5xl font-black text-white uppercase tracking-tighter">
|
||||
Available <span className="text-transparent bg-clip-text bg-gradient-to-r from-nofx-gold to-white">Units</span>
|
||||
STRATEGY <span className="text-transparent bg-clip-text bg-gradient-to-r from-nofx-gold to-white">UNITS</span>
|
||||
</h2>
|
||||
</div>
|
||||
<div className="font-mono text-right text-xs text-zinc-500 max-w-xs">
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
import { useState } from 'react'
|
||||
import { motion, AnimatePresence } from 'framer-motion'
|
||||
import { Terminal, Copy, Check, ChevronRight, Server, Command, Shield } from 'lucide-react'
|
||||
|
||||
export default function DeploymentHub() {
|
||||
const [copied, setCopied] = useState(false)
|
||||
const installCmd = "curl -fsSL https://raw.githubusercontent.com/NoFxAiOS/nofx/main/install.sh | bash"
|
||||
|
||||
const handleCopy = () => {
|
||||
navigator.clipboard.writeText(installCmd)
|
||||
setCopied(true)
|
||||
setTimeout(() => setCopied(false), 2000)
|
||||
}
|
||||
|
||||
return (
|
||||
<section className="py-24 bg-black relative overflow-hidden border-t border-zinc-800">
|
||||
{/* Background Grids */}
|
||||
<div className="absolute inset-0 bg-[linear-gradient(to_right,#80808012_1px,transparent_1px),linear-gradient(to_bottom,#80808012_1px,transparent_1px)] bg-[size:24px_24px]"></div>
|
||||
|
||||
<div className="max-w-7xl mx-auto px-6 relative z-10">
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-16 items-center">
|
||||
|
||||
{/* Left Column: Context */}
|
||||
<div className="space-y-8">
|
||||
<div className="flex items-center gap-2 text-nofx-gold font-mono text-xs tracking-[0.2em] uppercase">
|
||||
<Server className="w-4 h-4" /> System Deployment
|
||||
</div>
|
||||
|
||||
<h2 className="text-4xl md:text-6xl font-black text-white leading-tight">
|
||||
DEPLOY <span className="text-transparent bg-clip-text bg-gradient-to-r from-nofx-gold to-white">INSTANTLY</span>
|
||||
</h2>
|
||||
|
||||
<p className="text-zinc-400 text-lg leading-relaxed font-light">
|
||||
Initialize your own high-frequency trading node in seconds.
|
||||
Our optimized installer handles all dependencies, bringing your autonomous agent online with a single command.
|
||||
</p>
|
||||
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 pt-4">
|
||||
{[
|
||||
{ icon: Command, label: "One-Line Install", desc: "No configuration needed" },
|
||||
{ icon: Shield, label: "Secure Core", desc: "Sandboxed execution env" }
|
||||
].map((item, i) => (
|
||||
<div key={i} className="flex gap-4 items-start p-4 rounded bg-zinc-900/50 border border-zinc-800 hover:border-nofx-gold/30 transition-colors group">
|
||||
<div className="p-2 rounded bg-black border border-zinc-800 text-nofx-gold group-hover:bg-nofx-gold/10 transition-colors">
|
||||
<item.icon className="w-5 h-5" />
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="text-white font-bold font-mono text-sm mb-1">{item.label}</h4>
|
||||
<p className="text-zinc-500 text-xs">{item.desc}</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Right Column: Terminal */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, x: 50 }}
|
||||
whileInView={{ opacity: 1, x: 0 }}
|
||||
viewport={{ once: true }}
|
||||
className="relative"
|
||||
>
|
||||
{/* Glow effect */}
|
||||
<div className="absolute -inset-1 bg-gradient-to-r from-nofx-gold/20 to-blue-500/20 rounded-xl blur-xl opacity-50"></div>
|
||||
|
||||
<div className="relative rounded-xl overflow-hidden bg-[#0a0a0a] border border-zinc-800 shadow-2xl">
|
||||
{/* Terminal Header */}
|
||||
<div className="flex items-center justify-between px-4 py-3 bg-zinc-900/80 border-b border-zinc-800">
|
||||
<div className="flex gap-2">
|
||||
<div className="w-3 h-3 rounded-full bg-red-500/80"></div>
|
||||
<div className="w-3 h-3 rounded-full bg-yellow-500/80"></div>
|
||||
<div className="w-3 h-3 rounded-full bg-green-500/80"></div>
|
||||
</div>
|
||||
<div className="text-[10px] font-mono text-zinc-500 flex items-center gap-1.5">
|
||||
<Terminal className="w-3 h-3" />
|
||||
root@nofx-os:~
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Terminal Content */}
|
||||
<div className="p-8 font-mono text-sm md:text-base bg-black/50 backdrop-blur-sm min-h-[200px] flex flex-col justify-center">
|
||||
<div className="mb-2 text-zinc-500 text-xs tracking-wide"># Initialize NoFX Core Protocol</div>
|
||||
<div
|
||||
className="group relative flex items-start gap-3 p-4 rounded-lg bg-zinc-900/50 border border-zinc-800 hover:border-nofx-gold/50 cursor-pointer transition-all hover:bg-zinc-900/80"
|
||||
onClick={handleCopy}
|
||||
>
|
||||
<span className="text-nofx-gold mt-1"><ChevronRight className="w-4 h-4" /></span>
|
||||
<code className="text-zinc-100 flex-1 break-all">
|
||||
{installCmd}
|
||||
</code>
|
||||
|
||||
<div className="absolute right-4 top-1/2 -translate-y-1/2 opacity-0 group-hover:opacity-100 transition-opacity">
|
||||
<AnimatePresence mode='wait'>
|
||||
{copied ? (
|
||||
<motion.div
|
||||
initial={{ scale: 0.5, opacity: 0 }}
|
||||
animate={{ scale: 1, opacity: 1 }}
|
||||
exit={{ scale: 0.5, opacity: 0 }}
|
||||
className="flex items-center gap-1 text-green-400 bg-green-400/10 px-2 py-1 rounded text-xs font-bold"
|
||||
>
|
||||
<Check className="w-3 h-3" />
|
||||
</motion.div>
|
||||
) : (
|
||||
<div className="text-zinc-400 bg-zinc-800 p-1.5 rounded hover:text-white hover:bg-zinc-700">
|
||||
<Copy className="w-4 h-4" />
|
||||
</div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-4 flex gap-2">
|
||||
<div className="w-2 h-4 bg-nofx-gold animate-pulse"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import FooterSection from '../components/landing/FooterSection'
|
||||
import TerminalHero from '../components/landing/core/TerminalHero'
|
||||
import LiveFeed from '../components/landing/core/LiveFeed'
|
||||
import AgentGrid from '../components/landing/core/AgentGrid'
|
||||
import DeploymentHub from '../components/landing/core/DeploymentHub'
|
||||
import { useAuth } from '../contexts/AuthContext'
|
||||
import { useLanguage } from '../contexts/LanguageContext'
|
||||
|
||||
@@ -42,6 +43,8 @@ export function LandingPage() {
|
||||
|
||||
<AgentGrid />
|
||||
|
||||
<DeploymentHub />
|
||||
|
||||
<FooterSection language={language} />
|
||||
|
||||
{showLoginModal && (
|
||||
|
||||
Reference in New Issue
Block a user